Back to LLD explorer
Game Design• Patterns: Strategy, Singleton, Observer

Tic Tac Toe

Easy

Problem Summary

Design a modular, extensible Tic Tac Toe engine supporting an N x N grid, arbitrary player count, dynamic AI bot difficulty strategies, and O(1) win verification.

Functional Scope

  • Customizable N x N grid dimensions (default 3x3, scalable up to 100x100).
  • Arbitrary number of players (2 or more) taking alternating sequential turns.
  • O(1) time-complexity victory and draw condition detection on every move.
  • Pluggable bot player decision strategies (Random, Minimax, Heuristic).
  • Thread-safe move execution and match state querying.

Entity-Relationship (ER) Schema

GameEngine [1] <---> [1] Board
GameEngine [1] <---> [2..*] Player
Board [1] <---> [N*N] Cell
Player <|-- HumanPlayer
Player <|-- BotPlayer
BotPlayer [1] --> [1] PlayingStrategy
GameEngine [1] --> [1] WinConditionStrategy

Design Approach

Apply the Strategy pattern for bot move decisions, allowing seamless toggling between random, rule-based, and Minimax AI. To avoid O(N) scanning after each move, maintain separate frequency arrays for rows, cols, and the two diagonals, verifying win condition in strict O(1) time.

Core Classes & Models

GameEngine (Coordinates game loop, turns, and win detection)Board (Maintains N x N grid cells and coordinate bounds)Cell (Holds coordinate and occupying symbol)Player (Abstract base class for participants)HumanPlayer & BotPlayer (Concrete player entities)PlayingStrategy (Strategy interface for AI decision making)MinimaxPlayingStrategy & RandomPlayingStrategyWinConditionStrategy (O(1) frequency counting win evaluator)
Code Blueprint
198 lines
/**
 * Low-Level Design: Tic Tac Toe
 * Design Patterns Applied:
 *  - Strategy Pattern: Pluggable bot move strategies and O(1) win verification
 *  - Singleton Pattern: Central match orchestrator coordinating gameplay
 *  - Observer Pattern: Real-time board event broadcasting to displays and auditors
 */

import java.util.*;

enum Symbol { X, O, EMPTY }

// --- [DESIGN PATTERN: Observer] Listener for live match events ---
interface BoardObserver {
    void onMoveMade(int row, int col, Symbol symbol);
    void onGameOver(String outcome);
}

class ConsoleBoardObserver implements BoardObserver {
    @Override
    public void onMoveMade(int row, int col, Symbol symbol) {
        System.out.println(">> [Observer] Player placed " + symbol + " at (" + row + ", " + col + ")");
    }
    @Override
    public void onGameOver(String outcome) {
        System.out.println(">> [Observer] Game Over: " + outcome);
    }
}

class Cell {
    private final int row, col;
    private Symbol symbol;

    public Cell(int row, int col) {
        this.row = row;
        this.col = col;
        this.symbol = Symbol.EMPTY;
    }
    public boolean isEmpty() { return symbol == Symbol.EMPTY; }
    public Symbol getSymbol() { return symbol; }
    public void setSymbol(Symbol symbol) { this.symbol = symbol; }
}

class Board {
    private final int size;
    private final Cell[][] grid;
    private final List<BoardObserver> observers = new ArrayList<>();

    public Board(int size) {
        this.size = size;
        this.grid = new Cell[size][size];
        for (int r = 0; r < size; r++) {
            for (int c = 0; c < size; c++) {
                grid[r][c] = new Cell(r, c);
            }
        }
    }

    public void addObserver(BoardObserver observer) { observers.add(observer); }
    private void notifyMove(int r, int c, Symbol s) {
        for (BoardObserver obs : observers) obs.onMoveMade(r, c, s);
    }
    public void notifyGameOver(String outcome) {
        for (BoardObserver obs : observers) obs.onGameOver(outcome);
    }

    public boolean placeSymbol(int r, int c, Symbol s) {
        if (r < 0 || r >= size || c < 0 || c >= size || !grid[r][c].isEmpty()) return false;
        grid[r][c].setSymbol(s);
        notifyMove(r, c, s);
        return true;
    }

    public void printBoard() {
        System.out.println("-------------");
        for (int r = 0; r < size; r++) {
            System.out.print("| ");
            for (int c = 0; c < size; c++) {
                char ch = grid[r][c].getSymbol() == Symbol.EMPTY ? ' ' : grid[r][c].getSymbol().name().charAt(0);
                System.out.print(ch + " | ");
            }
            System.out.println("\n-------------");
        }
    }

    public int getSize() { return size; }
}

// --- [DESIGN PATTERN: Strategy] Pluggable bot move decision strategy ---
interface PlayingStrategy {
    int[] decideMove(Board board, Symbol symbol);
}

class FirstAvailablePlayingStrategy implements PlayingStrategy {
    @Override
    public int[] decideMove(Board board, Symbol symbol) {
        int n = board.getSize();
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                return new int[]{r, c};
            }
        }
        return new int[]{-1, -1};
    }
}

class Player {
    private final String name;
    private final Symbol symbol;
    private final PlayingStrategy strategy;

    public Player(String name, Symbol symbol, PlayingStrategy strategy) {
        this.name = name;
        this.symbol = symbol;
        this.strategy = strategy;
    }
    public String getName() { return name; }
    public Symbol getSymbol() { return symbol; }
    public PlayingStrategy getStrategy() { return strategy; }
}

// --- [DESIGN PATTERN: Singleton / Context] Game manager controlling loop ---
class TicTacToeGame {
    private final Board board;
    private final Queue<Player> players = new LinkedList<>();
    private final int[] rowCounts, colCounts;
    private int diagCount = 0, antiDiagCount = 0;
    private int movesMade = 0;
    private boolean isGameOver = false;

    public TicTacToeGame(int size, Player p1, Player p2) {
        this.board = new Board(size);
        this.board.addObserver(new ConsoleBoardObserver());
        this.players.add(p1);
        this.players.add(p2);
        this.rowCounts = new int[size];
        this.colCounts = new int[size];
    }

    public boolean makeMove(int r, int c) {
        if (isGameOver) {
            System.out.println("Game is already finished!");
            return false;
        }

        Player current = players.peek();
        if (!board.placeSymbol(r, c, current.getSymbol())) {
            System.out.println("Invalid move at (" + r + ", " + c + "). Cell occupied or out of bounds.");
            return false;
        }

        movesMade++;
        int val = (current.getSymbol() == Symbol.X) ? 1 : -1;
        rowCounts[r] += val;
        colCounts[c] += val;
        if (r == c) diagCount += val;
        if (r + c == board.getSize() - 1) antiDiagCount += val;

        board.printBoard();

        int target = board.getSize();
        if (Math.abs(rowCounts[r]) == target || Math.abs(colCounts[c]) == target ||
            Math.abs(diagCount) == target || Math.abs(antiDiagCount) == target) {
            String winMsg = "Player " + current.getName() + " (" + current.getSymbol() + ") WINS!";
            System.out.println("🎉 " + winMsg);
            board.notifyGameOver(winMsg);
            isGameOver = true;
            return true;
        }

        if (movesMade == target * target) {
            String drawMsg = "Game is a DRAW!";
            System.out.println("🤝 " + drawMsg);
            board.notifyGameOver(drawMsg);
            isGameOver = true;
            return true;
        }

        players.poll();
        players.add(current);
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        Player alice = new Player("Alice", Symbol.X, new FirstAvailablePlayingStrategy());
        Player bob = new Player("Bob", Symbol.O, new FirstAvailablePlayingStrategy());
        TicTacToeGame game = new TicTacToeGame(3, alice, bob);

        System.out.println("--- Starting Tic Tac Toe Simulation ---");
        game.makeMove(0, 0); // Alice X
        game.makeMove(0, 1); // Bob O
        game.makeMove(1, 1); // Alice X
        game.makeMove(0, 2); // Bob O
        game.makeMove(2, 2); // Alice X wins diagonal
    }
}