- 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
Jumprecord. 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
FixedDiceto 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
| Criterion | What the evaluator looks for | How this design addresses it |
|---|---|---|
| Working code | Runs, reads the input format, prints the exact output format | main parses stdin; a listener prints the exact lines |
| Modularity | Separate classes with single responsibilities | Board, Jump, Player, Dice, MoveRule, Game, listener |
| Extensibility | New rules without editing core loops | Dice and MoveRule strategies; config for turn rules |
| Edge cases | Overshoot, chains, cycles, invalid input | Board validation and chain resolution |
| Testability | Deterministic tests exist and pass | Injected FixedDice, JUnit tests |
| Readability | Names, small methods, no magic numbers | Named 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
| Type | Responsibility | Notes |
|---|---|---|
Jump | Start and end cell | Record; validates start ≠ end |
Board | Size, jump lookup, chain resolution, validation | Map<Integer, Jump> for O(1) lookup |
Player | Name and position | The only mutable domain object |
Dice | int roll() | Strategy: random, fixed, multi-dice |
MoveRule | Target cell for a roll (handles overshoot) | Strategy: exact finish, bounce back |
GameConfig | Extra turn on six, max sixes, play until last | Record; plain configuration |
GameListener | Reacts to moves, jumps, wins, forfeits | Observer; console printer is one implementation |
Game | Turn order, applying rules, ending the game | Depends only on interfaces |
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
- Two dice, take the sum or the max? New
MultiDicewith 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.
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.
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):
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
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.
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
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.
Core, end to end
Jump, Board with resolve, Player, Dice, Game loop, parser, printer. Run it on the sample input as early as possible.
Tests and validation
FixedDice tests for ladder, snake, overshoot, win. Add cycle and range validation.
One optional feature
Extra turn on six with three-six forfeit, or multiple dice. Show it is a local change.
Clean and demo
Rename, remove dead code, write a 5-line README on how to run and what is extensible.
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
Jumpinto aCellEffectinterface withint 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
PlayeraskipTurnscounter, 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 aCollisionRulestrategy 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
Gameis independent and single-threaded; a map from game id toGame, 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
new Random()inside the game loop, which makes testing impossible.System.out.printlnscattered through the game logic instead of one output component.- Applying a single jump instead of resolving chains, or resolving chains with no cycle protection (infinite loop on bad input).
- Hard-coding 100 and 6 in five places.
- Using
currentPlayer = (currentPlayer + 1) % nand then breaking it when a winner is removed. - An abstract
Entitybase class, aGameFactory, aBoardBuilderand aDiceFactoryfor a 150-line problem. Over-engineering reads as poor prioritisation. - 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
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 →