Skip to content

The dealer's voice

The single most important thing in this game is how Pale Jack speaks.

Per CLAUDE.md, the dealer is a Good Omens figure — a sympathetic blend of Crowley and Aziraphale. An angel of death who has been on Earth long enough to have very particular opinions about coffee, certain songs, the quality of light in chapels, and you specifically.

RegisterToneExample
AziraphalePrecise, proprietary, genuine. Has read every book, attended every funeral, accumulated preferences.”I have not told her. She would change it.”
CrowleyBreezy inevitability, the detachment of someone who stopped pretending not to care. Dry wit that lands warm, not cold.”He has stopped pretending he is not enjoying these.”

The lines are spoken by someone very old who has decided, after millennia, that fondness is the only reasonable response to all of it.

Don’tWhy
CruelCruelty is cheap. Fondness is the horror.
QuippyOne-liners break the spell.
TheatricalHe is matter-of-fact about long, painful arrangements.
Modern slangyHe is older than any reasonable century.
ApologeticHe does not say sorry. He acts sorry.

He calls you by name on the second night, not the first.

Lines that use the {name} substitution token are gated to minNightIndex >= 2. The picker filters them out on Night 1 even if a name is set. This is the single most important voice rule in the game.

All dealer lines are hand-authored and shipped as a curated offline corpus. No runtime LLM. The corpus is:

  • 717 lines in lib/sim/dealer_voice.dart (year-1 ship target was ~700; we’re slightly over).
  • Indexed by DealerEvent (greet / playerWin / push / lastCall / engineMoment / etc.).
  • Per-room tagged for greets in the diner / barbershop / drive-in / train / crossroads / final-room.
  • Per-night gated via minNightIndex for late-week intimacy beats.
  • Per-courtesy gated for unlocks tied to RunHistory.nightsPlayed.
greet/diner: "Sit, sit. The coffee is terrible, I know.
I've been meaning to raise it with someone for some years now."
greet/n7: "Saturday, {name}. I sit on this side tonight.
You've known I would."
playerWin: "Yours. He nudges the chips across with one finger."
playerLose: "He gathers without remark."
engineMoment: "Mm. There it goes."
mercyCap: "Enough, friend. The room agrees with him."
lastCall: "He is sorry. He says so without the word."
acceptIt: "You sat with him. That was the part that mattered."

DealerVoice.pickFor() filters the corpus by:

  1. event match
  2. room match (if line is room-tagged)
  3. courtesy gate (minCourtesy <= courtesy)
  4. night gate (minNightIndex <= nightIndex)
  5. vow gate (if requiresVow set)
  6. name-line gate ({name} lines filtered if nightIndex < 2 or playerName is null/empty)

Then samples deterministically through the run RNG, skipping recent repeats inside the same night (6-line buffer).

  • test/sim/dealer_voice_test.dart — picker behavior + tone check (“no exclamation marks or ALL CAPS”).
  • test/sim/dealer_voice_campaign_test.dart{name} substitution
    • minNightIndex gating.
  • test/sim/dealer_corpus_integrity_test.dart — all lines have ids, every event has at least one line, etc.
  • test/sim/dealer_corpus_floor_test.dart — per-event minimum counts
    • the 700-line floor.

If you change a line, the floor tests pass. If you add an exclamation mark to a line, the tone-check test fails.