v0.4.1 - bug fixes. initial d12 rolls determine what player in which seat.
This commit is contained in:
+118
@@ -21,6 +21,124 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.4.1 — 2026-08-13
|
||||
|
||||
### §4.4's opening D12 now decides who sits where
|
||||
|
||||
It had been rolled and thrown away — `void divisionRolls`, with seating fixed by array order — so the
|
||||
rule decided nothing, and `seating` was the identity mapping in every game ever played. `seating[seat]
|
||||
= player` now runs ascending by roll from seat 0 (west, beside the Western Division Point) to the last
|
||||
seat: "highest is the Eastern Division Point".
|
||||
|
||||
The rule names only those two ends, because at a table the players are already sitting in a chain and
|
||||
the roll only says which way round it is. There is no physical table here, so the roll orders
|
||||
everybody — it uses a number every player is already told to roll, and it makes the roll matter to
|
||||
more than the winner. Ties break toward the lower player index sitting further east, the same
|
||||
first-max-wins convention `argmax` already uses for the Superintendent roll.
|
||||
|
||||
**Turning it on immediately found three more of last commit's bug class.** Acting order, the opening
|
||||
deal and the Fedora all did `(player + n) % players`. Every one of them is a statement about the
|
||||
physical chain — "starting from the Superintendent and proceeding left" (Gap 1, §4.7, §5) — so every
|
||||
one of them is seat arithmetic, and all three were right only while seating was the identity mapping.
|
||||
They now go through a new `playerLeftOf(state, player, n)`. This is the payoff of the split being
|
||||
exercised by real games rather than only by tests that rotate `seating` by hand: eight of these were
|
||||
found by inspection last commit, and three more fell out of simply making the rule work.
|
||||
|
||||
`state.openingRolls` keeps both D12s, indexed by player, so a lobby can show the chain forming rather
|
||||
than only its result (`lobby-and-sessions.md` §4). `Frame.players` gained `seat` for the same reason —
|
||||
the list is in player order because it is about people, and a client that wants to draw the table
|
||||
west-to-east now can.
|
||||
|
||||
**Solitaire is untouched, and the proof is that it had better be:** one player is one seat, so the
|
||||
permutation is trivially `[0]`. All four published replays finish on their recorded Revenue (27, 31,
|
||||
29, 28) and the bot is unmoved at 7.0 mean over 200 games.
|
||||
|
||||
Nine multiplayer tests failed on the change and every one of them was the test being wrong — each had
|
||||
encoded the identity mapping, which is exactly what made them pass before. `readyToLeave` was putting
|
||||
a *player* index into a tray's `seat` field; the Subdivision tests upgraded "player 1's Office" when a
|
||||
Subdivision is a stretch of the physical chain; the Fedora test recorded player indices where §5 is
|
||||
about seats. Two new tests replace the one that asserted the identity mapping outright: seating is a
|
||||
permutation (and is still `[0]` in solitaire), and over 40 seeds the highest roller is easternmost with
|
||||
the chain ordered throughout.
|
||||
|
||||
|
||||
### The intents are canonical; the event log narrates
|
||||
|
||||
The README, four architecture documents and six source comments all claimed `state = fold(events)`.
|
||||
It was never true, and it was load-bearing — the stated justification for reconnection, restart
|
||||
recovery and persistence, none of which were built yet, so nothing had ever tested the claim.
|
||||
|
||||
Measured before deciding: **`advance.ts` never calls `reduce`.** Fourteen of the forty-six event types
|
||||
are emitted after the phase driver has already mutated state — the clock, and the whole Mainline
|
||||
phase. Folding the log rebuilds a district and not a railroad; every train movement in the game is
|
||||
missing.
|
||||
|
||||
**Settled the cheap way, because the plan never needed fold.** Persistence is
|
||||
`{ engineVersion, seed, config, history: Intent[] }` (`multiplayer.md` §10), the wire carries `Frame`s
|
||||
rather than events (D2/D3), and reconnection is a fresh `Frame` rather than an event tail — so making
|
||||
the phase driver reduce would have been a rewrite of the most rule-dense code in the project to buy
|
||||
something nothing uses. `lobby-and-sessions.md` §6 previously said "persist the event log"; it now
|
||||
persists the intents, which are smaller still and which `fromSave` already replays.
|
||||
|
||||
`test/events.test.ts` pins the unreduced set as a deliberate change-detector: shrink it and the test
|
||||
tells you which documents now understate the engine; grow it and you have added another event on the
|
||||
mutate-then-describe path. It also asserts the property that *does* hold — that replaying the intents
|
||||
reproduces the board, the score, the clock and the crews exactly — and that `Save` still carries
|
||||
nothing but a seed and a history.
|
||||
|
||||
### `lobby-and-sessions.md` reviewed, and four decisions taken
|
||||
|
||||
The persistence rewrite above left the document internally consistent but unreviewed — it was written
|
||||
before `multiplayer.md` and contradicted it in four places, and the code in two more.
|
||||
|
||||
*Contradictions with the newer plan:* it had **no access control at all** ("a game code is the whole
|
||||
discovery mechanism") where D14 added a server-wide join secret; its timeout table used the fictional
|
||||
`Switch.End`-style names and **omitted `freightAgent.end`**, so a player who chose Freight Agent never
|
||||
timed out; it opened "one actor at a time", which D19 stopped being true; and "do not substitute an AI
|
||||
player" contradicted D8's bots-at-lobby-time.
|
||||
|
||||
*Contradictions with the code:* §4 described the opening D12 for the Eastern Division Point as
|
||||
happening, when `setup.ts:266` rolls it and does `void divisionRolls` — seating is array order, so the
|
||||
rule decides nothing. `PlayerDisconnected` was written as an event; there is no such type, and it
|
||||
should stay out of `GameEvent`, which must remain replayable from a seed.
|
||||
|
||||
**Four decisions:**
|
||||
|
||||
- **The session token names the PLAYER, not the seat.** `multiplayer.md` §10 said seat; that became
|
||||
wrong in v0.4.0, and Employee Rotation is exactly the case where it bites — a token naming a chair
|
||||
seats a returning player in someone else's Office. Both documents now agree, and say the seat is
|
||||
`seatOf(state, player)`.
|
||||
- **2 to 4 players**, enforced in the lobby because the engine enforces nothing. Set to what is
|
||||
actually exercised rather than to the previous guess of 6. Noted as a lobby judgment, not a rules
|
||||
limit — the rules describe 5+ and the engine implements it.
|
||||
- **No forcing turn timer.** Cut, on the same objection that keeps bots out of running games: the
|
||||
clearance decision changes somebody else's score, so anything answering it automatically changes
|
||||
the game. Moved to `TODO.md` to explore only if halted games prove to be a real problem, keeping
|
||||
the one piece of reasoning worth saving — that **deny** is the safe default, since a held train
|
||||
costs a Stage and a wrecked one costs 5 Revenue and feeds the collision floor.
|
||||
- **A turn clock that records rather than enforces.** Wall-clock per player per phase, so "how long
|
||||
does a 4-player game take, and which phase is the wait?" becomes measured instead of guessed — the
|
||||
one measurement bot simulation cannot produce, because the bot does not think. Explicitly outside
|
||||
the rules engine (which has no clock and must not acquire one) and outside the canonical record
|
||||
(a replay must reproduce a game from decisions alone). Phase 3, item 16.
|
||||
- Plus: **host rights pass to the earliest-joined remaining player** if the host leaves before start,
|
||||
so no lobby is stuck behind a closed tab.
|
||||
|
||||
### Documentation reconciled with the code
|
||||
|
||||
`docs/design.md`'s status section was four milestones stale: a 115-card deck (it is 213 in play, from
|
||||
a 235-card catalogue), 134 tests (497), a 0% win rate and 0.9 Revenue/Day (7.0 mean, 1.4/Day, 5 wins
|
||||
in 200), and "Next: step 7 begins the server" when Phase 2 is deliberately held. Rewritten as the
|
||||
shape of the project rather than a running tally, pointing at the CHANGELOG and `TODO.md` for
|
||||
anything that moves — a hand-maintained tally is exactly what drifted.
|
||||
|
||||
The README also said the rules were **fully specified**, which overstates it: three questions are
|
||||
genuinely open — where the Local's coach stands while its engine works (a §A.4 question rather than a
|
||||
train-card one), Poling, the one card in the deck with no defined behaviour, and whether a Heavy
|
||||
Grade's orientation is rolled or chosen at setup. Thirteen prototype gaps were closed; these three
|
||||
came after, and the bullet now says so and names them.
|
||||
|
||||
|
||||
## 0.4.0 — 2026-08-13
|
||||
|
||||
### Phases 0 and 1 of the multiplayer plan — the seams, not the server
|
||||
|
||||
Reference in New Issue
Block a user