Completing TicTacToe: Strategies, Undo, and the Live Game

The final implementation: human vs bot players, O(1) strategy logic, and state-reversal undo. Polishing with custom exceptions.

April 27, 20265 min read9 / 9

The game loop and controller are wired up, but the game cannot run yet. Several methods are still stubs. This post finishes all of that: the human input path, the bot strategy, all three O(1) winning strategies, and the state-reversal undo.

The Essentials

  1. Human player reads from stdin: The game already knows whose turn it is; the user only provides the row and column.
  2. Bot player delegates to strategy: The strategy is created by the factory at construction time.
  3. EasyBotPlayingStrategy: Scans the board for the first empty cell.
  4. O(1) Winning Strategies: Row, Column, and Diagonal strategies use count maps to detect winners instantly.
  5. Undo reverses state in reverse order: Clear the cell, reverse the player index, and then reverse the strategy maps.
  6. Use Custom Exceptions: Create an exceptions package for named classes like InvalidPlayerCountException.

Bot Player: Strategy and Polymorphism

The Bot class delegates its makeMove logic to its playingStrategy. This is polymorphism in action -- the game just calls currentPlayer.makeMove(board) without needing to know if the player is human or bot.

class EasyBotPlayingStrategy implements BotPlayingStrategy { makeMove(board: Board): Cell { for (const row of board.getCells()) { for (const cell of row) { if (cell.getStatus() === CellStatus.EMPTY) { return cell; } } } throw new Error("No empty cell found"); } }

Implementing Undo (Direct Reversal)

Undo is the exact mirror of makeMove. If makeMove added a symbol to a cell and incremented a count in a map, undo must clear that cell and decrement that same count.

One critical rule: Call handleUndo on your winning strategies before you clear the player reference on the cell. The strategy needs that reference to know which player's count to decrement.

undo(): void { if (this.moves.length === 0) return; const lastMove = this.moves.pop()!; // Reverse strategy counts first for (const strategy of this.winningStrategies) { strategy.handleUndo(lastMove, this.board); } // Clear state const cell = lastMove.getCell(); cell.setStatus(CellStatus.EMPTY); cell.setPlayer(null); // Reverse turn order this.currentPlayerIndex = (this.currentPlayerIndex - 1 + this.players.length) % this.players.length; }

The Modular Arithmetic Bug

One of the most common bugs in TicTacToe implementations happens right here in the undo method. When the currentPlayerIndex is at 0 and you subtract 1, you get -1. In many languages (like Java), -1 % 4 is still -1, not 3. This leads to an immediate IndexOutOfBoundsException.

The Buggy Way

Results in negative indices when wrapping from 0 to N-1.

index = (index - 1) % size;
// (0 - 1) % 4 = -1 (CRASH)
The Safe Way

Always add the size before taking the modulo to ensure a positive result.

index = (index - 1 + size) % size;
// (0 - 1 + 4) % 4 = 3 (SUCCESS)

Replaying the Game

Because we store every Move object in a list, implementing a "Replay" feature is trivial. We simply clear the board and iterate through the list, printing the board after each move. This is why keeping a history of objects is always superior to just tracking the current state.

Custom Exceptions for Clean Error Handling

Instead of throwing a generic RuntimeException, we create specific exceptions. This makes stack traces self-documenting and allows callers to handle different errors uniquely.

class DuplicateSymbolException extends Error { constructor(message: string) { super(message); this.name = "DuplicateSymbolException"; } }

When coding in an interview, I follow this sequence:

  1. Models: Board, Cell, Player, etc.
  2. Builder: Validation logic for game creation.
  3. Core Loop: The Main/Controller flow.
  4. Strategies: Winner detection and bot AI.

This order ensures that you always have a runnable system at every stage of the 90-minute session. A working board with human moves is better than a half-implemented everything.

TicTacToe is complete. The next case study is Parking Lot, where the challenges shift from game state to resource allocation, concurrency, and richer entity hierarchies.

Further Reading and Watching

Practice what you just read.

Final Polish: Custom Exceptions
1 exercise