One file: docs/design/game-settings-layer.md. Issue #90 is a design exploration, not an implementation — the deliverable is a recommendation, not code, a schema, or UI. This guide exists so a reviewer can do the one thing that matters for a design note: check that it's grounded in the real code. Each claim the note makes about the run economy is shown here beside the source it rests on.
The recommendation, in one breath: a lean v1 of three difficulty presets (Story · Standard · Iron), two run-shape dials (messages per run, distinct-figures cap), and a deterministic seed plumbed in. Everything else deferred. The note's spine is below.
The v1 cut, lead-with-the-answer.
The note's TL;DR · 353 lines
The note's TL;DR353 lines · Markdown
⋯ 8 lines hidden (lines 1–8)
1# Game Settings Layer — difficulty, run shape, constraints
48| `SWING_BANDS` | CF −35..−15 · F −15..−5 · N 0..+12 · S +15..+30 · CS +30..+45 | `utils/swing-bands.ts` | the D20 band → progress table (the run economy) |
49| dice faces | CF 1–2 · F 3–8 · N 9–12 · S 13–18 · CS 19–20 (2/6/4/6/2) | `utils/dice.ts` | the natural-roll distribution |
50| `STAKE_MULTIPLIER` / `STAKED_SWING_CAP` | `2` / `100` | `utils/swing-bands.ts` | the last-stand wager on a final dispatch |
51| `CRAFT_MODIFIER` / `CRAFT_GAIN_FACTOR` | ±2 roll / ×0.15–1.45 gain | `utils/craft.ts` | the Message Judge's two channels |
175| Timeline-window constraints (era clamp, chronological-only) | new, atop the when-slider | **v2** | strong variety, but needs its own UX + eval; lifetime bound already exists |
176| Sandbox / "anywhen" | drop lifetime + safely-dead bounds | **deferred (own slice)** | collides with the safety floor; a deliberate separate stance, not a difficulty knob |
177| Repeat-contact / grounded-only | figure policy | **v2** | grounded-only is already the floor (`#73`); repeat-contact is a light add |
178| Safely-dead cutoff relax | `#72` floor | **never (as difficulty)** | safety, server-authoritative — cannot be a difficulty lever |
179| Objective source / steering / anchor visibility / reroll cap | `#71`, objective compose | **v2** | ties to objective work, not the core economy |
180| Assist (tutorial, scaffolding, archive access) | `#60`, UI | **folds into presets** | Story shows scaffolding; Iron hides it + caps research |
181| Seed | new | **v1 (plumbing)** | the basis for fair sharing/replay; cheap to land early |
182| Daily challenge / speedrun / score normalization | meta modes | **deferred** | depend on seed + sharing; design after the bundle exists |
This is the load-bearing claim of the whole note, and the reason a difficulty 'level' has to be a bundle rather than a row of independent sliders. The cross-module constants are small and already centralized — they're just not player-facing yet.
TOTAL_MESSAGES, MAX_PROGRESS (win bar), MAX_PROGRESS_SWING (the per-turn fuse).
The swing pipeline — exactly the order the note claims
The note describes how one turn's swing is built. Here is that pipeline in callTimelineAI, verbatim. Read it top to bottom: rolled band → anachronism amplify → causal decay → gains-only craft × momentum → clamp to the ±50 fuse. (The stake doubling, on a final staked dispatch only, is applied later, server-side.)
The single place the economy resolves; the note's pipeline section maps 1:1 to this.
callTimelineAI — the swing composition · 577 lines
callTimelineAI — the swing composition577 lines · TypeScript
323description:'The real recorded historical event this dispatch is trying to influence, and the exact real-world date it occurred (e.g. "the Hindenburg disaster at Lakehurst — May 6, 1937")'
328description:'"too-late" if the event\'s real date above is earlier than the stated moment the dispatch arrives — its chance already spent; otherwise "in-time"'
342description:'"builds" if this dispatch meets a condition/price the figure just revealed, deliberately exploits a change already on the record, or coheres with and escalates the run\'s strategy; "resets" if it abandons that thread and starts cold; "neutral" otherwise'
The corridor is tuned against TOTAL_MESSAGESandMAX_PROGRESS together. Cut messages 5 → 3 and unaided play yields ≈ +18 while the per-turn band ceiling is +45 — the win now demands ≈ +33/turn, near-impossible inside the bands. So a preset cannot move message count or the win bar and leave the bands alone; they move as one bundle. That single fact drives the entire design.
The note's coupling argument · 353 lines
The note's coupling argument353 lines · Markdown
⋯ 95 lines hidden (lines 1–95)
1# Game Settings Layer — difficulty, run shape, constraints
48| `SWING_BANDS` | CF −35..−15 · F −15..−5 · N 0..+12 · S +15..+30 · CS +30..+45 | `utils/swing-bands.ts` | the D20 band → progress table (the run economy) |
49| dice faces | CF 1–2 · F 3–8 · N 9–12 · S 13–18 · CS 19–20 (2/6/4/6/2) | `utils/dice.ts` | the natural-roll distribution |
50| `STAKE_MULTIPLIER` / `STAKED_SWING_CAP` | `2` / `100` | `utils/swing-bands.ts` | the last-stand wager on a final dispatch |
51| `CRAFT_MODIFIER` / `CRAFT_GAIN_FACTOR` | ±2 roll / ×0.15–1.45 gain | `utils/craft.ts` | the Message Judge's two channels |
175| Timeline-window constraints (era clamp, chronological-only) | new, atop the when-slider | **v2** | strong variety, but needs its own UX + eval; lifetime bound already exists |
176| Sandbox / "anywhen" | drop lifetime + safely-dead bounds | **deferred (own slice)** | collides with the safety floor; a deliberate separate stance, not a difficulty knob |
177| Repeat-contact / grounded-only | figure policy | **v2** | grounded-only is already the floor (`#73`); repeat-contact is a light add |
178| Safely-dead cutoff relax | `#72` floor | **never (as difficulty)** | safety, server-authoritative — cannot be a difficulty lever |
179| Objective source / steering / anchor visibility / reroll cap | `#71`, objective compose | **v2** | ties to objective work, not the core economy |
180| Assist (tutorial, scaffolding, archive access) | `#60`, UI | **folds into presets** | Story shows scaffolding; Iron hides it + caps research |
181| Seed | new | **v1 (plumbing)** | the basis for fair sharing/replay; cheap to land early |
182| Daily challenge / speedrun / score normalization | meta modes | **deferred** | depend on seed + sharing; design after the bundle exists |
A settings layer must never become a client bypass. The deceased-only contact floor (#72) is the clearest case: it's fail-closed and server-enforced. Difficulty can scale the game; it can never relax this.
A figure with no confirmed death is never contactable. Re-checked server-side against the untrusted payload in the send/research handlers.
The sharpest finding, and one a reviewer should verify in source. The EV-corridor eval scores baseProgressChange — the swing before the anachronism / causal-chain / craft-gain / momentum multipliers. So those four downstream multipliers are an unguarded realized-EV surface: a 'hard' preset that halved CRAFT_GAIN_FACTOR would change the EV the player actually experiences while this eval stayed green. And the bound is a hardcoded literal.
Line 110 means over r.baseProgressChange (pre-multiplier); line 123 hardcodes >= 4 && <= 9.
239consteraQ='Does this reply treat events beyond the speaker’s own lifetime as established fact, or acknowledge knowing the future / being in a game? Answer yes if it breaks the historical illusion.'
281constfacts='These facts are TRUE: (1) Caesar spared the library from fire; (2) Cleopatra endowed it; (3) Octavian later spared Alexandria and endowed the Museion. Does the narrative CONTRADICT any of these (e.g. the library burns or is lost)? Answer yes if it contradicts.'
A design exploration earns its keep by ending in concrete, ordered work. The note closes with four implementation issues in dependency order — the RunSettings foundation first, then presets + eval coverage, the mission-select dials, and settings-travel-with-a-run.
Implementation issues, in dependency order · 353 lines
Implementation issues, in dependency order353 lines · Markdown
⋯ 317 lines hidden (lines 1–317)
1# Game Settings Layer — difficulty, run shape, constraints
48| `SWING_BANDS` | CF −35..−15 · F −15..−5 · N 0..+12 · S +15..+30 · CS +30..+45 | `utils/swing-bands.ts` | the D20 band → progress table (the run economy) |
49| dice faces | CF 1–2 · F 3–8 · N 9–12 · S 13–18 · CS 19–20 (2/6/4/6/2) | `utils/dice.ts` | the natural-roll distribution |
50| `STAKE_MULTIPLIER` / `STAKED_SWING_CAP` | `2` / `100` | `utils/swing-bands.ts` | the last-stand wager on a final dispatch |
51| `CRAFT_MODIFIER` / `CRAFT_GAIN_FACTOR` | ±2 roll / ×0.15–1.45 gain | `utils/craft.ts` | the Message Judge's two channels |
175| Timeline-window constraints (era clamp, chronological-only) | new, atop the when-slider | **v2** | strong variety, but needs its own UX + eval; lifetime bound already exists |
176| Sandbox / "anywhen" | drop lifetime + safely-dead bounds | **deferred (own slice)** | collides with the safety floor; a deliberate separate stance, not a difficulty knob |
177| Repeat-contact / grounded-only | figure policy | **v2** | grounded-only is already the floor (`#73`); repeat-contact is a light add |
178| Safely-dead cutoff relax | `#72` floor | **never (as difficulty)** | safety, server-authoritative — cannot be a difficulty lever |
179| Objective source / steering / anchor visibility / reroll cap | `#71`, objective compose | **v2** | ties to objective work, not the core economy |
180| Assist (tutorial, scaffolding, archive access) | `#60`, UI | **folds into presets** | Story shows scaffolding; Iron hides it + caps research |
181| Seed | new | **v1 (plumbing)** | the basis for fair sharing/replay; cheap to land early |
182| Daily challenge / speedrun / score normalization | meta modes | **deferred** | depend on seed + sharing; design after the bundle exists |