Snake & Ladder is one of the most fun LLD questions we’ll get in an interview. It’s deceptively simple — a board, some snakes, some ladders, roll a dice, move forward. But designing it cleanly tests our OOP fundamentals: composition, encapsulation, and separation of concerns.
Let’s design it from scratch.
Requirements
Functional:
- Board with 100 cells (numbered 1 to 100)
- Snakes take a player DOWN from head to tail
- Ladders take a player UP from bottom to top
- Players take turns rolling a dice (1-6)
- First player to reach exactly cell 100 wins
- If a roll would take a player beyond 100, they stay put
Constraints:
- 2-4 players
- Snakes and ladders don’t overlap (no snake head on a ladder bottom or vice versa)
- No snake or ladder on cell 1 or cell 100
Key Classes & Relationships
- players: Player[]
- dice: Dice
- currentTurn: int
+ play() → runs game loop
- snakes: Snake[]
- ladders: Ladder[]
+ getNewPosition(pos)
- position: int
+ roll() → int
- tail: int (lower)
- top: int (higher)
The key composition here: Game has a Board, Players, and a Dice. Board has Snakes and Ladders. Clean separation — the Board handles position logic, the Game handles turn management.
Core Implementation
import random
class Dice:
def __init__(self, sides: int = 6):
self.sides = sides
def roll(self) -> int:
return random.randint(1, self.sides)
class Snake:
def __init__(self, head: int, tail: int):
if head <= tail:
raise ValueError("Snake head must be above tail")
self.head = head
self.tail = tail
class Ladder:
def __init__(self, bottom: int, top: int):
if bottom >= top:
raise ValueError("Ladder bottom must be below top")
self.bottom = bottom
self.top = top
class Player:
def __init__(self, name: str):
self.name = name
self.position = 0 # 0 means not on the board yet
class Board:
def __init__(self, size: int, snakes: list[Snake], ladders: list[Ladder]):
self.size = size
self.snakes = {s.head: s.tail for s in snakes}
self.ladders = {l.bottom: l.top for l in ladders}
def get_new_position(self, current: int, dice_value: int) -> int:
new_pos = current + dice_value
# Can't go beyond the board
if new_pos > self.size:
return current
# Check for snake or ladder at the new position
if new_pos in self.snakes:
print(f" 🐍 Snake! Sliding down from {new_pos} to {self.snakes[new_pos]}")
return self.snakes[new_pos]
if new_pos in self.ladders:
print(f" 🪜 Ladder! Climbing up from {new_pos} to {self.ladders[new_pos]}")
return self.ladders[new_pos]
return new_pos
class Game:
def __init__(self, players: list[Player], board: Board, dice: Dice):
self.players = players
self.board = board
self.dice = dice
self.current_turn = 0
def play(self) -> Player:
while True:
player = self.players[self.current_turn]
dice_value = self.dice.roll()
old_pos = player.position
player.position = self.board.get_new_position(old_pos, dice_value)
print(f"{player.name} rolled {dice_value}: {old_pos} → {player.position}")
if player.position == self.board.size:
print(f"\n{player.name} wins!")
return player
# Next player's turn
self.current_turn = (self.current_turn + 1) % len(self.players)
# Setup and play
snakes = [Snake(62, 5), Snake(33, 6), Snake(49, 9), Snake(88, 16)]
ladders = [Ladder(2, 37), Ladder(27, 46), Ladder(56, 95), Ladder(78, 98)]
board = Board(100, snakes, ladders)
dice = Dice()
players = [Player("Alice"), Player("Bob")]
game = Game(players, board, dice)
winner = game.play()
class Dice {
constructor(sides = 6) {
this.sides = sides;
}
roll() {
return Math.floor(Math.random() * this.sides) + 1;
}
}
class Snake {
constructor(head, tail) {
this.head = head;
this.tail = tail;
}
}
class Ladder {
constructor(bottom, top) {
this.bottom = bottom;
this.top = top;
}
}
class Player {
constructor(name) {
this.name = name;
this.position = 0;
}
}
class Board {
constructor(size, snakes, ladders) {
this.size = size;
this.snakes = new Map(snakes.map((s) => [s.head, s.tail]));
this.ladders = new Map(ladders.map((l) => [l.bottom, l.top]));
}
getNewPosition(current, diceValue) {
let newPos = current + diceValue;
if (newPos > this.size) return current;
if (this.snakes.has(newPos)) {
console.log(` Snake! ${newPos} → ${this.snakes.get(newPos)}`);
return this.snakes.get(newPos);
}
if (this.ladders.has(newPos)) {
console.log(` Ladder! ${newPos} → ${this.ladders.get(newPos)}`);
return this.ladders.get(newPos);
}
return newPos;
}
}
class Game {
constructor(players, board, dice) {
this.players = players;
this.board = board;
this.dice = dice;
this.currentTurn = 0;
}
play() {
while (true) {
const player = this.players[this.currentTurn];
const diceValue = this.dice.roll();
const oldPos = player.position;
player.position = this.board.getNewPosition(oldPos, diceValue);
console.log(`${player.name} rolled ${diceValue}: ${oldPos} → ${player.position}`);
if (player.position === this.board.size) {
console.log(`\n${player.name} wins!`);
return player;
}
this.currentTurn = (this.currentTurn + 1) % this.players.length;
}
}
}
// Setup
const snakes = [new Snake(62, 5), new Snake(33, 6), new Snake(49, 9)];
const ladders = [new Ladder(2, 37), new Ladder(27, 46), new Ladder(56, 95)];
const board = new Board(100, snakes, ladders);
const game = new Game([new Player("Alice"), new Player("Bob")], board, new Dice());
game.play();
import java.util.*;
class Dice {
private final int sides;
private final Random random = new Random();
public Dice(int sides) { this.sides = sides; }
public int roll() { return random.nextInt(sides) + 1; }
}
class Snake {
final int head, tail;
public Snake(int head, int tail) { this.head = head; this.tail = tail; }
}
class Ladder {
final int bottom, top;
public Ladder(int bottom, int top) { this.bottom = bottom; this.top = top; }
}
class Player {
final String name;
int position = 0;
public Player(String name) { this.name = name; }
}
class Board {
private final int size;
private final Map<Integer, Integer> snakes = new HashMap<>();
private final Map<Integer, Integer> ladders = new HashMap<>();
public Board(int size, List<Snake> snakeList, List<Ladder> ladderList) {
this.size = size;
snakeList.forEach(s -> snakes.put(s.head, s.tail));
ladderList.forEach(l -> ladders.put(l.bottom, l.top));
}
public int getNewPosition(int current, int diceValue) {
int newPos = current + diceValue;
if (newPos > size) return current;
if (snakes.containsKey(newPos)) {
System.out.println(" Snake! " + newPos + " → " + snakes.get(newPos));
return snakes.get(newPos);
}
if (ladders.containsKey(newPos)) {
System.out.println(" Ladder! " + newPos + " → " + ladders.get(newPos));
return ladders.get(newPos);
}
return newPos;
}
public int getSize() { return size; }
}
class Game {
private final List<Player> players;
private final Board board;
private final Dice dice;
private int currentTurn = 0;
public Game(List<Player> players, Board board, Dice dice) {
this.players = players;
this.board = board;
this.dice = dice;
}
public Player play() {
while (true) {
Player player = players.get(currentTurn);
int diceValue = dice.roll();
int oldPos = player.position;
player.position = board.getNewPosition(oldPos, diceValue);
System.out.println(player.name + " rolled " + diceValue
+ ": " + oldPos + " → " + player.position);
if (player.position == board.getSize()) {
System.out.println("\n" + player.name + " wins!");
return player;
}
currentTurn = (currentTurn + 1) % players.size();
}
}
}
Design Patterns Used
Composition over Inheritance — This is the big one here. Game HAS a Board, Board HAS Snakes and Ladders. We didn’t create a BoardEntity base class that Snake and Ladder inherit from. Why? Because they’re fundamentally different things — one moves us down, the other moves us up. Composition keeps it clean.
Encapsulation — The Board hides HOW it calculates positions. The Game doesn’t care about snakes or ladders — it just asks the Board “given this position and dice roll, where does the player end up?” Single place for all position logic.
SRP (Single Responsibility) — Each class does one thing:
Dice— random number generationBoard— position calculation (snakes, ladders, boundaries)Player— tracks name and positionGame— orchestrates turns and win detection
Extensions
These are common follow-up questions interviewers love to ask:
“Support multiple dice” — Change Dice.roll() to accept a count, or have the Game hold a list of Dice objects. Sum all rolls.
“Add special cells (power-ups)” — This is where we might introduce a Cell class with a landOn(player) method. Snakes, Ladders, and PowerUps would all be types of cell effects. Strategy pattern for cell behavior.
“Save and resume a game” — Serialize the game state: player positions, whose turn it is, board config. Memento pattern. Store as JSON and reload.
“What if a snake’s tail lands on another snake’s head?” — We could allow chaining: keep checking the new position until we land on a neutral cell. Just make the getNewPosition method loop until stable.
In simple language, Snake & Ladder is a composition exercise. Board owns the position logic, Game owns the turns, and everything talks through clean interfaces. No inheritance needed. Keep it flat, keep it simple.