Design Snake & Ladder Game

intermediate 2-4 YOE lld snake-ladder design-question

Snake & Ladder is one of the most fun LLD questions we’ll get in an interview. It’s deceptively simple — a board, some snakes, some ladders, roll a dice, move forward. But designing it cleanly tests our OOP fundamentals: composition, encapsulation, and separation of concerns.

Let’s design it from scratch.

Requirements

Functional:

  • Board with 100 cells (numbered 1 to 100)
  • Snakes take a player DOWN from head to tail
  • Ladders take a player UP from bottom to top
  • Players take turns rolling a dice (1-6)
  • First player to reach exactly cell 100 wins
  • If a roll would take a player beyond 100, they stay put

Constraints:

  • 2-4 players
  • Snakes and ladders don’t overlap (no snake head on a ladder bottom or vice versa)
  • No snake or ladder on cell 1 or cell 100

Key Classes & Relationships

Snake & Ladder — Class Diagram
Game
- board: Board
- players: Player[]
- dice: Dice
- currentTurn: int
+ play() → runs game loop
│ has-a
Board
- size: int
- snakes: Snake[]
- ladders: Ladder[]
+ getNewPosition(pos)
Player
- name: string
- position: int
Dice
- sides: int
+ roll() → int
Board contains ↓
Snake
- head: int (higher)
- tail: int (lower)
Ladder
- bottom: int (lower)
- top: int (higher)

The key composition here: Game has a Board, Players, and a Dice. Board has Snakes and Ladders. Clean separation — the Board handles position logic, the Game handles turn management.

Core Implementation

import random

class Dice:
    def __init__(self, sides: int = 6):
        self.sides = sides

    def roll(self) -> int:
        return random.randint(1, self.sides)

class Snake:
    def __init__(self, head: int, tail: int):
        if head <= tail:
            raise ValueError("Snake head must be above tail")
        self.head = head
        self.tail = tail

class Ladder:
    def __init__(self, bottom: int, top: int):
        if bottom >= top:
            raise ValueError("Ladder bottom must be below top")
        self.bottom = bottom
        self.top = top

class Player:
    def __init__(self, name: str):
        self.name = name
        self.position = 0  # 0 means not on the board yet

class Board:
    def __init__(self, size: int, snakes: list[Snake], ladders: list[Ladder]):
        self.size = size
        self.snakes = {s.head: s.tail for s in snakes}
        self.ladders = {l.bottom: l.top for l in ladders}

    def get_new_position(self, current: int, dice_value: int) -> int:
        new_pos = current + dice_value

        # Can't go beyond the board
        if new_pos > self.size:
            return current

        # Check for snake or ladder at the new position
        if new_pos in self.snakes:
            print(f"  🐍 Snake! Sliding down from {new_pos} to {self.snakes[new_pos]}")
            return self.snakes[new_pos]

        if new_pos in self.ladders:
            print(f"  🪜 Ladder! Climbing up from {new_pos} to {self.ladders[new_pos]}")
            return self.ladders[new_pos]

        return new_pos

class Game:
    def __init__(self, players: list[Player], board: Board, dice: Dice):
        self.players = players
        self.board = board
        self.dice = dice
        self.current_turn = 0

    def play(self) -> Player:
        while True:
            player = self.players[self.current_turn]
            dice_value = self.dice.roll()
            old_pos = player.position
            player.position = self.board.get_new_position(old_pos, dice_value)
            print(f"{player.name} rolled {dice_value}: {old_pos}{player.position}")

            if player.position == self.board.size:
                print(f"\n{player.name} wins!")
                return player

            # Next player's turn
            self.current_turn = (self.current_turn + 1) % len(self.players)

# Setup and play
snakes = [Snake(62, 5), Snake(33, 6), Snake(49, 9), Snake(88, 16)]
ladders = [Ladder(2, 37), Ladder(27, 46), Ladder(56, 95), Ladder(78, 98)]
board = Board(100, snakes, ladders)
dice = Dice()
players = [Player("Alice"), Player("Bob")]

game = Game(players, board, dice)
winner = game.play()
class Dice {
  constructor(sides = 6) {
    this.sides = sides;
  }
  roll() {
    return Math.floor(Math.random() * this.sides) + 1;
  }
}

class Snake {
  constructor(head, tail) {
    this.head = head;
    this.tail = tail;
  }
}

class Ladder {
  constructor(bottom, top) {
    this.bottom = bottom;
    this.top = top;
  }
}

class Player {
  constructor(name) {
    this.name = name;
    this.position = 0;
  }
}

class Board {
  constructor(size, snakes, ladders) {
    this.size = size;
    this.snakes = new Map(snakes.map((s) => [s.head, s.tail]));
    this.ladders = new Map(ladders.map((l) => [l.bottom, l.top]));
  }

  getNewPosition(current, diceValue) {
    let newPos = current + diceValue;

    if (newPos > this.size) return current;

    if (this.snakes.has(newPos)) {
      console.log(`  Snake! ${newPos} → ${this.snakes.get(newPos)}`);
      return this.snakes.get(newPos);
    }
    if (this.ladders.has(newPos)) {
      console.log(`  Ladder! ${newPos} → ${this.ladders.get(newPos)}`);
      return this.ladders.get(newPos);
    }
    return newPos;
  }
}

class Game {
  constructor(players, board, dice) {
    this.players = players;
    this.board = board;
    this.dice = dice;
    this.currentTurn = 0;
  }

  play() {
    while (true) {
      const player = this.players[this.currentTurn];
      const diceValue = this.dice.roll();
      const oldPos = player.position;
      player.position = this.board.getNewPosition(oldPos, diceValue);
      console.log(`${player.name} rolled ${diceValue}: ${oldPos} → ${player.position}`);

      if (player.position === this.board.size) {
        console.log(`\n${player.name} wins!`);
        return player;
      }
      this.currentTurn = (this.currentTurn + 1) % this.players.length;
    }
  }
}

// Setup
const snakes = [new Snake(62, 5), new Snake(33, 6), new Snake(49, 9)];
const ladders = [new Ladder(2, 37), new Ladder(27, 46), new Ladder(56, 95)];
const board = new Board(100, snakes, ladders);
const game = new Game([new Player("Alice"), new Player("Bob")], board, new Dice());
game.play();
import java.util.*;

class Dice {
    private final int sides;
    private final Random random = new Random();

    public Dice(int sides) { this.sides = sides; }

    public int roll() { return random.nextInt(sides) + 1; }
}

class Snake {
    final int head, tail;
    public Snake(int head, int tail) { this.head = head; this.tail = tail; }
}

class Ladder {
    final int bottom, top;
    public Ladder(int bottom, int top) { this.bottom = bottom; this.top = top; }
}

class Player {
    final String name;
    int position = 0;
    public Player(String name) { this.name = name; }
}

class Board {
    private final int size;
    private final Map<Integer, Integer> snakes = new HashMap<>();
    private final Map<Integer, Integer> ladders = new HashMap<>();

    public Board(int size, List<Snake> snakeList, List<Ladder> ladderList) {
        this.size = size;
        snakeList.forEach(s -> snakes.put(s.head, s.tail));
        ladderList.forEach(l -> ladders.put(l.bottom, l.top));
    }

    public int getNewPosition(int current, int diceValue) {
        int newPos = current + diceValue;
        if (newPos > size) return current;

        if (snakes.containsKey(newPos)) {
            System.out.println("  Snake! " + newPos + " → " + snakes.get(newPos));
            return snakes.get(newPos);
        }
        if (ladders.containsKey(newPos)) {
            System.out.println("  Ladder! " + newPos + " → " + ladders.get(newPos));
            return ladders.get(newPos);
        }
        return newPos;
    }

    public int getSize() { return size; }
}

class Game {
    private final List<Player> players;
    private final Board board;
    private final Dice dice;
    private int currentTurn = 0;

    public Game(List<Player> players, Board board, Dice dice) {
        this.players = players;
        this.board = board;
        this.dice = dice;
    }

    public Player play() {
        while (true) {
            Player player = players.get(currentTurn);
            int diceValue = dice.roll();
            int oldPos = player.position;
            player.position = board.getNewPosition(oldPos, diceValue);
            System.out.println(player.name + " rolled " + diceValue
                + ": " + oldPos + " → " + player.position);

            if (player.position == board.getSize()) {
                System.out.println("\n" + player.name + " wins!");
                return player;
            }
            currentTurn = (currentTurn + 1) % players.size();
        }
    }
}

Design Patterns Used

Composition over Inheritance — This is the big one here. Game HAS a Board, Board HAS Snakes and Ladders. We didn’t create a BoardEntity base class that Snake and Ladder inherit from. Why? Because they’re fundamentally different things — one moves us down, the other moves us up. Composition keeps it clean.

Encapsulation — The Board hides HOW it calculates positions. The Game doesn’t care about snakes or ladders — it just asks the Board “given this position and dice roll, where does the player end up?” Single place for all position logic.

SRP (Single Responsibility) — Each class does one thing:

  • Dice — random number generation
  • Board — position calculation (snakes, ladders, boundaries)
  • Player — tracks name and position
  • Game — orchestrates turns and win detection

Extensions

These are common follow-up questions interviewers love to ask:

“Support multiple dice” — Change Dice.roll() to accept a count, or have the Game hold a list of Dice objects. Sum all rolls.

“Add special cells (power-ups)” — This is where we might introduce a Cell class with a landOn(player) method. Snakes, Ladders, and PowerUps would all be types of cell effects. Strategy pattern for cell behavior.

“Save and resume a game” — Serialize the game state: player positions, whose turn it is, board config. Memento pattern. Store as JSON and reload.

“What if a snake’s tail lands on another snake’s head?” — We could allow chaining: keep checking the new position until we land on a neutral cell. Just make the getNewPosition method loop until stable.

In simple language, Snake & Ladder is a composition exercise. Board owns the position logic, Game owns the turns, and everything talks through clean interfaces. No inheritance needed. Keep it flat, keep it simple.