Tic-tac-toe
Small enough to finish in the time, rich enough to show judgement: who owns the rules, how to check for a win without scanning the board, and how to make it work for any board size.
1. The prompt
2. Clarify the requirements
Spend the first five minutes here. Ask, then write the answers down where the interviewer can see them.
| Ask | Assume (unless told otherwise) |
|---|---|
| Board size? | n × n, default 3; a win needs a full row, column or diagonal. |
| What happens on an invalid move? | Rejected with a reason; the same player moves again. |
| Human or computer players? | Humans for now; leave room for a strategy-based bot. |
Must do
- Two players alternate; X starts
- Reject moves off the board or on a taken cell
- Detect win or draw; reject moves after the game ends
Must be
- Win check faster than scanning the board
- Any n × n board
- Easy to add a computer player
Out of scope
- Graphics
- Networking
- Undo (a follow-up)
3. The classes
| Class | Its one job |
|---|---|
| Game | The rules: whose turn, when it's over, what to return |
| Board | Cells, and one running sum per row, column and diagonal |
| Player | X or O, with the value it adds to the sums |
4. How it works
Play it. The sums under the board are the whole trick: X adds +1 and O adds −1 to the move's row, column and diagonals. A win is a sum of +n or −n, found in O(1) instead of by scanning.
5. Patterns and why
| Pattern | Where | Why it's worth it here |
|---|---|---|
| Single responsibility | Board knows cells and sums; Game knows turns and results | Rules can change (e.g. a bot, or a timer) without touching the board |
| Encapsulation | Sums are updated only by place() | They can never drift out of step with the cells |
| Strategy (follow-up) | A MoveChooser interface for computer players | Random, minimax or human input, all plugged in the same way |
6. The code
The board raises errors for bad moves; the game turns them into messages and owns the turn order.
from __future__ import annotations
from enum import Enum
from typing import Optional
class Player(Enum):
X = 1
O = -1
class Board:
"""n x n board with an O(1) win check: one running sum per row, column and diagonal."""
def __init__(self, n: int = 3) -> None:
self.n = n
self.cells: list[list[Optional[Player]]] = [[None] * n for _ in range(n)]
self.rows, self.cols = [0] * n, [0] * n
self.diag = self.anti = 0
self.moves = 0
def place(self, r: int, c: int, p: Player) -> bool:
"""Returns True if this move wins."""
if not (0 <= r < self.n and 0 <= c < self.n):
raise ValueError("off the board")
if self.cells[r][c] is not None:
raise ValueError("cell taken")
self.cells[r][c] = p
self.moves += 1
v, n = p.value, self.n
self.rows[r] += v
self.cols[c] += v
if r == c:
self.diag += v
if r + c == n - 1:
self.anti += v
return n in (abs(self.rows[r]), abs(self.cols[c]), abs(self.diag), abs(self.anti))
def full(self) -> bool:
return self.moves == self.n * self.n
class Game:
"""Owns the rules: whose turn it is, and when the game is over."""
def __init__(self, n: int = 3) -> None:
self.board = Board(n)
self.turn = Player.X
self.result: Optional[str] = None
def play(self, r: int, c: int) -> str:
if self.result:
return f"rejected: game over ({self.result})"
try:
won = self.board.place(r, c, self.turn)
except ValueError as e:
return f"rejected: {e}"
mover = self.turn.name
if won:
self.result = f"{mover} wins"
elif self.board.full():
self.result = "draw"
self.turn = Player.O if self.turn is Player.X else Player.X
return f"{mover} at ({r},{c})" + (f": {self.result}" if self.result else "")
if __name__ == "__main__":
g = Game()
for r, c in [(0, 0), (1, 1), (1, 1), (0, 1), (2, 2), (0, 2), (2, 0)]:
print(g.play(r, c))
enum Player { X = 1, O = -1 }
/** n x n board with an O(1) win check: one running sum per row, column and diagonal. */
class Board {
private cells: (Player | null)[][];
private rows: number[];
private cols: number[];
private diag = 0;
private anti = 0;
private moves = 0;
constructor(readonly n = 3) {
this.cells = Array.from({ length: n }, () => Array<Player | null>(n).fill(null));
this.rows = Array(n).fill(0);
this.cols = Array(n).fill(0);
}
/** Returns true if this move wins. */
place(r: number, c: number, p: Player): boolean {
const n = this.n;
if (r < 0 || r >= n || c < 0 || c >= n) throw new Error("off the board");
if (this.cells[r][c] !== null) throw new Error("cell taken");
this.cells[r][c] = p;
this.moves++;
this.rows[r] += p;
this.cols[c] += p;
if (r === c) this.diag += p;
if (r + c === n - 1) this.anti += p;
return [this.rows[r], this.cols[c], this.diag, this.anti].some((s) => Math.abs(s) === n);
}
full(): boolean { return this.moves === this.n * this.n; }
}
/** Owns the rules: whose turn it is, and when the game is over. */
class Game {
private board: Board;
private turn = Player.X;
private result: string | null = null;
constructor(n = 3) { this.board = new Board(n); }
play(r: number, c: number): string {
if (this.result) return `rejected: game over (${this.result})`;
let won: boolean;
try {
won = this.board.place(r, c, this.turn);
} catch (e) {
return `rejected: ${(e as Error).message}`;
}
const mover = Player[this.turn];
if (won) this.result = `${mover} wins`;
else if (this.board.full()) this.result = "draw";
this.turn = this.turn === Player.X ? Player.O : Player.X;
return `${mover} at (${r},${c})` + (this.result ? `: ${this.result}` : "");
}
}
const g = new Game();
for (const [r, c] of [[0, 0], [1, 1], [1, 1], [0, 1], [2, 2], [0, 2], [2, 0]]) console.log(g.play(r, c));enum Player {
X(1), O(-1);
final int value;
Player(int value) {
this.value = value;
}
}
/** n x n board with an O(1) win check: one running sum per row, column and diagonal. */
class Board {
final int n;
private final Player[][] cells;
private final int[] rows, cols;
private int diag, anti, moves;
Board(int n) {
this.n = n;
cells = new Player[n][n];
rows = new int[n];
cols = new int[n];
}
/** Returns true if this move wins. */
boolean place(int r, int c, Player p) {
if (r < 0 || r >= n || c < 0 || c >= n) throw new IllegalArgumentException("off the board");
if (cells[r][c] != null) throw new IllegalArgumentException("cell taken");
cells[r][c] = p;
moves++;
rows[r] += p.value;
cols[c] += p.value;
if (r == c) diag += p.value;
if (r + c == n - 1) anti += p.value;
return Math.abs(rows[r]) == n || Math.abs(cols[c]) == n || Math.abs(diag) == n || Math.abs(anti) == n;
}
boolean full() {
return moves == n * n;
}
}
/** Owns the rules: whose turn it is, and when the game is over. */
class Game {
private final Board board;
private Player turn = Player.X;
private String result;
Game(int n) {
board = new Board(n);
}
String play(int r, int c) {
if (result != null) return "rejected: game over (" + result + ")";
boolean won;
try {
won = board.place(r, c, turn);
} catch (IllegalArgumentException e) {
return "rejected: " + e.getMessage();
}
String mover = turn.name();
if (won) result = mover + " wins";
else if (board.full()) result = "draw";
turn = turn == Player.X ? Player.O : Player.X;
return mover + " at (" + r + "," + c + ")" + (result != null ? ": " + result : "");
}
}
public class Main {
public static void main(String[] args) {
Game g = new Game(3);
int[][] moves = {{0, 0}, {1, 1}, {1, 1}, {0, 1}, {2, 2}, {0, 2}, {2, 0}};
for (int[] m : moves) System.out.println(g.play(m[0], m[1]));
}
}Output (the same in all three languages)
X at (0,0) O at (1,1) rejected: cell taken X at (0,1) O at (2,2) X at (0,2): X wins rejected: game over (X wins)
7. Concurrency and edge cases
- Two players clicking at once in an online version: the server must serialize moves for a game (one lock per game, or a single-threaded actor per game), and reject a move from the player whose turn it isn't.
- Edge cases: a move after the game ends, the same cell twice, coordinates off the board, the last move both filling the board and winning (it's a win, not a draw: check the win first).
8. Follow-up questions
What interviewers ask once the core works, and a short answer for each.
- How would you add a computer player?
- A Player interface with chooseMove(board); a human implementation reads input, a bot picks a move (random, or minimax for perfect play on 3 × 3).
- How would you support undo?
- Keep a stack of moves; undo pops one and subtracts its value from the sums.
- What about k-in-a-row on a big board (gomoku)?
- Sums no longer work; check the four directions from the last move, counting consecutive cells: O(k).
9. Common mistakes
10. Check yourself
How does the code detect a win in O(1)?
Which class should own whose turn it is?
The last empty cell is filled with a winning move. What's the result?
Your turn
- Add undo (10 min). A move stack; undo subtracts from the sums.
- Add a random bot (10 min). A Player interface with two implementations.