Skip to main content

Snake and Ladder

A board game with exactly one rule that makes it interesting: landing on certain cells teleports you somewhere else on the board. Everything else - turns, dice, winning - is bookkeeping. The design question is where that teleport rule should live.

Requirements

Functional

  • The board has a fixed number of cells (typically 100), numbered from 1 to N.
  • Some cells are the head of a snake (landing there sends you down to a lower cell) or the bottom of a ladder (landing there sends you up to a higher cell).
  • Two or more players take turns rolling a dice and advancing their position by the roll.
  • A player who reaches the last cell wins; a roll that would overshoot the last cell is simply not applied (the player stays put and passes the turn).

Non-functional

  • Adding a new game variant (loaded dice, two dice, a "must roll a 6 to start" house rule) should mean swapping one component, not editing the turn loop.
  • Board setup must reject an invalid snake or ladder (head below tail, or a ladder that goes down) at construction time, not three turns into a game.

Design

The whole game hinges on one decision: Snake and Ladder are configuration used only to build the board, never referenced again once the game starts. What Game actually reads at runtime is a single jumpTo value on each Cell - it has no idea whether that jump came from a snake or a ladder, and it doesn't need to.

RefereeGameDiceBoardCellplayTurn()1roll()2cellAt(newPos)3getJumpTo()4player.setPosition(finalPos)5
  1. 1The referee just asks the game to advance one turn; it never touches a player or the board directly.
  2. 2The current player's move count comes from whatever Dice implementation was configured.
  3. 3The game computes the naive new position, then asks the board for that cell.
  4. 4Only the cell knows if it redirects - the board and game never inspect a snake or ladder object.
  5. 5The player's position updates once, after any redirect is resolved.

That collapse is what keeps playTurn short: move by the roll, then ask the landing cell "do you redirect me?" once. No branching on snake-vs-ladder anywhere in the turn logic.

Class diagram

«interface»Dice+ roll(): intGame- board: Board- dice: Dice- players: Queue<Player>+ playTurn(): void+ isOver(): boolBoard- cells: Cell[]- size: int+ cellAt(pos): CellCell- id: int- jumpTo: int+ getJumpTo(): intSnake- head: int- tail: intLadder- bottom: int- top: intPlayer- name: string- position: intSingleDice+ roll(): int
implementsusescreates
Snake and Ladder only shape the board at setup. Game talks to Cell and Dice, never to either of them again.

Code

import java.util.*;
 
class Cell {
final int id;
private int jumpTo;
 
Cell(int id) {
this.id = id;
this.jumpTo = -1;
}
 
void setJumpTo(int target) {
this.jumpTo = target;
}
 
int getJumpTo() {
return jumpTo;
}
}
 
class Snake {
final int head;
final int tail;
 
Snake(int head, int tail) {
if (tail >= head) throw new IllegalArgumentException("Snake tail must be below its head");
this.head = head;
this.tail = tail;
}
}
 
class Ladder {
final int bottom;
final int top;
 
Ladder(int bottom, int top) {
if (top <= bottom) throw new IllegalArgumentException("Ladder top must be above its bottom");
this.bottom = bottom;
this.top = top;
}
}
 
class Board {
private final Cell[] cells;
final int size;
 
Board(int size, List<Snake> snakes, List<Ladder> ladders) {
this.size = size;
this.cells = new Cell[size + 1];
for (int i = 1; i <= size; i++) cells[i] = new Cell(i);
for (Snake s : snakes) cells[s.head].setJumpTo(s.tail);
for (Ladder l : ladders) cells[l.bottom].setJumpTo(l.top);
}
 
Cell cellAt(int pos) {
return cells[pos];
}
}
 
interface Dice {
int roll();
}
 
class SingleDice implements Dice {
private final int sides;
private final Random random = new Random();
 
SingleDice(int sides) {
this.sides = sides;
}
 
public int roll() {
return random.nextInt(sides) + 1;
}
}
 
class Player {
final String name;
int position;
 
Player(String name) {
this.name = name;
this.position = 0;
}
}
 
class Game {
private final Board board;
private final Dice dice;
private final Deque<Player> players;
 
Game(Board board, Dice dice, List<Player> players) {
this.board = board;
this.dice = dice;
this.players = new ArrayDeque<>(players);
}
 
Player playTurn() {
Player current = players.poll();
int roll = dice.roll();
int target = current.position + roll;
if (target <= board.size) {
Cell landed = board.cellAt(target);
int jump = landed.getJumpTo();
current.position = (jump != -1) ? jump : target;
}
players.offer(current);
return current;
}
 
Player winner() {
for (Player p : players) {
if (p.position == board.size) return p;
}
return null;
}
}

Design decisions

  • Snakes and ladders unify into one jumpTo field on Cell instead of two lookup maps checked every turn. The turn loop does not care which of the two sent a player from cell 47 to cell 12 - it only cares that it happened. Folding both into the same mechanism at board-build time means the hot path (playTurn) has one branch, not two.
  • Dice is an interface, not a fixed die roll. Rolling is the one part of this game that interview variants love to change: two dice, a loaded die for testing, "roll again on a 6." Isolating it behind roll() means those variants are a new class, never an edit to Game.
  • Validation happens in the Board constructor, not in playTurn. A snake whose head is below its tail, or a ladder that goes downward, is a setup bug - catching it when the board is built means a broken configuration fails loudly on line one instead of producing a silently wrong game forty turns later.
  • What's missing for a real system: a cell that is simultaneously a snake head and a ladder bottom needs an explicit tie-breaking rule (this design picks whichever the board applies last, which is worth calling out rather than leaving accidental), and a real multiplayer version needs turn timeouts and a move-history log for replay - neither is needed to demonstrate the class model.
0%0 of 122 pages studied