Add The Frontier, a second setting, and a picker to choose between them

Cold Fork is a freight yard: 29 events across two stages, a four-day cycle, and
its own relationship and skill vocabulary. src/engine/ was not touched. Adding it
needed three changes outside the engine: one save slot per pack, app.js holding
the current pack, and turn.js no longer naming the mailroom (DECISIONS #34).
build.js now handles imports wrapped across lines. A conformance suite runs over
every registered pack. DECISIONS #35 records three calls on open questions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxdJFbLq1rBKsUeticGV1g
This commit is contained in:
JesseMarkowitz
2026-09-12 21:03:50 -04:00
co-authored by Claude Opus 5
parent 4957c24e31
commit 7f20be83e8
25 changed files with 3015 additions and 300 deletions
+126
View File
@@ -421,3 +421,129 @@ Spans and pair-counts are now accumulated per run and pooled, so the figure is a
proper weighted average of within-run gaps. It is worth stating the general
form: any statistic over a log has to respect the run boundary, because a log is
not one sequence.
## 31. A pack declares the numbers it must hit
Decision #29 added a test that asserts Corporate Ladder's economic baseline
directly, because a tuning change made without re-deriving what it meant had
trapped a real playtester. That test named the pack, named its stages and
hard-coded its ranges, which meant a second pack got none of it — the guard
that exists precisely because a number went unchecked would itself have gone
uncopied.
So the numbers moved into the pack, as `tuning.baselinePerCycle`:
```js
tuning: {
baselinePerCycle: { hand: [0, 12], deputy: [10, 30] },
},
```
The conformance suite reads that and holds each pack to its own declared range.
A new setting is checked the moment it is in the registry, and what it is
checked against is its designer's stated intent rather than another setting's
numbers. The pack is required to declare it: a missing `tuning` block fails,
because the silent failure mode here is a pack that is never economically
tested at all.
The figure is **derived, not simulated**. An upkeep entry worth `v` every `n`
turns is worth `v * cycleLength / n` per cycle, exactly. Simulating it over a
fixed window reads differently depending on where the last rent lands: Corporate
Ladder measures −19 a fortnight over 280 turns and −42 over 560, and only the
second is the steady state. The original test used the 280-turn window and was
reading a number 23 dollars off the truth — inside its own tolerance, so it
never said so.
## 32. A skill gate is never gated behind itself
Corporate Ladder gates four options on `skills.negotiation >= 15`, and several
of its largest negotiation gains sit behind that same gate — `+4` on
`marlene_confides.ask_how_to_leave`, `+3` on `trevor_favor.trade`. Its logs show
players meeting the wall repeatedly through the first thirty turns.
Measured, the gate is not unreachable: a player steering directly for the skill
is through it by turn 11. What the logs actually show is ordinary players not
prioritising it and having no incidental route, which is a pacing failure rather
than a reachability one — and the reachability test could never have caught it,
because reachability is exactly what holds.
The Frontier is built to a rule instead, asserted in `test/frontier.test.js`:
for every skill threshold in the pack, the growth available *without* passing
that threshold must be able to reach it, from repeatable events — a single
one-off award is not a route. A second test measures it, requiring a player who
steers for a skill to be through its lowest gate inside twenty turns.
The rule is deliberately not in the conformance suite, because Corporate Ladder
does not satisfy it and retuning that pack is a separate decision from adding
this one. It is written down here so the choice is visible rather than implied.
## 33. One save slot per pack, and the picker is the front door
`theladder.save.v1` was a single global slot. The app guarded it by comparing
the packId inside the save against the pack it was about to run, and started a
new game when they differed — so with two settings, the first switch would have
silently destroyed the other career.
Slots are now `theladder.save.v1.<packId>`, one per setting, and switching is
non-destructive in both directions: each run is written to its own slot every
turn, and coming back resumes rather than restarts. `theladder.pack.v1` holds
the last setting played, so a returning player lands back where they were.
The pre-packs slot is still read once, for Corporate Ladder only, and only when
it really holds a Corporate Ladder save. It is never written. Someone mid-career
when this shipped finds their career.
A first-time player — nothing saved, nothing remembered — gets the setting
picker rather than being dropped into whichever pack is first in the registry.
A build shipping one pack skips it entirely.
## 34. What a second setting proved, and what it cost
The engine/content split was a claim until there were two settings. The
Frontier is 29 events across two stages, a four-day cycle instead of a
seven-day one, a different relationship vocabulary (`trusts_you`, `owes_you`,
`wary_of_you`), a different skill pair, and money about fifteen times smaller
that arrives from work rather than from a wage. `src/engine/` was not touched.
Three things outside the engine did have to change, and none of them was about
the setting:
- **`src/io/save.js`** — the single save slot, above.
- **`src/ui/app.js`** — it named one pack; it now holds the current one. The
screens, the engine and the rest of the io layer still take the pack as an
argument and are no less pack-agnostic than before.
- **`src/ui/screens/turn.js:40`** — hardcoded `'The mailroom'` as the heading
over the people panel. That was the interface naming a setting, against
decision #14, and it had gone unnoticed while only one setting existed. It is
now `pack.peopleHeading`.
`tools/build.js` needed one fix, unrelated to packs but exposed by them: its
module scan used `.*?` where `transform` used a pattern that spans lines, so a
destructured import wrapped across several lines was rewritten into a
`__require` for a module the scan had never collected. The bundle built clean
and failed only when run. Both now use `[\s\S]*?`.
The honest summary is that adding a setting required no engine change and three
interface changes, two of which were latent defects that a second pack merely
made visible.
## 35. Three open questions, closed by the designer
Recorded 2026-09-11, alongside 0.3.1.
**Corporate Ladder's negotiation gate stays.** A gate that takes deliberate work
to get past is acceptable, and so is an option that needs groundwork first. #32
remains the rule the Frontier is built to and tested against; it is a choice a
pack may make, not a standard every pack must meet, and Corporate Ladder is not
being retuned toward it.
**The feedback widget stays exactly as it is.** Zero ratings across three
sessions says nothing about the widget: those playtests were rapid, and feedback
came back directly rather than through it. It is not evidence either way until a
playtest runs without that direct channel.
**Money has to matter late, through the cost of standing.** A careful player
ends around $8,300 over 200 turns, and money stops being a constraint. The
direction is that rising standing brings rising expenses — the car, the place to
live, the clothes a position expects. Upkeep entries are already gated
conditions over state, so this is content work, not an engine change.