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).

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you design a Snake and Ladder game in an OOP interview?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Core classes: Board (manages snake/ladder teleporter map as dict[int,int]), Player (tracks name and position), Dice (abstract with StandardDice and LoadedDice implementations), Snake/Ladder (value objects with validation), and SnakeLadderGame (controller managing turn order, state, and history). Unify snakes and ladders into a single teleporter map for O(1) lookup at any square. Use a MoveResult value object to capture each turn’s full context for logging and replay.”}},{“@type”:”Question”,”name”:”How do you handle the overshoot rule in Snake and Ladder?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”When a player’s roll would move them beyond square 100 (or the board’s last square), they stay in their current position. This is handled in the game controller: if (current_position + roll) > board_size, return a MoveResult indicating no movement. Alternatively, some rule variants bounce back from 100 (e.g., roll takes you to 103, you bounce to 97). The game class is configurable for either behavior by passing the rule as a strategy.”}},{“@type”:”Question”,”name”:”Why use a Dice interface in Snake and Ladder design?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The Dice abstraction (abstract class with roll() method) enables testability through dependency injection. StandardDice uses random.randint() for real gameplay. LoadedDice takes a predetermined sequence of values, making tests deterministic and reproducible. Without this abstraction, testing requires mocking random, which is fragile. The same pattern applies to any randomness source in game design: cards, shuffling, AI opponent decisions.”}}]}

🏢 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