Designing Snake & Ladder — Turn-Based Board Game
⚡ Difficulty: Medium 🏷️ Patterns: Strategy, State, Command, Observer 🏢 Asked at: PhonePe, Flipkart, Amazon, Swiggy
Functional Requirements
- Configurable board — board size (default 100), configurable snakes and ladders with start/end positions
- Multiple players — support 2-4 players with turn-based play
- Dice rolling — pluggable dice strategy (normal, rigged, weighted)
- Game loop — turn management, automatic snake/ladder resolution, win detection
- Move validation — player can’t move beyond the last cell, must land exactly on final cell to win
- Game history — track all moves with undo capability
Non-Functional Requirements
- Thread-safety — concurrent game state access must be safe
- Extensibility — add new dice types or board elements without modifying existing code
- Correctness — no player can land on an invalid cell, snakes always go down, ladders always go up
- O(1) move resolution — snake/ladder lookup via HashMap
Core Entities
| Entity | Description |
|---|---|
Game |
Orchestrates the game loop, manages turns and win detection |
Board |
Holds the grid, snakes, ladders, and resolves cell transitions |
Player |
Name, id, current position on the board |
Snake |
Head (start) and tail (end) — moves player down |
Ladder |
Bottom (start) and top (end) — moves player up |
Dice |
Strategy interface — roll returns a value |
NormalDice |
Standard 1-6 random roll |
RiggedDice |
Returns a predetermined sequence (for testing) |
Cell |
Position on board, may contain a snake head or ladder bottom |
GameStatus |
Enum — IN_PROGRESS, FINISHED |
Move |
Command object — stores player, from, to, dice value for undo |
Class Diagram
classDiagram
class Dice {
<<interface>>
+roll() int
}
class NormalDice
class RiggedDice
class WeightedDice
class Board {
-int size
-Map~Integer-Integer~ snakes
-Map~Integer-Integer~ ladders
+resolvePosition(int pos) int
+isValidMove(int current, int roll) boolean
}
class Player {
-String id
-String name
-int position
}
class Move {
-Player player
-int fromPos
-int toPos
-int diceValue
}
class Game {
-Board board
-List~Player~ players
-Dice dice
-int currentPlayerIndex
-GameStatus status
+play()
+playTurn() Move
+undo()
}
Dice <|.. NormalDice
Dice <|.. RiggedDice
Dice <|.. WeightedDice
Game --> Board
Game --> Dice
Game --> Player
Game --> Move
Design Patterns
| Pattern | Where | Why |
|---|---|---|
| Strategy | Dice with Normal/Rigged/Weighted |
Swap dice behavior without changing game logic |
| State | GameStatus controlling game loop flow |
Game behaves differently when IN_PROGRESS vs FINISHED |
| Command | Move objects with undo support |
Record and reverse moves for game history |
| Observer | GameObserver on game events |
Notify UI or logger when moves happen, game ends |
How It All Fits Together
Here’s what happens during a single turn:
- Game checks if status is IN_PROGRESS — if FINISHED, no-op
- Current player is selected via round-robin index
- Dice strategy is invoked —
dice.roll()returns 1-6 (or whatever the strategy dictates) - Board validates the move — if
currentPos + roll > boardSize, player stays put - If valid, player moves to
currentPos + roll - Board resolves the new position — checks for snake head or ladder bottom at that cell
- If snake: player slides down to tail. If ladder: player climbs to top
- Move is recorded as a Command object for undo capability
- All registered GameObservers are notified of the move
- If player reaches the final cell (boardSize), game status → FINISHED, player wins
- Turn index advances to next player
💡 The board resolution is O(1) — snakes and ladders are stored in HashMaps keyed by their start position. No iteration over the board needed.
Complete Code
Player
A mutable entity representing a player on the board. Position starts at 0 (off-board) and advances toward the board size.
package snakeladder.model;
public class Player {
private final String id;
private final String name;
private int position;
public Player(String id, String name) {
this.id = id;
this.name = name;
this.position = 0; // starts off-board
}
public String getId() { return id; }
public String getName() { return name; }
public int getPosition() { return position; }
public void setPosition(int position) { this.position = position; }
@Override
public String toString() { return name + " (pos=" + position + ")"; }
}
class Player:
def __init__(self, id: str, name: str):
self.id = id
self.name = name
self.position = 0 # starts off-board
def __str__(self):
return f"{self.name} (pos={self.position})"
#pragma once
#include <string>
#include <iostream>
class Player {
public:
std::string id;
std::string name;
int position;
Player(std::string id, std::string name)
: id(std::move(id)), name(std::move(name)), position(0) {}
friend std::ostream& operator<<(std::ostream& os, const Player& p) {
os << p.name << " (pos=" << p.position << ")";
return os;
}
};
Snake and Ladder
Simple value objects. A Snake has a head (higher position) and tail (lower position). A Ladder has a bottom (lower) and top (higher). Validation ensures snakes go down and ladders go up.
package snakeladder.model;
public class Snake {
private final int head;
private final int tail;
public Snake(int head, int tail) {
if (head <= tail) throw new IllegalArgumentException("Snake head must be above tail");
this.head = head;
this.tail = tail;
}
public int getHead() { return head; }
public int getTail() { return tail; }
@Override
public String toString() { return "Snake[" + head + " → " + tail + "]"; }
}
// --- Ladder.java ---
package snakeladder.model;
public class Ladder {
private final int bottom;
private final int top;
public Ladder(int bottom, int top) {
if (bottom >= top) throw new IllegalArgumentException("Ladder bottom must be below top");
this.bottom = bottom;
this.top = top;
}
public int getBottom() { return bottom; }
public int getTop() { return top; }
@Override
public String toString() { return "Ladder[" + bottom + " → " + top + "]"; }
}
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
def __str__(self):
return f"Snake[{self.head} → {self.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
def __str__(self):
return f"Ladder[{self.bottom} → {self.top}]"
#pragma once
#include <stdexcept>
#include <iostream>
class Snake {
public:
int head;
int tail;
Snake(int head, int tail) : head(head), tail(tail) {
if (head <= tail) throw std::invalid_argument("Snake head must be above tail");
}
friend std::ostream& operator<<(std::ostream& os, const Snake& s) {
os << "Snake[" << s.head << " -> " << s.tail << "]";
return os;
}
};
class Ladder {
public:
int bottom;
int top;
Ladder(int bottom, int top) : bottom(bottom), top(top) {
if (bottom >= top) throw std::invalid_argument("Ladder bottom must be below top");
}
friend std::ostream& operator<<(std::ostream& os, const Ladder& l) {
os << "Ladder[" << l.bottom << " -> " << l.top << "]";
return os;
}
};
Dice (Strategy Interface)
The strategy interface for dice rolling. NormalDice gives a uniform random 1-6. RiggedDice returns a predetermined sequence (useful for testing deterministic game outcomes). WeightedDice allows custom probability distributions.
💡 Strategy pattern = define a family of algorithms, encapsulate each one, and make them interchangeable. The Game doesn’t know or care which dice implementation it uses — it just calls roll().
package snakeladder.dice;
public interface Dice {
int roll();
}
// --- NormalDice.java ---
package snakeladder.dice;
import java.util.Random;
public class NormalDice implements Dice {
private final Random random = new Random();
private final int faces;
public NormalDice() { this.faces = 6; }
public NormalDice(int faces) { this.faces = faces; }
@Override
public int roll() { return random.nextInt(faces) + 1; }
}
// --- RiggedDice.java ---
package snakeladder.dice;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class RiggedDice implements Dice {
private final Queue<Integer> values;
public RiggedDice(List<Integer> values) {
this.values = new LinkedList<>(values);
}
@Override
public int roll() {
if (values.isEmpty()) throw new IllegalStateException("No more rigged values");
return values.poll();
}
}
// --- WeightedDice.java ---
package snakeladder.dice;
import java.util.Random;
public class WeightedDice implements Dice {
private final double[] cumulativeWeights;
private final Random random = new Random();
public WeightedDice(double[] weights) {
this.cumulativeWeights = new double[weights.length];
double sum = 0;
for (int i = 0; i < weights.length; i++) {
sum += weights[i];
cumulativeWeights[i] = sum;
}
// Normalize
for (int i = 0; i < cumulativeWeights.length; i++) {
cumulativeWeights[i] /= sum;
}
}
@Override
public int roll() {
double r = random.nextDouble();
for (int i = 0; i < cumulativeWeights.length; i++) {
if (r <= cumulativeWeights[i]) return i + 1;
}
return cumulativeWeights.length;
}
}
import random as rand_module
from abc import ABC, abstractmethod
from collections import deque
class Dice(ABC):
@abstractmethod
def roll(self) -> int:
pass
class NormalDice(Dice):
def __init__(self, faces: int = 6):
self.faces = faces
def roll(self) -> int:
return rand_module.randint(1, self.faces)
class RiggedDice(Dice):
def __init__(self, values: list[int]):
self._values = deque(values)
def roll(self) -> int:
if not self._values:
raise RuntimeError("No more rigged values")
return self._values.popleft()
class WeightedDice(Dice):
def __init__(self, weights: list[float]):
total = sum(weights)
self._cumulative = []
cum = 0.0
for w in weights:
cum += w / total
self._cumulative.append(cum)
def roll(self) -> int:
r = rand_module.random()
for i, cw in enumerate(self._cumulative):
if r <= cw:
return i + 1
return len(self._cumulative)
#pragma once
#include <random>
#include <vector>
#include <queue>
#include <stdexcept>
class Dice {
public:
virtual ~Dice() = default;
virtual int roll() = 0;
};
class NormalDice : public Dice {
std::mt19937 rng{std::random_device{}()};
int faces;
public:
NormalDice(int faces = 6) : faces(faces) {}
int roll() override {
std::uniform_int_distribution<int> dist(1, faces);
return dist(rng);
}
};
class RiggedDice : public Dice {
std::queue<int> values;
public:
RiggedDice(const std::vector<int>& vals) {
for (int v : vals) values.push(v);
}
int roll() override {
if (values.empty()) throw std::runtime_error("No more rigged values");
int v = values.front();
values.pop();
return v;
}
};
class WeightedDice : public Dice {
std::vector<double> cumulative;
std::mt19937 rng{std::random_device{}()};
public:
WeightedDice(const std::vector<double>& weights) {
double sum = 0;
for (double w : weights) { sum += w; cumulative.push_back(sum); }
for (double& c : cumulative) c /= sum;
}
int roll() override {
std::uniform_real_distribution<double> dist(0.0, 1.0);
double r = dist(rng);
for (int i = 0; i < (int)cumulative.size(); i++) {
if (r <= cumulative[i]) return i + 1;
}
return (int)cumulative.size();
}
};
Board
The board holds all snakes and ladders in HashMaps for O(1) lookup. resolvePosition() checks if a position has a snake head or ladder bottom and returns the resolved final position. Validation ensures no overlapping snakes/ladders at the same cell.
package snakeladder.model;
import java.util.*;
public class Board {
private final int size;
private final Map<Integer, Integer> snakes; // head -> tail
private final Map<Integer, Integer> ladders; // bottom -> top
public Board(int size, List<Snake> snakeList, List<Ladder> ladderList) {
this.size = size;
this.snakes = new HashMap<>();
this.ladders = new HashMap<>();
for (Snake s : snakeList) {
if (snakes.containsKey(s.getHead()) || ladders.containsKey(s.getHead())) {
throw new IllegalArgumentException("Overlap at position " + s.getHead());
}
snakes.put(s.getHead(), s.getTail());
}
for (Ladder l : ladderList) {
if (ladders.containsKey(l.getBottom()) || snakes.containsKey(l.getBottom())) {
throw new IllegalArgumentException("Overlap at position " + l.getBottom());
}
ladders.put(l.getBottom(), l.getTop());
}
}
public int getSize() { return size; }
public boolean isValidMove(int currentPos, int diceValue) {
return currentPos + diceValue <= size;
}
public int resolvePosition(int position) {
if (snakes.containsKey(position)) {
System.out.println(" 🐍 Snake! Slide from " + position + " to " + snakes.get(position));
return snakes.get(position);
}
if (ladders.containsKey(position)) {
System.out.println(" 🪜 Ladder! Climb from " + position + " to " + ladders.get(position));
return ladders.get(position);
}
return position;
}
public boolean isWinningPosition(int position) {
return position == size;
}
}
class Board:
def __init__(self, size: int, snakes: list['Snake'], ladders: list['Ladder']):
self.size = size
self._snakes: dict[int, int] = {} # head -> tail
self._ladders: dict[int, int] = {} # bottom -> top
for s in snakes:
if s.head in self._snakes or s.head in self._ladders:
raise ValueError(f"Overlap at position {s.head}")
self._snakes[s.head] = s.tail
for l in ladders:
if l.bottom in self._ladders or l.bottom in self._snakes:
raise ValueError(f"Overlap at position {l.bottom}")
self._ladders[l.bottom] = l.top
def is_valid_move(self, current_pos: int, dice_value: int) -> bool:
return current_pos + dice_value <= self.size
def resolve_position(self, position: int) -> int:
if position in self._snakes:
print(f" 🐍 Snake! Slide from {position} to {self._snakes[position]}")
return self._snakes[position]
if position in self._ladders:
print(f" 🪜 Ladder! Climb from {position} to {self._ladders[position]}")
return self._ladders[position]
return position
def is_winning_position(self, position: int) -> bool:
return position == self.size
#pragma once
#include <unordered_map>
#include <vector>
#include <stdexcept>
#include <iostream>
#include "SnakeLadder.h"
class Board {
int size;
std::unordered_map<int, int> snakes; // head -> tail
std::unordered_map<int, int> ladders; // bottom -> top
public:
Board(int size, const std::vector<Snake>& snakeList,
const std::vector<Ladder>& ladderList) : size(size) {
for (const auto& s : snakeList) {
if (snakes.count(s.head) || ladders.count(s.head))
throw std::invalid_argument("Overlap at position");
snakes[s.head] = s.tail;
}
for (const auto& l : ladderList) {
if (ladders.count(l.bottom) || snakes.count(l.bottom))
throw std::invalid_argument("Overlap at position");
ladders[l.bottom] = l.top;
}
}
int getSize() const { return size; }
bool isValidMove(int currentPos, int diceValue) const {
return currentPos + diceValue <= size;
}
int resolvePosition(int position) const {
auto sit = snakes.find(position);
if (sit != snakes.end()) {
std::cout << " Snake! Slide from " << position
<< " to " << sit->second << "\n";
return sit->second;
}
auto lit = ladders.find(position);
if (lit != ladders.end()) {
std::cout << " Ladder! Climb from " << position
<< " to " << lit->second << "\n";
return lit->second;
}
return position;
}
bool isWinningPosition(int position) const {
return position == size;
}
};
Move (Command)
The Command pattern — each move is recorded as an object containing the player, from/to positions, and dice value. This enables undo functionality by simply restoring the player to the previous position.
💡 Command pattern = encapsulate a request as an object. Each Move stores everything needed to undo itself — the player reference and the original position. No need to recompute anything.
package snakeladder.model;
public class Move {
private final Player player;
private final int fromPosition;
private final int toPosition;
private final int diceValue;
public Move(Player player, int fromPosition, int toPosition, int diceValue) {
this.player = player;
this.fromPosition = fromPosition;
this.toPosition = toPosition;
this.diceValue = diceValue;
}
public Player getPlayer() { return player; }
public int getFromPosition() { return fromPosition; }
public int getToPosition() { return toPosition; }
public int getDiceValue() { return diceValue; }
public void undo() {
player.setPosition(fromPosition);
}
@Override
public String toString() {
return player.getName() + ": " + fromPosition + " → " + toPosition +
" (rolled " + diceValue + ")";
}
}
class Move:
def __init__(self, player: Player, from_pos: int, to_pos: int, dice_value: int):
self.player = player
self.from_pos = from_pos
self.to_pos = to_pos
self.dice_value = dice_value
def undo(self):
self.player.position = self.from_pos
def __str__(self):
return f"{self.player.name}: {self.from_pos} → {self.to_pos} (rolled {self.dice_value})"
#pragma once
#include "Player.h"
#include <iostream>
#include <memory>
class Move {
public:
std::shared_ptr<Player> player;
int fromPosition;
int toPosition;
int diceValue;
Move(std::shared_ptr<Player> player, int from, int to, int dice)
: player(std::move(player)), fromPosition(from), toPosition(to), diceValue(dice) {}
void undo() {
player->position = fromPosition;
}
friend std::ostream& operator<<(std::ostream& os, const Move& m) {
os << m.player->name << ": " << m.fromPosition << " -> "
<< m.toPosition << " (rolled " << m.diceValue << ")";
return os;
}
};
GameObserver
Observer interface to decouple game events from their consumers. Logging, UI updates, analytics — all can hook in without modifying the Game class.
package snakeladder.observer;
import snakeladder.model.Move;
import snakeladder.model.Player;
public interface GameObserver {
void onMoveMade(Move move);
void onGameOver(Player winner);
}
class GameObserver:
def on_move_made(self, move: Move):
pass
def on_game_over(self, winner: Player):
pass
#pragma once
#include "Move.h"
#include "Player.h"
#include <memory>
class GameObserver {
public:
virtual ~GameObserver() = default;
virtual void onMoveMade(const Move& move) = 0;
virtual void onGameOver(const std::shared_ptr<Player>& winner) = 0;
};
Game (Orchestrator)
The main game class that ties everything together. It manages the turn loop, delegates to the Board for move resolution, records Moves for history/undo, and notifies observers. The play() method runs the entire game to completion.
package snakeladder;
import snakeladder.dice.Dice;
import snakeladder.model.*;
import snakeladder.observer.GameObserver;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public enum GameStatus { IN_PROGRESS, FINISHED }
// --- Game.java ---
package snakeladder;
import snakeladder.dice.Dice;
import snakeladder.model.*;
import snakeladder.observer.GameObserver;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class Game {
private final Board board;
private final List<Player> players;
private final Dice dice;
private final Deque<Move> moveHistory = new ArrayDeque<>();
private final List<GameObserver> observers = new CopyOnWriteArrayList<>();
private int currentPlayerIndex = 0;
private GameStatus status = GameStatus.IN_PROGRESS;
private Player winner = null;
public Game(Board board, List<Player> players, Dice dice) {
if (players.size() < 2 || players.size() > 4) {
throw new IllegalArgumentException("Need 2-4 players");
}
this.board = board;
this.players = new ArrayList<>(players);
this.dice = dice;
}
public void addObserver(GameObserver observer) {
observers.add(observer);
}
public Move playTurn() {
if (status == GameStatus.FINISHED) return null;
Player current = players.get(currentPlayerIndex);
int diceValue = dice.roll();
int oldPos = current.getPosition();
System.out.println(" " + current.getName() + " rolled " + diceValue);
if (!board.isValidMove(oldPos, diceValue)) {
System.out.println(" Cannot move — would exceed board. Stay at " + oldPos);
Move move = new Move(current, oldPos, oldPos, diceValue);
moveHistory.push(move);
advanceTurn();
return move;
}
int newPos = oldPos + diceValue;
newPos = board.resolvePosition(newPos);
current.setPosition(newPos);
Move move = new Move(current, oldPos, newPos, diceValue);
moveHistory.push(move);
for (GameObserver obs : observers) obs.onMoveMade(move);
if (board.isWinningPosition(newPos)) {
status = GameStatus.FINISHED;
winner = current;
System.out.println(" 🏆 " + current.getName() + " WINS!");
for (GameObserver obs : observers) obs.onGameOver(winner);
} else {
advanceTurn();
}
return move;
}
public void play() {
System.out.println("Game started with " + players.size() + " players!");
System.out.println("Board size: " + board.getSize() + "\n");
int turn = 1;
while (status == GameStatus.FINISHED == false) {
System.out.println("Turn " + turn + ":");
playTurn();
System.out.println();
turn++;
}
}
public void undo() {
if (moveHistory.isEmpty()) return;
Move last = moveHistory.pop();
last.undo();
// Move back to previous player
currentPlayerIndex = (currentPlayerIndex - 1 + players.size()) % players.size();
if (status == GameStatus.FINISHED) {
status = GameStatus.IN_PROGRESS;
winner = null;
}
}
public GameStatus getStatus() { return status; }
public Player getWinner() { return winner; }
public List<Move> getMoveHistory() { return new ArrayList<>(moveHistory); }
private void advanceTurn() {
currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
}
}
from enum import Enum
from collections import deque
class GameStatus(Enum):
IN_PROGRESS = "IN_PROGRESS"
FINISHED = "FINISHED"
class Game:
def __init__(self, board: Board, players: list[Player], dice: Dice):
if len(players) < 2 or len(players) > 4:
raise ValueError("Need 2-4 players")
self.board = board
self.players = list(players)
self.dice = dice
self._move_history: deque[Move] = deque()
self._observers: list[GameObserver] = []
self._current_index = 0
self.status = GameStatus.IN_PROGRESS
self.winner = None
def add_observer(self, observer: GameObserver):
self._observers.append(observer)
def play_turn(self) -> Move | None:
if self.status == GameStatus.FINISHED:
return None
current = self.players[self._current_index]
dice_value = self.dice.roll()
old_pos = current.position
print(f" {current.name} rolled {dice_value}")
if not self.board.is_valid_move(old_pos, dice_value):
print(f" Cannot move — would exceed board. Stay at {old_pos}")
move = Move(current, old_pos, old_pos, dice_value)
self._move_history.append(move)
self._advance_turn()
return move
new_pos = old_pos + dice_value
new_pos = self.board.resolve_position(new_pos)
current.position = new_pos
move = Move(current, old_pos, new_pos, dice_value)
self._move_history.append(move)
for obs in self._observers:
obs.on_move_made(move)
if self.board.is_winning_position(new_pos):
self.status = GameStatus.FINISHED
self.winner = current
print(f" 🏆 {current.name} WINS!")
for obs in self._observers:
obs.on_game_over(self.winner)
else:
self._advance_turn()
return move
def play(self):
print(f"Game started with {len(self.players)} players!")
print(f"Board size: {self.board.size}\n")
turn = 1
while self.status != GameStatus.FINISHED:
print(f"Turn {turn}:")
self.play_turn()
print()
turn += 1
def undo(self):
if not self._move_history:
return
last = self._move_history.pop()
last.undo()
self._current_index = (self._current_index - 1) % len(self.players)
if self.status == GameStatus.FINISHED:
self.status = GameStatus.IN_PROGRESS
self.winner = None
def _advance_turn(self):
self._current_index = (self._current_index + 1) % len(self.players)
#pragma once
#include <vector>
#include <deque>
#include <memory>
#include <iostream>
#include <stdexcept>
#include "Board.h"
#include "Player.h"
#include "Dice.h"
#include "Move.h"
#include "GameObserver.h"
enum class GameStatus { IN_PROGRESS, FINISHED };
class Game {
Board board;
std::vector<std::shared_ptr<Player>> players;
std::shared_ptr<Dice> dice;
std::deque<Move> moveHistory;
std::vector<GameObserver*> observers;
int currentPlayerIndex = 0;
GameStatus status = GameStatus::IN_PROGRESS;
std::shared_ptr<Player> winner = nullptr;
void advanceTurn() {
currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
}
public:
Game(Board board, std::vector<std::shared_ptr<Player>> players,
std::shared_ptr<Dice> dice)
: board(std::move(board)), players(std::move(players)), dice(std::move(dice)) {
if (this->players.size() < 2 || this->players.size() > 4)
throw std::invalid_argument("Need 2-4 players");
}
void addObserver(GameObserver* obs) { observers.push_back(obs); }
Move playTurn() {
auto current = players[currentPlayerIndex];
int diceValue = dice->roll();
int oldPos = current->position;
std::cout << " " << current->name << " rolled " << diceValue << "\n";
if (!board.isValidMove(oldPos, diceValue)) {
std::cout << " Cannot move — stay at " << oldPos << "\n";
Move move(current, oldPos, oldPos, diceValue);
moveHistory.push_back(move);
advanceTurn();
return move;
}
int newPos = oldPos + diceValue;
newPos = board.resolvePosition(newPos);
current->position = newPos;
Move move(current, oldPos, newPos, diceValue);
moveHistory.push_back(move);
for (auto* obs : observers) obs->onMoveMade(move);
if (board.isWinningPosition(newPos)) {
status = GameStatus::FINISHED;
winner = current;
std::cout << " " << current->name << " WINS!\n";
for (auto* obs : observers) obs->onGameOver(winner);
} else {
advanceTurn();
}
return move;
}
void play() {
std::cout << "Game started with " << players.size() << " players!\n";
std::cout << "Board size: " << board.getSize() << "\n\n";
int turn = 1;
while (status != GameStatus::FINISHED) {
std::cout << "Turn " << turn << ":\n";
playTurn();
std::cout << "\n";
turn++;
}
}
void undo() {
if (moveHistory.empty()) return;
Move last = moveHistory.back();
moveHistory.pop_back();
last.undo();
currentPlayerIndex = (currentPlayerIndex - 1 + players.size()) % players.size();
if (status == GameStatus::FINISHED) {
status = GameStatus::IN_PROGRESS;
winner = nullptr;
}
}
GameStatus getStatus() const { return status; }
};
Demo (Runnable)
The demo creates a board with snakes and ladders, adds 3 players, uses a RiggedDice for deterministic output, and plays the game to completion — showing every turn, snake/ladder interactions, and the final winner.
package snakeladder;
import snakeladder.dice.*;
import snakeladder.model.*;
import snakeladder.observer.GameObserver;
import java.util.*;
public class Demo {
public static void main(String[] args) {
System.out.println("══════ SNAKE & LADDER DEMO ══════\n");
// Configure snakes
List<Snake> snakes = Arrays.asList(
new Snake(17, 7),
new Snake(54, 34),
new Snake(62, 19),
new Snake(98, 79)
);
// Configure ladders
List<Ladder> ladders = Arrays.asList(
new Ladder(3, 22),
new Ladder(5, 8),
new Ladder(11, 26),
new Ladder(20, 29),
new Ladder(71, 91)
);
// Create board
Board board = new Board(100, snakes, ladders);
// Create players
List<Player> players = Arrays.asList(
new Player("p1", "Alice"),
new Player("p2", "Bob"),
new Player("p3", "Charlie")
);
// Use rigged dice for deterministic demo
List<Integer> rolls = Arrays.asList(
3, 6, 5, // Turn 1: Alice->3(ladder to 22), Bob->6, Charlie->5(ladder to 8)
4, 5, 6, // Turn 2: Alice->26(ladder!), Bob->11(ladder to 26), Charlie->14
4, 4, 6, // Turn 3: Alice->30, Bob->30, Charlie->20(ladder to 29)
6, 5, 6, // Turn 4: Alice->36, Bob->35, Charlie->35
6, 6, 6, // Turn 5: Alice->42, Bob->41, Charlie->41
6, 6, 6, // Turn 6: Alice->48, Bob->47, Charlie->47
6, 6, 6, // Turn 7: Alice->54(snake! to 34), Bob->53, Charlie->53
6, 6, 6, // Turn 8: Alice->40, Bob->59, Charlie->59
6, 6, 6, // Turn 9: Alice->46, Bob->65, Charlie->65
6, 6, 5, // Turn 10: Alice->52, Bob->71(ladder to 91), Charlie->70
6, 5, 1, // Turn 11: Alice->58, Bob->96, Charlie->71(ladder to 91)
4, 4, 6 // Turn 12: Alice->62(snake! to 19), Bob->100 WIN!
);
Dice dice = new RiggedDice(rolls);
// Create game
Game game = new Game(board, players, dice);
game.addObserver(new GameObserver() {
public void onMoveMade(Move m) {}
public void onGameOver(Player w) {
System.out.println("\n 📣 Game Over! Winner: " + w.getName());
}
});
// Play the game
game.play();
// Show move history
System.out.println("--- Move History (last 5) ---");
List<Move> history = game.getMoveHistory();
int start = Math.max(0, history.size() - 5);
for (int i = start; i < history.size(); i++) {
System.out.println(" " + history.get(i));
}
System.out.println("\n══════ DONE ══════");
}
}
def demo():
print("══════ SNAKE & LADDER DEMO ══════\n")
# Configure snakes
snakes = [
Snake(17, 7),
Snake(54, 34),
Snake(62, 19),
Snake(98, 79),
]
# Configure ladders
ladders = [
Ladder(3, 22),
Ladder(5, 8),
Ladder(11, 26),
Ladder(20, 29),
Ladder(71, 91),
]
# Create board
board = Board(100, snakes, ladders)
# Create players
players = [
Player("p1", "Alice"),
Player("p2", "Bob"),
Player("p3", "Charlie"),
]
# Use rigged dice for deterministic demo
rolls = [
3, 6, 5, # Turn 1
4, 5, 6, # Turn 2
4, 4, 6, # Turn 3
6, 5, 6, # Turn 4
6, 6, 6, # Turn 5
6, 6, 6, # Turn 6
6, 6, 6, # Turn 7
6, 6, 6, # Turn 8
6, 6, 6, # Turn 9
6, 6, 5, # Turn 10
6, 5, 1, # Turn 11
4, 4, 6, # Turn 12
]
dice = RiggedDice(rolls)
# Create game
class ConsoleObserver(GameObserver):
def on_move_made(self, move):
pass
def on_game_over(self, winner):
print(f"\n 📣 Game Over! Winner: {winner.name}")
game = Game(board, players, dice)
game.add_observer(ConsoleObserver())
# Play the game
game.play()
# Show move history (last 5)
print("--- Move History (last 5) ---")
history = list(game._move_history)
for m in history[-5:]:
print(f" {m}")
print("\n══════ DONE ══════")
if __name__ == "__main__":
demo()
#include <iostream>
#include <vector>
#include <memory>
#include "Game.h"
#include "Dice.h"
#include "SnakeLadder.h"
class ConsoleObserver : public GameObserver {
public:
void onMoveMade(const Move& m) override {}
void onGameOver(const std::shared_ptr<Player>& w) override {
std::cout << "\n Game Over! Winner: " << w->name << "\n";
}
};
int main() {
std::cout << "══════ SNAKE & LADDER DEMO ══════\n\n";
// Configure snakes and ladders
std::vector<Snake> snakes = {
Snake(17, 7), Snake(54, 34), Snake(62, 19), Snake(98, 79)
};
std::vector<Ladder> ladders = {
Ladder(3, 22), Ladder(5, 8), Ladder(11, 26),
Ladder(20, 29), Ladder(71, 91)
};
Board board(100, snakes, ladders);
// Create players
auto alice = std::make_shared<Player>("p1", "Alice");
auto bob = std::make_shared<Player>("p2", "Bob");
auto charlie = std::make_shared<Player>("p3", "Charlie");
std::vector<std::shared_ptr<Player>> players = {alice, bob, charlie};
// Rigged dice for deterministic demo
std::vector<int> rolls = {
3, 6, 5, 4, 5, 6, 4, 4, 6, 6, 5, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 5, 6, 5, 1, 4, 4, 6
};
auto dice = std::make_shared<RiggedDice>(rolls);
// Create game
Game game(board, players, dice);
ConsoleObserver observer;
game.addObserver(&observer);
// Play
game.play();
std::cout << "\n══════ DONE ══════\n";
return 0;
}
Game Loop — Algorithm Walkthrough
flowchart LR
A[Start Turn] --> B[Get Current Player]
B --> C[Roll Dice via Strategy]
C --> D{Valid Move?}
D -->|No| E[Stay Put - Advance Turn]
D -->|Yes| F[Move to New Position]
F --> G[Resolve Snakes and Ladders]
G --> H{Reached Final Cell?}
H -->|Yes| I[Player Wins - Game Over]
H -->|No| J[Record Move - Advance Turn]
J --> A
E --> A
Turn Resolution:
- Roll dice → get value (1-6 for normal dice)
- Check if
currentPos + roll > boardSize→ if yes, skip turn - Move player to
currentPos + roll - Check HashMap for snake at that position → slide down if found
- Check HashMap for ladder at that position → climb up if found
- Check if position == boardSize → declare winner
- Advance turn index
How to Extend
| Feature | Implementation |
|---|---|
| Double Roll | New dice strategy that grants extra turn on doubles |
| Power-Ups | New BoardElement interface — shield (ignore next snake), boost (double next roll) |
| Multiplayer Online | Wrap Game in a network adapter, serialize Move objects |
| Undo/Redo | Already supported — undo() pops from move stack |
| Variable Board | Pass different size + snake/ladder configs to Board constructor |
What Interviewers Look For
- ✅ Strategy pattern for dice — no hardcoded random logic in Game
- ✅ Command pattern for moves — undo support with stored state
- ✅ O(1) snake/ladder resolution — HashMap lookup, no board traversal
- ✅ Validation — snakes go down, ladders go up, no overlaps, can’t exceed board
- ✅ Observer — decoupled event notifications
- ✅ Clean game loop — single responsibility per class
- ✅ Runnable demo — deterministic game with rigged dice proves correctness
Related Designs
- Splitwise — Strategy pattern and Observer
- Parking Lot — Strategy-based assignment and extensible design
💬 Comments