LLD · Machine Coding Round

Low Level Design: Snake and Ladder Game (Machine Coding Round)

A deceptively simple prompt that is graded on modularity, extensibility and tests. Here is the model, the code, the tests and a minute-by-minute plan for a 90-minute round.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 19 min read
Three-dimensional snake and ladder board with pawns, rolling dice and grid lines
TL;DR
  • Snake and ladder is a machine coding favourite because the logic is trivial; you are graded on structure, extensibility, edge cases and tests.
  • Model snakes and ladders as one Jump record. Validate the board: no jump from the last cell, no duplicate starts, no cycles.
  • Make the dice and the overshoot rule strategies; send output through a listener so the game never calls System.out.
  • Inject a FixedDice to write deterministic JUnit tests for ladders, snakes, chains, overshoot, three sixes and winning.
  • Budget a 90-minute round: 10 min clarify and model, 45 min core, 15 min tests, 10 min extension, 10 min buffer and demo.

If you can write snake and ladder in 20 minutes as a single class with a while loop, you are exactly the candidate this prompt is designed to filter. The game itself is a dozen lines. What the round measures is whether your code looks like something a team could maintain: rules you can change without editing the loop, randomness you can test, output separated from logic, and a board that rejects nonsense input.

This post treats it as the machine coding exercise it usually is in Indian product-company interviews. For the general format of these rounds (environment, evaluation, what to submit), read the machine coding round guide alongside this.

The Typical Snake and Ladder Machine Coding Prompt

The commonly circulated version of the problem looks like this:

Create a snake and ladder application. The board has 100 cells. Each snake has a head and a tail; landing on the head moves you to the tail. Each ladder has a start and an end; landing on the start moves you to the end. Players start outside the board (position 0) and take turns rolling a single six-sided die. A player must land exactly on 100 to win; if a roll would take them past 100, they do not move. The game ends when the first player reaches 100.

Input: number of snakes, then one line per snake (head tail); number of ladders, then one line per ladder (start end); number of players, then their names.

Output for each move: <name> rolled a <value> and moved from <from> to <to>, and finally <name> wins the game.

Optional: support configurable board size, multiple dice, extra turn on six, three consecutive sixes cancel the turn, and continuing the game until only one player is left.

The optional list is the real test. You may not be asked to implement all of it, but your design should make each item a small, local change.

How Machine Coding Rounds Are Graded

CriterionWhat the evaluator looks forHow this design addresses it
Working codeRuns, reads the input format, prints the exact output formatmain parses stdin; a listener prints the exact lines
ModularitySeparate classes with single responsibilitiesBoard, Jump, Player, Dice, MoveRule, Game, listener
ExtensibilityNew rules without editing core loopsDice and MoveRule strategies; config for turn rules
Edge casesOvershoot, chains, cycles, invalid inputBoard validation and chain resolution
TestabilityDeterministic tests exist and passInjected FixedDice, JUnit tests
ReadabilityNames, small methods, no magic numbersNamed constants, records, short methods

The single biggest failure mode is not finishing. A beautiful half-design with no running main usually scores lower than a plain working solution. Get something demoable first, then refactor toward the extension points.

Clarify the Rules Before Modelling

Rules vary by family and by interviewer. Ask, propose a default, and note which ones you will make configurable:

  • Overshoot past 100: stay in place (default) or bounce back by the excess?
  • Chains: if a ladder ends on a snake's head, does the snake apply? Default: yes, keep resolving.
  • Two players on one cell: allowed (default), or does the newcomer send the other back to start?
  • Sixes: extra turn on six? Three consecutive sixes cancel the whole turn?
  • End condition: first winner ends the game, or continue to rank everyone?
  • Board size and dice count: fixed 100 and one die, or configurable?
  • Invalid boards: what to do with a snake at 100 or a loop? Default: reject at construction with a clear error.

Entity Modelling: Fewer, Better Classes

One Jump instead of Snake and Ladder classes

The reflex is class Snake and class Ladder, maybe with an abstract parent. But both are "if you land on A, move to B". The board does not care which direction B is. A single record Jump(int from, int to) with a derived type() keeps the resolution logic to one loop. If a future rule makes snakes behave differently (for example "a snake bite also skips your next turn"), that is the moment to introduce polymorphism, and you can say so. Interviewers reward this kind of "not yet" reasoning.

The full entity list

TypeResponsibilityNotes
JumpStart and end cellRecord; validates start ≠ end
BoardSize, jump lookup, chain resolution, validationMap<Integer, Jump> for O(1) lookup
PlayerName and positionThe only mutable domain object
Diceint roll()Strategy: random, fixed, multi-dice
MoveRuleTarget cell for a roll (handles overshoot)Strategy: exact finish, bounce back
GameConfigExtra turn on six, max sixes, play until lastRecord; plain configuration
GameListenerReacts to moves, jumps, wins, forfeitsObserver; console printer is one implementation
GameTurn order, applying rules, ending the gameDepends only on interfaces
💡
Not everything needs a patternTurn rules like "extra turn on six" are booleans and integers in a config record, not a TurnStrategy hierarchy. Use a pattern when there are genuinely different algorithms (dice, overshoot); use configuration when there are different numbers. Saying this explicitly signals judgment. The SOLID guide discusses this over-engineering trap in detail.

Where the Extension Points Live

┌──────────────────────────────┐ stdin ──► │ InputParser (in main) │ └──────────────┬───────────────┘ ▼ ┌───────────┐ ┌────────────────────────────┐ ┌─────────────────┐ │ «Dice» │◄──│ Game │──► │ «GameListener» │ │ Random │ │ turnOrder: Deque<Player> │ │ ConsolePrinter │ │ Fixed │ │ playTurn() · play() │ │ (future: UI, │ │ MultiDice │ └──────┬──────────────┬──────┘ │ stats, replay) │ └───────────┘ │ │ └─────────────────┘ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Board │ │ «MoveRule» │ │ jumps by cell│ │ ExactFinish │ │ resolve() │ │ BounceBack │ └──────────────┘ └──────────────┘
  • Two dice, take the sum or the max? New MultiDice with a combining function. Game unchanged.
  • Bounce back on overshoot? New MoveRule. Game unchanged.
  • Print a different format, or drive a UI? New listener. Game unchanged.
  • Board of 200 cells? Constructor argument. Nothing hard-codes 100.
  • Rank all players? One config flag and a winners list.

Full Java Code for Snake and Ladder

Java 17+, single file for readability. In the actual round, split into packages (model, dice, rules, game, io); evaluators do look at structure. Run with java SnakeAndLadder.java < input.txt.

Java
import java.util.*;
import java.util.function.IntBinaryOperator;

public class SnakeAndLadder {

    // ===================== Dice (Strategy) =====================
    interface Dice { int roll(); }

    static final class RandomDice implements Dice {
        private final Random random;
        private final int faces;
        RandomDice(int faces, long seed) {
            if (faces < 2) throw new IllegalArgumentException("A die needs at least 2 faces");
            this.faces = faces;
            this.random = new Random(seed);      // seeded so a failing game can be replayed
        }
        public int roll() { return 1 + random.nextInt(faces); }
    }

    /** Scripted rolls: deterministic tests and demos. */
    static final class FixedDice implements Dice {
        private final Deque<Integer> values = new ArrayDeque<>();
        FixedDice(int... rolls) { for (int r : rolls) values.add(r); }
        public int roll() {
            if (values.isEmpty()) throw new IllegalStateException("No scripted rolls left");
            return values.poll();
        }
    }

    /** Several dice combined by SUM (Integer::sum), MAX (Math::max) or MIN (Math::min). */
    static final class MultiDice implements Dice {
        private final List<Dice> dice;
        private final IntBinaryOperator combine;
        MultiDice(IntBinaryOperator combine, Dice... dice) {
            this.combine = combine;
            this.dice = List.of(dice);
        }
        public int roll() {
            return dice.stream().mapToInt(Dice::roll).reduce(combine).orElseThrow();
        }
    }

    // ===================== Board =====================
    enum JumpType { SNAKE, LADDER }

    record Jump(int from, int to) {
        Jump {
            if (from == to) throw new IllegalArgumentException("Jump cannot start and end on " + from);
        }
        JumpType type() { return to < from ? JumpType.SNAKE : JumpType.LADDER; }
    }

    static final class Board {
        private final int size;
        private final Map<Integer, Jump> jumps = new HashMap<>();

        Board(int size, List<Jump> jumpList) {
            if (size < 2) throw new IllegalArgumentException("Board too small");
            this.size = size;
            for (Jump j : jumpList) {
                if (j.from() < 1 || j.from() >= size || j.to() < 1 || j.to() > size) {
                    throw new IllegalArgumentException("Jump out of range or starts on last cell: " + j);
                }
                if (jumps.putIfAbsent(j.from(), j) != null) {
                    throw new IllegalArgumentException("Two jumps start at " + j.from());
                }
            }
            rejectCycles();
        }

        int size() { return size; }

        /** Follows snakes and ladders until landing on a plain cell (chains allowed). */
        int resolve(int cell, List<Jump> taken) {
            Jump j;
            while ((j = jumps.get(cell)) != null) {
                taken.add(j);
                cell = j.to();
            }
            return cell;
        }

        private void rejectCycles() {
            for (int start : jumps.keySet()) {
                Set<Integer> seen = new HashSet<>();
                int cell = start;
                while (jumps.containsKey(cell)) {
                    if (!seen.add(cell)) throw new IllegalArgumentException("Jump cycle through cell " + cell);
                    cell = jumps.get(cell).to();
                }
            }
        }
    }

    // ===================== Rules =====================
    interface MoveRule { int target(int from, int roll, int boardSize); }

    /** Overshoot cancels the move. */
    static final class ExactFinishRule implements MoveRule {
        public int target(int from, int roll, int size) { return from + roll > size ? from : from + roll; }
    }

    /** Overshoot bounces back by the excess: 98 + 5 on a 100 board lands on 97. */
    static final class BounceBackRule implements MoveRule {
        public int target(int from, int roll, int size) {
            int t = from + roll;
            return t > size ? size - (t - size) : t;
        }
    }

    record GameConfig(boolean extraTurnOnSix, int maxConsecutiveSixes, boolean playUntilLastPlayer) {
        static GameConfig classic() { return new GameConfig(false, 3, false); }
    }

    // ===================== Players and events =====================
    static final class Player {
        private final String name;
        private int position;
        Player(String name) { this.name = name; }
        String name() { return name; }
        int position() { return position; }
    }

    interface GameListener {
        default void onMove(Player p, int roll, int from, int to, List<Jump> jumps) { }
        default void onTurnForfeited(Player p, int backTo) { }
        default void onWin(Player p, int rank) { }
    }

    static final class ConsolePrinter implements GameListener {
        public void onMove(Player p, int roll, int from, int to, List<Jump> jumps) {
            System.out.println(p.name() + " rolled a " + roll + " and moved from " + from + " to " + to);
        }
        public void onTurnForfeited(Player p, int backTo) {
            System.out.println(p.name() + " rolled three sixes; turn cancelled, back to " + backTo);
        }
        public void onWin(Player p, int rank) {
            System.out.println(rank == 1 ? p.name() + " wins the game" : p.name() + " finishes at rank " + rank);
        }
    }

    // ===================== Game =====================
    static final class Game {
        private final Board board;
        private final Dice dice;
        private final MoveRule moveRule;
        private final GameConfig config;
        private final Deque<Player> turnOrder = new ArrayDeque<>();
        private final Map<String, Player> byName = new LinkedHashMap<>();
        private final List<Player> finished = new ArrayList<>();
        private final List<GameListener> listeners = new ArrayList<>();

        Game(Board board, Dice dice, MoveRule moveRule, GameConfig config, List<String> names) {
            if (names.size() < 2) throw new IllegalArgumentException("Need at least 2 players");
            this.board = board;
            this.dice = dice;
            this.moveRule = moveRule;
            this.config = config;
            for (String n : names) {
                Player p = new Player(n);
                if (byName.putIfAbsent(n, p) != null) throw new IllegalArgumentException("Duplicate player " + n);
                turnOrder.addLast(p);
            }
        }

        void addListener(GameListener l) { listeners.add(l); }

        boolean isOver() {
            return config.playUntilLastPlayer() ? turnOrder.size() <= 1 : !finished.isEmpty();
        }

        int positionOf(String name) { return byName.get(name).position(); }

        List<String> rankings() { return finished.stream().map(Player::name).toList(); }

        void play() {
            while (!isOver()) playTurn();
        }

        void playTurn() {
            if (isOver()) throw new IllegalStateException("Game is over");
            Player p = turnOrder.pollFirst();
            int startOfTurn = p.position;
            int sixes = 0;
            while (true) {
                int roll = dice.roll();
                if (config.extraTurnOnSix() && roll == 6 && ++sixes == config.maxConsecutiveSixes()) {
                    p.position = startOfTurn;                       // the whole turn is cancelled
                    listeners.forEach(l -> l.onTurnForfeited(p, startOfTurn));
                    break;
                }
                int from = p.position;
                List<Jump> taken = new ArrayList<>();
                int landed = board.resolve(moveRule.target(from, roll, board.size()), taken);
                p.position = landed;
                listeners.forEach(l -> l.onMove(p, roll, from, landed, taken));

                if (landed == board.size()) {
                    finished.add(p);
                    int rank = finished.size();
                    listeners.forEach(l -> l.onWin(p, rank));
                    return;                                         // winner leaves the rotation
                }
                if (!(config.extraTurnOnSix() && roll == 6)) break;
            }
            turnOrder.addLast(p);
        }
    }

    // ===================== Input parsing + main =====================
    static Game fromInput(Scanner in, Dice dice) {
        List<Jump> jumps = new ArrayList<>();
        int snakes = in.nextInt();
        for (int i = 0; i < snakes; i++) {
            Jump j = new Jump(in.nextInt(), in.nextInt());
            if (j.type() != JumpType.SNAKE) throw new IllegalArgumentException("Snake must go down: " + j);
            jumps.add(j);
        }
        int ladders = in.nextInt();
        for (int i = 0; i < ladders; i++) {
            Jump j = new Jump(in.nextInt(), in.nextInt());
            if (j.type() != JumpType.LADDER) throw new IllegalArgumentException("Ladder must go up: " + j);
            jumps.add(j);
        }
        int players = in.nextInt();
        List<String> names = new ArrayList<>();
        for (int i = 0; i < players; i++) names.add(in.next());
        return new Game(new Board(100, jumps), dice, new ExactFinishRule(), GameConfig.classic(), names);
    }

    public static void main(String[] args) {
        Game game = fromInput(new Scanner(System.in), new RandomDice(6, System.nanoTime()));
        game.addListener(new ConsolePrinter());
        game.play();
    }
}

Choices worth explaining to the evaluator

  • Players are in a Deque. Poll the front, push to the back. Removing a winner is simply not pushing them back. No index arithmetic, no modulo bugs when players leave.
  • The three-sixes check happens before moving. The third six never moves the piece, so there is no "move then undo" flicker in the output.
  • Listeners receive the jumps taken. The console printer ignores them to match the required output format, but a UI could animate the snake.
  • Input validation lives in the parser and the board, not scattered through the game loop.

Unit Tests with Deterministic Dice

Most candidates skip tests in machine coding. A handful of focused tests, written in the last 15 minutes, is one of the cheapest ways to stand out. The trick that makes them possible is dependency injection of Dice.

Java (JUnit 5)
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;

class SnakeAndLadderTest {
    // assumes the nested types are imported or moved to top-level classes

    private static SnakeAndLadder.Game game(SnakeAndLadder.Dice dice, SnakeAndLadder.GameConfig config,
                                            SnakeAndLadder.Jump... jumps) {
        SnakeAndLadder.Board board = new SnakeAndLadder.Board(100, List.of(jumps));
        return new SnakeAndLadder.Game(board, dice, new SnakeAndLadder.ExactFinishRule(), config, List.of("A", "B"));
    }

    @Test
    void ladderTakesPlayerUp() {
        var g = game(new SnakeAndLadder.FixedDice(4), SnakeAndLadder.GameConfig.classic(),
                     new SnakeAndLadder.Jump(4, 25));
        g.playTurn();
        assertEquals(25, g.positionOf("A"));
    }

    @Test
    void snakeTakesPlayerDown() {
        var board = new SnakeAndLadder.Board(100, List.of(new SnakeAndLadder.Jump(30, 7)));
        assertEquals(7, board.resolve(30, new ArrayList<>()));
    }

    @Test
    void chainedJumpsResolveToTheEnd() {
        var board = new SnakeAndLadder.Board(100, List.of(
            new SnakeAndLadder.Jump(2, 10), new SnakeAndLadder.Jump(10, 50)));
        assertEquals(50, board.resolve(2, new ArrayList<>()));
    }

    @Test
    void cyclesAreRejected() {
        assertThrows(IllegalArgumentException.class, () -> new SnakeAndLadder.Board(100, List.of(
            new SnakeAndLadder.Jump(5, 20), new SnakeAndLadder.Jump(20, 5))));
    }

    @Test
    void jumpFromLastCellIsRejected() {
        assertThrows(IllegalArgumentException.class,
            () -> new SnakeAndLadder.Board(100, List.of(new SnakeAndLadder.Jump(100, 3))));
    }

    @Test
    void overshootRules() {
        assertEquals(98, new SnakeAndLadder.ExactFinishRule().target(98, 5, 100));
        assertEquals(97, new SnakeAndLadder.BounceBackRule().target(98, 5, 100));
        assertEquals(100, new SnakeAndLadder.ExactFinishRule().target(98, 2, 100));
    }

    @Test
    void threeSixesCancelTheWholeTurn() {
        var config = new SnakeAndLadder.GameConfig(true, 3, false);
        var g = game(new SnakeAndLadder.FixedDice(6, 6, 6, 1), config);
        g.playTurn();                        // A: 6, 6, then third 6 cancels the turn
        assertEquals(0, g.positionOf("A"));
        g.playTurn();                        // B plays next with the remaining roll
        assertEquals(1, g.positionOf("B"));
    }

    @Test
    void exactLandingOnLastCellWins() {
        var g = game(new SnakeAndLadder.FixedDice(2, 3, 1), SnakeAndLadder.GameConfig.classic(),
                     new SnakeAndLadder.Jump(2, 99));
        g.playTurn();                        // A: 0 -> 2 -> ladder -> 99
        g.playTurn();                        // B: 0 -> 3
        g.playTurn();                        // A: 99 -> 100
        assertTrue(g.isOver());
        assertEquals(List.of("A"), g.rankings());
    }
}

Eight tests cover every rule in the prompt. In a real submission, move the nested types to top-level classes so the test imports are clean; nested types are used here only to keep the article in one listing.

Sample Input and Output

Using the commonly circulated sample board (9 snakes, 8 ladders) with two players and scripted rolls 2, 6, 4, 4, 1, 1, 6, 5, 5, 3, 3, 2, 6, 1, 4, 2, 3 (inject new FixedDice(...) instead of RandomDice):

Input
9
62 5
33 6
49 9
88 16
41 20
56 53
98 64
93 73
95 75
8
2 37
27 46
10 32
51 68
61 79
65 84
71 91
81 100
2
Gaurav
Sagar
Output
Gaurav rolled a 2 and moved from 0 to 37
Sagar rolled a 6 and moved from 0 to 6
Gaurav rolled a 4 and moved from 37 to 20
Sagar rolled a 4 and moved from 6 to 32
Gaurav rolled a 1 and moved from 20 to 21
Sagar rolled a 1 and moved from 32 to 6
Gaurav rolled a 6 and moved from 21 to 46
Sagar rolled a 5 and moved from 6 to 11
Gaurav rolled a 5 and moved from 46 to 68
Sagar rolled a 3 and moved from 11 to 14
Gaurav rolled a 3 and moved from 68 to 91
Sagar rolled a 2 and moved from 14 to 16
Gaurav rolled a 6 and moved from 91 to 97
Sagar rolled a 1 and moved from 16 to 17
Gaurav rolled a 4 and moved from 97 to 97
Sagar rolled a 2 and moved from 17 to 19
Gaurav rolled a 3 and moved from 97 to 100
Gaurav wins the game

Trace a few lines to see each rule fire. Gaurav's first 2 hits the ladder at 2 → 37. His 4 from 37 lands on 41, a snake head → 20. Sagar's 1 from 32 lands on 33, snake → 6. Gaurav's 4 from 97 would reach 101, so under ExactFinishRule he stays at 97. His final 3 lands exactly on 100.

Python Version of the Core

Many machine coding rounds allow any language. If Python is your strongest, here is the same core design, compact but still modular: the board validates itself and the dice is injected as a callable. It produces the output above with the same scripted rolls.

Python
from collections import deque
from dataclasses import dataclass

@dataclass(frozen=True)
class Jump:
    start: int
    end: int

class Board:
    def __init__(self, size, jumps):
        self.size = size
        self.jumps = {}
        for j in jumps:
            if not (1 <= j.start < size and 1 <= j.end <= size) or j.start == j.end:
                raise ValueError(f"invalid jump {j}")
            if j.start in self.jumps:
                raise ValueError(f"two jumps start at {j.start}")
            self.jumps[j.start] = j
        for s in self.jumps:                      # reject 5->20, 20->5 style loops
            seen, p = set(), s
            while p in self.jumps:
                if p in seen:
                    raise ValueError(f"jump cycle through {p}")
                seen.add(p)
                p = self.jumps[p].end

    def resolve(self, pos):
        while pos in self.jumps:
            pos = self.jumps[pos].end
        return pos

class Game:
    def __init__(self, board, players, roll):
        self.board, self.roll = board, roll        # roll: () -> int, injected
        self.positions = {p: 0 for p in players}
        self.turns = deque(players)
        self.winner = None

    def play_turn(self):
        p = self.turns.popleft()
        r = self.roll()
        start = self.positions[p]
        target = start + r if start + r <= self.board.size else start
        end = self.board.resolve(target)
        self.positions[p] = end
        print(f"{p} rolled a {r} and moved from {start} to {end}")
        if end == self.board.size:
            self.winner = p
            print(f"{p} wins the game")
        else:
            self.turns.append(p)

    def play(self):
        while self.winner is None:
            self.play_turn()

# usage: rolls = iter([2, 6, 4, ...]); Game(board, ["Gaurav", "Sagar"], lambda: next(rolls)).play()
# random: import random; Game(board, names, lambda: random.randint(1, 6)).play()

A 90-Minute Machine Coding Plan

0–10 min

Clarify and model

Confirm rules and I/O format. Write the entity list and the two strategies as comments at the top of your file.

10–55 min

Core, end to end

Jump, Board with resolve, Player, Dice, Game loop, parser, printer. Run it on the sample input as early as possible.

55–70 min

Tests and validation

FixedDice tests for ladder, snake, overshoot, win. Add cycle and range validation.

70–80 min

One optional feature

Extra turn on six with three-six forfeit, or multiple dice. Show it is a local change.

80–90 min

Clean and demo

Rename, remove dead code, write a 5-line README on how to run and what is extensible.

⚠️
Run the code by minute 40The most common way to fail a machine coding round is a design that never runs. If you are at minute 45 without a passing sample, cut scope: hard-code one die and the exact-finish rule, get output, then reintroduce the interfaces.

Follow-Up Questions and Common Mistakes

Follow-ups you should be ready for

  • "Add a crocodile that moves you back 5 cells." A crocodile is not a fixed start→end jump. Generalise Jump into a CellEffect interface with int apply(int cell); snakes and ladders become one implementation, crocodile another. This is the moment polymorphism earns its place.
  • "Add mines that make you skip 2 turns." Effects now touch player state: give Player a skipTurns counter, and have the game decrement it instead of rolling.
  • "Landing on another player sends them home." A board-level occupancy map plus a listener-free rule inside playTurn; or a CollisionRule strategy if several variants are expected.
  • "Save and resume the game." The state is small: positions, turn order, finished list, dice seed. Serialise a snapshot; this is Memento if the interviewer wants a pattern name.
  • "Multiple games in parallel on a server." Each Game is independent and single-threaded; a map from game id to Game, with one lock or one actor per game, is enough. Do not add locks inside the game loop.
  • "Random board generation." Generate jumps, then reuse the same validation, retrying on cycles.

Mistakes that cost candidates the round

  1. new Random() inside the game loop, which makes testing impossible.
  2. System.out.println scattered through the game logic instead of one output component.
  3. Applying a single jump instead of resolving chains, or resolving chains with no cycle protection (infinite loop on bad input).
  4. Hard-coding 100 and 6 in five places.
  5. Using currentPlayer = (currentPlayer + 1) % n and then breaking it when a winner is removed.
  6. An abstract Entity base class, a GameFactory, a BoardBuilder and a DiceFactory for a 150-line problem. Over-engineering reads as poor prioritisation.
  7. No README or run instructions. Evaluators often run your code themselves.

Snake and ladder is a good warm-up for the harder board game, chess, where move validation and undo bring in polymorphism and the Command pattern for real. For broader preparation, the LLD interview guide lists the problems most often asked and how to sequence them, and the design patterns guide covers Strategy and Observer in more depth.

Frequently Asked Questions

What classes are needed for snake and ladder low level design?▾
Board, Jump (one class for both snakes and ladders since both move a piece from one cell to another), Player, a Dice interface with implementations, a MoveRule for overshooting the last cell, and a Game that manages turns. A listener interface for output keeps printing out of the game logic.
Should snakes and ladders be separate classes?▾
Usually not. Both are a start cell and an end cell; the only difference is direction. A single Jump record with a derived type keeps the board logic uniform. Separate classes are justified only when they gain different behaviour, for example a snake that also makes you skip a turn.
Which design patterns are used in snake and ladder?▾
Strategy for dice (random, fixed for tests, multiple dice with sum or max) and for the overshoot rule, Observer for printing or UI updates, and optionally Factory for building the board from input. The State pattern is not needed for the basic game.
How is a machine coding round evaluated?▾
Commonly reported criteria are working, demoable code first, then modularity and separation of concerns, extensibility for new rules, readability and naming, handling of edge cases, and tests. Interviewers often add a new requirement near the end to see how much code you have to change.
How do you test a game that uses random dice?▾
Inject the dice. A FixedDice implementation that returns scripted values makes every turn deterministic, so you can assert exact positions after ladders, snakes, overshoots and three consecutive sixes. Random dice should take a seed so failures are reproducible.
What happens if a player rolls past 100 in snake and ladder?▾
It is a rule choice, so ask. The common default is that the move is cancelled and the player stays in place; an alternative is bouncing back by the excess. Model it as a MoveRule strategy so either can be plugged in without touching the game loop.
How do you detect invalid boards in snake and ladder?▾
Reject jumps that start on the last cell, two jumps starting on the same cell, cells outside the board, and cycles such as a ladder from 5 to 20 with a snake from 20 back to 5, which would loop forever. Following each chain with a visited set catches cycles at construction time.

Train for Machine Coding Rounds

Prepflix runs timed machine coding mocks on problems like snake and ladder, with reviewers grading modularity, extensibility and tests the way product companies do.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.