Design Snake and Ladder, Low Level Design (LLD) Interview
Snake and ladder on any board size. Snakes and ladders live in one map from start square to end square. The dice and the finishing rule can be swapped. The board refuses bad layouts, and seeded simulations are checked against exact maths.
Where it shows up
A popular machine-coding question, often given as a 60 to 90 minute task where the code must run.
Two courses by the author of this page:
770 lessons · 18 free to read
₹499 in India · $49 elsewhere, once
Get System DesignYou own this course
204 lessons · 10 free to read
₹999 in India · $49 elsewhere, once
Get AI EngineeringYou own this course
Spec sheetSnake and Ladder
- 01Core classes
- Game, Board, Dice, FinishRule, Player, Turn
- 02Design patterns
- Strategy, One map for snakes and ladders, Check when built, A queue for turns
- 03Key methods
- 4 signatures, with the code skeleton
worked through below, with the maths
Why this is asked
The game is simple, so the round tests how the code is built. Does the candidate see that a snake and a ladder are both just a jump from one square to another? Do they keep the dice behind an interface, so tests can control it? Do they check the board before a game starts, so a bad layout cannot loop forever? Strong candidates also show how they test a game that uses random numbers.
Requirements
Functional
- A board has squares 1 to N, usually 100. Players start off the board, on square 0.
- Snakes take a player down and ladders take a player up.
- Two or more players take turns in a fixed order. Each turn is one roll of the dice.
- The first player to reach the last square wins.
- The rule for a roll that goes past the last square can be chosen: stay put (exact landing) or win anyway.
- The game can be played with a real random die, or with set rolls for tests.
Constraints & non-functional
- A snake or ladder may not start on the last square, may not leave the board, and may not go nowhere.
- A jump may not end where another jump starts. This rules out chains and loops.
- A game with a seeded die gives one result, every time, so a failed test can be replayed.
- New dice kinds and new finishing rules can be added without changing the game.
Core classes & entities
Game
Runs the turns. It takes the next player from the queue, rolls, applies the finishing rule and any jump, and puts the player back unless they won.
attrs: board: Board, dice: Dice, rule: FinishRule, queue: Deque<Player>, winner
methods: playTurn(): Turn, playToEnd(maxTurns), winner()
Board
The size and one map from start square to end square. A ladder goes up and a snake goes down. It checks the layout when it is made.
attrs: last, jumps: Map<Integer, Integer>
methods: after(square), jumps()
Dice
The interface for anything that gives a roll. RandomDice uses a seeded random number source. ScriptedDice returns set rolls for tests.
methods: roll(), faces()
FinishRule
Decides where a roll takes a player near the end. ExactLanding stays put on an overshoot. OvershootWins lets any big enough roll win.
methods: move(from, roll, last), name()
Player
A name, the current square and how many turns this player has taken.
attrs: name, square, turns
Turn
A record of one turn: who, the roll, where they started, where they landed, and where they ended after any jump. Useful for tests and for showing the game.
attrs: player, roll, from, landed, end
Relationships
- Game → association → Board. A board can be shared by many games.
- Game → association → Dice. The dice is passed in, so a test can pass set rolls.
- Game → association → FinishRule. The finishing rule is passed in.
- Game → composition → Player. The game makes its players from a list of names and holds them in a queue.
- Game → association → Turn. Each call to playTurn returns one Turn.
Design patterns used
Strategy in Dice (RandomDice, ScriptedDice) and FinishRule (ExactLanding, OvershootWins)
The parts that change from one version of the game to another sit behind small interfaces. The game loop never changes.
One map for snakes and ladders in Board.jumps
A snake and a ladder both move a player from one square to another. One map means one lookup and no special cases.
Check when built in the Board constructor
A bad layout is refused before any game starts. So a game can never loop forever or move a player off the board.
A queue for turns in Game.queue
Take the player at the front, play, and put them at the back. Any number of players works with no change.
Key API / methods
Board(int last, Map<Integer,Integer> jumps)Makes a board and checks the layout. It throws an error for a jump on the last square, a jump off the board, a jump that goes nowhere, or a jump that ends where another starts.
Turn Game.playTurn()Plays one turn for the player at the front of the queue and returns what happened.
int FinishRule.move(int from, int roll, int last)Returns the square the roll reaches before any jump, using the chosen rule near the end.
int Dice.roll()Returns the next roll. A seeded random die repeats its rolls on every run.
Code skeleton
import java.util.*;
// ---------- Dice (Strategy): random for real games, seeded or scripted for tests ----------
interface Dice { int roll(); int faces(); }
final class RandomDice implements Dice {
private final Random rnd; private final int faces;
RandomDice(long seed, int faces) { rnd = new Random(seed); this.faces = faces; }
public int roll() { return 1 + rnd.nextInt(faces); }
public int faces() { return faces; }
}
final class ScriptedDice implements Dice {
private final Deque<Integer> rolls;
ScriptedDice(Integer... r) { rolls = new ArrayDeque<>(List.of(r)); }
public int roll() { return rolls.pop(); }
public int faces() { return 6; }
}
// ---------- Finish rule (Strategy): what happens when a roll goes past the last square ----------
interface FinishRule { int move(int from, int roll, int last); String name(); }
final class ExactLanding implements FinishRule { // a roll that goes past the end is wasted: stay put
public int move(int from, int roll, int last) { return from + roll > last ? from : from + roll; }
public String name() { return "exact landing"; }
}
final class OvershootWins implements FinishRule { // any roll that reaches or passes the end wins
public int move(int from, int roll, int last) { return Math.min(from + roll, last); }
public String name() { return "overshoot wins"; }
}
// ---------- Board: squares 1..last, plus one map for every snake and ladder ----------
final class Board {
final int last; private final Map<Integer, Integer> jumps; // start square -> end square. Up = ladder, down = snake
Board(int last, Map<Integer, Integer> jumps) {
if (last < 2) throw new IllegalArgumentException("board too small");
for (var j : jumps.entrySet()) {
int s = j.getKey(), e = j.getValue();
if (s < 1 || s >= last || e < 1 || e > last) throw new IllegalArgumentException("jump " + s + "->" + e + " is off the board, or starts on the last square");
if (s == e) throw new IllegalArgumentException("jump " + s + "->" + e + " goes nowhere");
// a jump may not end where another starts: no chains, so no loops like 20->50, 50->20
if (jumps.containsKey(e)) throw new IllegalArgumentException("jump " + s + "->" + e + " ends on the start of another jump");
}
this.last = last; this.jumps = Map.copyOf(jumps);
}
int after(int square) { return jumps.getOrDefault(square, square); }
Map<Integer, Integer> jumps() { return jumps; }
}
final class Player { final String name; int square; int turns; Player(String n) { name = n; } }
record Turn(String player, int roll, int from, int landed, int end) {}
// ---------- Game: a queue of players, one turn at a time ----------
final class Game {
private final Board board; private final Dice dice; private final FinishRule rule;
private final Deque<Player> queue = new ArrayDeque<>(); private Player winner;
Game(Board b, Dice d, FinishRule r, List<String> names) {
if (names.size() < 1) throw new IllegalArgumentException("need a player");
board = b; dice = d; rule = r; for (String n : names) queue.add(new Player(n));
}
Turn playTurn() {
if (winner != null) throw new IllegalStateException("game over");
Player p = queue.poll(); int from = p.square, roll = dice.roll();
int landed = rule.move(from, roll, board.last), end = board.after(landed);
p.square = end; p.turns++;
if (end == board.last) winner = p; else queue.add(p);
return new Turn(p.name, roll, from, landed, end);
}
Player playToEnd(int maxTurns) { for (int i = 0; i < maxTurns && winner == null; i++) playTurn(); return winner; }
Optional<Player> winner() { return Optional.ofNullable(winner); }
}
// ---------- Demo with checks ----------
public class SnakeAndLadder {
// The classic 100-square board as listed in Kevin Ross, Applied Stochastic Processes (Chutes and Ladders appendix)
static final Map<Integer, Integer> CLASSIC = Map.ofEntries(
Map.entry(1, 38), Map.entry(4, 14), Map.entry(9, 31), Map.entry(21, 42), Map.entry(28, 84), Map.entry(36, 44),
Map.entry(51, 67), Map.entry(71, 91), Map.entry(80, 100),
Map.entry(16, 6), Map.entry(48, 26), Map.entry(49, 11), Map.entry(56, 53), Map.entry(62, 19), Map.entry(64, 60),
Map.entry(87, 24), Map.entry(93, 73), Map.entry(95, 75), Map.entry(98, 78));
public static void main(String[] args) {
System.out.println("Board rules");
check(rejects(100, Map.of(100, 5)), "a snake head on square 100 is rejected");
check(rejects(100, Map.of(20, 50, 50, 20)), "ladder 20->50 plus snake 50->20 (a loop) is rejected");
check(rejects(100, Map.of(10, 30, 30, 60)), "a chain 10->30->60 is rejected: one jump per landing");
check(rejects(100, Map.of(40, 101)), "a ladder off the board is rejected");
check(!rejects(100, CLASSIC), "the classic board passes: 9 ladders, 10 snakes");
System.out.println("One scripted game: 20 squares, ladder 3->11, snake 17->4, exact landing");
Board small = new Board(20, Map.of(3, 11, 17, 4));
Game g = new Game(small, new ScriptedDice(3, 5, 6, 6, 6, 6, 6, 2, 6, 5, 4), new ExactLanding(), List.of("Ann", "Ben"));
List<Turn> log = new ArrayList<>();
while (g.winner().isEmpty()) log.add(g.playTurn());
check(log.get(0).landed() == 3 && log.get(0).end() == 11, "turn 1: Ann rolls 3, lands on 3, climbs the ladder to 11");
check(log.get(2).landed() == 17 && log.get(2).end() == 4, "turn 3: Ann rolls 6, lands on 17, slides down the snake to 4");
check(log.get(8).from() == 16 && log.get(8).roll() == 6 && log.get(8).end() == 16, "turn 9: Ann on 16 rolls 6; 22 is past 20, so she stays on 16");
check(log.size() == 11 && g.winner().get().name.equals("Ann") && log.get(10).end() == 20, "turn 11: Ann rolls 4 and lands exactly on 20. She wins");
System.out.println("Simulation checked against exact maths");
Board tiny = new Board(9, Map.of(4, 7, 8, 2)); // Numberphile board: squares 0..9, ladder 4->7, snake 8->2
double exactTiny = expectedTurns(tiny, new ExactLanding(), 6), simTiny = simulate(tiny, new ExactLanding(), 200_000, 7);
check(Math.abs(exactTiny - 8.6) < 0.05 && Math.abs(simTiny - exactTiny) < 0.05,
String.format("10-square board, exact landing: maths %.3f turns, 200,000 seeded games %.3f (published: 8.6)", exactTiny, simTiny));
Board classic = new Board(100, CLASSIC);
for (FinishRule r : List.of(new OvershootWins(), new ExactLanding())) {
double ex = expectedTurns(classic, r, 6), sim = simulate(classic, r, 200_000, 11);
boolean exact = r instanceof ExactLanding;
check(Math.abs(sim - ex) < 0.2 && (!exact || Math.abs(ex - 39.598) < 0.001),
String.format("classic board, %s: maths %.3f turns, 200,000 seeded games %.2f%s", r.name(), ex, sim, exact ? " (published: 39.598)" : ""));
}
System.out.println("Many games, many players");
int games = 10_000, longest = 0; int[] wins = new int[4];
for (int s = 0; s < games; s++) {
Game m = new Game(classic, new RandomDice(s, 6), new ExactLanding(), List.of("P1", "P2", "P3", "P4"));
Player w = m.playToEnd(100_000);
if (w == null || w.square != 100) check(false, "game " + s + " did not end on square 100");
wins[Integer.parseInt(w.name.substring(1)) - 1]++; longest = Math.max(longest, w.turns);
}
check(Arrays.stream(wins).sum() == games, String.format("%,d four-player games all end on 100. Wins by seat: %s. Longest: %d turns for the winner", games, Arrays.toString(wins), longest));
Game same1 = new Game(classic, new RandomDice(42, 6), new ExactLanding(), List.of("A", "B")), same2 = new Game(classic, new RandomDice(42, 6), new ExactLanding(), List.of("A", "B"));
same1.playToEnd(100_000); same2.playToEnd(100_000);
check(same1.winner().get().name.equals(same2.winner().get().name) && same1.winner().get().turns == same2.winner().get().turns, "same seed, same game: a failing test can be replayed exactly");
}
/** Expected turns from square 0 to the end for one player: E[s] = 1 + average of E[next square] over the die faces. */
static double expectedTurns(Board b, FinishRule r, int faces) {
double[] e = new double[b.last + 1];
for (int it = 0; it < 100_000; it++) {
double change = 0;
for (int s = b.last - 1; s >= 0; s--) {
double sum = 0;
for (int d = 1; d <= faces; d++) sum += e[b.after(r.move(s, d, b.last))];
double v = 1 + sum / faces; change = Math.max(change, Math.abs(v - e[s])); e[s] = v;
}
if (change < 1e-12) break;
}
return e[0];
}
static double simulate(Board b, FinishRule r, int n, long seed) {
Dice d = new RandomDice(seed, 6); long total = 0;
for (int i = 0; i < n; i++) { Game g = new Game(b, d, r, List.of("solo")); total += g.playToEnd(1_000_000).turns; }
return (double) total / n;
}
static boolean rejects(int last, Map<Integer, Integer> j) { try { new Board(last, j); return false; } catch (IllegalArgumentException e) { return true; } }
static void check(boolean ok, String what) { System.out.println((ok ? " ok " : " FAIL ") + what); if (!ok) System.exit(1); }
}
/* Output of this exact program (javac + java 21, 2026-10-07):
* Board rules
* ok a snake head on square 100 is rejected
* ok ladder 20->50 plus snake 50->20 (a loop) is rejected
* ok a chain 10->30->60 is rejected: one jump per landing
* ok a ladder off the board is rejected
* ok the classic board passes: 9 ladders, 10 snakes
* One scripted game: 20 squares, ladder 3->11, snake 17->4, exact landing
* ok turn 1: Ann rolls 3, lands on 3, climbs the ladder to 11
* ok turn 3: Ann rolls 6, lands on 17, slides down the snake to 4
* ok turn 9: Ann on 16 rolls 6; 22 is past 20, so she stays on 16
* ok turn 11: Ann rolls 4 and lands exactly on 20. She wins
* Simulation checked against exact maths
* ok 10-square board, exact landing: maths 8.600 turns, 200,000 seeded games 8.603 (published: 8.6)
* ok classic board, overshoot wins: maths 36.193 turns, 200,000 seeded games 36.14
* ok classic board, exact landing: maths 39.598 turns, 200,000 seeded games 39.65 (published: 39.598)
* Many games, many players
* ok 10,000 four-player games all end on 100. Wins by seat: [2584, 2494, 2524, 2398]. Longest: 78 turns for the winner
* ok same seed, same game: a failing test can be replayed exactly
*/How it works

Look at what a snake and a ladder really are. Both move a player from one square to another. A ladder goes up and a snake goes down. So the board needs only one map, from the start square to the end square. After every roll, look the landing square up in the map. If it is there, move to the end square. That is the whole rule.
Check the map when the board is made, not during the game. A jump may not start on the last square, because a snake there would make winning impossible. A jump may not leave the board or go nowhere. And a jump may not end where another jump starts. That last check matters most. Without it, a ladder from 20 to 50 and a snake from 50 to 20 could send a player round and round forever.
The parts that change between versions of the game sit behind two small interfaces. Dice gives a roll. RandomDice uses a random number source with a fixed seed. A seed is the start number for the random source: one seed always gives one fixed list of rolls, so a game can be replayed exactly. ScriptedDice returns set rolls for tests. FinishRule decides what happens near the end. Some versions say a player must land exactly on the last square and stays put if the roll is too big. Others let any big enough roll win. The game takes both in its constructor, so a new rule never changes the game.
Players wait in a queue. Each turn takes the player at the front, rolls, moves them, and puts them at the back, unless they just won.
How do you test a game of chance? Three ways, and the program uses all of them. First, set rolls: a scripted game checks a ladder climb, a snake slide, a roll that is too big, and an exact win, turn by turn. Second, exact maths. The expected number of turns from each square is 1 plus the average over the six rolls of the expected turns from the square that roll leads to. The program solves this for every square. On a small 10-square board with one ladder and one snake, it gets 8.600 turns, which matches the published answer of 8.6. On the 100-square Chutes and Ladders board, with 9 ladders and 10 chutes and exact landing, it gets 39.598 turns, which matches a published research paper. Third, simulation: 200,000 seeded games land within a small margin of the maths in every case. So the code, the maths and the published numbers all agree.
The program also plays 10,000 four-player games. Every one ends on square 100.
Good follow-ups: an extra roll after a six, many dice, a board of any shape, saving and loading a game, and an online version where each player's roll comes in over the network.
Edge cases & gotchas
- A roll that goes past the last square. With exact landing the player stays put. With overshoot wins the player wins.
- A snake head on the last square would make winning impossible. The board refuses it.
- A ladder from 20 to 50 and a snake from 50 to 20. If jumps could chain, a player would bounce forever. The board refuses any jump that ends where another starts.
- A player lands on a jump end square. Nothing more happens, because only the square where a player lands can trigger a jump.
- One player alone. The queue still works: take, play, put back.
- A game that never ends because of a bug. The simulation has a turn limit, and the test fails if any game reaches it.
FAQ
Master LLD and system design interviews
770 interactive lessons and 90 real systems taken apart. One payment, lifetime access, no subscription.
course 1
System Design Masterclass
From absolute beginner to principal engineer, drawn step by step.
- 770 interactive lessons
- Step-by-step system design diagrams
- Live code editors
- Quizzes with instant feedback
- Progress tracking and streaks
- Lifetime access and all future lessons