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

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.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  self.size or ladder.bottom  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  {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)  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

🏢 Asked at: Atlassian Interview Guide

🏢 Asked at: Snap Interview Guide

🏢 Asked at: Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture

🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

Scroll to Top