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.