InterviewPrepKit

Home / Cheat Sheet / Object-Oriented Design

Cheat sheet

How to design Tic-Tac-Toe

Read the full lesson →

Tic-tac-toe is a restraint exercise: model it with the few objects it needs, decline the patterns it doesn’t, and make the win check touch a constant number of values per move.

The objects (3, or 4 with AI)

ObjectRole
Mark (enum: X, O)No state; only other() flips to the opposing mark. Not a Piece class.
BoardOwns the cells and the per-line win counters. Every grid change must go through it.
GameTurn order, end detection, move history.
Player (interface)Only with a computer opponent: choose(board, mark) -> (row, col).
  • play(r, c) returns the winner (or None) as the output of the move itself. No separate check_for_win().
  • Board size n is a constructor arg; Board reads self.n, never the literal 3, so 4x4 works unchanged.

Patterns declined (name + reason)

  • No Cell class: a cell is Optional[Mark]; wrapping it adds indirection.
  • No MoveValidator (chain of responsibility): validation is four < comparisons known at authoring time.
  • No Observer: one board, one renderer; the caller already knows the board changed.
  • No GameStateFactory (factory): three outcomes = one nullable winner + a full-board test.
  • No State hierarchy: a winner field plus board.full() is the whole state machine.
  • No Singleton: Game holds no global state, so a server runs thousands keyed by id.

The O(1) win check

  • Board keeps a counter per row, per column, and one per diagonal, split by mark.
  • A placed mark can only change lines through its cell: at most 4 counters (centre sits on both diagonals).
  • Update those counters, compare the largest to n. A win means one counter hit n with nothing else read.
  • Diagonal membership: r == c (main), r + c == n - 1 (anti).
  • unplace is the exact inverse of place (a -1 where the move was +1), so undo and minimax need no board snapshot, both O(1).
place (r,c)  -->  bump ≤4 counters  -->  largest == n ?  -->  yes: win / no: continue

Cost vs. rescan

Check3x3Scaling
Rescan every move~24 reads2n^2 + 2n cells
Incremental counters≤4 updatesconstant, ~6x less at 3x3, ~190x at 19x19

The k-in-a-row gotcha

  • Counters are correct only when k == n. For k < n a row counter of k does not mean the marks are adjacent (it misses a gapped X X . X X).
  • Fix: an O(k) directional scan outward from the placed cell, 4 directions x 2 opposite rays, stop at k. Independent of n.
  • Game, undo, history, and the Player interface all stay: place only ever promised “true if this move won”, never how.

AI and undo

  • Minimax: enumerate moves to game end, score +1/0/-1, negate on recursion so one function serves both players. 3x3 is a forced draw; empty-board search is 66,275 nodes with the if best == 1: break cutoff (~80% off the full 340,858).
  • 4x4 is intractable (16! ≈ 2.1e13): switch to alpha-beta + depth limit + heuristic. The Player interface absorbs all three.
  • Undo came free from the incremental counters (Command pattern: store coords, invert). A real PlaceCommand object is only worth it for redo/replay.

Data vs. code

  • Data (edit without recompiling): board size, occupancy, move history, scan directions.
  • Code (read as a sentence, type-checked): win condition, line geometry, two-player alternation, move legality.
  • The bet: board size varies, the rules don’t. Correct here, which is why the design stays at four boxes.
Want the full picture? The lesson has the derivations, worked examples, and diagrams this card compresses into bullets. Read the full lesson →
Report a bug