Commit 71e7364
authored
W89 — A Game-Length Life (#376)
Closes #370.
## What was built
The first evidence that the simulation kind survives being played as a
game rather than as a test. Two engine-owned, unpublished regression
fixtures under `src/engine/src/campaigns/long-horizon.ts` (permitted by
`20-contract.md` §19), sharing one job market, course, item, event pair,
opportunity, achievement and headline:
- **`long-horizon-win`** — reaches `goals_met` around week 153-156 via
`player.career.totalWeeksEmployed` hitting 150 and holding two weeks.
- **`long-horizon-loss`** — reaches `failed` via the eviction ladder
(`goalFailurePrecedence: "failure_wins"`), the terminal path no short
fixture had walked before.
`check-content.ts`'s campaign catalogue now imports and registers
`buildLongHorizonWinCampaign` — needed so the content-validation script
can author/build it the way it does every other fixture campaign;
`long-horizon-loss` isn't registered there because it's exercised only
by its own replay test, mirroring the catalogue's existing
one-entry-per-file relationship to other multi-fixture campaign files
(e.g. `stable-life*`). `replay-corpus.ts` needed no edit — it discovers
fixtures by filename prefix scan, so dropping the new
`long-horizon-*.fixture/outcome.json` pairs into `fixtures/replay/` was
sufficient.
Both fixtures' action logs were generated, not hand-authored — a
scripted weekly policy function drove a live engine week by week (via a
temporary, uncommitted generator script), reading current
`SimulationKindState` each week to decide the next plan, exactly like
`design/30-slices.md`'s own W89 sizing argument requires.
## W89.1-W89.8 status
- **W89.1** ✅ `long-horizon-win` plays 150+ weeks to a terminal
`outcome()`, frozen in the corpus like every other entry.
- **W89.2** ✅ Both win and loss committed; the loss ends via the
eviction ladder, not a goal-failure condition.
- **W89.3** ✅ All 27 dispatched `ActionType`s resolve at least once
across the two runs, asserted against `RESOLVER_TABLE`'s non-stub
entries.
- **W89.4** ✅ (with a logged caveat) 14 of the 15 end-of-week systems
are observed acting: 13 non-relationship/non-week_limit systems across
the win+loss runs, plus `week_limit_reached` via an isolated third
side-test campaign (a short scenario with no goals — neither
long-horizon run can reach it without contradicting W89.2's own terminal
paths). `relationships` is excluded by name and logged as a decision: no
weekly relationship rule is specified anywhere in
`10-simulation-kind.md`, so the system can never emit anything beyond
`system.ran`.
- **W89.5** ✅ Byte-identical on replay under the determinism harness,
and byte-identical between a continuous session and a save/restore split
partway through (`engine.serialize`/`.deserialize` mid-run).
- **W89.6** ✅ Serialized size and unbounded-collection counts (week one
vs. final week) are asserted with stated ceilings, not printed.
- **W89.7** ✅ Every previously-committed replay outcome is unchanged
byte-for-byte (verified by the full `npm test` run — all 80 suites /
1193 tests green, including every existing kind's own corpus-match
loop).
- **W89.8** ✅ `npm run typecheck`, `npm run lint`, `npm test` all pass
from `src/engine/`.
## Decisions logged (design/90-decisions.md, mirrored into
docs/docs/engine/OPEN-QUESTIONS.md)
1. **`relationships` stays the simulation kind's only remaining
end-of-week stub.** No weekly relationship rule (decay, drift, or
otherwise) is specified in `10-simulation-kind.md` — §6.11 declares the
state and §7.7 the NPC, but nothing names what a week does to either.
W89.4's coverage assertion counts 14 systems, not 15, excluding it by
name. Revisit when a weekly rule is actually specified (`/contract`
work) — the same call W56's own *Out of scope* already made once.
2. **`long-horizon-loss`'s `pendingEventResponses` grows unbounded** — 0
to 37 entries over its own 160-week run — because nothing in
`endOfWeek.ts`'s `events` system expires an unanswered
`PendingEventResponse`, and the loss run's weekly policy is deliberately
inactive so the eviction arithmetic stays exact. This is a real instance
of the exact defect shape W89 exists to find, found and deliberately
**not fixed** here per W89's own out-of-scope line. W89.6's ceiling
assertion states this as an observed fact of the fixture, not a claim of
boundedness. Revisit by giving `PendingEventResponse` an expiry or a
default-declined resolution — a content/contract decision, not a
slice-sized fix.
Neither is an oversight — both are the interpretations this unit's own
instructions named in advance, applied and recorded rather than
re-litigated.
## Verified
- `cd src/engine && npm run typecheck` — pass.
- `cd src/engine && npm run lint` — pass.
- `cd src/engine && npm test` — pass (80 test files, 1193 tests),
confirming W89.5 and W89.7.
- `./build/ConvertTo-HumanDocumentation.ps1` — run, regenerated
`docs/docs/engine/OPEN-QUESTIONS.md` from the `design/90-decisions.md`
edit (the only marked-block change this PR makes); diff is exactly the
mirrored decision-log entries.
- `./build/Test-Documentation.ps1` — **run to completion, exit code 0.**
Links, terminology, and generated-doc drift all pass.
- `./docs.ps1 -BuildOnly` — not run (Docker-based production build;
relying on the PR's own required *Verify Documentation Build* check for
that gate).
## Still undecided
Both logged decisions above are open follow-ups, not blocking this PR:
the `relationships` system needs a weekly rule specified before it can
be observed, and `pendingEventResponses` needs an
expiry/default-resolution policy before long simulation runs stop
growing that collection unboundedly.12 files changed
Lines changed: 20426 additions & 6 deletions
File tree
- design
- docs/docs/engine
- src/engine
- fixtures/replay
- scripts
- src/campaigns
- tools
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
314 | 314 | | |
315 | 315 | | |
316 | 316 | | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
317 | 345 | | |
318 | 346 | | |
319 | 347 | | |
| |||
1120 | 1148 | | |
1121 | 1149 | | |
1122 | 1150 | | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
304 | 304 | | |
305 | 305 | | |
306 | 306 | | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
307 | 335 | | |
308 | 336 | | |
309 | 337 | | |
| |||
0 commit comments