Design Snake & Ladder
Classic board-game LLD at Microsoft, Amazon, and service companies — turn order, dice, snakes/ladders, and win detection in clean OOP.
Model Player, Dice, Board (cells + snakes/ladders), and GameService with playTurn() until someone hits 100. Use Observer for winner events; keep board config injectable.
The interview question
“Design a Snake & Ladder game for N players.” Roll dice, move tokens, apply snake/ladder jumps, first to exactly 100 wins.
Interviewers want turn loop, board rules, and extensible snake/ladder setup — not a GUI.
Clarifying questions (ask before coding)
Spend 2–4 minutes here — shows you understand edge cases before coding.
GameService state.size and snake/ladder map.Dice so tests can inject fixed rolls.Requirements & assumptions (state aloud)
- Functional: start game → players take turns →
playTurn()rolls, moves, applies jump → declare winner. - Board: 1..100 cells; snakes (head > tail) and ladders (foot < top) as maps.
- Win: first player on cell 100; game ends.
- Overshoot: if
position + roll > 100, player stays put (v1). - v1: single JVM, in-memory; no persistence.
- Out of scope: UI, online matchmaking, undo — extensions only.
| Scope | In scope (v1) | Extension (mention) |
|---|---|---|
| Dice | Fair 1–6 die | Loaded die, two dice |
| Players | 2–4 human names | Bot strategy, reconnect |
| Board | Static snake/ladder list | Random board generator |
High-level design & class relationships
GameService owns turn index, player list, and Board. Each turn: roll → move → board.resolveJump() → check win.
Snakes/ladders live on Board as Map<from, to> — one lookup after every move.
- Domain:
Player,Dice,Snake,Ladder,Board - Service:
GameService—playTurn(),getWinner() - Optional:
GameObserveron win
🎲 roll → move 🐍 head → tail 🪜 foot → top
UML class diagram (optional — if interviewer asks)
Draw 5–6 classes after clarifying: GameService orchestrates; Board aggregates jumps.
Composition: GameService → Board, List<Player>, Dice.
GameService.playTurn() first — that is the scoring path.Relationships to label aloud
- Composition (◆):
GameServiceownsBoardand player list for one match. - Association:
Playerhas currentposition;Boardresolves cell jumps. - Dependency (- -▷):
GameServiceusesDiceinterface — swap for tests.
Design patterns used (and why)
| Pattern | Where | Why (interview soundbite) |
|---|---|---|
| 👁️Observer | GameObserver.onGameWon(Player) | UI / logger reacts without bloating GameService. |
| 🎯Strategy (light) | Dice interface | Inject FixedDice in tests; RandomDice in prod. |
| 🎬State (light) | Game NOT_STARTED → IN_PROGRESS → FINISHED | Reject playTurn() after winner declared. |
| 🪞Facade | GameService public API | Clients call playTurn() — not raw board math. |
Full solution — enums & domain model
public enum GameStatus { NOT_STARTED, IN_PROGRESS, FINISHED }
public class Player {
private final String name;
private final String id;
private int position;
// Creates player at cell 0 before game start
public Player(String id, String name) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.position = 0;
}
// Returns stable id for turn order and winner map
public String getId() { return id; }
// Display name on board / console
public String getName() { return name; }
// Current cell (0 = off-board, 100 = win)
public int getPosition() { return position; }
// Sets position after move and ladder/snake jump
public void setPosition(int position) { this.position = position; }
}
public interface Dice {
// Returns roll value in [1, faces]
int roll();
}
public class RandomDice implements Dice {
private final int faces;
private final Random random = new Random();
// Standard six-sided die when faces = 6
public RandomDice(int faces) {
if (faces < 1) throw new IllegalArgumentException("faces >= 1");
this.faces = faces;
}
// Uniform random roll 1..faces
@Override
public int roll() {
return random.nextInt(faces) + 1;
}
}public record Snake(int head, int tail) {
// Validates snake: head must be above tail on the board
public Snake {
if (head <= tail) throw new IllegalArgumentException("head > tail");
}
}
public record Ladder(int foot, int top) {
// Validates ladder: foot must be below top
public Ladder {
if (foot >= top) throw new IllegalArgumentException("foot < top");
}
}
public class Board {
private final int size;
private final Map<Integer, Integer> jumps = new HashMap<>();
// Builds board of given size (typically 100)
public Board(int size) {
this.size = size;
}
// Registers snake head → tail jump
public void addSnake(Snake snake) {
jumps.put(snake.head(), snake.tail());
}
// Registers ladder foot → top jump
public void addLadder(Ladder ladder) {
jumps.put(ladder.foot(), ladder.top());
}
// After landing on cell, returns destination (snake/ladder or same cell)
public int resolveJump(int cell) {
return jumps.getOrDefault(cell, cell);
}
// True when player reached winning cell
public boolean isWinningCell(int cell) {
return cell == size;
}
// Max cell number (100)
public int getSize() { return size; }
}Full solution — GameService & demo
public interface GameObserver {
// Fired once when a player wins
void onGameWon(Player winner);
}
public class GameService {
private final Board board;
private final Dice dice;
private final List<Player> players;
private final List<GameObserver> observers = new ArrayList<>();
private GameStatus status = GameStatus.NOT_STARTED;
private int currentPlayerIndex;
private Player winner;
// Wires board, dice, and player roster
public GameService(Board board, Dice dice, List<Player> players) {
this.board = board;
this.dice = dice;
this.players = new ArrayList<>(players);
}
// Subscribes listener for win event
public void addObserver(GameObserver observer) {
observers.add(observer);
}
// Moves all players to start and begins round-robin turns
public void startGame() {
if (players.isEmpty()) throw new IllegalStateException("need players");
players.forEach(p -> p.setPosition(0));
currentPlayerIndex = 0;
winner = null;
status = GameStatus.IN_PROGRESS;
}
// Executes one turn: roll, move if legal, apply jump, check win
public TurnResult playTurn() {
if (status != GameStatus.IN_PROGRESS) {
throw new IllegalStateException("game not in progress");
}
Player current = players.get(currentPlayerIndex);
int roll = dice.roll();
int from = current.getPosition();
int candidate = from + roll;
if (candidate <= board.getSize()) {
int landed = board.resolveJump(candidate);
current.setPosition(landed);
if (board.isWinningCell(landed)) {
winner = current;
status = GameStatus.FINISHED;
notifyWin(winner);
return new TurnResult(current, roll, from, landed, true);
}
}
currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
return new TurnResult(current, roll, from, current.getPosition(), false);
}
// Returns winning player after FINISHED; empty if still playing
public Optional<Player> getWinner() {
return Optional.ofNullable(winner);
}
// Current game lifecycle state
public GameStatus getStatus() { return status; }
// Notifies all observers of win
private void notifyWin(Player w) {
observers.forEach(o -> o.onGameWon(w));
}
public record TurnResult(Player player, int roll, int from, int to, boolean gameWon) {}
}public class SnakeAndLadderDemo {
// Builds classic board and runs until someone wins
public static void main(String[] args) {
Board board = new Board(100);
board.addSnake(new Snake(98, 12));
board.addLadder(new Ladder(6, 16));
board.addLadder(new Ladder(20, 38));
List<Player> roster = List.of(
new Player("p1", "Alice"),
new Player("p2", "Bob")
);
GameService game = new GameService(board, new RandomDice(6), roster);
game.addObserver(w -> System.out.println(w.getName() + " wins!"));
game.startGame();
while (game.getStatus() == GameStatus.IN_PROGRESS) {
GameService.TurnResult r = game.playTurn();
System.out.printf("%s rolled %d: %d → %d%n",
r.player().getName(), r.roll(), r.from(), r.to());
}
}
}While you code — questions interviewers ask
Answer briefly, then keep coding.
Extensions & follow-ups (senior loop)
- Board generator: random snakes/ladders with solvability check.
- Undo / replay: command stack of
TurnResult. - Online multiplayer:
GameRoom+ WebSocket broadcast after each turn. - Bot player: Strategy for dice-less auto-play.
- Extra rules: three sixes in a row → skip turn (Indian house rules).
Common mistakes (fail signals)
- No exact 100 rule — allowing position 101.
- Applying snake/ladder before checking overshoot.
- God class with board + dice + UI in one file.
- Forgetting to advance turn when move is illegal.
- No
GameStatus— allowing turns after game over. - Hard-coded board in
GameServiceinstead of injectableBoard.