Corpus structure
DealerLine shape
Section titled “DealerLine shape”DealerLine( id: 'greet_n2_3', // Stable id text: '"I remembered the name. {name}. It is a good one to keep in the mouth."', event: DealerEvent.greet, // What surface fires this room: 'diner', // Optional — null = any room minCourtesy: 0, // Run-count gate (default 0) minNightIndex: 2, // Campaign-night gate (default 0) requiresVow: null, // Optional vow id requirement)The DealerEvent enum
Section titled “The DealerEvent enum”| Event | When it fires |
|---|---|
greet | Player sits down at a new room. |
playerWin / playerLose / push | Hand resolution. |
playerBlackjack / dealerBlackjack | Naturals. |
playerBust / dealerBust | Bust paths. |
playerSurrender | Player surrenders. |
lastCall | Capital hits zero / terminal bust. |
clearedNight | All 6 rooms cleared. |
roomCleared | A single room cleared (between rooms). |
insurance | Insurance offer prompt. |
vowAccepted | Vow accepted at run-start. |
arcanumAcquired | Charm purchased / placed. |
midHand | Rare (~12%) aside during the player’s turn. |
engineMoment | A 2+ ×mult cascade with chips ≥ 100. |
mercyCap | Mult cap fires. |
acceptIt | Player Bergman-folds the final hand. |
Per-event line counts (current ship)
Section titled “Per-event line counts (current ship)”greet ~110engineMoment ~67midHand ~58playerWin ~58playerLose ~52push ~48playerSurrender ~41arcanumAcquired ~40clearedNight ~38roomCleared ~29lastCall ~28mercyCap ~27dealerBust ~27playerBust ~24vowAccepted ~23insurance ~23playerBlackjack ~21dealerBlackjack ~18acceptIt ~18The corpus-floor regression test (dealer_corpus_floor_test.dart)
pins per-event minimums so a refactor can’t silently shrink coverage.
The {name} substitution
Section titled “The {name} substitution”Lines tagged usesName (anything containing the literal {name})
fire only when:
nightIndex >= 2(CLAUDE.md “second night, not first” rule).playerNameis non-null and non-empty.
Pre-Night-2, the picker filters them out entirely. Post-pick, the
caller renders via renderDealerLine(text, playerName, nightIndex),
which performs the substitution and falls back to “you” if the
substitution can’t be made.
The picker
Section titled “The picker”DealerLine? pickFor({ required DealerEvent event, String? room, int courtesy = 0, int nightIndex = 1, String? playerName, Set<String> activeVows = const {},})- Filters by event, room, courtesy, nightIndex, vow.
- Filters out
{name}lines if no playerName / Night 1. - Removes lines used in the recent-buffer (6 lines) if any non-recent options exist.
- Falls back to non-most-recent if all candidates were used recently.
- Samples via
Rng.nextInt.
Recent-buffer
Section titled “Recent-buffer”DealerVoice maintains a 6-line FIFO buffer of recently-picked ids.
A picked line cannot repeat within 6 picks unless every candidate has
been used recently.
The buffer persists in run snapshots (primeRecent(ids)) so a
mid-run save / reload doesn’t reset the rotation.
Determinism
Section titled “Determinism”Same Rng(seed) + corpus + buffer state produces the same picks.
Share codes preserve seed; replays produce identical dealer lines.
Where to add lines
Section titled “Where to add lines”- Single file:
lib/sim/dealer_voice.dart. - Hand-authored only. No runtime LLM per
CLAUDE.md. - Tone gate: no exclamation marks, no ALL CAPS, no slang outside the Aziraphale-Crowley register.
- Add to the right event. Test the floor passes.
See Contributing → Writing dealer lines for the contributor flow.