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.
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
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
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)"(orO) 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)
821import 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}
1301"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}
1071class 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}
1141#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}
1381class 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
77Run 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
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?
Why is there no separate Player class in this design?
Where does the turn-switching rule live, and why does it belong there rather than in Board?
What makes a move 'invalid', and why does an invalid move not pass the turn?
How does the design extend to support a computer opponent, and which design pattern enables this?