← Home
🏗Crack System Design with DSA AnimatorSystem DesignNEW
🏗 System Design›LLD Interview Prep›LLD
🎲

Design Snake & Ladder

Classic board-game LLD at Microsoft, Amazon, and service companies — turn order, dice, snakes/ladders, and win detection in clean OOP.

📊 Easy → Medium⏱ 30–40 min🏢 Microsoft · Amazon · Flipkart · Goldman · most product SDE-1/2 LLD warm-ups
🎯 Interview goal

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.

💡
Treat as domain model + game loop, not multiplayer networking. Mention WebSocket sync only as an extension.
❓

Clarifying questions (ask before coding)

Spend 2–4 minutes here — shows you understand edge cases before coding.

1How many players and fixed turn order?
Why askRound-robin queue vs random start — drives GameService state.
2Standard 1–100 board or configurable size?
Why askBoard constructor takes size and snake/ladder map.
3Single die (1–6) or custom dice?
Why askInterface Dice so tests can inject fixed rolls.
4Exact landing on 100 only, or overshoot loses turn?
Why askClassic rule: roll > remaining cells → no move — state this aloud.
✅

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.
ScopeIn scope (v1)Extension (mention)
DiceFair 1–6 dieLoaded die, two dice
Players2–4 human namesBot strategy, reconnect
BoardStatic snake/ladder listRandom 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.

  1. Domain: Player, Dice, Snake, Ladder, Board
  2. Service: GameService — playTurn(), getWinner()
  3. Optional: GameObserver on win
100 🏁🐍 98→1297🪜 96→84

🎲 roll → move 🐍 head → tail 🪜 foot → top

Snake and ladder game flow
Turn loop: roll → move → jump → win check
📐

UML class diagram (optional — if interviewer asks)

Draw 5–6 classes after clarifying: GameService orchestrates; Board aggregates jumps.

Composition: GameService → Board, List<Player>, Dice.

💡
If they skip UML, code GameService.playTurn() first — that is the scoring path.
UML class diagram for snake and ladder
Figure — GameService, Board, Player, Dice, Snake/Ladder

Relationships to label aloud

  • Composition (◆): GameService owns Board and player list for one match.
  • Association: Player has current position; Board resolves cell jumps.
  • Dependency (- -▷): GameService uses Dice interface — swap for tests.
🎨

Design patterns used (and why)

⚠️
Do not over-pattern a 6-class game — mention Factory for board presets only if asked.
PatternWhereWhy (interview soundbite)
👁️ObserverGameObserver.onGameWon(Player)UI / logger reacts without bloating GameService.
🎯Strategy (light)Dice interfaceInject FixedDice in tests; RandomDice in prod.
🎬State (light)Game NOT_STARTED → IN_PROGRESS → FINISHEDReject playTurn() after winner declared.
🪞FacadeGameService public APIClients call playTurn() — not raw board math.
💻

Full solution — enums & domain model

1. Enums, Player & Dicejava
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;
    }
}
2. Snake, Ladder & Boardjava
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

3. GameService — playTurn & winnerjava
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) {}
}
4. Demo mainjava
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 GameService instead of injectable Board.
🧾

60-second cheat sheet

Ask firstPlayers, board size, overshoot rule, dice type
Core classesPlayer, Dice, Snake, Ladder, Board, GameService
Turnroll → if pos+roll≤100 → jump map → win?
PatternsObserver (win), Strategy (Dice), Facade (GameService)
APIstartGame(); playTurn() → TurnResult; getWinner()
ExtendBoard builder, online room, undo stack
🔗

More LLD problems