Board game · machine coding
Snake and ladder
You roll a six, land on 14, and climb a ladder to 31. That jump is wallpaper on the , not knowledge inside the . If Player.move() owns the snake table, the next board layout edits the player.
This page keeps the same game under different wallpaper: owns jumps, is injected, handles extra-on-six, and runs the turn. Compare with tic-tac-toe: board owns legality; a controller runs the turn.
Design journey
The naive start
Player.move() looks up snakes. The next board, or “no snakes in the last row,” edits the player.
Separate the board
owns cells and jumps via . Player holds a position and a name.
Inject chance
must be injected. Tests that cannot force a 4 cannot prove a snake.
Rules as policy
Extra turn on six is a . Exact landing is a Board or decision.
What actually happens when wallpaper changes
A snake or ladder is a jump from one square to another. The answers and returns the destination. Winning is “position equals last square,” maybe behind a if you want a variant. Turns are a queue. Extra turn on six is a rule on or a , not a boolean inside .
Exact landing on 100 versus bounce-back is a Board or WinRule decision. If the rule is “must land exactly,” a move that would pass 100 is a no-op (or bounce). That math does not belong on Player. See domain modeling and deterministic design.
The follow-up is a different board or a crocodile that sends you back two. That is more jump rules on the board, or a . Player still only stores a square.
Simple rule of thumb
The journey in the hero decides ownership before you type Java: jumps on the board, identity on the player, chance injected, policies outside both.
owns
- Board size is 100 by default, or N when you parameterize it.
- Jump map stores snakes and ladders as from→to pairs only.
- Optional bounce or exact-win math when applying a proposed square.
- Does not own turn order, extra-on-six rules, or ASCII printing.
owns
- Player has identity and a display name for the roster.
- Current square is a number, never a snake or ladder table.
- Not Random, not Scanner, not “if roll equals 6 play again.”
Rule of thumb
stores a square. . applies jumps. is injected. covers extra-on-six. names exact or bounce. is the controller.
How one playTurn runs
Once ownership is fixed, is the controller: next turn, roll, move, apply jumps, detect a winner. is a strategy you can fake in tests. A FakeDice that returns 4 is the whole point of the seam.
Read the diagram definition
sequenceDiagram
autonumber
participant G as Game
participant D as Dice
participant P as Player
participant B as Board
G->>D: roll()
D-->>G: 4
G->>P: position + 4
G->>B: landOn(14)
B-->>G: 31 (ladder)
G->>P: setPosition(31)
alt position == last square
G-->>G: winner
else extra turn on six
G-->>G: same player again
else
G-->>G: next in queue
endJava API sketch
new Random() inside roll is a failed design. You cannot test “land on 14, climb to 31” without a dice that returns 4. Inject .
interface Dice {
int roll();
}
class Board {
private final int size;
private final Map<Integer, Integer> jumps;
int landOn(int square) {
return jumps.getOrDefault(square, square);
}
boolean isLast(int square) {
return square == size;
}
int size() { return size; }
}
interface WinRule {
boolean won(int position, Board board);
}
interface TurnPolicy {
boolean extraTurn(int roll);
}
class Player {
private final PlayerId id;
private final String name;
private int square;
}
class Game {
private final Board board;
private final Dice dice;
private final Queue<Player> turns;
private final WinRule win;
private final TurnPolicy turnsPolicy;
void playTurn() {
Player p = turns.peek();
int roll = dice.roll();
int tentative = move(p.square(), roll);
p.setSquare(board.landOn(tentative));
if (win.won(p.square(), board)) { /* end */ return; }
if (!turnsPolicy.extraTurn(roll)) {
turns.add(turns.poll());
}
}
}FakeDice returns 4
The sketch above is only proven when a test can force the roll. with a ladder 14→31. at 10. FakeDice returns 4. .playTurn. Position 31. Player still has no snake table.
Numbers to know
- Start at 10, roll 4, landOn(14) climbs the ladder to 31.
- FakeDice returns 6; TurnPolicy keeps the same head of the queue.
- Exact-win: at 99, roll 4, stay 99 if that is the named rule.
- Winner exists: further playTurn rejects; new Random() makes this untestable.
Tests that prove the seams
@Test
void ladderFromFourteen() {
Board board = Board.withJump(14, 31);
Player a = new Player("A");
a.setSquare(10); // 10 + roll 4 = 14 → ladder to 31
Game g = new Game(board, new FakeDice(4), List.of(a));
g.playTurn();
assertEquals(31, g.current().square());
}What still breaks, and what the follow-up changes
The FakeDice test above goes green and you can still fail on a bad jump map, a wall-clock die, or a thread pool nobody asked for. Name these cases out loud.
Invalid cases
- Roll of 0 or 7 on a six-sided die: production must emit 1–6 only.
- Landing past 100 when exact-win is on: or rejects; position stays.
- Snake from 99 to 99: a jump must change square, or you loop; validate at construction.
- Two players on the same square: usually legal; occupancy is a Board rule, not Player's snake map.
playTurnafter a winner exists: state should reject (same as tic-tac-toe Won/Draw).
Concurrency
A board game is usually single-threaded: one loop. If two UIs call playTurn at once, you serialize on the Game object. There is no “two dice racing.” Do not invent a thread pool for a token moving on 100 squares.
Still fails when…
- Player.move() knows snakes, so the next board layout edits the player.
new Random()sits inside roll, so you cannot force a 4.
What the follow-up changes
- Crocodile or trampoline: or extra jump-map entries; Player stays a square.
- N-sided or two dice: new Dice implementation; playTurn still calls roll() once (or a sum).
- Must roll a six to start: TurnPolicy or PlayerState, not a flag through Board.
- ASCII board in main: Board.toString() is a query; Scanner stays in main.
Wrap-up
You have the ownership split, the FakeDice ladder proof, and the invalid cases. Follow the related notes when you want the seams named more generally.
Read next
Key takeaway
You're done when the owns snakes and ladders as jumps, are injectable for tests, winning on exact landing (if that is your rule) is enforced, and turn order advances only after a valid move. A crocodile is more wallpaper; still does not know the table.