System Design
Object-Oriented Design
Tic-Tac-Toe

Design Example: Tic-Tac-Toe Game

Tic-Tac-Toe is a classic low-level design problem that focuses on game state management, coordinate-based logic, and win-condition detection.


Step 1: Requirements Gathering

Core Use Cases:

  • Two players (X and O) take turns.
  • Players place their mark on a 3x3 grid.
  • The game detects a winner (3 in a row, column, or diagonal) or a draw.
  • Support for resetting the game.

Advanced Features:

  • Support for "Undo" functionality.
  • Score tracking for multiple rounds.

Step 2: Identify Core Objects

  • Board: Manages the 3x3 grid and cell occupancy.
  • Player: Represents a user with a specific mark (X or O).
  • Game: Orchestrates the turns, board updates, and win detection.
  • Cell: (Optional) Represents a single square on the board.
  • Move: Represents a coordinate pair (row, col) and the player who made it.

Step 3: Design Class Diagram


Step 4: Implementation in TypeScript

enum Symbol { X = 'X', O = 'O', EMPTY = ' ' }
 
class Board {
  private grid: Symbol[][];
 
  constructor() {
    this.grid = Array(3).fill(null).map(() => Array(3).fill(Symbol.EMPTY));
  }
 
  public mark(row: number, col: number, symbol: Symbol): boolean {
    if (this.grid[row][col] !== Symbol.EMPTY) return false;
    this.grid[row][col] = symbol;
    return true;
  }
 
  public getGrid() { return this.grid; }
  
  public isFull(): boolean {
    return this.grid.every(row => row.every(cell => cell !== Symbol.EMPTY));
  }
}
 
class Game {
  private board: Board;
  private players: [Symbol, Symbol] = [Symbol.X, Symbol.O];
  private turn: number = 0;
 
  constructor() {
    this.board = new Board();
  }
 
  public play(row: number, col: number) {
    const symbol = this.players[this.turn % 2];
    if (this.board.mark(row, col, symbol)) {
      if (this.checkWin(row, col, symbol)) {
        console.log(`${symbol} wins!`);
        return;
      }
      this.turn++;
    }
  }
 
  private checkWin(row: number, col: number, symbol: Symbol): boolean {
    // Logic to check row, col, and diagonals
    return false; // Simplified for example
  }
}

Deep Dive: Undo with Command Pattern

To implement Undo, we can treat every move as a "Command" object and store them in a stack.

interface Command {
  execute(): void;
  undo(): void;
}
 
class MoveCommand implements Command {
  constructor(
    private board: Board, 
    private row: number, 
    private col: number, 
    private symbol: Symbol
  ) {}
 
  execute() { this.board.mark(this.row, this.col, this.symbol); }
  undo() { /* Logic to clear the cell */ }
}

Wrap Up

Designing Tic-Tac-Toe is less about the game itself and more about how you handle state transitions and extensibility. Using patterns like Command for undo logic shows that you're thinking about real-world software maintenance.

[!TIP] For a $N \times N$ board, optimize win detection by keeping track of row and column counts rather than scanning the entire board on every move.