My first bot picked squares at random. I beat it in 3 moves.
I’m working through LLD problems, and tic-tac-toe was the third one I picked. Building it for two players was the easy part. Then I added a bot, and it hooked me for days. Every time I beat it, I added a new rule. Every time I added a rule, I found a new way to beat it. This went on until one day I just… couldn’t win anymore. Only draws.
This post isn’t about the board, the turns or undo (that part is boring, trust me). It’s about how the bot thinks. Let’s see how I made it.
How the bot is wired
Before the levels, one thing you need to know: the bot doesn’t have one big brain. It has a list of small brains, and it asks them one by one.
| |
Each strategy either returns a cell (“I’ve got this”) or null (“not my job, ask the next guy”). The bot takes the first answer that isn’t null.
| |
It works like a waterfall. In design-pattern terms it’s Strategy + Chain of Responsibility. The nice part: adding a new level means adding one new class. Bot never changes.

Level 1 — The blind swordsman
Random Strategy
I needed somewhere to start, and this was the spot. Pick a random row and column that is not occupied, and return it.
Playing against this was like a sword fight with a blind opponent. He’s swinging hard, really trying to attack you, but whether he hits anything is pure luck.

Center Strategy
After a few games I noticed something: the center is the key square. It sits on 4 lines (a row, a column and both diagonals), more than any other square. If our bot grabs it, it gets a head start.
So this strategy is simple: if the center is empty, take it.
Corner Strategy
Same idea, but for corners. Each corner sits on 3 lines, so they’re the next best thing. With this added, the bot really felt like it was trying sometimes.
All four corners go in a simple list, using last = size - 1 so it works for any board size. Then we take the first one that’s still empty:
| |
Random + Center + Corner together — that’s our level 1 bot. Still very easy to beat. It takes good squares, but it has no idea what I’m doing.

The turning point — everything is just a line
Before I could teach the bot to defend, I had to fix how it sees the board.
At first every strategy walked the rows, then the columns, then the diagonals, all by hand. Copy-paste everywhere.
Then I looked closer. Rows, columns, diagonals… they’re all just lines. A 3×3 board has 8 of them: 3 rows, 3 columns, 2 diagonals.
So GridStatus slices the board into those 8 lines, and each line becomes a LineStats: how many of each symbol it has, and which cells are still empty.
Row 0: X | X | . → X: 2, O: 0, empty: [(0,2)]
Now every strategy is just a one-line question on the same data:
- Can I win here? → I have 2, and 1 cell is empty.
- Do I need to block? → I have 0, and 1 cell is empty.
- Is this a fork line? → I have 1, and 2 cells are empty.
Nothing walks the board by hand anymore, so there’s no copy-paste left to get wrong.

Level 2 — The shield and the sword
Block Strategy
I should’ve added this to our arsenal way earlier. This is where the bot got its first shield.
The problem: I’d line up two X’s, and the bot just… didn’t care. So now it checks every line for one where I have all but one cell and it has nothing. If it finds one, it plugs the hole.
| |
This doesn’t mean we win. But at least we can delay the loss.

Win Strategy
Now it was time to get our sword too. Any time there’s a victory on the table, the bot should grab it. It works like Block, just flipped: find a line where we need one more piece.
With the sword and shield in hand, we had a level 2 bot. It had some tricks up its sleeve, but in the real world it was still an amateur. Because I had a new weapon.

Level 3 — The fork
Every game has moves that put you at an advantage, and in tic-tac-toe that move is the fork: one move that creates two threats at once. The opponent can only block one. The other one wins.
It’s a lethal weapon. I was beating the level 2 bot with it all the time, so it needed one too.
Finding a fork: let the lines vote
This part was a little tricky, so I built a voting system.
Go over every line. If a line has one of my pieces and two empty cells, it’s one move away from becoming a threat. So each of those two empty cells gets a vote.
| |
(On 3×3, size - 2 is just 1 piece.)
Any cell with 2 or more votes sits on two of those lines at once. Play it, and both lines become threats. That’s a fork.

Why it’s third in line
Fork is powerful, but it’s not first priority. If we can win right now, win (Win). If the opponent can win right now, block it (Block). Only then do we think about forks.
Once the fork landed, we had the victory. That’s our level 3 bot.
Level 4 — Blocking forks (and the trap)
What’s lethal for the enemy is lethal for us too. Now I had to teach the bot to stop my forks.
The first idea was easy: run the same vote from my side. If I have exactly one fork cell, the bot just takes it. Done.
But what if I have two?
The trap
X | . | .
. | O | .
. | . | X
I play two opposite corners, the bot is in the center. Run the vote for X, and I have two fork cells: (0,2) and (2,0).
The old bot did what seemed smart: it took one of them.
X | . | O
. | O | .
. | . | X
Looks like a block, right? But now O has two on the anti-diagonal, so I have to block at (2,0)… which is my other fork cell. Now I have two threats (column 0 and row 2), and the bot can only stop one.
The bot “blocked the fork” and still lost. I laughed for a good minute.

The fix: a forcing move
Instead of chasing my fork cells, the bot makes one threat of its own, so I’m forced to block. And it picks that threat carefully: the cell I’m forced to play must not be one of my fork cells.
| |
In the trap, the bot plays the edge:
X | . | .
O | O | .
. | . | X
Now I have to block at (1,2). That’s not a fork cell, so it’s harmless. The bot blocks my one remaining threat, and the game ends in a draw.
I replayed this game a lot. Draw. Every time.
And with that, the bot was unbeatable. Not the minimax approach everyone uses, just my own rules, but it works.

The aha: build order ≠ priority order
Here’s what surprised me. I built Center before Block. But the bot must check Block first. The order I learned things isn’t the order the bot should think.
The final chain:
| |
Why this order?
- Win and Block come first because they’re about this move. Forks are about the next one.
- Fork comes before BlockFork because if I fork, the opponent has to answer my threats. Their fork never gets played.
- Random is last, always. It never returns
null, so anything after it would never get a turn.

Wrapping up
I can’t beat it anymore. Only draws.
The funny part: only two of the seven strategies (Fork and BlockFork) look ahead at all. The rest just read the board as it is right now.
| My rules | Minimax | |
|---|---|---|
| How it works | Hand-written tic-tac-toe knowledge | Tries every possible game |
| Speed | Instant | Slower (it searches everything) |
| Works on | 3×3 only, really | Any board |
Next post, I’m throwing all these rules away and building the same unbeatable bot with zero tic-tac-toe knowledge: minimax.
The full code is on GitHub: system-design/lld/tic-tac-toe
