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
DiceandMoveStrategy:StandardDicerolls uniformly;BiasedDicerolls from a weighted distribution;LoadedDicereturns a scripted sequence for replay tests.MoveStrategyswaps between standard movement and chained jump resolution without touchingGame. - Iterator for player rotation:
turnQueueis a circular collection;playTurnpulls 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:
GameObserverlistens foronMoveandonWinevents. UI renderers, leaderboards, analytics sinks, and turn-time loggers each register as observers withoutGameknowing 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 thehistorystack 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.addorturnQueue.remove. - Networked multiplayer: replace direct calls into
Game.playTurnwith a turn-event bus. Each client sends aRollIntent; the authoritative server validates the dice via a shared seed or server-side RNG, computes the move, and broadcasts the resultingMoveto all clients. TheObserverpattern carries the result into client UIs unchanged. - Custom board sizes: pass
size: intand snake/ladder lists into aBoardConfig. Boards of 8x8 (kids' variant), 10x10 (classic), and 15x15 (long-form tournament) all reuse the sameBoardandGameclasses. The win condition staysposition == board.size. - Biased dice and weighted dice sets: swap the
Diceimplementation. ABiasedDiceweighted toward six is a common cheat-testing scenario; aMultiDice(count=2)returns the sum of two rolls and is used in variant rule sets. - Time-limited games: wrap
playTurnin a turn timer. If the active player does not roll withinTseconds, an auto-roll handler triggers, or the player is skipped via aSkipPolicyobserver. 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 changingGame. - Save and resume: serialise
players,turnQueueorder,winner, andhistory. Replayinghistoryfrom 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 throughrules.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.BoardConfigvalidation 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.playTurnchecks 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.validaterejects 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
ChainedMovestrategy detects this by tracking visited cells in aSetduring 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
killOnSameCellrule 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.maxConsecutiveSixesis consulted byGameafter 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.
Related Topics#
- Tic-Tac-Toe: turn-based board-game LLD
- Chess Engine: deeper board-game design
- State Machines: game-state transition pattern