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). Simplifiesget_final_position()to a single lookup instead of separate checks. - Dice as interface:
StandardDiceandLoadedDiceimplement 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