v0.5.0 — multiplayer Phases 2 and 3: a server that runs a game and survives being restarted

Phases 0-1 shipped in v0.4.0 (seat/identity split, per-player turn state, the Session boundary).
This lands Phase 2 (server core, one game, no lobby) and Phase 3 (persistence and resumption) per
docs/architecture/multiplayer.md §12. Phases 4-6 (lobby/reconnection, the 22 opponent-directed
cards, StartOS packaging) are still ahead.

Phase 2: src/server/session.ts hosts a game in pure logic (no sockets) on top of game.ts's existing
Game/submit/currentActor/actionMenu; it verifies seat === currentActor(game) itself before calling
submit, since submit() trusts its caller and a server can't. src/server/http.ts and index.ts add
POST /api/game, GET /api/stream (SSE, per-seat), POST /api/intent, and static serving of dist/.
src/sim/frame-delta.ts is a purpose-built per-seat board delta for one live push at a time. Found
and fixed along the way: actionMenu(game, seat) only used seat for the hand field, so a server
computing every connected seat's Menu would have handed the acting player's legal moves to a
waiting seat. Verified with a live end-to-end smoke test (2-player game, two SSE streams, a
rejected intent from the wrong seat, an idempotent resend) plus test/server/session.test.ts and
test/redaction.test.ts. Not verified: an actual browser (none available in this environment).

Phase 3: src/server/persistence.ts writes game.json and turn-timings.json, atomic-rewrite-then-
rename. game.ts gained fromMultiplayerSave, fixing a narration-attribution bug found while testing
it (fromSave's replay loop drops the actor argument, invisible in solitaire, unreadable the moment
there's more than one seat — fromSave itself still has this gap, deliberately untouched). Verified
live: server killed and restarted mid-game, both seats reconnected exactly where they left off.

Two rules bugs found while building this: the New Train phase never implemented its car-placement
round (every car of every train was placed by the Superintendent alone, in every mode, all along —
now reads the round position off tray.consist.length); and victory conditions are now one shared,
configurable GameConfig set across solitaire/competitive/coop instead of a fixed length lookup and
a dead firstToTarget condition.

Also folds in the three fixes already released on the patch line as v0.4.9b/c/d: a switching
train's crew badge failing to draw once it left the Office square, an unload that always took the
westmost car regardless of which was picked, and a legal decision that could render with zero
buttons.

docs/testing/0.5.0-test-plan.md and three reported-bug save files (docs/station-master-seed*.json)
included for reproducibility. tools/jitsi-harness/ deliberately left untracked — unrelated
side-project work, not part of this release. 635 tests, 0 failures.
This commit is contained in:
Jesse
2026-08-20 23:50:38 -04:00
parent f9c4d9fa92
commit c3c5cbfeec
52 changed files with 5282 additions and 420 deletions
+16 -5
View File
@@ -42,17 +42,28 @@ Operational Rail wheel icon, an industry track of the stated length, Laborer ico
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |
| Mine Tipple | Hopper | Outbound only | 3 | 3 | — | 4 | 2 |
| Produce Shed | Reefer | Outbound only | 2 | 2 | — | 3 | 2 |
| Grocer's Warehouse | Boxcar | Both | 2 | 2 | 2 | 3 | 2 |
| Oil Refinery | Tank car | Both | 3 | 2 | 2 | 4 | 2 |
| Grocer's Warehouse | Boxcar | Both | 1 | 1 | 1 | 3 | 3 |
| Oil Refinery | Tank car | Both | 1 | 1 | 1 | 4 | 3 |
| Power Plant | Hopper | Inbound only | 3 | — | 3 | 4 | 2 |
| Freight House | Boxcar | Both | 1 | 1 | 1 | — | 6 |
Directions follow the commodity: coal originates at a Mine Tipple and is consumed at a Power Plant;
produce ships out; a warehouse and a refinery do both. This gives §9 all three of its cases —
outbound-only, inbound-only, and both.
**"Freight House"** (§9.3, Appendix A) is not a card. It is the collective term for a freight facility
that loads *and* unloads — the Grocer's Warehouse and the Oil Refinery. §9.3's "Passenger Facilities
and Freight Houses permit cars to move each direction" therefore names exactly those two.
**"Freight House" IS a card** (corrected v0.5.0) — a sixth industry, dealt 6 copies, one Laborer and
one slot each direction. An earlier pass here read §9.3/Appendix A's "Passenger Facilities and
Freight Houses permit cars to move each direction" as meaning "Freight House" was only ever a
*collective term* for the Grocer's Warehouse and the Oil Refinery, never a card of its own — that
reading was wrong; the engine deals it as a real sixth industry (`content.ts`'s `freightHouse`
profile) and this table follows the engine.
<!-- TODO v0.5.0: Mine Tipple, Produce Shed and Power Plant above (3/3/4, 2/2/3, 3/3/4) were NOT
re-verified against the engine in this pass — only Grocer's Warehouse, Oil Refinery and Freight
House were. `mineTipple` and `powerPlant` in `content.ts` are also base 1/1/1, same as the three
corrected here, so these three rows and the "Throughput" reasoning below (built on the OLD
2–3-Laborer numbers) may be stale too. Flagged in TODO.md rather than silently rewritten — this
needs the same kind of decision Jesse made for Grocer's/Refinery/Freight House, not an assumption
that the same correction applies. -->
### Throughput — why these Laborer counts
+2 -2
View File
@@ -24,8 +24,8 @@ Every defined term, alphabetized for lookup. The core comes from the Definitions
| **Extra Platform** | Modifier card: +1 green and +1 red slot at a Passenger Facility. | §12.5 |
| **Extra Train** | A one-and-done train; its card returns to the Salvage Yard on completion. Head-on card image, so the drawing player picks its direction. Numbered with an "X" prefix; the following number gives its seniority, and it yields to the Timetabled train of that number. | §2.3, §8 |
| **Facility** | A business which loads/unloads cargo and freight. | §2.5 |
| **Freight Facility** | Mine Tipples, Produce Sheds, Grocer's Warehouses, Oil Refineries, Power Plants. Some allow only outbound, some only inbound, some both. Per-card values in §12.5. | §9, §12.5 |
| **Freight House** | Not a card — the collective term for a freight facility permitting both directions, namely the Grocer's Warehouse and the Oil Refinery. | §9.3, §12.5 |
| **Freight Facility** | Mine Tipples, Produce Sheds, Grocer's Warehouses, Oil Refineries, Power Plants, Freight Houses. Some allow only outbound, some only inbound, some both. Per-card values in §12.5. | §9, §12.5 |
| **Freight House** | A sixth Freight Facility card (corrected v0.5.0 — an earlier pass here read it as a collective term for the Grocer's Warehouse and the Oil Refinery rather than a card of its own; it is dealt like any other industry). Permits both directions, the same as a Grocer's Warehouse or Oil Refinery. | §9.3, §12.5 |
| **Highball** | When a train holding at an Office automatically departs. | §2.4 |
| **Home Office** | The primary face-down deck cards are drawn from. 52 cards. | §2.6, §12.1 |
| **Hopper** | Coal rolling stock (brown = loaded, white = empty). | §2.2 |
+6 -4
View File
@@ -82,10 +82,12 @@ direction. `DivisionNode.gradeUp` records the direction a train travels when **c
going the other way is descending. Brakeman and Airbrakes apply to the descent, Helpers to the
climb, so turning the card around swaps which trains each card helps.
One gap remains, and it is a **setup** gap rather than a rules one: `createGame` is synchronous and
returns a ready state, so there is no point at which a player can be asked. Orientation is currently
**rolled** — deterministic from the seed, and roughly even (51/49 east/west across 400 games) — and
should become a real player choice when setup gains an interactive phase.
**Settled (v0.5.0): orientation is never a player choice, and this overrides the card's own printed
"Player sets orientation."** A Heavy Grade sits on the shared west-to-east Division chain, between
two players (§4.2) or beyond an end Division Point next to one — never inside a single player's own
district — so whichever direction climbs advantages one neighbour over the other, and no one player
has a fair claim to the decision. Orientation is **rolled** from the seed instead — deterministic,
and roughly even (51/49 east/west across 400 games) — identically for solitaire and multiplayer.
**Still not implemented**: the Action (10) and Space-use (12) cards, which are genuinely
multiplayer-only. Playing them is rejected with `NOT_IMPLEMENTED`.
+6 -4
View File
@@ -718,10 +718,12 @@ leave no way at all to speed up a slow facility. Section Gang is the sole worker
keeps that power scarce. Capacity modifiers are stronger than they sound, since banking room rather
than worker count is what limits output.
**Sub-decision — what a "Freight House" is.** §9.3 and Appendix A refer to Freight Houses, but §9's
facility list never defines one. It is **not a card**: it is the collective term for a freight
facility that both loads and unloads — under 10a, the Grocer's Warehouse and the Oil Refinery. This
reconciles every reference without adding a card type or disturbing Gap 4a's deck composition.
**Sub-decision — what a "Freight House" is. SUPERSEDED (v0.5.0).** §9.3 and Appendix A refer to
Freight Houses, but §9's facility list never defines one. This originally concluded it was **not a
card** — the collective term for a freight facility that both loads and unloads, under 10a the
Grocer's Warehouse and the Oil Refinery. That reading was wrong: it **is** a card, a sixth Freight
Facility dealt 6 copies (`content.ts`'s `freightHouse` profile), which the engine has implemented
all along. Left here for the reasoning; do not read this entry as current.
### 10e — Revenue/Day revalidated, and the Gap 6 targets revised
+7 -5
View File
@@ -452,9 +452,10 @@ Loading and unloading of Rolling Stock (passengers and freight) takes place in t
(§11). A Whistle Post is not a Passenger Facility, so a player who has not upgraded has no
passenger business at all.
- **Freight Facilities:** Mine Tipples, Produce Sheds, Grocer's Warehouses, Oil Refineries, Power
Plants. Per-card values in §12.5.
- **Freight House:** **[Gap 10d]** not a facility type of its own, but the collective term for a
freight facility permitting both directions — the Grocer's Warehouse and the Oil Refinery.
Plants, Freight Houses. Per-card values in §12.5.
- **Freight House:** a Freight Facility card in its own right (corrected v0.5.0 — **[Gap 10d]**
previously read this as a collective term for the Grocer's Warehouse and the Oil Refinery rather
than a card of its own). Permits both directions, the same as those two.
- **Modifier Facilities:** Cards that, placed adjacent (on any of the nine nearby spots), increase a
passenger or freight facility's capacities. If two Facilities are adjacent to the Modifier, its
effects may only be used on one Facility per **Stage** **[Gap 7]**.
@@ -753,8 +754,9 @@ Modifier effects, and track geometries — is catalogued in
| Extra Platform | +1 green and +1 red slot (passenger) |
| Section Gang | +1 Laborer or +1 Porter |
**"Freight House"** (§9.3, Appendix A) is not a card — it is the collective term for a freight
facility that both loads and unloads, namely the Grocer's Warehouse and the Oil Refinery.
**"Freight House"** (§9.3, Appendix A) is a Freight Facility card (corrected v0.5.0 — previously
read as not a card, only a collective term for a facility that both loads and unloads). It permits
both directions, the same as the Grocer's Warehouse and the Oil Refinery.
---