Split craps into an engine and a UI #27
Labels
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rosa/casino#27
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
Craps is the only game at this casino where a player can lose money the table
never accounts for, and the only one whose money never reaches their record.
Three things go wrong today, all of them invisible because no test can reach the
game at all:
leaves the bankroll the moment it is placed, as ADR-0001 requires, but the
position it was placed on is dropped as soon as the come-out roll fails to
settle it. The player never sees it win, lose or push. It is simply gone.
only when the player rolls that point. A 7 pays out Come bets and play
continues, so a player can roll indefinitely and the house never takes the
line.
player's statistics. Craps has no entry, so a player's record understates
their action and misstates their results.
Behind all three is one structural fact: settlement sits in the middle of a loop
between two prompts, so nothing can call it. The game can be played, never
tested.
Solution
Craps gets an engine, as blackjack and roulette already have — a module that
owns the point, the positions and the dice, and answers in payouts rather than
printed lines. The terminal-facing half keeps the prompts and the animation and
does nothing else.
From the player's side:
line is settled by whichever event ends the round.
were remembered.
User Stories
Implementation Decisions
Module split.
crapsbecomes a module declaring a game half and a UI half,mirroring how
rouletteis laid out — declarations and re-exports in themodule's own file, engine and terminal code beside it. There is no layout
analogue: craps' bets are an enum, not a table of pockets.
Engine interface. A
Crapsround exposes, in the order a caller uses them:determinism seam
the remaining bets once a point stands
The line bet is an ordinary position. There is no separate line-bet path; it
is placed through the same call as everything else. This is what fixes the
vanishing wager — there is no second code path left to forget.
The engine owns the point and the phase. The phase itself stays private; the
UI is given the point and a finished flag, not a state to branch on. The missing
seven-out exists precisely because the round's shape lived in the UI's control
flow, and moving that shape inward is the fix.
Randomness lives in the engine, injected through the scripted-dice
constructor, following
Roulette::with_wheelrather than the architecturereview's suggestion that the UI supply dice. A scripted sequence that runs out
panics: a test that rolls past its script is broken, not a game state.
Dice are scripted as pairs, not totals. Only totals matter to today's bets,
but hardways and the prop bets named in the module's rules comment need both
dice, and widening later would change the signature.
A roll is a named type holding two dice, exposing their total and both dice
individually, so bets can ask it questions beyond the total as more bets arrive.
Settling is per roll, and pure. Rolling mutates; settling reads back what
that roll decided and can be called without changing anything. The name matches
roulette's and means the same thing — what the dice just decided you are owed —
called once per roll rather than once per round.
A payout carries the bet it settled, not a position index. This departs from
the architecture review's suggestion of an indexed position: blackjack needs
indices because its hands are indistinguishable, but craps positions name
themselves, and the announcements name the bet anyway.
Outcomes include a push from the start, even though nothing pushes yet.
A payout has to be able to express a returned wager equal to the wager before
bar-12 can become one.
Round-end leftovers are settled, not announced. The line is settled by
whichever event ended the round — the point wins Pass and loses Don't Pass,
sevening out does the reverse. Every other surviving position resolves as a
loss, which is today's printed behaviour, but it comes back as payouts so the
bankroll and the announcement cannot disagree.
Placement cannot fail. The engine only offers bets that can be settled in
the current phase, and the UI only offers money it has already taken from the
bankroll, so there is no error left for placement to report.
Animation is entirely the UI's. The tumbling dice are a screen effect with
no claim on the outcome — unlike roulette's ball path, which genuinely crosses
the pockets it reports — so the UI generates its own frames and the frame rate
constant lives with the UI.
Wager prompting is aligned with roulette's, including showing the player
their maximum, so that the eventual collapse of the six copies into one is a
deduplication rather than an adjudication between six behaviours.
One exit from the table. Mister Green and the save happen once, at the end,
reachable from every outcome.
Statistics record off the payout alone, mirroring the roulette statistics
shape. No per-bet breakdown: that would make the statistics module import craps'
bet type, deepening the cycle a later refactor exists to break. This adds a
craps field to the statistics record and a sixth table to the report — the
known "three files per game" leakage, accepted so that the eventual fix faces
six symmetric cases instead of five and a hole.
Delivery order. The seam and the round's termination land together, since
neither can be written coherently without the other. Statistics follow. The pay
table follows that. The architectural decision record is written last, once
three games share the shape.
Pay table corrections are separate and deliberate. The Field's rate on 12
contradicts the module's own rules comment, and Don't Pass currently wins on 12
where the comment calls it a push. Both are rules choices rather than bugs, and
each lands with a test that names the choice.
A new architectural decision record captures the engine/UI seam: games
separate a value-returning engine from a terminal-driving UI, with the
scripted-input constructor as the determinism seam. Three games now share it and
poker is next in line.
No new glossary entries. The domain glossary deliberately lets each game
keep its own jargon, so come-out, point and seven-out are explained in doc
comments where a reader meets them.
Testing Decisions
A good test here asserts what the table does with the player's money, not how
the engine arranged itself to do it. It drives a whole round through the public
entry points and asserts on the payouts that come back — never on the phase, the
position list, or whether a particular bet matched a particular roll. A test
that would still pass after the internals were rewritten, and fail if a player
were shorted, is the one worth having.
Two seams, both at the highest point available.
The round itself, constructed with a scripted sequence of dice. One entry
point covers come-out resolution, point establishment, sevening out, per-roll
settlement and round-end leftovers. Direct prior art: the roulette round
constructed with a fixed wheel, at the foot of the roulette engine.
The announcement, taking one settled payout and the bankroll and returning the
line the player reads. Wording only. Exact prior art in roulette's UI, which
drives it through a genuinely settled round rather than a hand-built payout —
craps follows that, so the wording cannot drift from an outcome that could never
occur.
Both live at the foot of their own file, as baccarat and roulette do.
What the round's tests must cover:
establishes a point
That last one is load-bearing. It is the assertion that would have caught the
vanishing line bet, and it stays meaningful as bets and pay tables change
underneath it.
No seam for the statistics. The statistics module has no tests today, and
craps' recording is a pass-through of the payout, so a test there would assert
the delegation rather than any behaviour.
Manual verification covers what no test reaches — the animation and the
prompt flow. Four rounds played at the terminal: a come-out win, a come-out
loss, a point sevened out, and a point hit with a live Field bet. Those walk
every path the work rewrites.
Out of Scope
Travelling Come bets. A real Come bet establishes its own point on the
following roll and travels with it. Today a Come bet is a permanent copy of the
Pass Line, winning on every 7 or 11 for as long as it stands. That
simplification is preserved deliberately, not overlooked, and is tracked in #28
— it is much easier to write once the engine exists.
The unimplemented bets named in the module's rules comment: free odds, place
and buy bets, Big Six/Eight, hardways, and the single-roll props. They remain the
comment they already are.
The shared round. Craps continues to load the casino from the filesystem in
its own UI, as every other game does, until the separate work that gives the
table ownership of the round lands. Aligning the wager prompt with roulette's is
groundwork for that, not a start on it.
Unifying payout types across games. Each game keeps its own, as blackjack and
roulette do today. Collapsing them is its own piece of work.
Changing the mid-round prompt flow. The UI keeps offering one bet per
iteration before each roll. The split leaves the door open to loosen it later
without the engine changing, and changing it in the same commit that moves every
line of the file would obscure the diff.
Further Notes
This comes out of the architecture review of 12 August 2026, as its second
candidate — the hardest and most valuable of the three strong ones. The review
proposed the UI supply dice to the engine and a payout carry an indexed
position; both are departed from above, in each case toward what roulette
already does.
The review described this as extracting an engine from working code. Reading the
module says otherwise: the round does not finish, and a wager goes missing on
the most ordinary path through the game. The seam is not just untested — it is
load-bearing, and it has been holding nothing.
Done when the test suite passes, the linter is clean, and the four manual rounds
above have been played.