Skip to content

Snake & Ladders#

Problem statement (interviewer prompt)

Design a Snake & Ladders game: configurable N×N board, snakes + ladders placed at arbitrary cells, K players who roll dice in turn, winner is whoever lands on the last cell, with replay + undo. Focus on the class model + game-loop state machine.

classDiagram
  class Game
  class Board
  class Player
  class Dice
  class Snake
  class Ladder
  Game *-- Board
  Game *-- "many" Player
  Board *-- "many" Snake
  Board *-- "many" Ladder
  Game --> Dice

How the game works#

At the highest level a Snake & Ladders game is a turn loop over a finite state machine. Each round the active Player rolls a Dice, the Board computes the new cell (applying any snake bite or ladder climb), and the turn either ends or extends if the rolled value qualifies (for example, a six often grants a bonus turn). The first player to reach the last cell wins. The model deliberately keeps Board and Player dumb: Board is a pure function from (current, roll) to next cell, and Player is just an id with a position. All policy lives in Game and Rules.

The reason this is asked as an LLD interview question is not the gameplay; it is the way the design forces clean separation between state, behaviour, and configuration. Variants such as biased dice, exact-landing rules, multiple dice, and snake-ladder chaining each touch a different seam in the design. Candidates who model Game as a god-object cannot extend it; candidates who isolate Board, Dice, Rules, and a turn-queue can.

Detailed class diagram with multiplicities#

classDiagram
  direction TB

  class Game {
    -board: Board
    -players: Queue~Player~
    -dice: Dice
    -rules: Rules
    -strategy: MoveStrategy
    -history: Deque~Move~
    -winner: Player
    +playTurn() TurnResult
    +start()
    +undo()
    +addObserver(o)
  }

  class Board {
    +size: int
    -snakes: List~Snake~
    -ladders: List~Ladder~
    +nextPosition(curr, roll)
    +entityAt(cell)
  }

  class Player {
    +id
    +name
    +position: int
  }

  class Dice {
    <<interface>>
    +roll() int
  }

  class StandardDice {
    +sides: int
    +count: int
    +roll() int
  }

  class BiasedDice {
    -weights: List~double~
    +roll() int
  }

  class Snake {
    +head
    +tail
  }

  class Ladder {
    +bottom
    +top
  }

  class MoveStrategy {
    <<interface>>
    +resolve(curr, roll, board) int
  }

  class StandardMove {
    +resolve(curr, roll, board)
  }

  class ChainedMove {
    +resolve(curr, roll, board)
  }

  class Rules {
    +diceForExtraTurn
    +mustLandExactlyAtEnd
    +killOnSameCell
    +maxConsecutiveSixes
  }

  class GameObserver {
    <<interface>>
    +onMove(turn)
    +onWin(player)
  }

  Game "1" *-- "1" Board
  Game "1" *-- "2..K" Player
  Game "1" --> "1" Dice
  Game "1" --> "1" Rules
  Game "1" --> "1" MoveStrategy
  Game "1" o-- "0..*" GameObserver
  Board "1" *-- "0..*" Snake
  Board "1" *-- "0..*" Ladder
  Dice <|.. StandardDice
  Dice <|.. BiasedDice
  MoveStrategy <|.. StandardMove
  MoveStrategy <|.. ChainedMove

Core classes in Java#

The Java sketch below pins down the most interesting class, Game, and the Board lookup that powers it. Snake and ladder placement is precomputed into a single integer map so nextPosition is a single O(1) lookup; that is the trick that lets variants such as biased dice or larger boards reuse the same logic.

public final class Game {
    private final Board board;
    private final Deque<Player> turnQueue;
    private final Dice dice;
    private final Rules rules;
    private final MoveStrategy moveStrategy;
    private final Deque<Move> history = new ArrayDeque<>();
    private final List<GameObserver> observers = new ArrayList<>();
    private Player winner;

    public Game(Board b, List<Player> players, Dice d, Rules r, MoveStrategy m) {
        this.board = b;
        this.turnQueue = new ArrayDeque<>(players);
        this.dice = d;
        this.rules = r;
        this.moveStrategy = m;
    }

    public TurnResult playTurn() {
        if (winner != null) return TurnResult.gameOver(winner);
        Player p = turnQueue.peek();
        int roll = dice.roll();
        int from = p.getPosition();
        int to = moveStrategy.resolve(from, roll, board);

        if (rules.mustLandExactlyAtEnd() && to > board.size()) {
            to = from; // overshoot: stay in place
        }
        p.setPosition(to);
        history.push(new Move(p, from, to, roll));
        observers.forEach(o -> o.onMove(history.peek()));

        if (to == board.size()) {
            winner = p;
            observers.forEach(o -> o.onWin(p));
            return TurnResult.win(p);
        }
        if (!rules.grantsExtraTurn(roll)) {
            turnQueue.add(turnQueue.poll()); // rotate to next player
        }
        return TurnResult.advanced(p, from, to);
    }

    public void undo() {
        if (history.isEmpty()) return;
        Move last = history.pop();
        last.player().setPosition(last.from());
        if (winner == last.player()) winner = null;
    }
}

Board.nextPosition is the other half of the puzzle. Internally it stores a Map<Integer, Integer> of jump cells (snake heads map to their tails, ladder bottoms map to their tops). On every move, the strategy advances the player by the dice roll, then looks up the destination in this jump map until it stabilises. Stabilisation is required to handle chained snakes and ladders or to detect bad configurations.

Design patterns used#

The design earns its keep because each pattern lines up with a real interview follow-up. Naming them in the answer signals fluency with both OOP and gameplay variations.

  • Strategy for Dice and MoveStrategy: StandardDice rolls uniformly; BiasedDice rolls from a weighted distribution; LoadedDice returns a scripted sequence for replay tests. MoveStrategy swaps between standard movement and chained jump resolution without touching Game.
  • Iterator for player rotation: turnQueue is a circular collection; playTurn pulls the head and re-enqueues it, which is exactly how an iterator over a ring buffer behaves. The pattern lets you change "skip a turn" or "reverse direction" rules without touching the loop.
  • Observer for winner notification and UI: GameObserver listens for onMove and onWin events. UI renderers, leaderboards, analytics sinks, and turn-time loggers each register as observers without Game knowing about them.
  • Factory for board setup: BoardFactory.standardBoard() returns the canonical 10x10 board with the printed-on-the-box snake and ladder positions; BoardFactory.fromConfig(BoardConfig) builds custom boards from a JSON or YAML descriptor for tournament variants and bot-training scenarios.
  • Command for Move: each entry pushed onto the history stack carries enough state (player, from, to, dice roll) to be inverted, enabling undo and full replay from a serialised game.
  • State (implicit): the game flows through WaitingToRoll, Rolling, Moving, Won, Paused. Although the Java sketch uses guard-style checks, an interviewer who asks "how would you formalise turn flow" expects a state-machine answer.

Common variants & follow-ups#

These are the questions that come after the base design and reward a layered model.

  • Multi-player beyond two: the queue-based turn loop already supports K players. The only change is dynamically resizing the queue when a player joins or leaves a long-running game, which is one-line turnQueue.add or turnQueue.remove.
  • Networked multiplayer: replace direct calls into Game.playTurn with a turn-event bus. Each client sends a RollIntent; the authoritative server validates the dice via a shared seed or server-side RNG, computes the move, and broadcasts the resulting Move to all clients. The Observer pattern carries the result into client UIs unchanged.
  • Custom board sizes: pass size: int and snake/ladder lists into a BoardConfig. Boards of 8x8 (kids' variant), 10x10 (classic), and 15x15 (long-form tournament) all reuse the same Board and Game classes. The win condition stays position == board.size.
  • Biased dice and weighted dice sets: swap the Dice implementation. A BiasedDice weighted toward six is a common cheat-testing scenario; a MultiDice(count=2) returns the sum of two rolls and is used in variant rule sets.
  • Time-limited games: wrap playTurn in a turn timer. If the active player does not roll within T seconds, an auto-roll handler triggers, or the player is skipped via a SkipPolicy observer. Long-running networked games use this to keep matches moving.
  • Multiple winners and ranking: instead of stopping at the first player to reach the end, continue until all players finish. The ranking is the order in which players hit board.size, which the observer can record without changing Game.
  • Save and resume: serialise players, turnQueue order, winner, and history. Replaying history from an empty board reconstructs the exact game state, which is the cheapest cross-platform persistence format.

Edge cases#

A solid LLD answer enumerates the tricky board configurations before the interviewer has to ask.

  • Overshoot past 100: when the dice roll would place the player beyond board.size, the canonical rule is to stay in place. The Java sketch above implements this through rules.mustLandExactlyAtEnd(); setting it false lets players "wrap" or always win on first overshoot for casual variants.
  • Landing exactly on 100: the win event fires only when position == board.size. This guards against an off-by-one that grants the win to anyone past the threshold; it also keeps the rule consistent with the exact-land variant.
  • Snake on the final cell: rules forbid placing a snake head on board.size, since the player would win before the snake could bite. BoardConfig validation rejects such configurations at construction time rather than at runtime.
  • Ladder leading to the final cell: this is allowed and is a popular shortcut on classic boards (for example, 80 to 100). The move strategy must resolve the ladder before checking the win condition, which is why Game.playTurn checks the win after the strategy returns the resolved cell.
  • Snake or ladder placed on the starting cell: usually disallowed, since a player starting at cell 0 or 1 should not instantly teleport. BoardConfig.validate rejects entries whose bottom cell equals the start.
  • Chained snake-then-ladder cycle: two badly configured jumps can route a player back to the same cell (snake from 80 to 30, ladder from 30 to 80). The ChainedMove strategy detects this by tracking visited cells in a Set during resolution; if a cycle is detected, the player stops at the first cell to break the infinite loop and logs the configuration error.
  • Two players on the same cell: by default both players coexist. The killOnSameCell rule sends the earlier occupant back to the start; the rules object lets you toggle this without touching the move strategy.
  • Maximum consecutive sixes: many traditional variants forbid more than three sixes in a row, treating the fourth as a forfeited turn. Rules.maxConsecutiveSixes is consulted by Game after each roll and resets the streak on a non-six roll.

Glossary & fundamentals#

Concept What it is Fundamentals
Idempotency retried turn ops in multiplayer idempotency-retries
Pub/Sub turn events pub-sub-pattern

Quick reference#

Functional#

  • Multi-player; turn-based.
  • Roll dice, advance.
  • Snakes bite; ladders climb.
  • Optional rules: exact land at end, six-rolls-extra-turn, kill-on-same-cell.

Patterns#

  • Strategy (Rules).
  • Queue (turn order).
  • Observer (UI updates).

Refs#

  • Classic OOD exercises.

FAQ#

How is a Snake and Ladders board modelled?#

An N times N grid of Cell objects. Snake and Ladder are entries on cells that override the landing position, so movement is a one-line lookup.

How do you handle the turn loop?#

Game owns a circular list of Player objects and an index pointer. Each tick rolls Dice, computes the new position via Board.next, then advances the pointer to the next player.

Why use a state machine for the game?#

States like WaitingToRoll, Rolling, Moving, Won, and Paused make the rules explicit. Events such as rollDice or playerWon trigger transitions and forbid illegal sequences.

How do you support undo and replay?#

Treat each turn as a Move command with player, dice value, and resulting position. Push it to a history stack so undo reverts and replay rebuilds state from the start.

How do you make the board configurable?#

Inject the size, snakes, and ladders through a BoardConfig object so different variants and N times N boards reuse the same Game and Player code without changes.