🧠 Snakes & Ladders LLD — Thought Process Guide¶
Goal: Learn how to think when designing a Low-Level Design.
📊 Class Diagram¶
classDiagram
direction TB
class Game {
-board: Board
-players: List~Player~
-dice: Dice
-overshoot: OvershootPolicy
-extra_turn_on_doubles: bool
-max_doubles: int
-observers: List~GameObserver~
-lock: Lock
+status: GameStatus
+winner: Optional~str~
+current_player: str
+positions() Dict~str, int~
+history() List~TurnResult~
+add_observer(observer: GameObserver) None
+play_turn(player_name: Optional~str~) TurnResult
+play(max_turns: int) str
-advance(start: int, steps: int) int
}
class Board {
+size: int
-jumps: Dict~int, Jump~
+jump_at(cell: int) Optional~Jump~
+resolve(cell: int) int
+jumps(kind: JumpKind) List~Jump~
}
class Jump {
<<dataclass>>
+start: int
+end: int
+kind: JumpKind
}
class Player {
<<dataclass>>
+name: str
+position: int
}
class Dice {
<<abstract>>
+roll()* Roll
}
class StandardDice {
-count: int
-sides: int
-rng: Random
+roll() Roll
}
class CrookedDice {
-rng: Random
+roll() Roll
}
class ScriptedDice {
-rolls: List~Tuple~
+roll() Roll
}
class Roll {
<<dataclass>>
+faces: Tuple~int~
+total: int
+is_doubles: bool
}
class TurnResult {
<<dataclass>>
+turn_no: int
+player: str
+roll: Roll
+start: int
+landed: int
+end: int
+jump: Optional~Jump~
+forfeited: bool
+extra_turn: bool
+won: bool
}
class GameObserver {
+on_turn(result: TurnResult) None
+on_game_over(winner: Player) None
}
class ConsoleLogger {
+on_turn(r: TurnResult) None
+on_game_over(winner: Player) None
}
class GameStatus {
<<enumeration>>
NOT_STARTED
IN_PROGRESS
FINISHED
}
class JumpKind {
<<enumeration>>
SNAKE
LADDER
}
class OvershootPolicy {
<<enumeration>>
STAY
BOUNCE
ALLOW
}
Game "1" o-- "1" Board
Game "1" *-- "2..*" Player
Game "1" o-- "1" Dice
Game "1" o-- "*" GameObserver : notifies
Game "1" *-- "*" TurnResult : history
Game --> OvershootPolicy
Game --> GameStatus
Board "1" *-- "*" Jump : keyed by start cell
Jump ..> JumpKind
Dice <|-- StandardDice
Dice <|-- CrookedDice
Dice <|-- ScriptedDice
Dice ..> Roll : produces
TurnResult --> Roll
TurnResult --> Jump
GameObserver <|-- ConsoleLogger
GameObserver ..> TurnResult
⏱️ How to Run This in a 45–60 min Interview¶
| Time | Step | What to say out loud |
|---|---|---|
| 0–5 | Clarify | "Board size and layout fixed or configurable? How many players? One die or two, extra turn on doubles? Exact roll to win, bounce, or any overshoot wins? Can snakes/ladders chain? Single process or online multiplayer?" |
| 5–12 | Entities + interfaces | "Snakes and ladders are the same thing — a jump from one cell to another — so one Jump type. Dice is the strategy that varies. Board validates once; Game owns turn order." Sketch Dice.roll() -> Roll, Board.resolve(cell), Game.play_turn(player) -> TurnResult. |
| 12–35 | Core code | Write Board with validation first, then Game.play_turn. Say which invariants the board guarantees so the loop doesn't re-check them. |
| 35–45 | Concurrency | "Online, two requests can race. One lock per game; the 'is it your turn?' check and the move live in the same critical section, so duplicates are rejected instead of double-moving." |
| 45–60 | Extension | Take the interviewer's "now add X" (two dice + doubles, play until everyone finishes, power-ups) and show it lands in one class. |
Clarifying questions worth asking (and the defaults to propose if they shrug):
- Board size? → 100, configurable.
- Overshoot rule? → exact roll needed (
STAY), configurable. - Two dice? Doubles grant an extra turn? Three doubles in a row? → yes, yes, forfeit the turn.
- Can a jump end on another jump's start? → no; it removes cycles.
- Game ends at first winner, or play on for ranks? → first winner, ranking as an extension.
- Local only, or concurrent remote players? → design for concurrent requests.
Phase 1: Identify the Nouns¶
"Players roll dice to move across a board. Landing on a snake head slides you down; landing on a ladder bottom climbs you up."
| Noun | Decision | Why |
|---|---|---|
| Board | Class | Owns size + jumps, validates the layout once |
| Snake / Ladder | One Jump dataclass |
Same data (start -> end); kind derived from direction |
| Cell | Not a class | An int is enough; per-cell objects are 80% empty |
| Player | Dataclass | Name + position |
| Dice | ABC | Varies independently (count, sides, rigged, scripted) |
| Roll | Frozen dataclass | Doubles depend on faces, not the sum |
| Overshoot rule | Enum | Three fixed behaviours; no state |
| TurnResult | Frozen dataclass | The event: history, observers, replay |
| GameObserver | Base class | Output/side effects out of the game logic |
| GameStatus | Enum | NOT_STARTED → IN_PROGRESS → FINISHED |
Phase 2: Assign Responsibilities¶
| Action | Owner | Why |
|---|---|---|
| Roll | Dice.roll() |
Strategy encapsulates randomness; inject rng for determinism |
| Validate layout | Board.__init__ |
Fail fast; the game loop can trust the board |
| Snake/ladder lookup | Board.resolve() |
O(1) because chains are banned |
| Overshoot | Game._advance() |
Policy chosen at construction |
| Turn order, doubles, win | Game.play_turn() |
One place owns the turn state machine |
| Output | GameObserver |
Console / websocket / achievements without touching Game |
Phase 3: Composition¶
Game HAS-A Board, Dice, list[Player], list[GameObserver], history: list[TurnResult]
Board HAS-A dict[start -> Jump]
Phase 4: The Turn State Machine¶
roll → doubles streak +1 (or reset)
→ streak hits limit? yes: forfeit, no move, next player
→ advance (overshoot policy) → take jump if any
→ reached last cell? yes: FINISHED
→ doubles and extra turns enabled? yes: same player again
no: next player, streak = 0
Edge cases to say out loud: winning on doubles ends the game (no extra turn); a bounce can land on a snake; overshoot under STAY still counts as the player's turn.
Phase 5: Quick Checklist¶
✅ Board invariants enforced in the constructor (no cycles, nothing on the goal)
✅ Dice is swappable and seedable; tests use ScriptedDice
✅ Doubles computed from faces, not total % 2
✅ Turn check + move under one lock; typed errors for out-of-turn and game-over
✅ Every turn produces an immutable TurnResult