Facebook Pixel

Design Tic-Tac-Toe

Tic-tac-toe is a game problem, and that shape drives the design: the state lives in a board, a referee object enforces the rules, and it extends naturally to a larger board or a computer opponent. The rules are familiar, so the entire session focuses on how to translate a small set of rules into classes with clear responsibilities.

See It in Action

The demo below runs the finished design: it alternates turns, detects wins and draws, and rejects moves onto occupied cells. The rest of the article shows how to build exactly this behavior.

Tic-Tac-Toe

Clarifying Requirements

A focused set of questions turns "design tic-tac-toe" into a spec you can design against. The functional requirements we will commit to: two players alternate placing marks, X first, on a 3x3 grid; a player wins by completing a full row, column, or either diagonal; the game is a draw if all nine cells fill with no winner; a move onto an occupied or out-of-range cell is rejected without ending the game or passing the turn; and the system can report the result after each move.

The decision flow for a single move captures all these rules at a glance:

Core Entities

Scanning the requirements for the nouns that carry state or enforce rules gives a short list. A Mark is the symbol a player owns, X or O — a natural enum. A Board owns the 3x3 grid: it knows which cells are filled, whether a placement is legal, and whether the last move completed a line. A Game is the orchestrator. It tracks whose turn it is, drives each move through the board, decides the outcome, and is the single object the outside world talks to.

The prompt also names "players." Before reading on, decide whether a player here earns its own class — what state or behavior would a Player object hold that Game does not already track?

Decision checkpoint

1)

The prompt mentions two players who alternate turns. Should Player be its own class?

As the checkpoint anticipated, "Player" stays a plain value. A Player here carries no state beyond its mark and whose turn it is — both of which Game already tracks. The turn is simply an index into the two marks, so a separate Player class adds a layer of indirection with no benefit at this scope. Model the state that matters, not every noun in the prompt.

A quick ownership diagram shows how the three entities relate before diving into the class design:

Designing the Classes

Step 3 of the delivery framework asks you to derive each field from a specific requirement and each method signature from an operation the class must support. Nothing goes in because it "seems natural" — every class is there for a reason. The guiding principle throughout is Tell, Don't Ask: the object that owns state should make decisions about that state, so callers never reach past the API to inspect raw fields.

Game

Game is the orchestrator and referee. It is the only object the outside world touches, so it gets the first design pass.

The requirement "two players alternate" demands a list of marks in turn order — players = [Mark.X, Mark.O] — and an index turn to remember whose move it is. The requirement "moves after the game ends return a special message" demands a boolean over so Game can short-circuit any further calls. The requirement "the board enforces cell rules" means Game holds a board reference in composition: Game creates its Board and the Board has no life outside its Game.

Game's behavior is a single public method: move(r, c) -> str. Every caller interaction happens through this one entry point. Inside, Game reads turn to identify the current mark, delegates placement to Board, and reads the outcome back from Board's rule-checking methods. That last point is the Tell-Don't-Ask split made concrete: Game does not read board.grid directly to check for a winner; it calls board.has_won(r, c, mark) and trusts the answer. The transition logic — advancing the turn with turn ^= 1, setting over = True — lives in Game because it depends on game-flow state, not grid state.

No separate Player class is needed. The requirement "two players, X first" is fully satisfied by players = [Mark.X, Mark.O] and turn = 0. A Player object would add indirection with no benefit at this scope.

Before designing Board, settle one ownership question. Checking whether a move completed a row, column, or diagonal needs the grid contents. Game drives each move and reports the outcome — so should the win-detection logic live on Game or on Board?

Decision checkpoint

1)

The win check (did this move complete a row, column, or diagonal?) needs the grid. Should it live on Game or on Board?

Board

Board owns the grid and every rule that depends solely on grid contents. It knows nothing about whose turn it is or whether the game is over — those are Game's concerns.

The requirement "3x3 grid" demands a size field and a 2-D grid initialized to None. The requirement "draw when all cells fill" motivates a separate filled counter rather than scanning the entire grid on every move — is_full() becomes an O(1) comparison. The requirement "mark a cell" becomes place(r, c, mark), which writes to the grid and increments filled. The requirement "reject out-of-range or occupied cells" becomes is_free(r, c), which checks both bounds and occupancy in one predicate.

has_won(r, c, mark) is the most consequential method. The requirement "a player wins by completing a row, column, or diagonal" means four checks — the row r, the column c, the main diagonal, and the anti-diagonal. A key observation: only the row and column that contain (r, c) can be completed by this move, and only the diagonals that pass through (r, c). The diagonal check is guarded by r == c; the anti-diagonal by r + c == size - 1. This focused scan runs in O(n) regardless of board size instead of scanning all lines.

Mark

The requirement "each player owns a symbol, X or O" is the whole spec for Mark. An enum gives type-safe identity and a human-readable string value in one line. There is no behavior, no state beyond the value, and no constructor logic. Mark is listed last in the design pass because it supports Board and Game but has no dependencies of its own.


Reading the diagram: Game holds a Board in composition and references the Mark enum for its player list; Board stores Marks in its grid cells. The public surface is deliberately tiny — one method on Game, four on Board. A small public surface is what makes the design safe to extend and easy to reason about.

Try It Yourself

Before reading the implementation, build it. Implement a function tic_tac_toe(instructions) that replays a sequence of commands and returns one result line per move.

Each instruction is a list of strings. The only command is ["move", r, c]: the current player marks cell (r, c). Players alternate starting with X. For each move, append one line to the output:

  • "X placed at (r, c)" (or O) for a normal valid move,
  • "X wins" when the move completes a line,
  • "Draw" when the move fills the final empty cell with no winner,
  • "Invalid move" when the target cell is occupied or off the board (the turn does not pass),
  • "Game already over" for any move attempted after the game has ended.

Run your solution against the test cases. The reference implementation follows below.

Code Walkthrough

The design is three small classes plus a thin entry function. The walkthrough goes in dependency order: Mark first because everything else references it, then Board because it is complete in itself, then Game which orchestrates the two, then the entry function that replays the command stream.

Mark is a two-value enum. Its only job is to give X and O a type-safe identity with a human-readable string representation.

The string values "X" and "O" matter because the output format requires them directly — f"{mark.value} wins" in Game relies on this. If the values were auto-integers, the output would read "1 wins".

1class Mark(Enum):
2    X = "X"
3    O = "O"

Full Solution

1from enum import Enum
2
3class Mark(Enum):
4    X = "X"
5    O = "O"
6
7
8class Board:
9    def __init__(self, size: int = 3) -> None:
10        self.size = size
11        self.grid: list[list[Mark | None]] = [
12            [None] * size for _ in range(size)
13        ]
14        self.filled = 0
15
16    def is_free(self, r: int, c: int) -> bool:
17        in_range = 0 <= r < self.size and 0 <= c < self.size
18        return in_range and self.grid[r][c] is None
19
20    def place(self, r: int, c: int, mark: Mark) -> None:
21        self.grid[r][c] = mark
22        self.filled += 1
23
24    def is_full(self) -> bool:
25        return self.filled == self.size * self.size
26
27    def has_won(self, r: int, c: int, mark: Mark) -> bool:
28        n = self.grid
29        size = self.size
30        if all(n[r][j] == mark for j in range(size)):
31            return True
32        if all(n[i][c] == mark for i in range(size)):
33            return True
34        if r == c and all(n[i][i] == mark for i in range(size)):
35            return True
36        if r + c == size - 1 and all(
37            n[i][size - 1 - i] == mark for i in range(size)
38        ):
39            return True
40        return False
41
42
43class Game:
44    def __init__(self, size: int = 3) -> None:
45        self.board = Board(size)
46        self.players = [Mark.X, Mark.O]
47        self.turn = 0
48        self.over = False
49
50    def move(self, r: int, c: int) -> str:
51        if self.over:
52            return "Game already over"
53        mark = self.players[self.turn]
54        if not self.board.is_free(r, c):
55            return "Invalid move"
56        self.board.place(r, c, mark)
57        if self.board.has_won(r, c, mark):
58            self.over = True
59            return f"{mark.value} wins"
60        if self.board.is_full():
61            self.over = True
62            return "Draw"
63        self.turn ^= 1
64        return f"{mark.value} placed at ({r}, {c})"
65
66
67def tic_tac_toe(instructions: list[list[str]]) -> list[str]:
68    game = Game()
69    output: list[str] = []
70    for instruction in instructions:
71        command, *args = instruction
72        if command == "move":
73            r, c = int(args[0]), int(args[1])
74            output.append(game.move(r, c))
75    return output
76
77if __name__ == "__main__":
78    instructions = [input().split() for _ in range(int(input()))]
79    res = tic_tac_toe(instructions)
80    for line in res:
81        print(line)
82
1import java.util.ArrayList;
2import java.util.Arrays;
3import java.util.List;
4import java.util.Scanner;
5
6class Solution {
7    static final String MARK_X = "X";
8    static final String MARK_O = "O";
9
10    static class Board {
11        int size;
12        String[][] grid;
13        int filled;
14
15        Board(int size) {
16            this.size = size;
17            this.grid = new String[size][size];
18            this.filled = 0;
19        }
20
21        boolean isFree(int r, int c) {
22            return r >= 0 && r < size && c >= 0 && c < size && grid[r][c] == null;
23        }
24
25        void place(int r, int c, String mark) {
26            grid[r][c] = mark;
27            filled++;
28        }
29
30        boolean isFull() {
31            return filled == size * size;
32        }
33
34        boolean hasWon(int r, int c, String mark) {
35            boolean rowWin = true;
36            for (int j = 0; j < size; j++) {
37                if (!mark.equals(grid[r][j])) { rowWin = false; break; }
38            }
39            if (rowWin) return true;
40
41            boolean colWin = true;
42            for (int i = 0; i < size; i++) {
43                if (!mark.equals(grid[i][c])) { colWin = false; break; }
44            }
45            if (colWin) return true;
46
47            if (r == c) {
48                boolean diagWin = true;
49                for (int i = 0; i < size; i++) {
50                    if (!mark.equals(grid[i][i])) { diagWin = false; break; }
51                }
52                if (diagWin) return true;
53            }
54
55            if (r + c == size - 1) {
56                boolean antiDiagWin = true;
57                for (int i = 0; i < size; i++) {
58                    if (!mark.equals(grid[i][size - 1 - i])) { antiDiagWin = false; break; }
59                }
60                if (antiDiagWin) return true;
61            }
62
63            return false;
64        }
65    }
66
67    static class Game {
68        Board board;
69        String[] players;
70        int turn;
71        boolean over;
72
73        Game() {
74            this.board = new Board(3);
75            this.players = new String[]{MARK_X, MARK_O};
76            this.turn = 0;
77            this.over = false;
78        }
79
80        String move(int r, int c) {
81            if (over) return "Game already over";
82            String mark = players[turn];
83            if (!board.isFree(r, c)) return "Invalid move";
84            board.place(r, c, mark);
85            if (board.hasWon(r, c, mark)) {
86                over = true;
87                return mark + " wins";
88            }
89            if (board.isFull()) {
90                over = true;
91                return "Draw";
92            }
93            turn ^= 1;
94            return mark + " placed at (" + r + ", " + c + ")";
95        }
96    }
97
98    public static List<String> ticTacToe(List<List<String>> instructions) {
99        Game game = new Game();
100        List<String> output = new ArrayList<>();
101        for (List<String> instruction : instructions) {
102            String command = instruction.get(0);
103            if (command.equals("move")) {
104                int r = Integer.parseInt(instruction.get(1));
105                int c = Integer.parseInt(instruction.get(2));
106                output.add(game.move(r, c));
107            }
108        }
109        return output;
110    }
111
112    public static List<String> splitWords(String s) {
113        return s.isEmpty() ? List.of() : Arrays.asList(s.split(" "));
114    }
115
116    public static void main(String[] args) {
117        java.util.Scanner scanner = new java.util.Scanner(System.in);
118        int instructionsLength = Integer.parseInt(scanner.nextLine());
119        List<List<String>> instructions = new ArrayList<>();
120        for (int i = 0; i < instructionsLength; i++) {
121            instructions.add(splitWords(scanner.nextLine()));
122        }
123        scanner.close();
124        List<String> res = ticTacToe(instructions);
125        for (String line : res) {
126            System.out.println(line);
127        }
128    }
129}
130
1"use strict";
2
3class Board {
4    constructor(size = 3) {
5        this.size = size;
6        this.grid = Array.from({length: size}, () => Array(size).fill(null));
7        this.filled = 0;
8    }
9
10    isFree(r, c) {
11        return r >= 0 && r < this.size && c >= 0 && c < this.size && this.grid[r][c] === null;
12    }
13
14    place(r, c, mark) {
15        this.grid[r][c] = mark;
16        this.filled++;
17    }
18
19    isFull() {
20        return this.filled === this.size * this.size;
21    }
22
23    hasWon(r, c, mark) {
24        const n = this.grid;
25        const size = this.size;
26        if (n[r].every(cell => cell === mark)) return true;
27        if (n.every(row => row[c] === mark)) return true;
28        if (r === c && n.every((row, i) => row[i] === mark)) return true;
29        if (r + c === size - 1 && n.every((row, i) => row[size - 1 - i] === mark)) return true;
30        return false;
31    }
32}
33
34class Game {
35    constructor() {
36        this.board = new Board(3);
37        this.players = ['X', 'O'];
38        this.turn = 0;
39        this.over = false;
40    }
41
42    move(r, c) {
43        if (this.over) return 'Game already over';
44        const mark = this.players[this.turn];
45        if (!this.board.isFree(r, c)) return 'Invalid move';
46        this.board.place(r, c, mark);
47        if (this.board.hasWon(r, c, mark)) {
48            this.over = true;
49            return `${mark} wins`;
50        }
51        if (this.board.isFull()) {
52            this.over = true;
53            return 'Draw';
54        }
55        this.turn ^= 1;
56        return `${mark} placed at (${r}, ${c})`;
57    }
58}
59
60function ticTacToe(instructions) {
61    const game = new Game();
62    const output = [];
63    for (const instruction of instructions) {
64        const [command, ...args] = instruction;
65        if (command === 'move') {
66            const r = parseInt(args[0]);
67            const c = parseInt(args[1]);
68            output.push(game.move(r, c));
69        }
70    }
71    return output;
72}
73
74function splitWords(s) {
75    return s === "" ? [] : s.split(" ");
76}
77
78function* main() {
79    const instructionsLength = parseInt(yield);
80    const instructions = [];
81    for (let i = 0; i < instructionsLength; i++) {
82        instructions.push(splitWords(yield));
83    }
84    const res = ticTacToe(instructions);
85    for (const line of res) {
86        console.log(line);
87    }
88}
89
90class EOFError extends Error {}
91{
92    const gen = main();
93    const next = (line) => gen.next(line).done && process.exit();
94    let buf = "";
95    next();
96    process.stdin.setEncoding("utf8");
97    process.stdin.on("data", (data) => {
98        const lines = (buf + data).split("\n");
99        buf = lines.pop();
100        lines.forEach(next);
101    });
102    process.stdin.on("end", () => {
103        buf && next(buf);
104        gen.throw(new EOFError());
105    });
106}
107
1class Board {
2    size: number;
3    grid: (string | null)[][];
4    filled: number;
5
6    constructor(size: number = 3) {
7        this.size = size;
8        this.grid = Array.from({length: size}, () => Array(size).fill(null));
9        this.filled = 0;
10    }
11
12    isFree(r: number, c: number): boolean {
13        return r >= 0 && r < this.size && c >= 0 && c < this.size && this.grid[r][c] === null;
14    }
15
16    place(r: number, c: number, mark: string): void {
17        this.grid[r][c] = mark;
18        this.filled++;
19    }
20
21    isFull(): boolean {
22        return this.filled === this.size * this.size;
23    }
24
25    hasWon(r: number, c: number, mark: string): boolean {
26        const n = this.grid;
27        const size = this.size;
28        if (n[r].every(cell => cell === mark)) return true;
29        if (n.every(row => row[c] === mark)) return true;
30        if (r === c && n.every((row, i) => row[i] === mark)) return true;
31        if (r + c === size - 1 && n.every((row, i) => row[size - 1 - i] === mark)) return true;
32        return false;
33    }
34}
35
36class Game {
37    board: Board;
38    players: string[];
39    turn: number;
40    over: boolean;
41
42    constructor() {
43        this.board = new Board(3);
44        this.players = ['X', 'O'];
45        this.turn = 0;
46        this.over = false;
47    }
48
49    move(r: number, c: number): string {
50        if (this.over) return 'Game already over';
51        const mark = this.players[this.turn];
52        if (!this.board.isFree(r, c)) return 'Invalid move';
53        this.board.place(r, c, mark);
54        if (this.board.hasWon(r, c, mark)) {
55            this.over = true;
56            return `${mark} wins`;
57        }
58        if (this.board.isFull()) {
59            this.over = true;
60            return 'Draw';
61        }
62        this.turn ^= 1;
63        return `${mark} placed at (${r}, ${c})`;
64    }
65}
66
67function ticTacToe(instructions: string[][]): string[] {
68    const game = new Game();
69    const output: string[] = [];
70    for (const instruction of instructions) {
71        const [command, ...args] = instruction;
72        if (command === 'move') {
73            const r = parseInt(args[0]);
74            const c = parseInt(args[1]);
75            output.push(game.move(r, c));
76        }
77    }
78    return output;
79}
80
81function splitWords(s: string): string[] {
82    return s === "" ? [] : s.split(" ");
83}
84
85function* main() {
86    const instructionsLength = parseInt(yield);
87    const instructions = [];
88    for (let i = 0; i < instructionsLength; i++) {
89        instructions.push(splitWords(yield));
90    }
91    const res = ticTacToe(instructions);
92    for (const line of res) {
93        console.log(line);
94    }
95}
96
97class EOFError extends Error {}
98{
99    const gen = main();
100    const next = (line?: string) => gen.next(line ?? "").done && process.exit();
101    let buf = "";
102    next();
103    process.stdin.setEncoding("utf8");
104    process.stdin.on("data", (data: string) => {
105        const lines = (buf + data).split("\n");
106        buf = lines.pop() ?? "";
107        lines.forEach(next);
108    });
109    process.stdin.on("end", () => {
110        buf && next(buf);
111        gen.throw(new EOFError());
112    });
113}
114
1#include <algorithm>
2#include <iostream>
3#include <iterator>
4#include <limits>
5#include <sstream>
6#include <string>
7#include <vector>
8
9#include <string>
10#include <vector>
11
12class Board {
13public:
14    int size;
15    std::vector<std::vector<std::string>> grid;
16    int filled;
17
18    Board(int size = 3) : size(size), grid(size, std::vector<std::string>(size, "")), filled(0) {}
19
20    bool isFree(int r, int c) {
21        return r >= 0 && r < size && c >= 0 && c < size && grid[r][c].empty();
22    }
23
24    void place(int r, int c, const std::string& mark) {
25        grid[r][c] = mark;
26        filled++;
27    }
28
29    bool isFull() {
30        return filled == size * size;
31    }
32
33    bool hasWon(int r, int c, const std::string& mark) {
34        bool rowWin = true;
35        for (int j = 0; j < size; j++) {
36            if (grid[r][j] != mark) { rowWin = false; break; }
37        }
38        if (rowWin) return true;
39
40        bool colWin = true;
41        for (int i = 0; i < size; i++) {
42            if (grid[i][c] != mark) { colWin = false; break; }
43        }
44        if (colWin) return true;
45
46        if (r == c) {
47            bool diagWin = true;
48            for (int i = 0; i < size; i++) {
49                if (grid[i][i] != mark) { diagWin = false; break; }
50            }
51            if (diagWin) return true;
52        }
53
54        if (r + c == size - 1) {
55            bool antiDiagWin = true;
56            for (int i = 0; i < size; i++) {
57                if (grid[i][size - 1 - i] != mark) { antiDiagWin = false; break; }
58            }
59            if (antiDiagWin) return true;
60        }
61
62        return false;
63    }
64};
65
66class Game {
67public:
68    Board board;
69    std::string players[2];
70    int turn;
71    bool over;
72
73    Game() : board(3), turn(0), over(false) {
74        players[0] = "X";
75        players[1] = "O";
76    }
77
78    std::string move(int r, int c) {
79        if (over) return "Game already over";
80        std::string mark = players[turn];
81        if (!board.isFree(r, c)) return "Invalid move";
82        board.place(r, c, mark);
83        if (board.hasWon(r, c, mark)) {
84            over = true;
85            return mark + " wins";
86        }
87        if (board.isFull()) {
88            over = true;
89            return "Draw";
90        }
91        turn ^= 1;
92        return mark + " placed at (" + std::to_string(r) + ", " + std::to_string(c) + ")";
93    }
94};
95
96std::vector<std::string> tic_tac_toe(std::vector<std::vector<std::string>> instructions) {
97    Game game;
98    std::vector<std::string> output;
99    for (const auto& instruction : instructions) {
100        const std::string& command = instruction[0];
101        if (command == "move") {
102            int r = std::stoi(instruction[1]);
103            int c = std::stoi(instruction[2]);
104            output.push_back(game.move(r, c));
105        }
106    }
107    return output;
108}
109
110template<typename T>
111std::vector<T> get_words() {
112    std::string line;
113    std::getline(std::cin, line);
114    std::istringstream ss{line};
115    ss >> std::boolalpha;
116    std::vector<T> v;
117    std::copy(std::istream_iterator<T>{ss}, std::istream_iterator<T>{}, std::back_inserter(v));
118    return v;
119}
120
121void ignore_line() {
122    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
123}
124
125int main() {
126    int instructions_length;
127    std::cin >> instructions_length;
128    ignore_line();
129    std::vector<std::vector<std::string>> instructions;
130    for (int i = 0; i < instructions_length; i++) {
131        instructions.emplace_back(get_words<std::string>());
132    }
133    std::vector<std::string> res = tic_tac_toe(instructions);
134    for (const std::string& line : res) {
135        std::cout << line << '\n';
136    }
137}
138
1class Board
2  def initialize(size = 3)
3    @size = size
4    @grid = Array.new(size) { Array.new(size, nil) }
5    @filled = 0
6  end
7
8  def free?(r, c)
9    r >= 0 && r < @size && c >= 0 && c < @size && @grid[r][c].nil?
10  end
11
12  def place(r, c, mark)
13    @grid[r][c] = mark
14    @filled += 1
15  end
16
17  def full?
18    @filled == @size * @size
19  end
20
21  def won?(r, c, mark)
22    n = @grid
23    size = @size
24    return true if n[r].all? { |cell| cell == mark }
25    return true if n.all? { |row| row[c] == mark }
26    return true if r == c && n.each_with_index.all? { |row, i| row[i] == mark }
27    return true if r + c == size - 1 && n.each_with_index.all? { |row, i| row[size - 1 - i] == mark }
28    false
29  end
30end
31
32class Game
33  def initialize
34    @board = Board.new(3)
35    @players = ['X', 'O']
36    @turn = 0
37    @over = false
38  end
39
40  def move(r, c)
41    return 'Game already over' if @over
42    mark = @players[@turn]
43    return 'Invalid move' unless @board.free?(r, c)
44    @board.place(r, c, mark)
45    if @board.won?(r, c, mark)
46      @over = true
47      return "#{mark} wins"
48    end
49    if @board.full?
50      @over = true
51      return 'Draw'
52    end
53    @turn ^= 1
54    "#{mark} placed at (#{r}, #{c})"
55  end
56end
57
58def tic_tac_toe(instructions)
59  game = Game.new
60  output = []
61  instructions.each do |instruction|
62    command = instruction[0]
63    if command == 'move'
64      r = instruction[1].to_i
65      c = instruction[2].to_i
66      output << game.move(r, c)
67    end
68  end
69  output
70end
71
72if __FILE__ == $0
73  instructions = Array.new(Integer(gets, 10)) { gets.split }
74  res = tic_tac_toe(instructions)
75  res.each { |line| puts(line) }
76end
77

Run and Test

The exercise above drives the design through a command stream so it can be checked deterministically. The input is a list of instructions; the output is one line per move. A win short-circuits the rest of the game — every later move returns "Game already over".

Trace the first test to see the contract. The moves X(0,0), O(1,0), X(0,1), O(1,1), X(0,2) produce:

X placed at (0, 0)
O placed at (1, 0)
X placed at (0, 1)
O placed at (1, 1)
X wins

X completes the top row on the fifth move, so the board reports the win immediately rather than waiting for the grid to fill. The other tests cover a full-board draw, an invalid move onto an occupied cell (which neither passes the turn nor ends the game), and a diagonal win.

The object interactions for the first two moves and the winning fifth move look like this:

Concurrency and Thread Safety

A single game played move by move is inherently single-threaded — there is one referee and one turn at a time, so no two move calls overlap. The question only becomes interesting when one process hosts many games, or when two clients share a game over a network.

If a Game object is shared across threads, two concurrent move calls could both read over == false and both place a mark before either updates the turn, corrupting the board. The fix is the same as for any shared mutable object: make move the critical section, guarding it with a lock so each move reads and updates the game state atomically. For a server hosting thousands of independent games, lock per game rather than globally, so unrelated games never wait on each other — the lock-striping idea from the concurrency module.

Extensions

The design extends to larger boards, configurable win lengths, a computer opponent, and a scoreboard. An N×N board with a configurable win length is nearly free: the Board already takes a size, so only has_won changes to scan a window of k cells around the last move instead of the whole line. A computer opponent slots in behind a Player strategy interface — a human player reads a move from input, an AI player runs minimax over the board — and the Game alternates between strategies without caring which is which, exactly the Strategy pattern. A scoreboard across rounds is a new object that subscribes to game-over events rather than anything the Game needs to know about, keeping the core untouched.

The Strategy pattern for the computer-opponent extension illustrates the open/closed principle — Game depends only on the Player interface and never changes when a new strategy is added:


Each extension lands by adding a class or swapping an implementation, never by rewriting Game. This follows from splitting responsibilities correctly at the outset: Game depends only on the Player interface and never changes when a new strategy is added.

Quiz

Test your understanding of the design decisions made in this case study.

Tic-Tac-Toe Design Quiz

1)

Why does has_won check only the row, column, and at most two diagonals that pass through the last move, rather than scanning all rows, columns, and diagonals?

2)

Why is there no separate Player class in this design?

3)

Where does the turn-switching rule live, and why does it belong there rather than in Board?

4)

What makes a move 'invalid', and why does an invalid move not pass the turn?

5)

How does the design extend to support a computer opponent, and which design pattern enables this?

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro