Requirements and Scope
Functional Requirements
1. Board:
- 3x3 grid (generalizable to NxN)
- Each cell is empty, or holds X, or holds O
2. Players:
- Two players: X and O
- X always goes first
- Players alternate turns
3. Moves:
- Player selects a cell (row, col)
- Cell must be empty and game must be in progress
- After placement, check for win or draw
4. Win Condition:
- Three of same mark in a row (horizontal, vertical, or diagonal)
5. Draw Condition:
- Board is full and no player has won
6. Game End:
- Game stops immediately on win or draw
- No further moves accepted
Non-Functional Requirements
- Modularity: Win detection, board, and game logic are separate concerns
- Testability: Each component independently testable
- Extensibility: Easy to change board size, add AI, or support online play
- Performance: Move validation and win check must be O(N) for NxN board
Core Entities
| Entity | Responsibility |
|---|---|
Cell |
Holds a player mark (X, O, or empty) |
Board |
NxN grid, place pieces, query cell state |
Player |
Symbol (X or O), name |
Move |
Row + column of a placement |
MoveValidator |
Checks if a move is legal |
WinChecker |
Checks rows, cols, diagonals for a winner |
Game |
Orchestrates turns, delegates validation and win checks |
Game State Diagram
┌──────────────────────────────────────────────────────┐
│ Game States │
├──────────────────────────────────────────────────────┤
│ │
│ NOT_STARTED ──→ PLAYING ──→ WON ──→ GAME_OVER │
│ │ │
│ └──→ DRAW ──→ GAME_OVER │
│ │
└──────────────────────────────────────────────────────┘
Transitions:
start() : NOT_STARTED → PLAYING
makeMove() : PLAYING → PLAYING (valid move, no winner yet)
makeMove() : PLAYING → WON (move creates a winning line)
makeMove() : PLAYING → DRAW (move fills last cell, no winner)
any after END : throw IllegalStateException
Game Flow
Player makes move
│
▼
┌─────────────┐ invalid ┌──────────┐
│ Validate │───────────────→ │ Reject │
│ Move │ │ Move │
└──────┬──────┘ └──────────┘
│ valid
▼
┌─────────────┐
│ Place piece │
│ on board │
└──────┬──────┘
│
▼
┌─────────────┐ yes ┌──────────┐
│ Check Win? │──────────────→ │ Set WON │
└──────┬──────┘ └──────────┘
│ no
▼
┌─────────────┐ yes ┌──────────┐
│ Check Draw? │──────────────→ │ Set DRAW │
└──────┬──────┘ └──────────┘
│ no
▼
┌─────────────┐
│ Switch │
│ Player │
└─────────────┘
Board and Cell Design
Cell
A cell holds one of three states: empty, X, or O. An enum is the cleanest representation.
public enum CellState {
EMPTY, X, O;
}
Board Class
The board owns the 2D grid and provides methods to place pieces, query cells, and check fullness.
public class Board {
private final int size;
private final CellState[][] grid;
private int emptyCells;
public Board(int size) {
this.size = size;
this.grid = new CellState[size][size];
this.emptyCells = size * size;
for (int r = 0; r < size; r++) {
for (int c = 0; c < size; c++) {
grid[r][c] = CellState.EMPTY;
}
}
}
public int getSize() { return size; }
public CellState getCell(int row, int col) {
checkBounds(row, col);
return grid[row][col];
}
public void placePiece(int row, int col, CellState state) {
checkBounds(row, col);
if (grid[row][col] != CellState.EMPTY) {
throw new IllegalStateException("Cell already occupied");
}
grid[row][col] = state;
emptyCells--;
}
public boolean isFull() {
return emptyCells == 0;
}
public boolean isEmpty(int row, int col) {
checkBounds(row, col);
return grid[row][col] == CellState.EMPTY;
}
private void checkBounds(int row, int col) {
if (row < 0 || row >= size || col < 0 || col >= size) {
throw new IndexOutOfBoundsException(
"Position (" + row + "," + col + ") out of bounds"
);
}
}
}
Coordinate Record
public record Position(int row, int col) {
public Position {
if (row < 0 || col < 0) {
throw new IllegalArgumentException("Negative coordinates");
}
}
}
Board Visual (3x3)
Col 0 Col 1 Col 2
┌───────┬───────┬───────┐
Row │ │ │ │
0 │(0,0) │(0,1) │(0,2) │
├───────┼───────┼───────┤
Row │ │ │ │
1 │(1,0) │(1,1) │(1,2) │
├───────┼───────┼───────┤
Row │ │ │ │
2 │(2,0) │(2,1) │(2,2) │
└───────┴───────┴───────┘
Example mid-game:
┌───────┬───────┬───────┐
│ X │ O │ X │
├───────┼───────┼───────┤
│ │ O │ │
├───────┼───────┼───────┤
│ O │ │ │
└───────┴───────┴───────┘
Time and Space Complexity
| Operation | Time | Space |
|---|---|---|
getCell(row, col) |
O(1) | O(1) |
placePiece(row, col) |
O(1) | O(1) |
isFull() |
O(1) | O(1) |
isEmpty(row, col) |
O(1) | O(1) |
| Board construction | O(N²) | O(N²) |
Win and Draw Detection
Strategy Pattern for Win Checking
Using Strategy pattern allows swapping win-checking algorithms (e.g., 3x3 vs NxN, or custom variants).
public interface WinChecker {
boolean checkWinner(Board board, CellState lastMoveCell, int row, int col);
}
Standard 3x3 Win Checker
Only the row, column, and two diagonals passing through the last move need checking.
public class StandardWinChecker implements WinChecker {
@Override
public boolean checkWinner(Board board, CellState piece, int row, int col) {
int size = board.getSize();
// Check row
boolean winRow = true;
for (int c = 0; c < size; c++) {
if (board.getCell(row, c) != piece) {
winRow = false;
break;
}
}
if (winRow) return true;
// Check column
boolean winCol = true;
for (int r = 0; r < size; r++) {
if (board.getCell(r, col) != piece) {
winCol = false;
break;
}
}
if (winCol) return true;
// Check main diagonal (top-left to bottom-right)
if (row == col) {
boolean winDiag = true;
for (int i = 0; i < size; i++) {
if (board.getCell(i, i) != piece) {
winDiag = false;
break;
}
}
if (winDiag) return true;
}
// Check anti-diagonal (top-right to bottom-left)
if (row + col == size - 1) {
boolean winAnti = true;
for (int i = 0; i < size; i++) {
if (board.getCell(i, size - 1 - i) != piece) {
winAnti = false;
break;
}
}
if (winAnti) return true;
}
return false;
}
}
NxN Generalized Win Checker (Connect K)
For an NxN board with a configurable "connect K" requirement:
public class GeneralizedWinChecker implements WinChecker {
private final int connectK;
public GeneralizedWinChecker(int connectK) {
this.connectK = connectK;
}
@Override
public boolean checkWinner(Board board, CellState piece, int row, int col) {
int[][] directions = {{0,1}, {1,0}, {1,1}, {1,-1}};
for (int[] dir : directions) {
int count = 1;
count += countInDirection(board, piece, row, col, dir[0], dir[1]);
count += countInDirection(board, piece, row, col, -dir[0], -dir[1]);
if (count >= connectK) return true;
}
return false;
}
private int countInDirection(Board board, CellState piece,
int row, int col, int dr, int dc) {
int count = 0;
int r = row + dr;
int c = col + dc;
while (r >= 0 && r < board.getSize() &&
c >= 0 && c < board.getSize() &&
board.getCell(r, c) == piece) {
count++;
r += dr;
c += dc;
}
return count;
}
}
Draw Detection
public class DrawChecker {
public static boolean isDraw(Board board) {
return board.isFull();
}
}
Win Detection Walkthrough
After X plays (0,0):
┌───────┬───────┬───────┐
│ X │ │ │
├───────┼───────┼───────┤
│ │ │ │ Check: row 0, col 0,
├───────┼───────┼───────┤ diag (0,0), anti-diag N/A
│ │ │ │ Result: NO WIN
└───────┴───────┴───────┘
After X plays (0,1):
┌───────┬───────┬───────┐
│ X │ X │ │
├───────┼───────┼───────┤
│ │ │ │ Check: row 0 has 2 X's
├───────┼───────┼───────┤ Result: NO WIN
│ │ │ │
└───────┴───────┴───────┘
After X plays (0,2) — WIN:
┌───────┬───────┬───────┐
│ X │ X │ X │ ← Row 0 all X
├───────┼───────┼───────┤
│ │ │ │ Check row 0: X X X ✓
├───────┼───────┼───────┤ Result: X WINS!
│ │ │ │
└───────┴───────┴───────┘
Complexity
| Checker | Time | Notes |
|---|---|---|
| Standard 3x3 | O(N) | N = board size, check 4 lines max |
| Generalized (connect K) | O(K) | Walk at most K cells in each direction |
| Draw | O(1) | Single counter check |
Game Class and State Management
Game Status Enum
public enum GameStatus {
NOT_STARTED, PLAYING, WON, DRAW, GAME_OVER
}
Player Class
public class Player {
private final String name;
private final CellState symbol;
public Player(String name, CellState symbol) {
this.name = name;
this.symbol = symbol;
}
public String getName() { return name; }
public CellState getSymbol() { return symbol; }
}
Move Class
public record Move(Player player, int row, int col) {
public Move {
if (player == null) throw new NullPointerException("Player required");
}
}
MoveValidator
public class MoveValidator {
public void validate(Board board, GameStatus status, int row, int col) {
if (status != GameStatus.PLAYING) {
throw new IllegalStateException(
"Game is not in progress. Status: " + status
);
}
if (row < 0 || row >= board.getSize() ||
col < 0 || col >= board.getSize()) {
throw new IllegalArgumentException(
"Position out of bounds: (" + row + "," + col + ")"
);
}
if (!board.isEmpty(row, col)) {
throw new IllegalStateException(
"Cell (" + row + "," + col + ") is already occupied"
);
}
}
}
Game Class — The Orchestrator
public class Game {
private final Board board;
private final Player playerX;
private final Player playerO;
private final MoveValidator validator;
private final WinChecker winChecker;
private GameStatus status;
private Player currentPlayer;
private final List<Move> moveHistory;
private Player winner;
public Game(Player playerX, Player playerO, int boardSize) {
this(playerX, playerO, boardSize, new StandardWinChecker());
}
public Game(Player playerX, Player playerO, int boardSize,
WinChecker winChecker) {
this.playerX = playerX;
this.playerO = playerO;
this.board = new Board(boardSize);
this.validator = new MoveValidator();
this.winChecker = winChecker;
this.status = GameStatus.NOT_STARTED;
this.currentPlayer = playerX; // X always goes first
this.moveHistory = new ArrayList<>();
this.winner = null;
}
public void start() {
if (status != GameStatus.NOT_STARTED) {
throw new IllegalStateException("Game already started");
}
status = GameStatus.PLAYING;
}
public void makeMove(int row, int col) {
validator.validate(board, status, row, col);
CellState piece = currentPlayer.getSymbol();
board.placePiece(row, col, piece);
Move move = new Move(currentPlayer, row, col);
moveHistory.add(move);
if (winChecker.checkWinner(board, piece, row, col)) {
status = GameStatus.WON;
winner = currentPlayer;
} else if (board.isFull()) {
status = GameStatus.DRAW;
} else {
switchPlayer();
}
}
private void switchPlayer() {
currentPlayer = (currentPlayer == playerX) ? playerO : playerX;
}
public void reset() {
for (int r = 0; r < board.getSize(); r++) {
for (int c = 0; c < board.getSize(); c++) {
// Recreate board to clear
}
}
status = GameStatus.NOT_STARTED;
currentPlayer = playerX;
moveHistory.clear();
winner = null;
}
// Getters
public Board getBoard() { return board; }
public GameStatus getStatus() { return status; }
public Player getCurrentPlayer() { return currentPlayer; }
public Player getWinner() { return winner; }
public List<Move> getMoveHistory() {
return Collections.unmodifiableList(moveHistory);
}
public int getMoveCount() { return moveHistory.size(); }
}
State Transition Table
| Current State | Action | Condition | New State |
|---|---|---|---|
| NOT_STARTED | start() | — | PLAYING |
| PLAYING | makeMove() | Valid + Win | WON |
| PLAYING | makeMove() | Valid + Full | DRAW |
| PLAYING | makeMove() | Valid + No win | PLAYING |
| PLAYING | makeMove() | Invalid move | throw |
| WON | makeMove() | — | throw |
| DRAW | makeMove() | — | throw |
Observer Pattern for Game Events
public interface GameListener {
void onMoveMade(Move move, Board board);
void onWin(Player winner, List<Move> moveHistory);
void onDraw(List<Move> moveHistory);
}
// Usage in Game class:
private final List<GameListener> listeners = new ArrayList<>();
public void addListener(GameListener listener) {
listeners.add(listener);
}
private void notifyMove(Move move) {
listeners.forEach(l -> l.onMoveMade(move, board));
}
private void notifyWin(Player winner) {
listeners.forEach(l -> l.onWin(winner, moveHistory));
}
private void notifyDraw() {
listeners.forEach(l -> l.onDraw(moveHistory));
}
Follow-ups and Extensions
Follow-up 1: NxN Board with Connect-K
Swap the WinChecker strategy:
Game game = new Game(playerX, playerO, 5, new GeneralizedWinChecker(4));
// 5x5 board, connect 4 to win
Follow-up 2: Undo Move
Maintain a move stack and revert board state:
public class UndoableGame extends Game {
private final Deque<Move> undoStack = new ArrayDeque<>();
@Override
public void makeMove(int row, int col) {
super.makeMove(row, col);
undoStack.push(new Move(getCurrentPlayer(), row, col));
}
public Move undo() {
if (undoStack.isEmpty()) return null;
Move last = undoStack.pop();
// Revert board cell to EMPTY
getBoard().getCell(last.row(), last.col()); // placeholder
return last;
}
}
Follow-up 3: AI Opponent (Random / Minimax)
public interface AIStrategy {
Position getMove(Board board, CellState aiSymbol);
}
public class RandomAI implements AIStrategy {
private final Random rand = new Random();
@Override
public Position getMove(Board board, CellState aiSymbol) {
List<Position> empty = new ArrayList<>();
for (int r = 0; r < board.getSize(); r++)
for (int c = 0; c < board.getSize(); c++)
if (board.isEmpty(r, c))
empty.add(new Position(r, c));
return empty.get(rand.nextInt(empty.size()));
}
}
public class MinimaxAI implements AIStrategy {
@Override
public Position getMove(Board board, CellState aiSymbol) {
// Implement minimax with alpha-beta pruning
// Evaluate board: +10 for AI win, -10 for opponent win, 0 for draw
return null; // placeholder for full implementation
}
}
Follow-up 4: Online Multiplayer
┌─────────┐ WebSocket ┌─────────────┐
│ Player 1 │◄─────────────────►│ Game Server │
└─────────┘ │ (Game Room) │
│ │
┌─────────┐ WebSocket │ - Board │
│ Player 2 │◄─────────────────►│ - Players │
└─────────┘ │ - State │
└──────────────┘
- Each player sends moves via WebSocket
- Server validates and broadcasts state to both players
- Sync conflicts: server is source of truth
Follow-up 5: Tournament Mode
public class Tournament {
private final List<Game> games;
private final Map<Player, Integer> scores;
public void recordWin(Player winner) {
scores.merge(winner, 3, Integer::sum); // 3 pts for win
}
public void recordDraw(Player p1, Player p2) {
scores.merge(p1, 1, Integer::sum); // 1 pt each
scores.merge(p2, 1, Integer::sum);
}
public Player getLeader() {
return scores.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
}
}
Design Pattern Summary
| Pattern | Where Used | Why |
|---|---|---|
| Strategy | WinChecker interface |
Swap 3x3 vs NxN vs custom rules |
| State | GameStatus enum + transitions |
Enforce legal state changes |
| Observer | GameListener interface |
Decouple UI from game logic |
| Factory | AI strategy selection | Create AI based on difficulty |
Time & Space Complexity
| Operation | Time | Space |
|---|---|---|
makeMove() |
O(N) | O(1) |
| Win check (3x3) | O(N) | O(1) |
| Win check (generalized) | O(K) | O(1) |
| Move history | O(M) total | O(M) |
| Undo | O(1) | O(M) |
N = board size, K = connect length, M = total moves made
Practice Problems
Design a scalable Tic Tac Toe system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Tic Tac Toe to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Tic Tac Toe and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. Which design pattern is best for swapping win-checking algorithms (3x3 vs NxN)?
2. Why check only the row, column, and diagonals through the last move instead of scanning the entire board?
3. What is the correct sequence of operations after a valid move is made?
4. How do you detect a draw in Tic Tac Toe?
5. What should happen when a player tries to make a move after the game has ended?
6. In a generalized NxN board with connect-K, how many directions need checking from the last move?
7. What is the time complexity of making a single move and checking for a win on a 3x3 board?
Flashcards
Question
What are the 5 game states in Tic Tac Toe?
Click to reveal answer
Answer
NOT_STARTED → PLAYING → WON / DRAW → GAME_OVER. NOT_STARTED transitions to PLAYING on start(). PLAYING transitions to WON if win detected, DRAW if board full, or stays PLAYING otherwise. WON and DRAW are terminal states.
Question
Which lines do you need to check for a win after placing a piece at (row, col)?
Click to reveal answer
Answer
Exactly 4 lines: the row, the column, the main diagonal (if row==col), and the anti-diagonal (if row+col==size-1). No other lines can be completed by a single new piece.
Question
How do you generalize win detection from 3x3 to NxN with connect-K?
Click to reveal answer
Answer
Use 4 direction vectors: (0,1), (1,0), (1,1), (1,-1). From the placed piece, count consecutive matching cells in both directions along each vector. If any count reaches K, it is a win.
Question
What design pattern allows swapping win-checking algorithms?
Click to reveal answer
Answer
Strategy Pattern. Define a WinChecker interface with checkWinner(). Implement StandardWinChecker for 3x3, GeneralizedWinChecker for NxN. Inject the desired strategy into Game at construction time.
Question
How does the Observer pattern help in a Tic Tac Toe game?
Click to reveal answer
Answer
GameListener interface decouples game logic from UI. The Game class notifies listeners on move made, win, and draw events. UI components subscribe without tight coupling to Game internals.
Question
What validation must happen before placing a piece?
Click to reveal answer
Answer
Three checks: (1) Game status must be PLAYING, (2) Row and col must be within bounds, (3) The target cell must be EMPTY. Violations throw IllegalStateException or IllegalArgumentException.
Question
Why is X always the first player?
Click to reveal answer
Answer
By convention, X always goes first in standard Tic Tac Toe. This is enforced in the Game constructor by setting currentPlayer = playerX. It ensures consistency and simplifies state management.
Question
What is the time complexity of win detection for a general NxN board?
Click to reveal answer
Answer
O(N). We check at most 4 lines (row, column, 2 diagonals), each requiring O(N) comparisons. This is optimal since we must inspect N cells in the worst case for each line.
Revision Notes
Key Takeaways
- 1.Always validate move legality before modifying board state
- 2.Only check lines passing through the last move — this cuts win-check from O(N²) to O(N)
- 3.Strategy pattern makes win-checking algorithms swappable without changing Game class
- 4.GameStatus enum enforces state machine — terminal states reject all moves
- 5.Observer pattern decouples game logic from presentation layer
- 6.Design for NxN from the start — it costs almost nothing extra
- 7.MoveValidator as a separate class keeps Game class focused on orchestration
Interview Tips
- •Start by clarifying board size (3x3 vs NxN) and win condition (connect-3 vs connect-K)
- •Draw the state diagram early — it shows structured thinking
- •Mention Strategy pattern for win detection — interviewers love swappable algorithms
- •Walk through the move flow step by step with a concrete board example
- •Discuss follow-ups proactively: AI opponent, undo, online multiplayer, tournament
- •Always mention validation: bounds check, empty cell check, game-in-progress check
- •Explain why checking only the last move's row/col/diagonals is correct and optimal
Cheat Sheet
Tic Tac Toe - Cheat Sheet
Core Entities
Board: NxN grid of CellState, placePiece(), isEmpty(), isFull()CellState: enum EMPTY, X, OPlayer: name + symbol (X or O)Move: record of (player, row, col)Game: orchestrator holding board, players, status, history
Game States
NOT_STARTED → start() → PLAYING
PLAYING → makeMove() → WON (win detected)
PLAYING → makeMove() → DRAW (board full, no win)
WON/DRAW → GAME_OVER (terminal)
Win Detection (3x3)
Check only 4 lines from last move:
- Row: board[row][0..2]
- Column: board[0..2][col]
- Main diagonal: board[i][i] if row==col
- Anti-diagonal: board[i][size-1-i] if row+col==size-1
Win Detection (NxN, connect-K)
4 direction vectors: (0,1), (1,0), (1,1), (1,-1)
Count matching cells in both directions. Win if count ≥ K.
Move Flow
validate(status == PLAYING && bounds && empty)
→ placePiece(row, col, piece)
→ checkWin(piece, row, col)
→ if true: status = WON
→ else checkDraw()
→ if true: status = DRAW
→ else: switchPlayer()
Complexity
| Operation | Time |
|---|---|
| placePiece | O(1) |
| winCheck (3x3) | O(N) |
| winCheck (connect-K) | O(K) |
| drawCheck | O(1) |
| makeMove total | O(N) |
Design Patterns
| Pattern | Usage |
|---|---|
| Strategy | WinChecker (swap algorithms) |
| State | GameStatus transitions |
| Observer | GameListener for UI events |
| Factory | AI strategy creation |