BackendEasy25 min

Design Snake and Ladder (LLD)

A toy game with real design questions: one jump type for snakes and ladders, dice and the ending rule as strategies, and a layout check that stops jump loops.

Say it in 30 seconds
  • 1 jump mapsnakes and ladders share one structure
  • 3 end rulesexact, finish or bounce, one class each
  • 0 chainsjumps may not land on another jump
Show what to say

“I'd settle three rules first: what happens on overshoot, whether jumps chain, and whether a six repeats. A board holds one map of jumps, since a snake and a ladder only differ in direction. The dice and the ending rule are strategies, so tests use fixed rolls, and bad boards are refused at construction.”

Trade-offs

  • One jump type vs Snake and Ladder classes: less code and no duplicate logic, at the cost of being less literal.
  • Refusing chains vs following them: no loops are possible, but some fun layouts are ruled out unless you add a visited check.
  • Rule as a strategy vs a switch: new rules need no edits to play, in exchange for one more small interface.

Bring it up before they ask

A ladder that lands on a snake that lands on the ladder never ends a turn, so validate the layout up front and refuse any jump that lands on another jump's start.

0 of 0 sections ticked
30-second answer

A board holds one map of jumps (a snake and a ladder are the same thing pointing different ways), and a game rolls, applies the ending rule, then applies one jump per turn. Make the dice and the ending rule strategies, and the game becomes easy to test and easy to change.

Functional

  • A board of N squares with snakes and ladders
  • Two or more players who take turns, starting off the board
  • A dice roll moves the player, and a jump on the landing square moves them again
  • First to reach the last square wins, with a chosen rule for overshooting
  • Optional extra turn on a six
  • Events for a UI: who moved, from where, to where

Non-functional

  • Invalid boards are refused up front: no loops, no jump from the last square
  • Deterministic tests: the dice are injected
  • A new ending rule or dice plugs in without editing Game
  • One game is safe if several threads call play, and games never block each other

Out of scope

  • A graphical board
  • Online multiplayer (an HLD follow-up)
  • Betting, scores and leaderboards

A low-level design (LLD) question that looks like a toy. The marks come from how you handle the rules people argue about, and from keeping the dice swappable.

New to this? Read Swap behaviour first.

Scope and interface

Read more · 166 words
  • What happens if the roll goes past the last square: stay, finish, or bounce?
  • Do jumps chain, so a ladder can end on a snake?
  • Does a six give another turn? Is there a limit?
  • How many players, and how big is the board?
  • Real dice only, or fixed rolls for tests and demos?

The public interface:

CallReturnsNotes
new Board(size, jumps)A boardRefuses a bad layout
new Game(board, players, dice, rule, sixAgain)A gameEveryone starts at 0
game.play()A move result, or nothingOne roll by whoever is next
game.on(listener)Called after every move
game.winnerName or nothingSet when someone reaches the end

What it remembers:

  • board.size and board.jumps: square → square. Never changes after setup.
  • position: player → square, with 0 meaning off the board.
  • turn: an index into the players. winner: set once, and never cleared.

A move result is player, roll, from, to and an event: move, ladder, snake, stay or win.

Stages and patterns

Step through: a game with a board and players, then the dice, then the ending rule, then results and listeners.

Read more · 90 words

Green marks what the latest stage added.

Patterns.

Strategy twice. The dice decide where a roll comes from. The ending rule decides what happens at the last square. Both are small and both change in real life. Observer lets a UI watch without the game knowing.

Skip the rest. A Snake class and a Ladder class with a shared base add nothing: a jump from 17 to 7 is a snake because 7 is less than 17. No GameFactory, no BoardBuilder for two numbers, no Cell object for every square.

Stage 1 of 4

A game, a board and players

A game owns a board, the players, and whose turn it is. A snake and a ladder are one idea, a jump from a square to another, so the board has one map and no Snake and Ladder classes.

Snake and Ladder class diagramGame owns a Board, the players and their positions, and takes its dice and ending rule as strategies. The Board holds one map of jumps, because a snake and a ladder differ only in direction. Each turn returns a MoveResult, and listeners observe the game.Playername: stringposition: int, 0 = off boardGameboard: Boardplayers: Player[]turn: indexwinner: Player or nullplay(): MoveResulton(listener)Boardsize: intjumps: Map<from, to>land(square)kindAt(square)Jumpfrom, toto > from: ladderto < from: snake2..*0..*

Game owns a Board, the players and their positions, and takes its dice and ending rule as strategies. The Board holds one map of jumps, because a snake and a ladder differ only in direction. Each turn returns a MoveResult, and listeners observe the game.

  • ◆ composition (owns)
  • ◇ aggregation
  • △ inheritance / implements (dashed)
  • → association · ⇢ dependency

One turn, rules and threads

Pick a branch: a plain move, a jump, a refused overshoot, and the winning move.

Read more · 149 words

Rules to settle out loud:

  • Overshoot: exact (must land on the last square, else stay), finish (any roll that reaches it wins), or bounce (extra steps walk back).
  • Chains: allow them and you need a loop check. The simpler rule is to refuse any layout where a jump lands on another jump's start.
  • Last square: no jump may start there, or the winner could slide away.
  • Six: the same player rolls again. State whether a refused move still earns it.

Concurrency.

One game has one writer at a time. Make play a single critical section: a lock around the whole turn, so two threads calling at once take turns and the same player never moves twice by accident. Different games share nothing and never wait for each other. For a server, give each game an owner (one thread, or one queue), and tell listeners after the turn is complete.

Press Play to watch it run · 6 steps

Game takes the next player, asks the dice for a roll, and asks the ending rule where the roll lands. The board then applies any jump on that square. Pick a branch to see a plain move, a ladder, a snake, a refused overshoot and a winning move.

One call to play()Game takes the next player, asks the dice for a roll, and asks the ending rule where the roll lands. The board then applies any jump on that square. Pick a branch to see a plain move, a ladder, a snake, a refused overshoot and a winning move.play()player A at 8, board size 10roll()the rollapply(8, roll, 10)alt [plain move]roll 1: the rule says 9, an empty squarekindAt(9): no jumpA 8 to 9, move
  • Client
  • Service
  • Worker
  • Database
Text description
  1. Caller → Game: play()
  2. Game → Dice: roll()
  3. Dice replies to Game: the roll
  4. Game → EndRule: apply(8, roll, 10)
  5. Game → Board: kindAt(9): no jump
  6. Game replies to Caller: A 8 to 9, move

Step-by-step answer

In one line each:

Read more · 199 words
  1. Clarify the rules

    Overshoot, chains, sixes, player count.

  2. Name the nouns

    Game, Board, Player, Dice, Jump.

  3. Write play()

    Roll, apply the rule, apply a jump, check the win.

  4. Pull out the two strategies

    Dice and ending rule.

  5. Validate and test

    Bad boards refused, fixed dice for every case.

In full:

  1. One map for both jumps

    jumps goes from square to square. Higher means a ladder, lower means a snake.

  2. Validate at construction

    A jump must start on 1 to size minus 1, end on the board, and move. No jump may land on another jump's start, which rules out chains and so loops.

  3. A turn is five small steps

    Roll, ask the rule for the landing square, apply the jump, check for the win, pass the turn.

    const landed = this.rule(from, roll, this.board.size);   // null: refused
    const to = landed === null ? from : this.board.land(landed);
    if (to === this.board.size) this.winner = player;
  4. Dice are injected

    The real game gets random dice with a seedable source. Tests get a fixed list of rolls that repeats.

  5. Tests

    Each event, each ending rule, the six, a ladder onto the last square, play after a win, and every invalid board.

Implementation

The same design in JavaScript, Java or Python. Java and Python add a real lock around play.

Read more · 57 words
  • A rule returns the landing square, or nothing when the move is refused. The game turns that into a stay.
  • A win is any move that ends on the last square, even by ladder.

Then code it yourself. Try the Snake and Ladder exercise: jumps, three ending rules, sixes and invalid boards, all driven by fixed dice.

// A snake and a ladder are the same thing pointing different ways, so the board stores one map of jumps.
export class Board {
  constructor(size, jumps) {                        // jumps: square -> square, e.g. [[4, 14], [17, 7]]
    this.size = size;
    this.jumps = new Map(jumps);
    for (const [from, to] of this.jumps) {
      if (from < 1 || from >= size) throw new RangeError(`a jump cannot start on ${from}`);   // never on the last square
      if (to < 1 || to > size || to === from) throw new RangeError(`bad jump ${from} -> ${to}`);
      if (this.jumps.has(to)) throw new RangeError(`${from} lands on the start of another jump`);  // no chains, so no cycles
    }
  }

  land(square) { return this.jumps.get(square) ?? square; }
  kindAt(square) { const to = this.jumps.get(square); return to === undefined ? 'move' : to > square ? 'ladder' : 'snake'; }
}

// Strategy 1: where the roll comes from. The game never knows if it is random.
export class RandomDice {
  constructor(rng = Math.random, sides = 6) { this.rng = rng; this.sides = sides; }
  roll() { return 1 + Math.floor(this.rng() * this.sides); }
}

export class FixedDice {                            // for tests and demos: the same rolls, in order, then again
  constructor(rolls) { this.rolls = rolls; this.next = 0; }
  roll() { return this.rolls[this.next++ % this.rolls.length]; }
}

// Strategy 2: how the last square is reached. A rule maps (position, roll, size) to a square, or null if the move is refused.
export const exact = (pos, roll, size) => (pos + roll > size ? null : pos + roll);      // must land on the last square
export const finish = (pos, roll, size) => Math.min(pos + roll, size);                  // any roll that reaches it wins
export const bounce = (pos, roll, size) => {                                            // the extra steps walk back
  const target = pos + roll;
  return target > size ? size - (target - size) : target;
};

Failure modes

Read more · 67 words
What breaksEffectMitigation
Jumps form a loopA turn never endsRefuse chains at construction
Jump from the last squareThe winner slides backRefuse it up front
Overshoot unspecifiedPlayers argue, tests flakePick a rule and test it
Two threads call playA player moves twiceOne lock around the turn
Unseeded dice in testsFailures that never repeatInject fixed or seeded dice

Trade-offs

Read more · 101 words
ChoiceProsCons
One jump type vs Snake and Ladder classesLess code, no duplicatesA little less literal
Rule as a strategy vs an enum and switchNew rules need no editsAn extra interface
Refuse chains vs follow themNo loops possibleRules out fun layouts
Lock per game vs a global lockGames run in parallelA lock object per game

Follow-up questions

How would you add a loaded dice?

A new Dice class that skews its roll. The game is unchanged, which is the point of passing the dice in.

How would you let a jump chain safely?

Follow jumps in a loop and keep the squares you have visited. If you see one twice, the layout loops, so refuse it at construction.

How would you support undo or replay?

Keep the list of move results. Replay by feeding the same rolls to a fresh game. Undo means going back one result.

How would you run thousands of games on a server?

Give each game one owner, a single thread or queue, so no locks are needed. Store only the rolls and the setup, and rebuild a game from them.

How would you test it?

Fixed dice for every rule and event, a seeded random game that must finish, and a table of invalid boards that must all be refused.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. A ladder goes from 3 to 6 and a snake from 6 to 1. A player lands on 3. Where does the player end up?