0.7.9:0 — bundle Station Master v0.7.9

Submodule pinned to v0.7.9 (6058f6c), current.ts bumped in place, release
notes rewritten in all five locales, README.md and instructions.md
updated. No new version file and no migration: the outgoing 0.7.8:0's
`up` was empty, which is versions.md's common case.

GAMES IN PROGRESS DO NOT CARRY OVER, and both docs lead with it.

v0.7.9 changes a rule. advance.ts gated §3.4's collision check on
`mode === 'competitive' || mode === 'coop'`, while SOLO_CONFIG carried
both limits (3 a Day, 5 total) and the setup screen offered them as live
settings with "the game ends in a loss" printed beside them. A solitaire
player could set a limit of 1 and crash all game. Jesse's ruling was that
the settings do what they say, so the gate went rather than the controls.

A solitaire save is { seed, history, rules? } where `rules` is the house
rules only — the collision caps are NOT in the save, so a restore takes
today's defaults. A 0.7.8 save therefore replays under live caps and
stops at the move that crossed one.

MEASURED WITH A CONTROL, because two earlier attempts at measuring it
were wrong. With the caps disabled, 1200 of 1200 generated solitaire
saves replay every intent — that is the control. Of those, 74 games
crossed the 5-collision total and 70 of the 74 truncate once the caps
are enforced; one replays 141 of its 532 intents and stops. So it is
"will break for anyone who crashed", not "may break". The earlier run
that reported no breakage had in fact been failing every replay at
intent 2 on an unrelated house-rules mismatch, and said nothing at all —
the control is the only reason that was caught.

It fails safe: the save is left untouched and the game declines to open
rather than loading a position the rules could not have produced, so a
player who cares can put 0.7.8 back and finish. Nothing in this package
can act on it either way — a solitaire save lives in the player's own
browser, not on this server. MULTIPLAYER GAMES ARE UNAFFECTED:
competitive and coop have been inside that gate all along.

TWO THINGS FIXED IN PASSING, both pre-existing and both in the way of
the release check. UPDATING.md was not prettier-formatted. And there was
no .prettierignore, so `prettier --check .` walked the bundled
application — a separate repo with its own style, which this package does
not own and does not commit — and reported 171 warnings it could do
nothing about. That check now passes clean, which is the point of having
it in the release sequence at all.

Verified: npm run check clean, prettier clean, make x86 packs as
v0.7.9:0. NOT yet installed on a box and NOT played.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTaNBL1jVxNqgFdjHkHoo3
This commit is contained in:
Jesse
2026-08-30 20:41:27 -04:00
co-authored by Claude Opus 5
parent 3b7b334968
commit bd802cb512
6 changed files with 169 additions and 67 deletions
+46 -15
View File
@@ -13,7 +13,38 @@ Station Master is a railroad operations board game with an authoritative multipl
solitaire needs no server and is not what this package is for. This package runs that server —
the browser client, the lobby, and the intent/SSE API — as a single StartOS service.
**Bundled version: 0.7.8.** Makes the solitaire setup screen reachable at all. 0.7.5 skipped it
**Bundled version: 0.7.9.** Enforces solitaire's collision limits, which had been offered as
settings and never checked — and that is a rules change, so **a solitaire game in progress will not
resume if it has had collisions**.
`advance.ts` gated §3.4's check on `mode === 'competitive' || mode === 'coop'`, while `SOLO_CONFIG`
carried both limits (3 a Day, 5 total) and the setup screen offered them as live settings with "the
game ends in a loss" printed beside them. A solitaire player could set a limit of 1 and crash all
game. Jesse's ruling: the settings do what they say, so the gate went rather than the controls.
**Why saves break, and how it was measured.** A solitaire save is `{ seed, history, rules? }` where
`rules` is the house rules only — **the collision caps are not in the save**, so a restore takes
today's defaults. A 0.7.8 save therefore replays under live caps and stops at the move that crossed
one. Measured with a control, because two earlier attempts were wrong: with the caps disabled 1200
of 1200 generated saves replay every intent; of those, 74 crossed the 5-collision total and **70 of
the 74 truncate** once the caps are enforced, one at 141 of its 532 intents. An earlier run that
reported no breakage had in fact been failing every replay at intent 2 on an unrelated house-rules
mismatch — the control is what caught that. It fails safe: the save is untouched and the game
declines rather than loading a position the rules could not have produced. **Multiplayer games are
unaffected** — competitive and coop were always inside that gate.
**Also in 0.7.9.** The end-of-game dialog now asks whether to play one more Day; the buttons existed
but were written into `#actions` _underneath_ a modal whose only control was Close, so a solitaire
player reaching the end was never offered the extension — and the Gitea#11 verification missed it
because it drove the HTTP API, which renders no dialog. `Frame.actor` carried `clock.currentActor`,
null for the whole Mainline Phase, so all three interruptions reported that nobody was holding the
game up; it carries `actingPlayer` and an `awaiting` field now. The game settings moved off the top
line into a **This Game** card drawn by the same renderer as the lobby's join preview, with the
running collision counts taking their place on the top line. The Office Area's auto-hide button was
a cycle that could not reach every state and is now three controls. The history reads newest first.
A Heavy Grade card draws which way it climbs.
**Bundled version: 0.7.8.** Made the solitaire setup screen reachable at all. 0.7.5 skipped it
whenever `load()` found a saved game — reasoned as "a saved game is a game to resume" — and a browser
that has ever played solitaire always has one, so the door could never reach the screen again. The
door (`?solitaire`) outranks a save now; a bare reload still resumes. Since dealing calls
@@ -116,11 +147,11 @@ same server it always was.
Built from source with a custom `Dockerfile` — there is no published Station Master image.
| What to Document | Value |
| --- | --- |
| Image source | Custom multi-stage `Dockerfile`: `node:*-slim` builder runs `npm run build:web` to produce the static client, then a second `node:*-slim` stage copies only `package.json`, `src/`, and the built `dist/` — no `node_modules` in the runtime stage, since the server has zero runtime dependencies |
| Architectures | x86_64, aarch64 |
| Entrypoint | `node src/server/index.ts` — runs straight from TypeScript source; no compile step, see Limitations |
| What to Document | Value |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image source | Custom multi-stage `Dockerfile`: `node:*-slim` builder runs `npm run build:web` to produce the static client, then a second `node:*-slim` stage copies only `package.json`, `src/`, and the built `dist/` — no `node_modules` in the runtime stage, since the server has zero runtime dependencies |
| Architectures | x86_64, aarch64 |
| Entrypoint | `node src/server/index.ts` — runs straight from TypeScript source; no compile step, see Limitations |
One subcontainer: `station-master-sub`, running the single daemon `server`.
@@ -128,12 +159,12 @@ One subcontainer: `station-master-sub`, running the single daemon `server`.
One volume, `data`, mounted at `/data` (`DATA_DIR`).
| What to Document | Value |
| --- | --- |
| Volume names | `data` |
| Mount points | `/data` |
| StartOS files | `store.json` — holds the join and admin secrets (see File Models) |
| Database | None — flat files. The server holds any number of games at once: each is `games/<gameId>/game.json` (`{ engineVersion, seed, config, playerNames, history, status, createdAt, lastMoveAt, botSeats }`) plus `turn-timings.json`, with a top-level `index.json` naming every game and lobby state for those not yet started |
| What to Document | Value |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Volume names | `data` |
| Mount points | `/data` |
| StartOS files | `store.json` — holds the join and admin secrets (see File Models) |
| Database | None — flat files. The server holds any number of games at once: each is `games/<gameId>/game.json` (`{ engineVersion, seed, config, playerNames, history, status, createdAt, lastMoveAt, botSeats }`) plus `turn-timings.json`, with a top-level `index.json` naming every game and lobby state for those not yet started |
## File Models
@@ -179,7 +210,7 @@ has been retrieved and shared with them.
**Updating the package keeps games in progress, unless the rules actually changed.** On boot the
server replays each saved game's moves through the current engine and resumes it if they all still
apply — the version that wrote the file is recorded and reported but decides nothing. When a move
*is* rejected, that game is refused and the log names it: `move 3 of 8 (localOps.choose) is
_is_ rejected, that game is refused and the log names it: `move 3 of 8 (localOps.choose) is
rejected by the current rules with OPTION_ALREADY_CHOSEN`. A refused game is never modified or
deleted, so reinstalling the previous version makes it loadable again and it can be played out.
@@ -225,8 +256,8 @@ answer.
## Health Checks
- **Multiplayer Server** — fetches the server's own `/api/health` and reports what it says:
*"Multiplayer server is ready — 3 games in progress"*, with *", 1 waiting to start"* appended
only when a lobby exists, and *"no games in progress"* on an idle server. So the check that
_"Multiplayer server is ready — 3 games in progress"_, with _", 1 waiting to start"_ appended
only when a lobby exists, and _"no games in progress"_ on an idle server. So the check that
proves the server is answering also says how much is going on.
A server that does not answer is reported as **starting, never failed**. The server replays its