A worked demonstration
Thirty-one steps from nothing to a game you can sit down and play, against an opponent that taught itself — written by a model, specified and verified by you. Every prompt is here, ready to copy.
On this page: Before you start | What this is | The method | Standing grounding | The thirty-one steps | Deliberately excluded | Things to watch
This builds a complete backgammon game in two phases. Phase 1 is the engine: the rules, the doubling cube (the marker that records what a game is worth), judgement, and finally a neural network (a program that learns from experience rather than from written rules) that learns to play from nothing but self-play. Phase 2 is the game itself: a graphical board you move checkers on with mouse or keyboard, undo, hints drawn from the engine, and dice that roll. The code is written by an AI model. You never write a line of it.
The two phases are separable. Phase 1 ends with something thoroughly tested that nobody can play. Phase 2 ends with something anybody can. Each is worth doing alone. The seam between them is deliberate — twenty-two steps of engine never learn that a screen exists.
You write no Shoddy at all — not the engine, not the game, and not the tests. What you write is English: what must be true, and what would prove it. That division is the subject. The engine is the evidence.
Ask a model to write a chess engine in C (a widely used programming language) and you cannot tell engineering from recall. Stockfish and Sunfish, two well-known chess engines, sit in its training data (the text the model learned from), along with a thousand tutorials about them. The result may be excellent while proving nothing.
There is no backgammon engine in Shoddy to remember. Every line the model produces has to come from the requirement, the language reference, and nothing else. That makes the requirement the independent variable (the one input the experiment turns on), which is what a demonstration of requirements-driven development needs.
Backgammon supplies the second necessary property: small enough to finish, and intricate enough that a plausible-looking implementation is wrong in several specific, catchable ways.
If the model writes the tests too, what is left to stop it agreeing with itself? The gate at the end of every step below — a list, in English, of what must be true, written before the model has seen anything. The assertions (the checks the tests make) transcribe it. The gate is the contract. Writing the assertions by hand would buy nothing the gate has not already bought. It would cost the thing this method is for: an implementation cheap enough to delete and regenerate, with a test suite that regenerates alongside it.
The risk is real all the same, and it has a name: a requirement misread once can be encoded twice, in the code and in the test, and pass. Four things make that expensive:
Things to watch returns to it, because it is the one failure this method cannot design away.
mill run), a model you
can hold a conversation with, and somewhere to keep files. Nothing else.Tests call functions by name, and names come from a design. So the cycle has a design gate before the test gate:
Tests still precede implementation, and you still validate twice — once on the shape, once on the coverage. What you do not do is type. You write no Shoddy in this build: not the engine, not the game, and not the tests.
Assert calls is transcription.
Doing the transcribing yourself would buy you nothing the gate did not already buy. It
would cost you the ability to throw the whole implementation away and regenerate it.You do not say "represent the board from the mover's perspective and flip it to pass the turn." That is a design, and handing it over makes the model a typist. You say:
No function may need to know which player is moving in order to decide how a checker travels.
That is a property any acceptable design must have. It rules out the colour flag without naming the alternative. If the model finds the perspective-flip on its own, that is a result. If it does not, you reject and restate — and that exchange is worth more than the answer would have been.
Never let one reply contain both the tests and the implementation. The model writes the tests, which is what keeps your hands off the keyboard. But a model that has already written the implementation will write tests that agree with it, fluently, proving nothing. Ask for the tests, read them, then ask for the code. The separation does the work that secrecy used to do, and it does it without you typing an assertion.
Do not hand-patch generated code. When something fails, the question is not what is wrong with the code but what did my requirement fail to say? Every hand-patch is a defect that returns the next time you regenerate.
Scope the context to the step. Hand over the rules that step needs, not the rulebook. A model told the deduplication rule (the removal of duplicate plays) at step 6 will apply it at step 6, and step 7's finding never happens.
Each step produces three artifacts (three separate pieces of work) in three separate replies. You review each at its own moment, and each review looks for a different thing.
| D1 — Design | D2 — Tests | D3 — Implementation | |
|---|---|---|---|
| Arrives | after the requirement | after the design is accepted | after the tests are accepted |
| Contains | types, signatures, rationale, questions | test.shoddy: one assertion per gate clause | Shoddy source |
| Contains no | implementation, not even a sketch | implementation, and no test for anything out of scope | new public interfaces, no test changes |
| You ask | is this the right shape? | what would pass this that shouldn't? | does it pass? |
| Reviewed by | reading and arguing | reading the assertion messages | running the tests |
Steps marked extends below skip D1 — the interface is already settled. No step skips D2.
Every step below states its deliverables explicitly: whether a design document is expected, and which files the tests and the implementation must produce. A step that produces something not on that list has exceeded its scope, however good the extra code is.
D2 is where the gate stops being prose. That is why the design arrives first: nothing can assert against a function whose name has not been settled. And it is why the implementation arrives last: assertions written after the code that will satisfy them are a description of the answer, not a test of it.
You will not read D2 line by line — you do not have to. The assertion messages are sentences, and they are what you read. Three questions, in order:
What you find goes back as a sentence — a new gate clause, or a correction to the requirement. Never send it back as an assertion you wrote yourself. The moment you hand-write one, you own it, and it stops being regenerable with everything else.
Three responses, and the difference matters. Accept and proceed to tests. Redirect when the shape is wrong but your requirement was adequate — the model's miss. Reject and rewrite the requirement when the requirement permitted the wrong design — yours, and the more valuable finding. A record in which every design issue turns out to be a redirect is a record that has been flattering its author.
Write one entry per cycle, as it happens. Each entry records:
Write it live. A log reconstructed afterwards comes out tidier than the truth, and the tidiness shows.
The most instructive artifact this produces is the diff (the line-by-line comparison) between a step's first requirement and its last. Everyone publishes the final, clean version. Nobody publishes how it got there.
Give the model this once, at the start of every session, before anything else. Grounding is the background text that tells the model how to work. This block is the only grounding that stays constant — the backgammon rules and the list of what already exists arrive fresh with each step.
# Backgammon in Shoddy
You are writing Shoddy. One file per step, named as the step tells you.
## The language
Read these before writing anything. Do not summarise them back to me.
Three trees, all of them fair game:
- `docs/*` — the documentation. `quickref.html` first (every builtin word,
type and syntax form on one page), then `spec.html` for the grammar and
the semantics, `errors.html` for every error with its causes and fixes,
and `machines/*.html` for a word reference per machine.
- `machines/*` — the standard library in readable Shoddy. The definitive
answer on any library word's name, arguments and types, and the house
style you should be writing in. Read the machine rather than guessing.
- `mills/*` — complete programs built the way this one is: a pure core, a
thin shell, a headless test. Worth reading before phase 2 in particular,
where a window and an event loop arrive.
## Constraints that will bite
Verify each against the docs; do not trust this list.
- **Everything is immutable.** There is no assignment. `SetNth` returns a copy.
- **`Number` is an IEEE double and the only numeric type.** No integers, and
**no bitwise operators** — bit work is `bool.shoddy`'s words (`BoolBitAnd`,
`BoolShl`, `BoolMask`), never `&` or `<<`. If you want the operators
themselves, the design is wrong for Shoddy.
- The network is gated behind `--allow-net`. A plain socket never blocks; a
secured socket blocks on `Recv` and refuses `Ready`.
- **A picture does not need a screen.** `ScribblerSave(sc, path)` writes the
buffer to a PNG with no window involved; `mill run --no-window` opens every
scribbler hidden.
- **Indexes are 1-based.**
- **Arrays** are fixed-length with O(1) `Nth`; **lists** are cons cells for
recursion. Index into arrays, recurse over lists.
- **Records** compare structurally with `=`. `With(r, F = v)` copies.
- **Self-tail-recursion compiles to a loop.** Mutual recursion consumes stack.
- **No exceptions.** `Error(msg)` aborts. Failure must be a returned value.
- Word names are case-insensitive; string contents are exact.
## Machines you may include
`seq`, `str`, `math`, `random`, `file`. Ask before any other.
## How we work
Each step has three phases, in three separate replies. Do not run them
together, and do not start the next one until I accept the last.
**Phase 1 — design.** You propose. Give me:
- the types you will introduce, and what each field means
- the signature of every function you will expose, one line on each
- the representation choices you made and **why**, especially where you
considered an alternative and rejected it
- anything in the requirement you found ambiguous
**Write no implementation in phase 1.** Not a sketch, not an example body.
**Phase 2 — tests.** Only after I accept the design. Write `test.shoddy`
against the accepted interface, from the GATE at the end of the step:
- one assertion per clause of the gate, in the gate's order
- each message a sentence saying what is true, so a failure names the
requirement it broke and not the function it called
- where the requirement holds for every input and not just the examples,
assert the property, not three cases of it
- nothing for behaviour this step does not own
**Write no implementation in phase 2**, and do not tell me how you intend
to make these pass.
**Phase 3 — implementation.** Only after I accept the tests. Implement exactly
what was designed. If implementing reveals the design was wrong, stop and say
so rather than quietly changing it.
## Rules of engagement
- Build only what the step asks. If you can see what the next step needs,
**do not build it.** Out-of-scope code is a defect even when correct.
- Anything under "What already exists" is fixed. Call it. Do not reimplement
it or change its signature.
- **Never change a test to make the implementation pass.** If a test looks
wrong, stop and say which requirement you think it has misread. A test
changes only when I change the gate it came from.
- **If the requirement is ambiguous, stop and ask.** Do not choose and proceed.
An ambiguity you surface is worth more than a guess that happens to be right.
Each step gives the prompt to send, what to look for in the design, and the gate the implementation must pass. Where a step says extends, there is no design proposal — the interface is settled and the implementation comes directly.
Step 1 · design proposal
### What already exists
Nothing. This is the first step.
### The game
Backgammon is a game for two players. It is played on a board of 24 points,
with fifteen checkers each, TWO DICE, and a DOUBLING CUBE that records what
the game is currently worth. A player wins by bearing all fifteen checkers off
the board; how much that win is worth depends on how badly the loser was beaten
and on the cube.
This step is only the board. Dice, movement and the cube come later.
### The rules
The board has 24 points, numbered 1 to 24. Each player has 15 checkers.
The two players move in opposite directions. Numbering the points from one
player's side, that player travels from 24 down toward 1; the other travels the
other way, so what is point 24 to one player is point 1 to the other.
A point may hold any number of checkers of one colour. It never holds both
colours at once.
A checker that has been hit sits on the bar, off the board, until it re-enters.
A checker that has completed its journey is borne off and takes no further part.
The starting position, described from one player's side: 2 checkers on point
24, 5 on point 13, 3 on point 8, 5 on point 6. The other player's checkers are
the mirror image.
### What the program must be able to do
1. Represent any legal board state, including checkers on the bar and borne off.
2. Produce the starting position.
3. Report, for either player, the total checkers they have anywhere — on points,
on the bar, and borne off. For a legal position this is always 15.
4. Compare two board states for equality.
### Design constraints
- **No function may need to be told which player is moving in order to decide
how a checker travels.** Direction must fall out of the representation, not
out of a conditional in every function.
- A point's occupancy is a single value, not two counts. Both colours cannot
occupy a point.
- The bar and the borne-off checkers are not points and must not be stored as
though they were. Both players can have checkers on the bar at once.
- Shoddy is immutable: every operation returns a new board. Choose a
representation whose copy is cheap.
### Out of scope
Dice, moves, legality, display, passing the turn, the cube.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.board.shoddy: the board type, a constructor for the starting position, two total-checker functions, and an equality function.## Types
Board
Points Array Of Number 26 slots. Slots 1-24 are the points; 0 and 25 are
unused padding so point N sits at index N.
Positive = the player to move; negative = the
opponent; zero = empty. Never both signs at once.
BarMine Number Checkers of the player to move on the bar. >= 0.
BarTheirs Number The opponent's. >= 0.
OffMine Number Borne off by the player to move. >= 0.
OffTheirs Number The opponent's. >= 0.
## Interface
StartBoard() As Board The opening position.
CheckersMine(b) As Number Total for the player to move: 15 when legal.
CheckersTheirs(b) As Number Total for the opponent.
SameBoard(a, b) As Boolean Structural equality.
## Rationale
Direction. The requirement forbids any function needing to be told whose turn it
is in order to know how a checker travels. I considered an absolute board with a
side-to-move field, but every movement function would then branch on it, which is
what the constraint rules out. Instead the board is always recorded from the point
of view of the player to move: that player always travels from 24 toward 1, and
their home is always 1-6. Direction is therefore a property of the representation,
not a parameter. Handing over the turn will need an operation that re-reads the
board from the other side; I have not built it, as it is out of scope here.
Sign, not two counts. The requirement states a point never holds both colours, so
one signed number suffices and makes the impossible state unrepresentable. Two
counts would permit a board that cannot exist.
Padding. Slots 0 and 25 are unused so that point N is at index N. It costs two
numbers and removes an off-by-one from every future function.
Bar and off as fields. I considered storing them in the unused slots 0 and 25.
Rejected: both players can be on the bar simultaneously, and two players' bars
cannot share one signed slot.
Copy cost. A board is 26 numbers plus four, so a copy is about 30 numbers. Cheap
enough that immutability costs nothing here.
## Questions
1. "A point may hold any number of checkers of one colour" — is fifteen a hard
ceiling I should assert, or genuinely unbounded? I have assumed unbounded.
2. Should equality distinguish two boards differing only in whose turn it is?
With this representation the question does not arise, but it would under an
absolute-board design, and I want to confirm it is not a requirement I have
designed away.
Three things to notice. The bar rationale names the failure it avoids rather than restating the constraint — that is understanding rather than compliance. The direction rationale ends by declining to build step 2, which is the scope discipline holding. And both questions are real holes in the requirement, found before a line of code exists.
Step 2 · extends
### What already exists
[ board type, starting position, checker counts, equality ]
### The rules
When a turn ends, the other player moves. The board does not change; only whose
turn it is changes. What was one player's point 24 is the other's point 1.
### What the program must be able to do
1. Take a board and produce the same board as the other player now sees it.
2. Doing that twice returns exactly the original, for every legal position.
3. The starting position is unchanged by it, being symmetric.
### Design constraints
- Total: defined for every legal board, with no failure case.
- Loses no information. Everything recoverable before is recoverable after.
### Out of scope
Dice, moves, legality, display, the cube.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.board.shoddy extended with one turn-passing function. No new types, no other additions.Step 3 · extends
### What already exists
[ board type, starting position, turn-passing, counts, equality ]
### The rules
None beyond the layout already described.
### What the program must be able to do
Render any board as text so a reader can identify which points hold which
player's checkers and how many, how many each player has on the bar, and how
many each has borne off.
This exists to be printed inside a failing assertion. Legibility is the only
requirement.
### Design constraints
- Pure: returns text, prints nothing.
- Rendering from the other player's side produces the mirror image, with no
separate code path.
### Out of scope
Colour, terminal control, interactivity, anything a player would use.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.show.shoddy: one function returning a board as text.Step 4 · design proposal
### What already exists
[ board type, starting position, turn-passing, rendering, counts, equality ]
### The rules
A turn in backgammon is played by rolling TWO DICE. Each die shows 1 to 6, and
each die value moves ONE checker that many points, in the moving player's
direction of travel. A player who rolls two different numbers makes two moves,
one of each value; a player who rolls the same number twice makes four moves of
that value.
**This step is about a single move by a single die value only** — the building
block. How the two dice combine into a whole turn, and which combinations are
legal, come in later steps. Build the piece, not the turn.
A point holding two or more of the opponent's checkers is blocked; a checker
may not land there.
A point holding exactly one opponent checker is a blot. Landing on it hits that
checker, which goes to the bar; the arriving checker takes the point.
A point that is empty, or holds any number of the moving player's own checkers,
is open. There is no limit on how many may stack on a point.
A checker on the bar re-enters that many points from the far end of the board:
a 1 enters on the farthest point from home, a 6 on the sixth-farthest. Entry is
subject to the same blocking and hitting rules.
While a player has any checker on the bar, they may move nothing else. Every
checker on the bar must re-enter first.
### What the program must be able to do
1. Report where the moving player could legally move a checker from.
2. Given a board, a source, and one die value, either produce the resulting
board together with whether a checker was hit, or report the move illegal.
Shoddy has no exceptions, so illegality must be a value you return.
### Design constraints
- Moving from the bar and moving on the board are the same operation from the
caller's point of view. Do not expose two functions.
- Whether a move is legal and what board it produces must be answered by one
call, not two.
- The result must compose: later steps will chain these moves to build whole
turns, so the output of one must be a valid input to the next with nothing
in between.
### Out of scope
Bearing off — a move carrying a checker past the end of the board is simply
illegal at this step. Rolling dice. Combining two dice. Whole turns. The cube.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.move.shoddy: a source-enumeration function and a single-die move function returning either the resulting board with a hit flag, or the illegal outcome.Step 5 · extends
### What already exists
[ board type, turn-passing, rendering, source enumeration, single-die moves ]
### The rules
The last six points in a player's direction of travel are their home board.
Bearing a checker off — removing it as finished — is legal only when all fifteen
of that player's checkers are in their home board or already borne off. A
checker on the bar disqualifies the player entirely.
Given that, for a checker whose remaining distance is exactly the die value,
that checker bears off.
For a die larger than the checker's remaining distance, the checker bears off
only if that player has no checker further from home. If they do, the move is
illegal.
For a die smaller than the remaining distance, this is an ordinary move within
the home board. It is never a bear-off, and a player is never obliged to bear
off when an ordinary move is available.
### What the program must be able to do
1. Report whether a player is in a position to bear off at all.
2. Extend single-die movement so bear-offs are produced under the three rules.
### Design constraints
- The single-die move signature does not change. Bearing off is a kind of move.
- The "no checker further from home" test is a property of the whole board, not
of the source point. Do not answer it locally.
### Out of scope
Whole turns, rolling dice, the cube.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.move.shoddy extended: a bear-off eligibility test, and the existing move function extended to produce bear-offs. Its signature does not change.Step 6 · design proposal
### What already exists
[ board type, turn-passing, rendering, source enumeration, single-die moves
including bear-off, the bear-off eligibility test ]
### The rules
A turn is played by rolling two dice.
If the dice show different values, the player makes two moves, one of each
value. If both show the same value, the player makes four moves of that value.
The dice may be used in either order, and the order can matter: a play reachable
one way round may be unreachable the other.
Within a turn, moves may use different checkers or move the same checker more
than once.
### What the program must be able to do
Given a board and two die values, produce every complete way the turn could be
played, each paired with the board it produces.
A way of playing is complete when no further move is legal with any die still
unused.
### Design constraints
- **Correctness matters and speed does not.** This will later be the reference
a faster version is checked against, so it must be short enough that a reader
can convince themselves it is right by reading it. Do not prune. Do not exit
early. Do not optimise anything.
- Every result must record how many dice it used.
- The recursive structure should be visible in the code, because the rule is
recursive.
### Out of scope
Removing duplicate results. Discarding results using fewer dice. Any rule about
which die must be played. Choosing between plays. Rolling the dice — the values
are given to you. **Return everything you generate, unfiltered.**
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.turn.shoddy: one generator returning every complete way to play a roll, each with its resulting board and the number of dice it used. Unfiltered.Step 7 · design proposal
### What already exists
[ everything through whole-turn generation, unfiltered ]
### The rules
Two ways of playing a turn that leave the board in the same state are the same
play. How the checkers got there does not matter.
Moving one checker from point 13 to 7 and then on to 2 leaves the same board as
moving it 13 to 8 and then 8 to 2. One play, not two.
It also leaves the same board as moving one checker from 13 to 8 while a
different checker goes from 8 to 2. Still one play.
### What the program must be able to do
Reduce the generated results so at most one remains per distinct resulting
board.
### Design constraints
- The generator can produce on the order of 50,000 candidates for a doubles
roll, reducing to a few hundred distinct boards. **A design comparing each
candidate against every result kept so far performs tens of millions of
comparisons for one roll.** Choose a design whose cost grows better than that.
- Sameness is a property of the resulting board and nothing else. Do not compare
the moves.
### Out of scope
Any rule about how many dice must be used, or which die must be played.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.turn.shoddy extended: an identity key for a board, a reduction function, and the generator now returning reduced results.The 6-and-5 case is worth working by hand before you run it. Most people count 8 and have to be argued down to 7.
Step 8 · extends
### What already exists
[ whole-turn generation with duplicates removed ]
### The rules
Two rules constrain a turn as a whole rather than any individual move.
A player must use as many dice as it is legally possible to use. If some way of
playing uses both dice, the player may not choose one that uses only one.
If it is possible to use only one die, and either die could be the one used,
the player must use the higher.
If no move at all is legal, the player forfeits the turn and play passes.
### What the program must be able to do
Apply both rules so only genuinely legal plays are returned, and report the
forfeited case distinctly from an error.
### Design constraints
- **Both rules are properties of the complete set of possibilities and cannot be
decided while generating.** A way of playing beginning with the lower die may
permit both dice where beginning with the higher does not. Any design deciding
which die to use before generating everything will be wrong on exactly the
positions where the rule matters.
- The forfeited turn is a legal outcome of the rules, not a failure.
### Out of scope
Choosing among the surviving plays.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.turn.shoddy extended: the two whole-turn filters applied after generation, and a distinct forfeited-turn result.Step 9 · design proposal
### What already exists
[ the complete, correct, slow turn generator; board type; turn-passing;
rendering; board equality ]
### The rules
None new. The fast generator must produce exactly what the slow one produces.
### What the program must be able to do
1. Generate the legal plays for a board and a roll, substantially faster, by any
means preserving the result.
2. Report whether the two generators produce the same SET of resulting boards
for a given board and roll.
3. Play a specified number of games choosing uniformly at random among legal
plays, collecting every board visited, reproducibly from a seed.
### Design constraints
- The slow generator is the definition of correct. Where they differ the fast
one is wrong. Do not modify the slow one to agree.
- Set comparison, not sequence comparison. Order is not significant.
- The harness must run thousands of boards without accumulating state that
changes its behaviour.
### Out of scope
Evaluation, opponents, choosing a play, the cube.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.turn-fast.shoddy and harness.shoddy: the fast generator, an agreement check between the two generators, and a seeded random-play harness that collects every board visited.Step 10 · extends
### What already exists
[ the complete move generator, board type, turn-passing, rendering, the harness ]
### The rules
A player wins by bearing off all fifteen checkers.
If the loser has borne off at least one checker, the win is a plain win and
counts 1.
If the loser has borne off none, it is a gammon and counts 2.
If the loser has borne off none and additionally still has a checker on the bar
or anywhere in the winner's home board, it is a backgammon and counts 3.
These counts are the base value of the game. The doubling cube multiplies them,
and comes in the next step.
### What the program must be able to do
1. Determine whether a board is a finished game and, if so, which of the three
outcomes it is.
2. Report the base value of that outcome.
### Design constraints
- The three outcomes are decided by one rule set, not three independent checks.
- The base value and the cube's multiplier are separate concerns. Do not
anticipate the cube here, but do not design something the cube cannot
multiply.
### Out of scope
The cube. Notation. The game loop.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.outcome.shoddy: a finished-game test, the outcome, and its base value.Step 11 · design proposal
### What already exists
[ the complete move generator, outcome determination and base values, board
type, turn-passing, rendering ]
### The rules
Alongside the two dice, a game is played with a doubling cube: a marker showing
2, 4, 8, 16, 32, 64, recording what the game is currently worth.
At the start the cube is worth 1, sits in the middle, and belongs to neither
player. Either may be the first to double.
A player may offer to double the stakes only when it is their turn and only
BEFORE they roll. They may not double after seeing their dice.
The opponent, offered a double, either:
- PASSES, and immediately loses the game at its value BEFORE the double; or
- TAKES, and the game continues at twice the previous value, with the cube now
belonging to the taker.
Only the owner of the cube may offer the next double. While it sits in the
middle either player may. Once a player owns it, their opponent cannot double
until they have taken a double themselves and so gained ownership.
A cube at 64 cannot be doubled further.
The value of a completed game is the cube's value multiplied by the base value
of the outcome — 1 for a plain win, 2 for a gammon, 3 for a backgammon.
### What the program must be able to do
1. Represent the cube: its current value, and who if anyone owns it.
2. Report whether a given player may offer a double at a given moment.
3. Resolve an offered double, given the opponent's answer, producing either a
finished game with its value or a continuing game with the cube updated.
4. Score a finished game as the cube's value times the outcome's base value.
### Design constraints
- Cube ownership has THREE states — neither player, one player, the other. Not
two, and not a boolean with a special case.
- Whether a double is legal depends on ownership and on where you are within the
turn. It is not a property of the board.
- **A player who passes loses the value BEFORE the double, not after.** This is
the most commonly implemented-wrong rule in the game.
- The existing outcome rules must not be rewritten. The cube multiplies a
result; it does not change what the result is.
### Out of scope
Whether doubling is a good idea, and whether to take — those need judgement and
come much later. Automatic doubles, beavers, raccoons, the Jacoby rule, match
play and the Crawford rule. This is money play with a plain cube.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.cube.shoddy: the cube type, a may-double predicate, a resolution function for take and pass, and a game-scoring function combining cube value with base value.Step 12 · extends
### What already exists
[ move generator, outcome determination, base values, the cube and its rules,
rendering ]
### The rules
Moves are written as source and destination separated by a slash: a move from
point 8 to point 5 is "8/5". A checker entering from the bar onto point 22 is
"bar/22". A checker borne off from point 6 is "6/off". A move that hits carries
a trailing asterisk: "8/5*". A turn is its moves in the order played, separated
by spaces.
Cube actions are recorded too: a player offering a double, and the opponent's
take or pass, are each part of the record of a game.
### What the program must be able to do
1. Render a play in the notation above.
2. Render a cube action — an offer, a take, a pass — distinguishably.
3. Take a notation string and a list of legal plays, and identify which play it
denotes, or report that it denotes none of them.
### Design constraints
- Notation must round-trip: rendering a play and reading it back identifies that
same play, for every play the generator produces.
- Reading notation must not re-derive any rule. It matches against plays the
generator already produced.
- A cube action and a checker play are different kinds of thing. Do not force
them into one representation to share a parser.
### Out of scope
The game loop, input handling, opponents.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.notation.shoddy: render a play, render a cube action, and match a string against a list of legal plays.Step 13 · design proposal
### What already exists
[ move generator, outcomes and base values, the cube and its rules, notation
both ways, rendering, turn-passing ]
### The rules
To begin, each player rolls one die. The higher roll moves first and plays that
pair of dice as their opening turn — the dice are not re-rolled. Equal rolls are
re-rolled.
After that, a turn has TWO decision points. First, before rolling, the player on
turn may offer to double if the cube rules permit; if they do, the opponent must
take or pass, and a pass ends the game. Then the player rolls two dice and plays
a legal turn. A player with no legal play forfeits the turn.
The game ends when either player bears off all fifteen checkers, or when a
double is passed.
### What the program must be able to do
1. Determine who moves first and with what dice.
2. Run a complete game: offer the cube decision, then roll, offer the legal
plays, apply the chosen one, test for a finished game, pass the turn.
3. Choose uniformly at random among legal plays, reproducibly from a seed.
4. Accept a play or a cube action proposed by a human and either apply it or
reject it.
5. Make a forfeited turn visible rather than silently skipping it.
6. Report the final value of a game, cube included.
### Design constraints
- **A proposed play is legal if and only if it is among the plays the generator
produced.** No rule of backgammon may be re-checked at this layer. There is
exactly one rules engine in this program and it already exists.
- **A turn contains two independent decisions — whether to double, and how to
play.** Both must be supplied to the loop from outside, and either must be
replaceable without touching the loop or each other. Later steps supply better
versions of both.
- A game can end without any checker being borne off. Do not assume the terminal
condition is a board state.
### Out of scope
Judgement of any kind. Any opponent that is not random, and any doubling policy
beyond "never double, always take."
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.game.shoddy: the opening roll, the turn loop, a random chooser, a never-double policy, human-proposal validation, and final scoring.Step 14 · extends
### What already exists
[ move generator, the cube, the game loop, outcomes, turn-passing, rendering ]
### The rules
A player's pip count is the total distance their checkers must still travel to
be borne off. A checker needs as many pips as its distance from bearing off. A
checker on the bar must travel the whole board and counts 25.
Contact is broken when the two players' checkers have passed each other so
completely that neither can ever hit the other again, whatever is rolled. From
that moment the game is a pure race: no hitting or blocking can occur and only
distance matters.
A player on roll has an advantage worth roughly one move.
### What the program must be able to do
1. Report either player's pip count.
2. Report whether contact is broken.
3. Given a board with contact broken, estimate the probability the player on
roll wins.
### Design constraints
- The race estimate is a probability between 0 and 1. Not a score, not a pip
difference.
- Constants it uses must be named values changeable in one place; they will be
tuned.
- Contact detection must be exact, not a heuristic. Either a hit is possible or
it is not.
### Out of scope
Positions where contact still exists. Choosing plays. The cube.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.race.shoddy: two pip counts, an exact contact test, and a race win-probability estimate with its constants named.Step 15 · design proposal
### What already exists
[ pip counts, contact detection, race estimation, move generator, the cube, the
game loop, turn-passing ]
### The rules
A point holding exactly one checker is a blot and can be hit. A point holding
two or more is made and is safe. Consecutive made points form a prime; six in a
row cannot be passed by any roll. A made point inside the opponent's home board
is an anchor and gives a player somewhere safe to wait.
A blot is hit directly, by a single die, when an opponent checker is one to six
points away. It is hit indirectly by two dice combined — but only when the
intermediate landing point is not blocked.
A game does not simply end in a win or a loss. It ends in one of six results: a
plain win, a gammon win, a backgammon win, and the three corresponding losses.
A position where the opponent has borne off nothing is very different from one
where they are nearly home, even at the same winning chances.
### What the program must be able to do
1. Report the probability a given blot is hit by the opponent's next roll.
2. Estimate, for any board, the probabilities of ALL SIX RESULTS from the point
of view of the player on roll. They describe one game and must be consistent
with that.
3. Use the race estimate when contact is broken and the contact estimate
otherwise.
### Design constraints
- **Hit probability must be computed by enumerating all 36 outcomes of two dice
and asking whether each admits a hitting reply.** Do not derive or approximate
a formula. The enumeration is the definition.
- Estimating a single winning chance is NOT sufficient. Later steps need gammon
and backgammon chances to value the cube, and adding them afterwards would
mean redesigning this.
- Every weight or coefficient must be a separately named value.
- The estimate must be consistent under a change of viewpoint: a board judged
from one side and the same board judged from the other must agree about who is
winning and by how much.
### Out of scope
Turning these probabilities into points. Choosing plays. Cube decisions.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.judge.shoddy: a blot hit-probability function by enumeration, a six-result distribution for any board, and the race/contact switch. Every weight a named value.Step 16 · extends
### What already exists
[ the six result probabilities for any board, race and contact estimation, the
cube and its rules, the game loop ]
### The rules
The value of a position is measured in points, expected. That is its equity.
Ignoring the cube, equity is the sum over every possible result of its point
value times its probability: a plain win is +1, a gammon +2, a backgammon +3,
and the corresponding losses are negative.
Equity is what makes cube decisions computable, and it is also the right measure
for choosing a play, because a play that raises your winning chances while
losing gammon chances may be worse than it looks.
### What the program must be able to do
1. Compute the equity of a board in points, from the six result probabilities.
2. Multiply an equity by a cube value to give the equity of a game played at
those stakes.
### Design constraints
- **Equity is in points and is not bounded by 0 and 1.** It ranges from -3 to
+3. Do not reuse the probability type for it. Confusing the two is the defect
this step exists to catch.
- Equity from the other player's viewpoint is the negative of equity from this
one. That must hold exactly, not approximately.
### Out of scope
Cube decisions. Choosing plays. Searching.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.equity.shoddy: equity in points from a result distribution, and equity at a given cube value.Step 17 · extends
### What already exists
[ equity, the six result probabilities, the move generator, the game loop with
pluggable decisions, random choice ]
### The rules
None new.
### What the program must be able to do
1. Given a board and a roll, choose among the legal plays by judging the
position each leaves, and pick the best.
2. Offer at least three strengths of opponent: random, one judging only by the
race, and one using the full judgement.
3. Play a specified number of games between any two opponents and report the
result as points per game, alternating who moves first.
### Design constraints
- **A play is judged by the position it leaves the OPPONENT.** A high equity for
them is a low one for you. Getting this backwards produces an opponent that
plays the worst available move, and it will look like weak judgement rather
than an inverted comparison.
- Ties break deterministically, so a match is reproducible from a seed.
- Opponents are interchangeable without the game loop changing.
### Out of scope
Cube decisions. Looking more than one move ahead.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.choose.shoddy: a one-ply chooser, three opponent strengths, and a seeded match runner reporting points per game.Step 18 · design proposal
### What already exists
[ equity, the six result probabilities, the cube and its rules, the game loop
with a pluggable cube decision, one-ply play selection, match play ]
### The rules
Facing a double, a player has two choices with computable values. Passing loses
the current stake exactly. Taking means playing on for twice the stake, and is
worth twice the equity of the resulting position.
In the simplest case — where the taker will never get to double back — taking is
correct precisely when it is no worse than passing. Writing p for the taker's
winning chances, with no gammons:
pass = -1
take = 2 x (p - (1 - p)) = 4p - 2
take when 4p - 2 >= -1, that is, p >= 0.25
A quarter is the take point. Below it, pass; above it, take.
Owning the cube after taking is worth something, because it permits doubling
back later. That advantage lowers the take point below a quarter — to roughly a
fifth in practice. How much is a matter of estimate, not arithmetic.
A player may also be TOO GOOD to double: so far ahead that playing on for a
gammon is worth more than the opponent's certain pass.
### What the program must be able to do
1. Given a board and a cube state, decide whether to offer a double.
2. Given an offered double, decide whether to take or pass.
3. Play matches between a cube-aware opponent and one that never doubles and
always takes, reporting points per game.
### Design constraints
- **The take point with no recube possible is exact arithmetic, not an estimate,
and your implementation must reproduce it exactly.** The adjustment for owning
the cube is an estimate and must be a separately named value.
- Whether to double and whether to take are different questions with different
answers. Do not implement one as a threshold on the other.
- Gammon chances change both answers. Use the full result distribution, not the
winning chance alone.
- The decision must be replaceable in the game loop without touching play
selection.
### Out of scope
Match play, match equity tables, the Crawford rule. Recube vantage beyond a
single named constant.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.cube-policy.shoddy: a should-double decision and a take-or-pass decision, with the recube adjustment as a named constant.Step 19 · extends
### What already exists
[ one-ply play selection, cube decisions, equity, the result distribution,
match play, the move generator ]
### The rules
Two dice produce 21 distinct rolls: 15 unequal pairs, each occurring twice in
36, and 6 doubles, each once in 36.
### What the program must be able to do
Choose a play by considering, for each candidate, what the opponent would do
with each of the 21 possible replies, and preferring the candidate with the best
average outcome weighted by how likely each reply is.
### Design constraints
- The average over replies must be weighted by actual probability. The 21 rolls
are not equally likely, and treating them as such is wrong by a factor of two
on most of them.
- **Cutting off branches by comparing against a best-so-far is invalid where the
value is an average**, because every outcome contributes. Do not do it.
- Considering every candidate costs roughly 8,000 judgements per decision. If
you reduce the candidates before expanding them, **report how many were
dropped.** A silent reduction reads as full coverage.
### Out of scope
Three plies. At about 20 plays and 21 rolls per level it is 3.5 million
judgements per decision.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.choose.shoddy extended: a two-ply chooser weighting all 21 rolls, reporting how many candidates it dropped.Step 20 · extends
### What already exists
[ board type, turn-passing, the result distribution, equity, the move generator ]
### The rules
None from backgammon. This is a data format, given exactly, to be implemented as
given rather than improved.
### What the program must be able to do
Turn any board into exactly 198 numbers:
- For each of the 24 points, for each of the two players, four numbers
describing that player's checker count n on that point:
the first is 1 if n is at least 1, otherwise 0
the second is 1 if n is at least 2, otherwise 0
the third is 1 if n is at least 3, otherwise 0
the fourth is (n - 3) / 2 if n is more than 3, otherwise 0
That is 24 x 2 x 4 = 192 numbers.
- Two numbers for the bar: each player's count divided by 2.
- Two numbers for borne-off checkers: each player's count divided by 15.
- Two numbers indicating which player is to move.
### Design constraints
- Implement this format. Do not substitute a one-hot encoding, a raw count, or
anything that seems more natural. The four-number scheme is deliberate: the
first three mark thresholds and the fourth carries the excess.
- Exactly 198 numbers for every board.
### Out of scope
The network. Training. The cube — it is not part of this description.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.encode.shoddy: one function turning a board into exactly 198 numbers.Step 21 · design proposal
### What already exists
[ the board description; the neural machine — feed-forward networks with one
tanh hidden layer, gradient descent with momentum, binary save and load ]
### The rules
Nothing from backgammon. **This step does not touch the game.**
### What the program must be able to do
1. Learn to predict the value of a state from experience, using temporal
differences: after each transition, adjust the prediction toward the next
prediction, and credit that adjustment not only to the state just left but to
states visited earlier in the episode, with credit decaying the further back
they are.
2. Demonstrate this on a problem whose answer is computable by hand: five states
in a line, an end at each side worth 0 and 1, moving left or right with equal
probability. The true value of each state can be worked out on paper.
3. Show that the learned values converge to the computed ones.
### Design constraints
- **The decaying credit must persist across transitions within an episode and
reset between episodes.** An implementation adjusting only the most recent
state is ordinary gradient descent toward a moving target — it will look
similar, learn far more slowly, and present as a tuning problem for weeks. If
nothing in your design survives from one transition to the next, it is wrong.
- The decay rate and the step size must be named values.
- **Do not connect this to backgammon in this step.** The entire point is to be
wrong somewhere you can prove it.
### Out of scope
Backgammon. Self-play. The 198-number description.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.
test.shoddy: one assertion per clause of the gate below, each message a sentence.td.shoddy and randomwalk.shoddy: the learner with eligibility traces, the five-state walk, and a convergence demonstration.Step 22 · extends
### What already exists
[ the 198-number board description; temporal-difference learning verified on the
random walk; equity; the six result probabilities; one-ply play selection;
cube decisions; match play; the full-judgement opponent as a fixed reference ]
### The rules
A game does not end in a win or a loss but in one of six results, and the cube
multiplies whichever occurs. Correct play and correct cube decisions both depend
on the whole distribution, not on the chance of winning alone.
### What the program must be able to do
1. Judge a board with a network producing enough numbers to give the full result
distribution for the player on roll — from which equity, play selection, and
cube decisions all follow.
2. Play against itself, choosing plays and cube actions by its own current
judgement, and learn from every move by temporal difference. No human games,
no opening book, no exploration rule — **the dice supply all the exploration
this game needs.**
3. Save the network at intervals so training can resume and past versions be
retained.
4. Report strength as points per game over at least 1,000 games.
### Design constraints
- The network's outputs are not independent probabilities. State what
relationship holds among them and enforce it.
- The terminal signal must reflect what actually happened: a game ending in a
gammon trains a different target from one ending plainly, and a game ended by
a passed double is different again.
- **Play selection and cube decisions use the same network.** Do not train two.
- **Strength must be measured against a FIXED opponent that does not change.**
Measuring against the current network shows improvement by construction and
proves nothing.
- One thousand games is not optional. Backgammon variance is enormous and a
hundred-game result is noise that will look like signal.
### Out of scope
Match play. Match equity tables. Terminal display.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.
test.shoddy: one assertion per clause of the gate below, each message a sentence.selfplay.shoddy: the network, the self-play training loop, checkpoint save and load, and a strength report against the fixed reference.Phase 1 produced something thoroughly tested that nobody can play. Phase 2 wraps it in a window: a board you move checkers on, dice that roll, undo, and hints taken from the engine itself. Nine steps.
The method does not change, and neither does the engine. No step in Phase 2 may add, alter, or re-decide a rule of backgammon. Every question about what is legal is answered by asking Phase 1 what it already worked out. That is what keeps a graphical program from quietly growing a second, disagreeing rules engine inside its interface.
Step 23 · design proposal
### What already exists
[ the complete engine: board type, turn-passing, the move generator, the cube,
outcomes, notation, the game loop, judgement, opponents ]
### What this phase is
Phase 1 built a backgammon engine that nobody can play. Phase 2 builds the game
around it: a window, a board you move checkers on with mouse or keyboard, dice
that roll, undo, and hints taken from the engine itself.
**The engine does not change.** Nothing in Phase 2 may add, alter, or re-decide
a rule. Every question about legality is answered by asking the engine what it
already knows.
### The rules
None. This step is arithmetic.
### What the program must be able to do
Given the width and height of a drawing surface, describe where everything sits:
1. The rectangle occupied by each of the 24 points, laid out as a real board —
two rows of twelve, split by a bar down the middle, with the player's home
quadrant nearest them.
2. The position of the Nth checker resting on a given point, so that checkers
stack and a point holding many checkers still fits its rectangle.
3. The rectangles for the bar, for each player's borne-off tray, for the cube,
and for the area where the dice are shown.
4. Given a coordinate on the surface, which of those regions it falls in — a
point number, the bar, a tray, the cube, the dice, or nothing at all.
### Design constraints
- **This step touches no drawing surface and opens no window.** It is arithmetic
over a width and a height, and every function in it is pure.
- The region lookup must be the exact inverse of the layout: the centre of the
rectangle computed for point N must map back to point N, for all 24.
- The layout must be computed from the surface size, not written as a table of
fixed pixel coordinates. A different window size must produce a proportionate
board, not a broken one.
- Checkers on a heavily loaded point must remain inside that point's rectangle.
Decide how — overlap them, shrink them, or show a count — and say which.
- Nothing here may consult the engine. This describes a board, not a position.
### Out of scope
Drawing anything. Colour. Events. Any board state at all.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.test.shoddy: one assertion per clause of the gate below, each message a sentence.layout.shoddy: a layout descriptor from a width and height, a rectangle per point, a checker position function, rectangles for bar, trays, cube and dice, and a coordinate-to-region lookup.Step 24 · extends
### What already exists
[ the engine; the layout — rectangles per point, checker positions, region
rectangles, coordinate lookup ]
### The rules
None.
### What the program must be able to do
Draw a complete board onto a drawing surface from a position and a layout:
1. The board itself — points alternating in two colours, the bar, the two trays.
2. Every checker, in the colour of its owner, stacked on its point.
3. Checkers on the bar and checkers borne off.
4. The doubling cube, showing its current value, positioned to show who owns it
— one side, the other, or the middle.
5. The dice, when there are dice to show.
6. Whose turn it is, and the score if there is one.
### Design constraints
- **This step draws and decides nothing else.** It takes a position and a layout
and produces pixels. It must not consult the engine, test legality, or hold
any state of its own.
- Draw the whole frame, then present it once. Do not present a partly drawn
board.
- Every coordinate must come from the layout. No drawing function may compute a
position of its own.
- It must draw a position handed to it, not "the current position" — including
positions that could not arise in play, because tests will hand it those.
### Out of scope
Events, input, animation, highlighting, selection, the game loop.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.test.shoddy: one assertion per clause of the gate below, each message a sentence.draw.shoddy: one function drawing a whole frame from a position, a cube state, an optional pair of dice, and a layout; plus whatever private helpers it needs.Step 25 · design proposal
### What already exists
[ the engine — in particular the generator that returns every legal play for a
board and a roll; the layout; drawing ]
### The rules
A player with a roll has a set of legal plays, and the engine already computes
it. A play is a sequence of individual checker moves.
A person does not choose a play from a list. They pick up one checker at a time.
So the game must work out, at each moment, which checkers may be picked up and
where each may be put down — such that whatever the player builds can always be
completed into a legal play.
This matters more than it sounds. A player who moves a checker somewhere that
happens to be legal on its own, but leaves no legal way to use the remaining
dice, has been led into a position the rules do not allow. The game must never
offer that move.
### What the program must be able to do
Given a board, a roll, and the sequence of moves the player has committed so far
this turn:
1. Report which points hold a checker that may be picked up.
2. Given a chosen source, report every destination it may be put down on.
3. Report whether what the player has committed so far is a complete turn.
4. Report the board as it currently appears, part-way through the turn.
### Design constraints
- **Nothing here decides what is legal.** Every answer must be derived from the
set of legal plays the engine already produced for this board and roll, by
keeping only those beginning with the moves the player has committed. A source
is available when some remaining play moves a checker from it; a destination is
available when some remaining play moves this checker there.
- It follows that a partially built turn can always be completed. If your design
can strand a player, it is wrong.
- The engine is asked for the legal plays **once per roll**, not once per click.
- This layer holds no pixels and no coordinates. It speaks in point numbers.
### Out of scope
Mouse, keyboard, drawing, highlighting, undo, the cube.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.test.shoddy: one assertion per clause of the gate below, each message a sentence.select.shoddy: available sources, available destinations for a source, a completeness test, and the board as it currently stands mid-turn.Step 26 · design proposal
### What already exists
[ the engine; the layout; drawing; available sources, available destinations,
the completeness test, the mid-turn board ]
### The rules
A turn is built one checker at a time. The player picks up a checker, puts it
down on one of the places it may go, and repeats until the turn is complete.
They may change their mind about a checker they have picked up but not yet put
down.
### What the program must be able to do
Hold everything about the turn in progress, and change it in response to what the
player intends:
1. Pick up a checker from a point.
2. Put the held checker down on a destination.
3. Put down a held checker without moving it — change of mind.
4. Report what should be shown: which point is held, which destinations are
available, and the board as it currently stands.
5. Report when the turn is complete and what play was built.
Intent must be described in the game's own terms — pick up point 8, put down on
point 5 — not in terms of buttons or keys.
### Design constraints
- **The state must be a value, changed by a function of the current state and one
intent.** No hidden state, no partial updates. This is what makes it testable
without a window and identical under mouse and keyboard.
- Picking up a checker from a point with no available destination must be
refused, not accepted and then undone.
- A forfeited turn — no legal play at all — must be a state the player is told
about, not a hang.
- When exactly one legal play exists, say so, so the interface can offer to play
it rather than making the player reconstruct the only option.
### Out of scope
Mouse, keyboard, drawing, undo, the cube, the opponent.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.test.shoddy: one assertion per clause of the gate below, each message a sentence.turnstate.shoddy: the state type, a constructor for a new roll, one function applying an intent, and readers for what to display and whether the turn is done.Step 27 · extends
### What already exists
[ the engine; the turn-in-progress state and its intents; the game loop from
Phase 1 ]
### The rules
A player may take back the last checker they moved this turn, and go on taking
back until the turn is as it was when they rolled.
A player may also take back their whole last turn, along with the opponent's
reply to it, returning the game to the moment before they moved.
### What the program must be able to do
1. Take back the last committed move of the turn in progress.
2. Take back every move of the turn in progress at once.
3. Take back the last completed turn together with the opponent's reply, so the
player is on roll again with the dice they had.
### Design constraints
- **Do not compute an inverse move.** There is no such thing as un-hitting a
checker by reasoning backwards. Everything in this program is immutable, so
the earlier state still exists — keep it and return to it. If your design
contains a function that reverses a move, it is the wrong design.
- Taking back must restore everything that changed, not only the board: the
dice, whose turn it is, the cube, and the moves available.
- Taking back a completed turn must restore the dice that were rolled for it, so
the player replays the same roll rather than getting a new one.
- The limit of how far back one can go must be stated, not implicit.
### Out of scope
Redo. Saving a game. Taking back the opponent's decision to double.
### Phase
Extends the accepted design — no new design. Tests first, then implementation.test.shoddy: one assertion per clause of the gate below, each message a sentence.undo.shoddy: a history value, a function to record a state onto it, and three take-back operations — last move, whole turn, last completed exchange.Step 28 · design proposal
### What already exists
[ the turn-in-progress state and its intents; undo; the layout; drawing ]
### The rules
None. This is about control.
### What the program must be able to do
Let a player build a turn entirely from the keyboard, with no pointing device:
1. Move a highlight between the points that currently hold a movable checker.
2. Pick up the highlighted checker.
3. Move a highlight between the destinations available to it.
4. Put it down, or change their mind.
5. Reach the bar and the borne-off tray the same way.
And, separately, let a player do all of the same things with a pointing device:
pressing on a checker picks it up, releasing on a destination puts it down, and
releasing anywhere else is a change of mind.
### Design constraints
- **Both devices must produce the same intents.** Neither may reach the turn
state through a path the other does not use, and no rule or availability check
may live in either. They translate; they do not decide.
- The keyboard must be able to reach every region the pointer can, including the
bar and the trays.
- Highlight movement follows the board's geometry, so that moving the highlight
in a direction goes somewhere a person would expect.
- Nothing may require a device the player does not have. A player with only a
keyboard must be able to complete a game.
### Out of scope
Commands beyond building a turn. Animation. The cube.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.test.shoddy: one assertion per clause of the gate below, each message a sentence.control.shoddy: a highlight position within the turn state, a translator from key events to intents, and a translator from pointer events to intents.Step 29 · design proposal
### What already exists
[ the engine, including judgement and the opponents at each difficulty; the turn
state; undo; both control translators; drawing ]
### The rules
A game needs more than moving checkers: starting a new one, giving up, changing
who you are playing against, offering the cube, answering a double.
And a player who is stuck should be able to ask what the engine would do.
### What the program must be able to do
1. Recognise a set of commands and carry them out: start a new game, take back
(each of the three kinds), ask for a hint, offer a double, take, pass, choose
an opponent strength, and quit.
2. Refuse a command that does not apply right now, with a reason, rather than
doing something surprising. Offering a double after the dice are rolled is a
refusal, not an error.
3. Produce a hint: the play the engine would choose for this board and roll, and
the cube decision it would make when one is due.
### Design constraints
- **The hint must be produced by asking the current opponent what it would play.**
Not a simplified version, not a separate weaker path, not a heuristic written
for the purpose. A hint from the strongest opponent and that opponent's own
move must be the same play, always.
- A hint is shown, never played. The player remains the one who moves.
- Whether a command applies is decided in one place, so that every way of
invoking it agrees.
- Commands and turn-building intents are different things. Do not force them
into one vocabulary.
### Out of scope
Menus, buttons, decoration, help text. This step decides what the commands are
and what they do, not how they are offered.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.test.shoddy: one assertion per clause of the gate below, each message a sentence.command.shoddy: the command type, an applicability test, an execution function returning a new game state or a refusal with a reason, and a hint function returning a play and, when due, a cube decision.Step 30 · design proposal
### What already exists
[ the engine; the turn state; commands; drawing; the layout's dice region ]
### The rules
Dice are rolled, and a player likes to watch them roll. What they show when they
stop is what was rolled.
### What the program must be able to do
1. Show dice tumbling briefly and settling on the values that were rolled.
2. Show the settled values for as long as the turn lasts.
### Design constraints
- **The values are decided before the animation begins.** The animation displays
a result; it never produces one. Nothing about how it runs, how long it lasts,
or whether it runs at all may change what was rolled.
- What is shown at any moment must be a function of the rolled values and how
much time has passed since the roll — nothing else. Given the same values and
the same elapsed time it must show the same thing.
- At the end of the animation the display must equal the rolled values exactly.
- **The animation must be skippable and interruptible.** Any input during it ends
it immediately, showing the settled values, and the game continues from a state
identical to the one it would have reached had the animation finished.
- The game must remain playable if frames are slow or dropped. Elapsed time
drives it, not a count of frames drawn.
- Keep it plain: a short tumble and a settle. No physics, no bouncing, no
shadows.
### Out of scope
Sound. Moving checkers with animation. Anything else that moves.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.test.shoddy: one assertion per clause of the gate below, each message a sentence.dice.shoddy: a function from rolled values and elapsed time to what should be shown, a settled test, a skip operation, and the drawing of a die face.Step 31 · design proposal
### What already exists
[ everything: the engine and its opponents; the layout; drawing; the turn state;
undo; both control translators; commands and hints; the dice animation ]
### The rules
None. This is assembly.
### What the program must be able to do
Be a game a person can start and play:
1. Open a window and set a steady frame rate.
2. Let the player choose an opponent strength before starting, and load the
trained network for the strongest one.
3. Run the game: show the board, take the player's input, run the opponent's
turns, roll and animate dice, apply the cube rules, and declare the result.
4. Show the player what they need — whose turn it is, the dice, the cube, the
score, which checker is held, where it may go, and any hint they asked for.
5. Close cleanly when asked, and when the window is closed.
### Design constraints
- **This program contains no rule and no judgement.** It translates events into
intents, hands them to the pieces already built, and draws what comes back. If
a rule of backgammon appears anywhere in it, the design is wrong and one of the
earlier layers is missing something.
- The opponent must not block the window. A player must be able to close the game
while the opponent is thinking.
- Draw the whole frame and present it once.
- Everything the player needs must be visible or reachable without instructions
printed elsewhere.
- The strongest opponent must load the trained network at startup. A player must
never face an untrained one by accident.
### Out of scope
Saving and loading games. Match play. Sound. Settings that persist.
### Phase
Design proposal first. Then tests. Then implementation. Three replies.test.shoddy: one assertion per clause of the gate below, each message a sentence.backgammon.shoddy: the entry point, opponent selection, network loading, the event loop, and the frame draw. Plus a short note on how to run it.These are stated so a knowledgeable reader can see they were decisions rather than omissions.
| Excluded | Why |
|---|---|
| Match play, match equity tables | Cube decisions in a match depend on the score, needing a match equity table — a large body of tabulated external data and a different decision theory. A second project. |
| The Crawford rule | Match play only. |
| Jacoby rule | A money-play convention: gammons count single until the cube is turned. Nearly free to add and it changes cube strategy substantially. The one most worth adding back — as its own step with its own gate, never folded quietly into step 11. |
| Beavers, raccoons | Optional money-play conventions, not part of the game proper. |
| Automatic doubles | A house convention on the opening roll. |
Ordered by how much damage they do if missed.
test.shoddy on every repair. A test changes only after the gate changes, and
both go in the log with a reason.