Home/Learn/Low Level Design/Design Snake and Ladder Game

Design Snake and Ladder Game

Beginner
LLD Interview Problems
Java Source Code

Model a configurable Snake and Ladder board game with multiple players, pluggable dice strategies, and observer-based game-event notifications.

Overview

Snake and Ladder is a classic board game where players roll dice and advance tokens; landing on a ladder head teleports the player up, landing on a snake head sends them back. Board holds two maps: snakes (head → tail) and ladders (bottom → top). Player tracks its position. Dice is a Strategy interface supporting standard, weighted, or fixed (testing) implementations. Observer fires events for snake bites, ladder climbs, and game completion. GameController drives the turn loop: roll → move → check teleport → check win → notify observers → advance turn.

Requirements Analysis

Functional: 2–6 players, configurable snakes and ladders, roll dice to advance, detect win condition (exact landing on 100), support multiple dice. Non-functional: pluggable dice (swap standard for loaded dice in tests), extensible event notifications, board configuration injectable at construction (no hard-coding).

Requirements
// Entities : Game, Board, Player, Dice, Snake, Ladder, Cell, GameController
// Patterns : Strategy (dice rolling), Observer (game events), Factory (board setup)

Core Classes & Relationships

DiceStrategy interface has roll(). StandardDice uses Random; FixedDice accepts a list of preset values for testing. GameEvent enum: NORMAL_MOVE, SNAKE_BITE, LADDER_CLIMB, GAME_WON. GameObserver interface has onEvent(GameEvent, Player, int from, int to). Snake and Ladder are simple records storing start and end positions. Board validates positions and resolves teleports.

Java — enums & interfaces
public interface DiceStrategy {
    int roll();
}

public class StandardDice implements DiceStrategy {
    private final Random rng = new Random();
    private final int sides;
    public StandardDice(int sides) { this.sides = sides; }
    @Override public int roll() { return rng.nextInt(sides) + 1; }
}

public class FixedDice implements DiceStrategy {   // deterministic for tests
    private final Queue<Integer> values;
    public FixedDice(List<Integer> vals) { this.values = new LinkedList<>(vals); }
    @Override public int roll() { return values.isEmpty() ? 6 : values.poll(); }
}

public enum GameEvent { NORMAL_MOVE, SNAKE_BITE, LADDER_CLIMB, GAME_WON }

public interface GameObserver {
    void onEvent(GameEvent event, Player player, int fromCell, int toCell);
}

public record Snake(int head, int tail)   {}
public record Ladder(int bottom, int top){}

Java Implementation

Board resolves a position to its final cell after snake or ladder teleport. Player holds current position. GameController runs the turn loop: roll, compute new position (capped at board size), resolve teleport, notify observers, check win. The loop continues until a player lands exactly on cell 100.

Java — core classes
public class Player {
    private final String name;
    private int position = 0;

    public Player(String name) { this.name = name; }
    public String getName()    { return name; }
    public int getPosition()   { return position; }
    public void setPosition(int p) { this.position = p; }
}

public class Board {
    private final int size;
    private final Map<Integer, Integer> snakes;   // head  -> tail
    private final Map<Integer, Integer> ladders;  // bottom -> top

    public Board(int size, List<Snake> snakes, List<Ladder> ladders) {
        this.size    = size;
        this.snakes  = snakes.stream().collect(Collectors.toMap(Snake::head,   Snake::tail));
        this.ladders = ladders.stream().collect(Collectors.toMap(Ladder::bottom, Ladder::top));
    }

    public int getSize() { return size; }

    public int resolve(int position) {
        if (snakes.containsKey(position))  return snakes.get(position);
        if (ladders.containsKey(position)) return ladders.get(position);
        return position;
    }

    public boolean isSnake(int pos)  { return snakes.containsKey(pos); }
    public boolean isLadder(int pos) { return ladders.containsKey(pos); }
}

public class GameController {
    private final Board board;
    private final List<Player> players;
    private final DiceStrategy dice;
    private final List<GameObserver> observers = new ArrayList<>();
    private int currentIndex = 0;
    private boolean gameOver = false;

    public GameController(Board board, List<Player> players, DiceStrategy dice) {
        if (players.size() < 2) throw new IllegalArgumentException("Need at least 2 players");
        this.board = board; this.players = players; this.dice = dice;
    }

    public void addObserver(GameObserver o) { observers.add(o); }

    public void playTurn() {
        if (gameOver) { System.out.println("Game already over."); return; }
        Player player = players.get(currentIndex);
        int rolled    = dice.roll();
        int from      = player.getPosition();
        int target    = from + rolled;

        if (target > board.getSize()) {
            System.out.printf("%s rolled %d but cannot move (would exceed %d).%n",
                player.getName(), rolled, board.getSize());
            advanceTurn();
            return;
        }

        int resolved  = board.resolve(target);
        player.setPosition(resolved);

        GameEvent event;
        if      (resolved == board.getSize()) event = GameEvent.GAME_WON;
        else if (board.isSnake(target))       event = GameEvent.SNAKE_BITE;
        else if (board.isLadder(target))      event = GameEvent.LADDER_CLIMB;
        else                                  event = GameEvent.NORMAL_MOVE;

        observers.forEach(o -> o.onEvent(event, player, from, resolved));

        if (event == GameEvent.GAME_WON) {
            gameOver = true;
            System.out.printf("%s wins!%n", player.getName());
            return;
        }
        advanceTurn();
    }

    private void advanceTurn() { currentIndex = (currentIndex + 1) % players.size(); }
    public boolean isGameOver() { return gameOver; }

    public void play() { while (!gameOver) playTurn(); }
}

Key Points to Remember

  • 1Strategy pattern for dice means swapping StandardDice for FixedDice in unit tests requires zero changes to GameController.
  • 2Storing snakes and ladders as Map<Integer, Integer> gives O(1) teleport resolution; no iteration over the board is needed.
  • 3Observer decouples event reactions (logging, UI updates, sound effects) from the core game loop entirely.
  • 4Capping movement at exactly board size (not wrapping) is a rules-correctness detail interviewers specifically probe.

Interview Questions

Sign in to ask Aria
1

How would you modify the design to support multiple dice per roll and arbitrary board sizes?

EasyAmazon
2

How would you detect and prevent an infinite game if a snake immediately follows every ladder?

MediumGoogle
3

How would you persist and resume an in-progress game state across server restarts?

MediumUber

Ask Aria about Design Snake and Ladder Game

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…