v0.8.3 — the engine half of the audit, and the deal 0.8.2 silently changed
Seven rules faults and one dealing fault, from a four-way code audit (engine, server, client, tests) read against the code before anything was acted on. Each is pinned by a test that failed first. CHANGELOG has the reasoning; this is the list. THE DEAL. 0.8.2 put the Second Section card into the deck after its save check had run and without a line in its notes. A deck one card larger shuffles differently from the same seed, so every save on the test server refused at move 3 — the boot log shows thirteen of thirteen — while the release notes said three would resume. `withSavedDeal` (was `withSavedOpening`) now sets `secondSectionCard: false` for a config that predates the setting, and the thirteen replay exactly as 0.8.2 described: three resume, ten refuse, the same ten at the same moves. THE RULES. `check` never tested that a switching tray was in the actor's own district, so a rival's train could be shunted and the rival charged the Moves. Occupancy matched on coordinates alone, so a rival's crew blocked your track. A Department draw that emptied the deck duplicated the drawn card and destroyed the refill card. The unjam cleared the first load rather than the one named. The collision floor could not fire in Stage 12. The Expedite fault was charged once per clearance question rather than once per phase. A train held at the Limits was only ever released by another arrival, never by a departure. Docs: rules.md describes each as built (and no longer says an Expedited train departs at Shift Change — that was v0.4.8's reading, corrected in v0.4.9's code and never in the document); game-state.md's collision-floor note now matches the code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrCWubm9GAftYCm2hWdKwK
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6f2a8dff09
commit
4d222a7eba
@@ -19,6 +19,90 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.8.3 — 2026-09-29
|
||||
|
||||
The first of three releases from a code audit (engine, server, client, tests and hygiene, each read
|
||||
by a separate reviewer and every finding re-verified against the code before it was acted on). This
|
||||
one is the engine: seven rules faults and one dealing fault, each pinned by a test written to fail
|
||||
first. **Every save on the test server was replayed under this build before release** — and that is
|
||||
how the dealing fault was found, because under 0.8.2 none of them replayed at all.
|
||||
|
||||
### 0.8.2 stranded every game on the server, and said it stranded ten
|
||||
|
||||
The 0.8.2 notes said three of thirteen saves would resume. The server's own boot log, read for this
|
||||
release, refused all thirteen at **move 3**, `CARD_NOT_IN_HAND` — including the game saved by 0.8.1.0
|
||||
the notes had counted as safe. A refusal at move 3 is not a rule; it is a different deal.
|
||||
|
||||
The **Second Section card went into the deck in 0.8.2** (Q9, one copy — it had been defined and
|
||||
never dealt), after that release's save check had been run and without a line in its notes. A deck
|
||||
one card larger shuffles into a different order from the same seed, so every save older than the
|
||||
card was replaying a different railroad from intent one. `withSavedOpening` could not help: it names
|
||||
the opening, and the opening was not the problem.
|
||||
|
||||
Now `withSavedDeal`, and it names both. `secondSectionCard` is a house rule with no dial — a fact
|
||||
about how a game was dealt, kept for the same reason `startingOffice` is — set false for a saved
|
||||
config that predates the setting and true for everything dealt since. Under this build the thirteen
|
||||
replay exactly as 0.8.2's notes described: **three resume, ten refuse, the same ten at the same moves
|
||||
for the same rules.** The process rule this breaks is already written down ("say which way games in
|
||||
progress go, every time"); what it adds is that the save check has to be the LAST thing before the
|
||||
tag, not a thing done during the work.
|
||||
|
||||
### A player could switch a rival's train, and the rival paid for it
|
||||
|
||||
`check` resolved a switching tray with no seat test at all. The legal-move generator filtered trays
|
||||
by seat; `check` did not; the server validates with `check` alone. Every district opens on the same
|
||||
coordinates, so a destination legal for your own tray at (0,0) was "legal" for a rival's tray at
|
||||
THEIR (0,0) — and the Moves came off the rival's turn, because `trayMoved` charges whoever sits in
|
||||
the district the tray is in. All four switching intents now refuse a tray outside the actor's own
|
||||
district with `NO_SUCH_TRAY`. Invisible in solitaire, which is why it lasted.
|
||||
|
||||
### A rival's crew blocked your own track
|
||||
|
||||
`occupancyFor().trayAt` matched on coordinates alone, so a crew standing at seat 0's (0,2) was
|
||||
"another train standing here" at seat 1's (0,2) — a phantom that refused Moves and closed the Yard
|
||||
Office walk in any game with more than one seat. It asks the seat now.
|
||||
|
||||
### Drawing the last card off a Department could duplicate it and destroy the refill
|
||||
|
||||
When a Department draw emptied the Home Office deck, the reshuffle was computed from the table
|
||||
BEFORE the draw and the refill had been reduced: it swept up the card being drawn and missed the
|
||||
refill card. The reducers then dealt the drawn card into the new deck while the refill card, moved
|
||||
onto a pile the reshuffle wiped a moment later, left the game. Proven by counting — 47 cards in, 46
|
||||
distinct out — and fixed by describing the sweep as the table will be after the events ahead of it.
|
||||
|
||||
### The unjam cleared the first load, not the one you named
|
||||
|
||||
`facilityUnjammed` carried no index, so the reducer cleared the FIRST load on MEN | AT | WORK and the
|
||||
first car of the named type in a box. With an inbound tank load on MEN and a stranded outbound
|
||||
hopper on WORK, unjamming the hopper deleted the tank load, sent a loaded hopper to the yard and
|
||||
left the jam. The event carries the index now; events are regenerated on replay, so no save changes.
|
||||
|
||||
### The collision floor could not fire in Stage 12
|
||||
|
||||
`shiftChange` reset the Day's collision count at the rollover and only then judged it, so a breach
|
||||
reached in the last Stage of a Day read as zero. The limits are judged on the Stage just played,
|
||||
before the Day rolls over. `docs/architecture/game-state.md` had said the check was immediate; it
|
||||
never was, and it now says what happens.
|
||||
|
||||
### The Expedite fault was charged once per question
|
||||
|
||||
The Mainline phase is re-entered from the top after every clearance, Yard Office and Red Flag
|
||||
ruling, and the Q3 fault loop at the top ran unguarded — an Expedited train left on a siding was
|
||||
fined once per interruption. Once per phase now. The rules document had also still described the
|
||||
v0.4.8 reading of Expedite (departs at Shift Change), corrected in the code in v0.4.9; both passages
|
||||
now describe the rule as built.
|
||||
|
||||
### A train held at the Limits was only ever released by another arrival
|
||||
|
||||
`arriveAtOffice` promised "held at the Limits until an A/D track frees up", and the only release
|
||||
was inside `arriveAtOffice` for a DIFFERENT train. An Office that emptied by departures kept its
|
||||
held train at the Limits for the rest of the game, invisible — no transit, no A/D track, no part in
|
||||
the clearance check. At the end of every Mainline phase, held trains now take any free tracks in
|
||||
the order they were held, and the history says a departure freed the track rather than naming an
|
||||
arriving train that does not exist.
|
||||
|
||||
---
|
||||
|
||||
## 0.8.2 — 2026-09-23
|
||||
|
||||
A playtest read back against the save file, and the rules that came out of it. Nine questions were
|
||||
|
||||
Reference in New Issue
Block a user