# Snake and Ladder — decision log

The Phase 5 answer key. Every entry is a question an interviewer actually asks ("why is that a
separate class?") and the answer that earns the mark: **what change the seam anticipates, and what
the change would cost without it.**

Twelve classes, none over ~110 lines. `Game` holds no rule that a requirement could reach except
one, and that exception is argued for below rather than hidden.

| File | One reason to change |
|---|---|
| `Game` | the *sequence* of taking a turn changes |
| `BoardLayout` | how a board is stored, or what makes one legal |
| `JumpRule` / `BoardGeometry` | what a snake or a ladder does to you |
| `Jumps` | how far a chain of jumps is followed |
| `TurnRule` / `SixRollsAgain` | when a player gets another roll |
| `Landing`, `Jump` | what we record about a move |
| `Pawn` | what we know about a player |
| `Announcer` | how listeners are told |
| `Demo` | what we show when we run it |

---

## The dice is the whole reason this problem is gradeable

`Dice` is in the contract and arrives through `Entry.create(Dice)`. Nothing in the reference calls
`Math.random()` or constructs a `Random`, so every fact in every test — *"a two from the start ends on
9"* — is a fact rather than a probability.

This is dependency injection in the form that matters most, and it is the same argument as injecting
a clock: **a dependency that decides things for itself makes the code around it untestable, and that
is a design defect before it is a testing inconvenience.** The tell that it is being done properly is
that `Game` has no method that produces a number nobody asked it for.

`RepeatingDice` is given rather than left to the candidate for the same reason the contract's records
are given: it crosses the boundary, so it must not become a design decision.

## Seam 1 · `JumpRule` — what a square does, asked one square at a time

**Anticipates:** every house rule about snakes and ladders themselves. Bites that send you back to
the start. Ladders that work only once. A snake that drops you to the nearest safe square. A square
that swaps you with the player in front.

**Why it is a *single step* and not a chain.** This is the decision worth defending. Two questions
look like one:

- where does *this* square send me? — volatile, differs table to table;
- how far do I keep going? — stable, and the same for every variant of the first.

`JumpRule` is the first; `Jumps` is the second, written once and blind to what any individual square
does. **Curveball 2 — snakes bite you back to the start — is therefore one new file and one line of
wiring: measured at 2 lines.** With the two questions fused into one method, that requirement edits
the walk, the "has the chain finished?" test and the announcement in the same breath, and the old
rule is gone rather than composed with.

**Why the exhaustive switch lives in `BoardGeometry` alone.** One class in the design has to know
every kind of square there is. Confining it to one means a new kind of square produces one compile
error in a class of fifteen lines, and `BiteToTheStart` — which cares about snakes and nothing else —
needs no change at all when a fourth kind of square appears.

## Seam 2 · `TurnRule` — when you roll again

**Anticipates:** the most house-dependent rule in the game. A six rolls again; three sixes forfeit
the turn; only a six off the start counts; certain squares grant a bonus go.

**Cost without it.** With `if (die == 6) { … }` in the turn loop, curveball 1's power-up squares mean
editing the method that also advances the pawn, resolves the chain and announces the move — and
"a six rolls again" and "this square gives you another go" become one tangle instead of two
sentences. **With it, the behaviour is one new file wrapping the old rule and one line of wiring:
2 of curveball 1's 6 measured lines.**

**Why it is asked about the die *and* the square, when today's rule needs only the die.**
`SixRollsAgain` ignores `endedOn`, which looks like an unused parameter and is the thing to defend.
The rule is about the turn, and a turn is *what was rolled and where it left you*; passing only the
die is exactly what makes "this square gives you another go" an edit to the game loop rather than a
new class. `PowerUpRollsAgain` is the proof: it is nine lines, it wraps rather than replaces, and
stacking is impossible by construction because the two reasons are combined with an `or`, not
counted.

## A3 · why a sealed interface, and what it costs

`SquareEffect` is `sealed permits Plain, Snake, Ladder`. A square's effect is a genuinely **closed**
set: the board has three kinds of square and the game's rules enumerate them.

**Why not an enum with a target field.** `enum Kind { PLAIN, SNAKE, LADDER }` plus an `int target`
allows `PLAIN` with a target of 14 and `SNAKE` with no target at all — states that cannot be
constructed here, because `Snake` carries a tail, `Ladder` carries a top, and `Plain` carries
nothing. Invariants placed in the shape of the type need no validation.

**Why not a `Square` interface with an `apply` method.** Because the effect crosses the API boundary
into `GameListener.jumped`, and a listener must be able to ask *what kind of thing bit me* without
being handed something that can move a pawn. Data crossing the boundary, behaviour on this side of
it.

**What it costs, stated honestly.** A sealed hierarchy **cannot be extended by adding a file alone.**
Curveball 1 adds a fourth kind of square and 4 of its 6 charged lines exist because of this choice:
one case in `BoardGeometry`, one in `Demo`'s commentator, and two lines of new board data. Both
switch cases were found by `javac` — *"the switch expression does not cover all possible input
values"*, with the file and the line — **before any test ran**. That is the trade: four named compile
errors instead of one silent wrong answer in the middle of a game. The seams' job is to keep those
four lines tiny and in files that do one thing each, and they do.

## B4 · `GameListener` — the side effects are outside the move logic

Working out where a player lands, and reacting to it, are different jobs with different reasons to
change. The commentary board, the sound effect on a snake, the move log, the analytics counter — all
of them want to know and none of them belongs inside the code that computes the landing.

**Cost without it.** `System.out.println` inside `roll()` is the version that cannot be tested and
cannot be turned off. Worse, it is the version where adding a second reaction means editing the turn
loop, so every new audience is a risk to the rules.

**What `Announcer` buys over a bare `List<GameListener>` in `Game`.** Every announcement in `Game` is
one line; iteration order and fan-out are not something the turn loop contains. And `Announcer`
**implements `GameListener` itself** rather than inventing a parallel set of `notifyX` methods, so a
new callback on the interface cannot be silently forgotten in the fan-out — the compiler catches it.

**Notification order is part of the contract, and deliberately so.** `moved`, then one `jumped` per
step of the chain, then `won`. An unspecified order would be a rule the candidate could fail on
without being able to know it, which is the one failure mode this corpus is not allowed to have.

## `Jumps` returns a `Landing` and moves nothing

`Jumps.from(square)` is a pure calculation: it decides where the chain ends and hands back the jumps
it took, and `Game` applies the answer. Nothing is announced from inside the walk.

That is what makes "where *would* this land?" answerable without playing a turn, and it is why the
jump events can be replayed in order after the fact. Had the walk moved the pawn and printed as it
went, the same question would need a second, duplicated walk — the duplication that goes stale.

## The one rule kept out of a seam, and why

**Overshooting is wasted, not bounced.** That lives in `BoardLayout.advance(from, die)` as a single
expression, not behind an interface.

The honest reason: it is a rule about the *board's edge*, it is one line, and no requirement in sight
changes it. A `MoveRule` interface with one implementation and one line of body would be a
speculative seam — the kind that reads as sophistication and is really just an extra file. If the
requirement ever did arrive ("bounce back off the last square"), the cost is that one expression, and
that is the cost to state out loud rather than pre-pay.

Naming the non-seams is part of the defence. Also deliberately absent:

- **No `Player` interface, no `HumanPlayer`/`BotPlayer`.** A player is a name and a square. An
  interface with one implementation would be speculation about a feature nobody asked for.
- **No locks.** The contract states the game is called from one thread. The right answer is to name
  the shared mutable state — the pawns' squares, `atTurn`, `winner` — and say where the lock would go
  if two gates ran at once, not to sprinkle `synchronized` and hope.
- **No persistence, no undo, no AI, no board rendering.** Stated out of scope up front.

## Identity · `Pawn` is an entity, everything else is a value

`Pawn` is the only mutable thing in the design, and it is mutable because a player's square genuinely
changes over time while their identity does not. Its `equals` is left as identity, deliberately: two
players called "A" would be two pieces, and moving a piece has to move *that* piece. Nothing is ever
looked up or removed by field equality — `Game` holds the pawns and finds them by name in a
`LinkedHashMap`, so turn order is the insertion order rather than something re-derived.

`Landing`, `Jump`, `Snake`, `Ladder`, `Plain` and the contract's `MoveResult` are values: two
identical ones are the same one, so they are records and none of them can be changed after the fact.

## Invariants sit in constructors

`BoardLayout` refuses a board with fewer than two squares, an effect on square 0 or the winning
square, a snake that climbs, a ladder that descends, or a chain of jumps that loops. `Game` refuses
fewer than two players and two players with the same name. `Pawn` refuses a blank name and a square
before the start. `Jump` refuses to describe a plain square as having moved someone.

There is no `validate()` method anywhere, because an invalid instance never gets far enough to need
one. The loop check earns its place in particular: a looping board would **hang** rather than lose,
and a hang is the one failure a player cannot interpret. Curveball 3's three rejection tests pass
without a line written for them, which is what an invariant in the right place looks like.

## Collections never escape

`players()` returns a list built once with `List.copyOf` and handed out unchanged — immutable, so a
caller cannot reorder the game by reordering the list they were given. `BoardLayout` copies the
effects map it is handed and never returns it. `Landing` copies its jumps. `Announcer` never hands
out its listeners, so one listener cannot unsubscribe a rival mid-notification.

## Errors: exceptions for broken requests, `Optional` for an ordinary absence

`winner()` returns `Optional<String>`, because "nobody has won yet" is the normal state of a game in
progress, not an error and not a null. `roll()` after the game is over throws
`IllegalStateException`, because the *request* is impossible. `positionOf` with an unknown name throws
`IllegalArgumentException`, because the argument is wrong.

That is the boundary being drawn on purpose: optionals for expected absence, exceptions for a broken
request. **Why no custom exception types:** the contract fixes the exception classes the grader
asserts, so `GameOverException extends IllegalStateException` would add files and buy nothing the
message does not already say. If the contract had left the choice open the answer would be different,
and *that* — rather than a rule — is the thing to say out loud.
