Design Tic Tac Toe
OOP design for a tic-tac-toe game covering board management, turn validation, win detection with O(1) checking, and extensible grid sizes.
The Problem
You are building an online board game platform. The MVP is tic-tac-toe: two players take turns placing marks on a 3x3 grid, and the first to get three in a row wins. The current prototype crams all logic into a single function that scans the entire board after every move to check for a winner. With plans to support NxN boards and AI opponents, that approach will not scale.
Tic-tac-toe looks deceptively simple, but it tests several core OOP skills: separating game orchestration from board state, validating turns without duplicating logic, detecting wins efficiently, and designing for extensibility beyond the classic 3x3 grid. The interviewer wants clean entity separation, O(1) win detection, and a design that can absorb follow-up twists without a rewrite.
Design the core classes for a tic-tac-toe game that handles player turns, move validation, O(1) win detection, and draw detection.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "What size is the board? Classic 3x3 only, or should we support arbitrary NxN grids?"
Interviewer: "Start with 3x3, but design it so we could change the size easily."
That tells you to parameterize the board size rather than hardcoding 3. Keep N as a constructor argument.
You: "How do players make moves? Do they specify row and column coordinates?"
Interviewer: "Yes, a player provides a row and column index (zero-based), and we place their mark there."
Simple coordinate-based input. No drag-and-drop, no column-drop gravity like Connect Four.
You: "What should happen if a player tries to place on an already occupied cell, or it is not their turn?"
Interviewer: "Reject the move and let them try again. The game state should not change on an invalid move."
Good. Validation must happen before mutating any state. Reject early, reject clearly.
You: "Is the win condition always N in a row (matching the board size), or could it differ?"
Interviewer: "For now, it matches the board size. 3x3 needs 3 in a row, 5x5 needs 5 in a row."
That simplifies win detection. You can use counter-based tracking where a row/column/diagonal counter reaching N means a win.
You: "Do we need undo or move history support?"
Interviewer: "Not required for the base design, but mention how you would add it."
Perfect candidate for the extensibility section. You will not build it now, but you will show where it plugs in.
You: "Should we support AI players, or is this strictly two human players?"
Interviewer: "Two human players for now. Mention AI as an extension."
That means the Player abstraction should be simple for now but extensible to different player types.
You: "Is any UI rendering in scope?"
Interviewer: "No. Just the core game logic."
Perfect. You have clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- Two players alternate turns placing X and O marks on an NxN grid (default 3x3)
- The system validates each move: correct player's turn, cell is empty, coordinates are in bounds
- The system detects a win (N marks in any row, column, or diagonal) in O(1) time per move
- The system detects a draw when the board is full with no winner
- Invalid moves are rejected without changing game state
Non-Functional Requirements:
- Win detection runs in O(1) per move, not O(NΒ²)
- Board size is parameterized, not hardcoded to 3
- The design supports adding new player types (AI) without modifying existing classes
Out of Scope:
- UI rendering
- Persistence / database
- Undo / move history (covered in extensibility)
- AI opponent logic (covered in extensibility)
- Network multiplayer
- Timer / time limits per move
Example Inputs and Outputs
Scenario 1: X wins with a row
- Move sequence: X(0,0), O(1,0), X(0,1), O(1,1), X(0,2)
- Expected: After X(0,2), game state changes to
X_WINS - Board state:
X | X | X
---------
O | O |
---------
| |
Scenario 2: Draw on a 3x3 board
- Move sequence: X(0,0), O(0,1), X(0,2), O(1,0), X(1,1), O(2,0), X(1,2), O(2,2), X(2,1)
- Expected: After X(2,1), all cells are filled with no three in a row. Game state is
DRAW
Scenario 3: Invalid move rejection
- X plays at (0,0). O tries to play at (0,0) again.
- Expected: Move rejected. O must choose a different cell. Game state remains
IN_PROGRESS.
Try It Yourself
Try it yourself
Before reading the solution, spend 10-15 minutes sketching your class diagram. Focus on two things: which class owns win detection logic, and how you would make that check O(1) per move instead of scanning the entire board. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look at your requirements and pull out the nouns: game, board, player, cell, mark, game state. Each noun is a candidate entity.
A common mistake here is putting everything in one Game class. If placement validation, win detection, and turn management all live in one class, that class becomes impossible to test in isolation and impossible to extend. Good design means each class has a single, clear job.
| Entity | Responsibility | Key attributes |
|---|---|---|
Game | The orchestrator. Manages turn order, delegates moves to Board, updates game state. | board, players, currentPlayerIndex, state |
Board | The grid. Owns cell state, placement, and win detection counters. | grid (2D array), size, moveCount |
Player | Data holder with a name and assigned mark. No game logic. | name, mark |
Mark | Enum representing X, O, or EMPTY. The value stored in each cell. | X, O, EMPTY |
GameState | Enum representing game progress. | IN_PROGRESS, X_WINS, O_WINS, DRAW |
Notice we separated Game from Board. Game manages the "who plays when" rules, while Board manages the "where can you play and did someone win" rules. Merging them violates SRP because turn management and grid logic change for different reasons.
Step 2: Define Relationships and Class Design
Class Diagram
Class Interface Derivation
Game (The Orchestrator)
Game is the entry point. Every player action flows through it. It validates turn order and delegates to the Board for placement and win detection.
Deriving state from requirements:
| Requirement | What Game must track |
|---|---|
| "Two players alternate turns" | The two players, whose turn it is |
| "Game detects a win or draw" | The current game state |
| "Moves happen on a board" | A Board reference |
This gives us:
Game:
board: Board
players: Player[2]
currentPlayerIndex: int // 0 or 1, toggles each turn
state: GameState // starts as IN_PROGRESS
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Players place marks" | makeMove(row, col): boolean |
| "Validate turn order" | getCurrentPlayer(): Player |
| "Check game progress" | getState(): GameState |
The makeMove method is the core of the design. It is the only mutating method on Game. Everything else is read-only.
Board (Grid + Win Detection)
Board owns the grid and the O(1) win-detection counters. This is where the clever part lives.
Deriving state from requirements:
| Requirement | What Board must track |
|---|---|
| "NxN grid of marks" | 2D array of Mark values |
| "O(1) win detection" | Row counts, column counts, diagonal counts |
| "Detect draw (board full)" | Total move count |
This gives us:
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.