Board/Player/Dice/Snake-Ladder-mapping classes, a generic turn-based flow reusable for similar dice games, and multiplayer turn management.
Published September 23, 2026
class Board {
private final int size; // e.g. 100
private final Map<Integer, Integer> jumps = new HashMap<>(); // cell -> destination, for BOTH snakes and ladders
void addSnake(int head, int tail) { jumps.put(head, tail); } // head > tail
void addLadder(int bottom, int top) { jumps.put(bottom, top); } // top > bottom
int resolveLanding(int position) {
return jumps.getOrDefault(position, position); // uniform lookup — caller doesn't need to know snake vs ladder
}
}
class Dice { int roll() { return ThreadLocalRandom.current().nextInt(1, 7); } }
class Player { String name; int position = 0; }
Modeling both snakes and ladders as the same jumps map (rather than two separate structures) is the key simplification: from the board's perspective, both are just "landing on cell X actually means you end up at cell Y" — the distinction between a snake (Y < X) and a ladder (Y > X) is purely cosmetic/narrative, not structurally different, so there's no reason to model them as two different classes or maintain two lookup structures.
class Game {
private final Board board;
private final Dice dice;
private final List<Player> players;
private int currentPlayerIndex = 0;
Player playTurn() {
Player current = players.get(currentPlayerIndex);
int roll = dice.roll();
int newPosition = current.position + roll;
if (newPosition <= board.getSize()) { // overshoot rule: a roll past the final cell doesn't move the player
current.position = board.resolveLanding(newPosition);
}
currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
return current;
}
boolean hasWinner() { return players.stream().anyMatch(p -> p.position == board.getSize()); }
}
This structure — Board, Dice, Player list, a turn-cycling index, and a playTurn()/hasWinner() pair — generalizes directly to other simple dice-based board games (a straightforward racing game with different board rules) with minimal change, precisely because none of the turn-management logic is snake-and-ladder-specific; only Board.resolveLanding()'s jump semantics are.
The currentPlayerIndex modulo cycling is the same pattern used in Tic-Tac-Toe / Board Game Design and Elevator System's request handling — a simple, robust way to cycle through N players without special-casing 2-player vs N-player. hasWinner() checking position == board.getSize() exactly (not >=) directly reflects the overshoot rule already enforced in playTurn() — a roll that would overshoot the final cell is a no-op move, not a win, which is a real rule worth stating explicitly since it's easy to get wrong (some implementations incorrectly clamp to the final cell instead of skipping the move entirely).
Q: Why store jumps as cell-to-destination rather than separate snake/ladder data structures with head/tail semantics? A: The game never needs to know WHY a jump happened, only that landing on cell X means ending at cell Y — collapsing snakes and ladders into one uniform lookup removes an entire unnecessary branch ("is this a snake or ladder?") from every turn's resolution logic.
Q: How would you validate that a snake/ladder configuration doesn't create an infinite loop (e.g., a snake head placed at a ladder's bottom)? A: A validation pass at board-setup time checking that no jump destination is itself a jump source (or more generally, that following the jump chain from any starting cell terminates) — this is worth mentioning as a real edge case the interviewer might probe, even if not fully implemented under time pressure.
Q: How would you extend this design to support a game where players can be sent backward by other mechanisms, not just fixed board cells? A: The resolveLanding() abstraction already generalizes well — a different game's 'go back 3 spaces' card-draw mechanic would just be a different kind of position modifier, computed dynamically rather than looked up from a static jumps map, but plugging into the same 'compute where you actually land after a move' extension point.
Q: Is Dice.roll() being a hard dependency inside Game/Player a design concern? A: Yes — injecting Dice (as an interface) rather than hard-coding random rolling would make the game deterministically testable (a FixedSequenceDice test double returning a scripted sequence of rolls), the same Dependency Inversion argument applied throughout this course to any source of external/random behavior.