Tic Tac Toe

Classic Board Game

Design a two-player Tic Tac Toe with move validation, constant-time win detection, a draw rule and a scoreboard that follows many games.

Overview

Two players take turns marking cells of a 3 by 3 grid. The first to fill a row, a column or a diagonal wins; a full grid with no line is a draw. Everyone knows the rules, so the interviewer spends the time on how you split the work between classes, how you validate a move, and how you detect a win without scanning the whole board.

The problem is a warm-up, but it separates tidy designs from tangled ones fast. A tidy design has a Board that only knows about cells, a Game that only knows about turns and rules, immutable Players, and a Scoreboard that learns about results without the Game knowing what a scoreboard is. A tangled one puts everything in one class with a nest of if statements.

A strong answer also shows the constant-time win check: keep a signed counter per row, per column and per diagonal, and test only the four counters the last move touched. Then it shows that the same code plays a 4 by 4 board with no change but a parameter.

Scoping the problem

Before writing code, ask these questions; each answer changes the design a little.

  • Board size fixed at 3, or N by N? — A parameter costs nothing and shows generality — N by N, default 3
  • Two players only? — More players need a list and a turn index — Two: X and O
  • What happens on a bad move? — Return false, or raise; the turn must not pass — Raise, and the turn stays
  • Track results across games? — Adds a scoreboard and a way to tell it — Yes
  • Undo, replay, an AI opponent? — Each is a follow-up with its own design — Follow-ups, not the core

Core entities

The Board does not know whose turn it is or what a win is. The Game does not know how a cell is stored. That split is the Single Responsibility Principle in a form an interviewer can see in ten seconds.

Win detection in constant time

Scanning the board after every move is O(n²) for an n by n board. Counters make it O(1). Keep one signed counter per row, per column, and one for each diagonal. X adds 1, O subtracts 1. A line is complete when its counter reaches +n or −n, and only the counters of the last move's cell can have changed.

The trace runs the first game of the demo: X plays (0,0), O (0,1), X (1,1), O (0,2), X (2,2).

  • X (0,0) — +1, 0, 0 — +1, 0, 0 — +1 — 0 — continue
  • O (0,1) — 0, 0, 0 — +1, −1, 0 — +1 — 0 — continue
  • X (1,1) — 0, +1, 0 — +1, 0, 0 — +2 — +1 — continue
  • O (0,2) — −1, +1, 0 — +1, 0, −1 — +2 — 0 — continue
  • X (2,2) — −1, +1, +1 — +1, 0, 0 — +3 — 0 — diag is +3: X wins
  • Scan the whole board — O(n²) — none — Fine for n = 3; the first thing most people write
  • Scan only the row, column and diagonals of the last move — O(n) — none — The natural middle step
  • Signed counters (the code) — O(1) — 2n + 2 integers — Works for any n; undo means subtracting

Telling the scoreboard

The Game keeps a list of listeners and calls on_game_over(game) on each when the status leaves IN_PROGRESS. Scoreboard is one listener. A display, a logger or a matchmaking service would be others, and Game would not change for any of them. That is the Observer pattern, and the point is the direction of the dependency: Scoreboard knows Game; Game does not know Scoreboard.

  • Game holds a Scoreboard and calls its methods by name — Game holds anything with on_game_over
  • A second consumer means editing Game — A second consumer is one more append
  • Game is hard to test without a Scoreboard — Game is tested alone; the demo passes a Scoreboard only where it is checked

What you will be able to do

  • Split the problem into Board, Game, Player and Scoreboard and say what each one does not know.
  • Validate a move at the right layer and keep the turn from passing on a bad move.
  • Explain and trace the O(1) win check with signed counters, and say how it generalizes to n by n.
  • Use a frozen dataclass for Player and say what it buys.
  • Add a scoreboard as an observer and explain why the dependency points the way it does.

Functional requirements

  • Two players, X and O, alternate turns on an n by n board (default 3)
  • play(row, col) places the current player's mark; an off-board cell, a taken cell, or a move after the game ends is refused and the turn does not pass
  • A move that completes a row, column or diagonal wins; a move that fills the last cell with no line is a draw
  • The game reports its status (in progress, won, draw) and the winner
  • Listeners such as a Scoreboard are told when a game ends and count wins and draws across games

Non-functional requirements

  • Win detection is O(1) per move through signed line counters
  • Board size is a parameter; no code names the number 3
  • Player is immutable (a frozen dataclass), so a game cannot swap marks mid-way
  • Game does not depend on Scoreboard; consumers register as listeners
  • Every rule is covered by an assert in the demo, including each kind of invalid move

Interview questions

  • How would you support a bigger board, or a rule like four in a row on a 6 by 6 board?
  • Why not check for a win by scanning the board? It is only nine cells.
  • Where do you validate a move, and what should happen on a bad one?
  • How would you add an unbeatable computer player?
  • How do you add undo?

Sign up free to read the full topic

More Low-Level Design topics

Start free on Cariara · Coding problems · System design studio