# Low-Level Design: Snake and Ladder Game (OOP Interview)

Source: https://www.techinterview.org/post/3233463735/lld-snake-and-ladder-game/
Updated: 2026-07-05 · techinterview.org

## Low-Level Design: Snake and Ladder Game

Snake and Ladder (also called Chutes and Ladders) is a popular LLD interview problem that tests board game modeling, state management, and OOP design. It's simpler than chess but still exercises good design principles.

## Requirements

- N×N board (typically 10×10 = 100 squares)

- 2+ players take turns rolling dice and moving

- Snakes move players backward (from head to tail)

- Ladders move players forward (from bottom to top)

- First player to reach square 100 wins

- Support for configurable snakes, ladders, and dice

## Core Class Design


```
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
import random
from typing import Optional

class GameStatus(Enum):
    NOT_STARTED = "not_started"
    IN_PROGRESS = "in_progress"
    FINISHED = "finished"

@dataclass
class Player:
    player_id: str
    name: str
    position: int = 0  # 0 = off board; 1-100 = current square

    def __str__(self):
        return f"{self.name} (pos={self.position})"

@dataclass
class Snake:
    head: int  # Where player lands (triggers snake)
    tail: int  # Where player ends up (lower number)

    def __post_init__(self):
        if self.head <= self.tail:
            raise ValueError("Snake head must be higher than tail")

@dataclass
class Ladder:
    bottom: int  # Where player lands (triggers ladder)
    top: int  # Where player ends up (higher number)

    def __post_init__(self):
        if self.bottom >= self.top:
            raise ValueError("Ladder bottom must be lower than top")

class Dice(ABC):
    @abstractmethod
    def roll(self) -> int:
        pass

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

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

class LoadedDice(Dice):
    """Deterministic dice for testing."""
    def __init__(self, values: list[int]):
        self.values = values
        self.index = 0

    def roll(self) -> int:
        val = self.values[self.index % len(self.values)]
        self.index += 1
        return val

class Board:
    def __init__(self, size: int = 100):
        self.size = size
        self._teleporters: dict[int, int] = {}  # from -> to (both snakes and ladders)
        self._snakes: list[Snake] = []
        self._ladders: list[Ladder] = []

    def add_snake(self, snake: Snake):
        if snake.head > self.size or snake.tail < 1:
            raise ValueError("Snake out of bounds")
        self._snakes.append(snake)
        self._teleporters[snake.head] = snake.tail

    def add_ladder(self, ladder: Ladder):
        if ladder.top > self.size or ladder.bottom < 1:
            raise ValueError("Ladder out of bounds")
        self._ladders.append(ladder)
        self._teleporters[ladder.bottom] = ladder.top

    def get_final_position(self, position: int) -> tuple[int, Optional[str]]:
        """
        Returns (final_position, event_description).
        If position is > board size, player stays put (no move).
        """
        if position > self.size:
            return position, "Overshoot — no move"

        if position in self._teleporters:
            destination = self._teleporters[position]
            if destination < position:
                return destination, f"Snake! {position} -> {destination}"
            else:
                return destination, f"Ladder! {position} -> {destination}"

        return position, None

    def is_winning_position(self, position: int) -> bool:
        return position == self.size
```


### Game Controller


```
class MoveResult:
    def __init__(self, player: Player, roll: int, from_pos: int,
                 to_pos: int, event: Optional[str], won: bool):
        self.player = player
        self.roll = roll
        self.from_pos = from_pos
        self.to_pos = to_pos
        self.event = event
        self.won = won

    def __str__(self):
        parts = [f"{self.player.name} rolled {self.roll}: {self.from_pos} -> {self.to_pos}"]
        if self.event:
            parts.append(f"({self.event})")
        if self.won:
            parts.append("WON!")
        return " ".join(parts)

class SnakeLadderGame:
    def __init__(self, board: Board, dice: Dice):
        self.board = board
        self.dice = dice
        self.players: list[Player] = []
        self.current_player_idx = 0
        self.status = GameStatus.NOT_STARTED
        self.winner: Optional[Player] = None
        self.move_history: list[MoveResult] = []

    def add_player(self, player: Player):
        if self.status != GameStatus.NOT_STARTED:
            raise RuntimeError("Cannot add players after game starts")
        self.players.append(player)

    def start(self):
        if len(self.players) < 2:
            raise RuntimeError("Need at least 2 players to start")
        self.status = GameStatus.IN_PROGRESS

    def play_turn(self) -> MoveResult:
        if self.status != GameStatus.IN_PROGRESS:
            raise RuntimeError("Game not in progress")

        player = self.players[self.current_player_idx]
        roll = self.dice.roll()
        from_pos = player.position
        tentative_pos = from_pos + roll

        if tentative_pos > self.board.size:
            # Overshoot: stay in place (common rule variant)
            result = MoveResult(player, roll, from_pos, from_pos,
                              f"Overshoot ({tentative_pos} > {self.board.size})", False)
            self.move_history.append(result)
            self._advance_turn()
            return result

        final_pos, event = self.board.get_final_position(tentative_pos)
        player.position = final_pos
        won = self.board.is_winning_position(final_pos)

        if won:
            self.status = GameStatus.FINISHED
            self.winner = player

        result = MoveResult(player, roll, from_pos, final_pos, event, won)
        self.move_history.append(result)

        if not won:
            self._advance_turn()

        return result

    def _advance_turn(self):
        self.current_player_idx = (self.current_player_idx + 1) % len(self.players)

    def get_current_player(self) -> Player:
        return self.players[self.current_player_idx]

    def play_full_game(self) -> Player:
        """Auto-play until someone wins. Returns winner."""
        self.start()
        while self.status == GameStatus.IN_PROGRESS:
            result = self.play_turn()
            print(result)
        return self.winner
```


### Usage Example


```
def create_standard_game(player_names: list[str]) -> SnakeLadderGame:
    board = Board(size=100)

    # Standard snakes
    for head, tail in [(99,78),(95,75),(92,88),(89,68),(74,53),(64,60),(62,19),(49,11),(46,25),(16,6)]:
        board.add_snake(Snake(head, tail))

    # Standard ladders
    for bottom, top in [(2,38),(7,14),(8,31),(15,26),(21,42),(28,84),(36,44),(51,67),(71,91),(78,98),(87,94)]:
        board.add_ladder(Ladder(bottom, top))

    dice = StandardDice(sides=6)
    game = SnakeLadderGame(board, dice)

    for name in player_names:
        game.add_player(Player(player_id=name.lower(), name=name))

    return game

# Run a game
game = create_standard_game(["Alice", "Bob", "Charlie"])
winner = game.play_full_game()
print(f"Winner: {winner.name} after {len(game.move_history)} total moves")
```


## Key Design Decisions

- **Unified teleporter map**: Both snakes and ladders stored in a single `dict[int, int]` (from -> to). Simplifies `get_final_position()` to a single lookup instead of separate checks.

- **Dice as interface**: `StandardDice` and `LoadedDice` implement the same interface — makes the game easily testable with deterministic rolls.

- **MoveResult value object**: Captures full context of each turn (roll, from, to, event, won). Enables replay, undo, analytics, and clean logging.

- **Input validation in Board**: Snake/ladder positions validated at setup time, not during gameplay — fail fast principle.

## Interview Extensions

- **Multiple dice**: Roll N dice and sum (e.g., two 6-sided). Dice becomes a DiceSet class.

- **Undo move**: Store full history; restore player position from previous MoveResult.

- **Network multiplayer**: Event-sourcing — broadcast MoveResult events; all clients apply same events for consistent state.

- **Statistics**: Expected turns to finish a standard 10×10 board is ~39.2 (can be computed via Markov chain analysis).

🏢 Asked at: [Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering](https://techinterview.org/meta-interview-guide/)

🏢 Asked at: [Atlassian Interview Guide](https://techinterview.org/atlassian-interview-guide/)

🏢 Asked at: [Snap Interview Guide](https://techinterview.org/snap-interview-guide/)

🏢 Asked at: [Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture](https://techinterview.org/apple-interview-guide/)

🏢 Asked at: [Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering](https://techinterview.org/uber-interview-guide/)
