0.8.6:0 — bundle Station Master v0.8.6, the four audit releases
Submodule pinned 3befc42 (v0.8.2) → def4820 (v0.8.6). `current.ts` bumped in place — the outgoing `up` was empty, no migration — with the resume question measured on this server's own thirteen saves rather than argued from the diff: 0.8.2 had refused all thirteen (the Second Section card went into the deck after its save check), and this build restores the three it promised while leaving the ten it refused for a rule refused. Release notes in all five locales; README's bundled-version summary rewritten for 0.8.3 through 0.8.6. `instructions.md` is unchanged: nothing about running the service moved. 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
56284b29a9
commit
644b24051a
@@ -13,278 +13,31 @@ 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.8.2.** A playtest read back against its own save file. **The change a player
|
||||
notices first: every district now opens on a DEPOT rather than a Whistle Post** — two A/D tracks and
|
||||
passengers working from Stage 1, where a Whistle Post has one track and is not a Passenger Facility
|
||||
at all. "Players start with Whistle Posts, not Depots" is offered at game creation for a table that
|
||||
wants the harder game, and the deck follows the choice: starting on Depots, the four Depot upgrade
|
||||
cards are left out, since an upgrade must be to the next tier.
|
||||
**Bundled version: 0.8.6.** Four releases from a code audit of the game — engine, server, browser
|
||||
client, tests — with every finding verified against the code before it was acted on.
|
||||
|
||||
**Three bugs, all reported from one game and all confirmed against its save.** An Office held two
|
||||
trains on one A/D track — the train an Interlocking had been holding at the Limits took the free
|
||||
slot and the arriving train was pushed in after it without anyone asking again, so the collision
|
||||
§8.3 calls for never happened. A seat's history froze permanently part-way through a game, because
|
||||
each seat's "what have I sent you" bookmark was an index into a log the game trims. And §8.1 asked
|
||||
the wrong question twice: "trains may pass" short-circuited the whole Subdivision, and a train
|
||||
standing at an Office was invisible to the scan although it was about to re-enter.
|
||||
**The one that matters to this server first: 0.8.2 stranded every game on it, and said it stranded
|
||||
ten.** The server's own boot log refused all thirteen saves at move 3. The Second Section card had
|
||||
gone into the deck after 0.8.2's save check had been run, and a deck one card larger shuffles into a
|
||||
different order from the same seed. 0.8.3 deals a save that predates the card from the deck it was
|
||||
dealt from, and the thirteen replay exactly as 0.8.2's notes promised: three resume, ten refuse, the
|
||||
same ten at the same moves. Nothing refused under 0.8.2 for a rule is un-refused here.
|
||||
|
||||
**GAMES IN PROGRESS: most will not resume**, and it was measured before release rather than
|
||||
discovered after. Of the thirteen saves on the test server, three resume and ten refuse; six of
|
||||
those ten are the new rule that a passenger Modifier may not be played at a Whistle Post. A refused
|
||||
save stops at a named move and is left untouched, so a game worth finishing can be finished by
|
||||
putting the previous version back. The Depot change itself strands nothing.
|
||||
**The engine (0.8.3):** a player could switch a rival's train and the rival paid the Moves; a rival's
|
||||
crew blocked your own track; drawing the last card off a Department could duplicate it and destroy
|
||||
the refill; the unjam cleared the first load rather than the one named; the collision limit could not
|
||||
fire in Stage 12; an Expedite fault was charged once per clearance question; a train held at the
|
||||
Limits was only ever released by another arrival, never by a departure.
|
||||
|
||||
**The documentation is a set of rendered pages now**, served by this package and linked from inside
|
||||
the game, rather than five Markdown files served as plain text. `instructions.md` is a manual again:
|
||||
the twenty "What changed in …" blocks it had accumulated — 380 of its 469 lines — are gone, because
|
||||
what changed in a release is what `releaseNotes` is for.
|
||||
**The transport (0.8.4):** a player who left a lobby kept a token that could stream and move for
|
||||
whoever took the seat next; the first move after a page reload could be silently swallowed; two moves
|
||||
at once could tear the save file, and a torn save took every game on the server down at boot; an
|
||||
error after a stream had started crashed the process; request bodies were unbounded; a double-click
|
||||
did the thing twice. `http.ts` has an end-to-end test suite on a real port now.
|
||||
|
||||
**Previously, 0.8.1.0.** A table pass over 0.8.0.17, and a second-digit bump taken
|
||||
deliberately — 0.8.1 had been reserved for the seatless display table, which is getting more
|
||||
thought. **The one that changes state:** the Freight Agent's "clear the red Inbound box" returned
|
||||
the car to the Classification Yard still marked loaded, carrying a load already delivered and
|
||||
already paid for. `pooled()` strips a load's `origin` stamp and keeps `loaded` on purpose (a train
|
||||
can retire with freight aboard), so the call site's comment — "a car back in a yard is back in the
|
||||
common supply, carrying nothing" — described something it never did. Coaches suffered worst:
|
||||
detraining needs `coach && !loaded` out of the Division Yard, so a cleared coach came back as stock
|
||||
that could never unload a passenger. 18 loaded coaches against 6 empty over five measured games.
|
||||
|
||||
**Games in progress resume, and it was measured before release**, not argued from the diff: all
|
||||
twelve saves on the test box were replayed through `tryResumeSession` — the server's own boot check
|
||||
— against the new build. Six resume, six refuse, and the six refusals are the same six, at the same
|
||||
moves, with the same codes, that 0.8.0.17 already logged. **That replay is the check to repeat on
|
||||
any release that touches state**, because it answers before the install rather than after.
|
||||
|
||||
Also: leaving a running game no longer strands you in a lobby with no way to join or create
|
||||
(`runLobby` never restored the section `enterSeating` hides); the lobby's action buttons carry the
|
||||
board's amber; an Enhancement in the history names the Mainline card it went on, slot and type, by
|
||||
its printed name; the Manage Game dropdown says how long ago each game was last touched; and §6.2's
|
||||
Department refill — always implemented, never documented — is written into the game's rules
|
||||
reference.
|
||||
|
||||
**Previously, 0.8.0.17.** Four fixes from a live playtest of 0.8.0.16, every one of them a case
|
||||
of the game knowing something and the screen not saying it. **ABS Signals could be played on only
|
||||
one Mainline card** although its own tooltip said "any": the engine allowed all of them, but the
|
||||
action list drops duplicate labels and every placement described itself identically, so all but the
|
||||
lowest-index card were discarded before the menu saw them. A card carrying ABS now **draws a signal
|
||||
mast**, and the three Heavy Grade modifiers draw as BRK, AIR and HLP — none of them marked the board
|
||||
before, so the only way to learn one was there was to hover it. **A Freight Agent turn claimed a car
|
||||
moved when none had** (§6.3 requires no action, and the bot declines on purpose rather than wreck a
|
||||
stocked box); it now says what it may do, says so when it idles and why, and names the industry
|
||||
rather than a grid coordinate. And **the log and the action list spelled the same square
|
||||
differently** — one in map order, one in storage order — which is fixed in favour of the map.
|
||||
|
||||
**The full documentation is now served by the package and reachable from inside a running game**,
|
||||
under the This Game card: Quickstart, Rules, every card, both deck references and Components. 0.8.0.16
|
||||
published the Quickstart alone and its "Where to read more" links all 404'd. Games in progress
|
||||
**resume normally** — no rule changed.
|
||||
|
||||
**Previously, 0.8.0.16.** Six fixes from a live playtest of 0.8.0.13, plus two follow-ups. One is a rules change and
|
||||
the rest are the game saying what it was already doing. The Sparrow running empty and a Depot that
|
||||
could not unload its passengers turned out to be the same thing: §9.2 sends an emptied coach to the
|
||||
Classification Yard and takes a fresh empty out of the Division Yard, and §2.2 returns Classification
|
||||
only when the Division Yard runs bare — so coaches travel one way. Measured over one three-Day game:
|
||||
sixteen coaches in the Division Yard at setup, none from Day 2 Stage 8 onward, fifteen piled in
|
||||
Classification while the Division Yard sat at 46-47 freight cars and stopped draining. The rule
|
||||
stands (Jesse's ruling, the same one Gitea#2 got); what changed is that a train made up short now
|
||||
reports what it wanted and why none is coming, and the yard panel warns while the shortage lasts.
|
||||
|
||||
Also: a Small Yard sort shows the train each option would build, laid out west to east as the board
|
||||
draws it and saying whether the result may leave the Office, and may now leave cars ahead of the
|
||||
engine; making up a train names the train in the history; a switching turn keeps its first move,
|
||||
industry work, the sort and a closing summary instead of a line per move; and the Freight Agent,
|
||||
Porter and Laborer groups say what each role is for.
|
||||
|
||||
Two smaller releases ride along. **v0.8.0.15** stopped the Interchange advertising a car-sorting
|
||||
action it has never had — the text reached players on the board through `mainlineDescription`, which
|
||||
`view.ts` renders as a Mainline card's description, so a player could only go hunting for a button
|
||||
that was not there. What the card actually offers is the one place on the Mainline an Extra may be
|
||||
made up and started. That release also carries a **Quickstart for new players** and brings the four
|
||||
hand-written reference documents up to the game as it runs. As of 0.8.0.17 **all of them are served
|
||||
by this package** — `/quickstart.md`, `/rules.md`, `/home-deck.md`, `/mainline-deck.md`,
|
||||
`/components.md` and the generated `/rules/as-built.md` — linked from the splash page and from the
|
||||
This Game card in a running game. They are served as `text/plain`, so their tables render as rows of
|
||||
pipes; rendering them as styled pages is filed upstream as TODO #109. **v0.8.0.16** is a doc comment on the flag behind that card, which is the
|
||||
most likely source of the wrong sentence in the first place.
|
||||
|
||||
**GAMES IN PROGRESS MAY NOT RESUME ON THIS ONE**, and it was measured rather than assumed. A Modifier
|
||||
card may no longer be placed outside a player's Limits, which reverses an earlier ruling and is the
|
||||
one shape of change that strands a save: a predicate in `apply.ts` changed its answer, so a move that
|
||||
was legal when played is refused on replay. The Day 3 playtest save stops at intent 528 of 539 —
|
||||
`card.play c55 → (-2,4)` against a sign at column 3 — eleven moves short of where the table had got
|
||||
to. The file is left untouched and the server names the move it stopped at, so re-installing
|
||||
0.8.0.13 finishes a game worth finishing. (0.8.0.10 was the first release to do this, for the §8.1
|
||||
clearance correction.)
|
||||
|
||||
The rest of the engine diff strands nothing: `makeUpShort` and `switchingEnded` are new events,
|
||||
`trayMoved` and `consistSorted` gained fields, and `switch.sortConsist` gained an OPTIONAL `engineAt`
|
||||
whose absence means what every sort did before — so an older save replays to the same train. Events
|
||||
are derived by replaying a save rather than stored in one.
|
||||
|
||||
The paragraphs below were written for the 0.7.9.8 bump and still describe how a bump is checked.
|
||||
|
||||
**Why saves are safe this time, and how that was established.** 0.7.9's breakage came from a rules
|
||||
change, and the general rule behind it is that a save is a seed plus the moves played, replayed
|
||||
through the *current* rules — so any change that makes a once-legal move illegal stops an older save.
|
||||
The question for this bump is therefore narrow: did anything become illegal? `git diff v0.7.9..HEAD
|
||||
-- src/engine/` is, in its entirety, four things — `isExpedited` exported and widened to a structural
|
||||
parameter type, `isFreight` exported, one new read-only helper (`freightRuleSpentHere`, which only
|
||||
the blocked panel asks), and `check('draw.end')`'s inline hand-limit test replaced by a call to
|
||||
`overHandLimit(state, player)` holding the identical expression. **No predicate changed its answer.**
|
||||
Eight releases, none of them in the rules.
|
||||
|
||||
Note that `engineVersion` is *not* a gate. `src/server/index.ts` attempts the replay and reports the
|
||||
stored and running versions only in the failure message; a game is refused because a move no longer
|
||||
replays, never because the version string differs.
|
||||
|
||||
**Two multiplayer information leaks.** The random seed was announced in the shared narration log at
|
||||
game creation, and a blind Home Office draw resolved the drawn card to its real name for every seat.
|
||||
Both were found by reading a plan rather than by a test, which is why 0.7.9.4 added a systematic
|
||||
redaction net: a seat's Frame, the spectator projection and the narration are serialised and searched
|
||||
for every opponent card id, every card name unique to one hand, the seed, and any private decision
|
||||
data — with an allow-list of every public property that fails the suite when a field is added.
|
||||
|
||||
**The history panel came back empty after a mid-game reload.** `Frame.lines` carried the whole
|
||||
narration log on every push to every seat and nothing read it — `RemoteSession` accumulates from
|
||||
`Push.lines` alone. The waste was masking the fault: `connect()` cleared the frame cache but not the
|
||||
narration watermark, so a reconnecting seat was told "nothing new since your last push" while the
|
||||
browser it was answering had just reloaded from an empty accumulator.
|
||||
|
||||
**Whose turn it is, during the extended-play vote.** The §3.3 vote is parallel — every un-voted seat
|
||||
may vote at once — so there is no actor to be, and the engine says so. The turn chart named the last
|
||||
seat to move anyway, beside a tally correctly showing three seats outstanding. Cause: two functions
|
||||
answering one question, one carrying a status guard and one not; there is one now, and it is the same
|
||||
function that *refuses* an intent, so the screen can no longer name somebody the server would turn
|
||||
away.
|
||||
|
||||
**A train held at the Limits was drawn nowhere.** An Interlocking stops an inbound train on the Limit
|
||||
Track instead of colliding with a full Office. `arriveAtOffice` removes the tray from the Mainline
|
||||
node's `transits` and the Interlocking branch never assigns `tray.position`, so with the map drawing
|
||||
mainline nodes from `transits` and squares from `position.at === 'grid'`, the train was in neither —
|
||||
it vanished off the board until an A/D track freed. Fixed in the view; the engine state was right.
|
||||
|
||||
**Also in this range.** Trains queued for a Crew Tray (a Timetabled departure, a played Extra, an
|
||||
ordered second section) now report themselves with the free-tray count, where two of the three had
|
||||
been waiting invisibly behind an exhausted pool. A Red Flag standing at an Office's Limits is drawn
|
||||
on the map. A spent Telegraph/Telephone/Radio is struck through and says when another district holds
|
||||
the Fedora — it is the Superintendent's own devices that get spent, and the Fedora moves every three
|
||||
Stages. The Campaign Train says whether its speeches are made, which is what decides whether leaving
|
||||
it off the Office square is a fault. Two bugs from the last playtest session: the Express's one
|
||||
freight car per location rule is explained where it refuses you (the panel had been describing the
|
||||
industry instead), and westbound trains are drawn in the correct half of a Mainline card.
|
||||
|
||||
**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
|
||||
`clearSave()`, the screen carries a **Continue saved game** button and states what Deal costs, so the
|
||||
door cannot destroy a game in progress. A solitaire save is browser-side, so nothing on this server
|
||||
is involved either way. The splash footer also now names both ways to play.
|
||||
|
||||
**This was reported three times before it was found, and the first two fixes were real bugs that were
|
||||
not it** — a routing fault (0.7.6) and a caching fault (0.7.7). Both were reported as verified, and
|
||||
both verifications read what the SERVER returned rather than exercising the path with the state a
|
||||
returning player actually has. What found it was a failing test written before the fix. Worth knowing
|
||||
when the next "that didn't work" arrives: reproduce the reporter's state first.
|
||||
|
||||
**0.7.7 fixed the client caching** that stopped the two releases before it from ever reaching a
|
||||
browser. `build-web.ts` stamps a build tag onto every module URL as a cache key, and
|
||||
its fallback when `git rev-parse` fails was the literal `nogit` — which is precisely the `.s9pk`
|
||||
case, since the Dockerfile copies the working tree in without `.git`. So every packaged release
|
||||
published `./web/main.js?v=nogit`, byte-identical to the one before, and a returning browser refetched
|
||||
nothing. `serveStatic` also sent no `Cache-Control` at all, so the pages that carry those tags were
|
||||
themselves served from cache. The tag is now the package version plus the build timestamp, and a
|
||||
request carrying `?v=` is `immutable` for a year while everything else is `no-cache`.
|
||||
|
||||
**Diagnosing "my fix did not ship".** A hard reload is NOT a sufficient check — confirmed in the
|
||||
field on 0.7.6: the document refetches but ES module sub-imports keep their cached `?v=` URLs, so the
|
||||
module graph stays stale. A fresh private window is the reliable test. Reading what the server
|
||||
returns (`curl` inside the container) proves what was installed, never what a browser is running.
|
||||
|
||||
**0.7.6 fixed the solitaire door 0.7.5 introduced**: a browser that had ever held a multiplayer seat
|
||||
could not reach the new setup screen at all — a bare page load could not tell "clicked Play
|
||||
solitaire" apart from "reloaded mid multiplayer game", so the door lost to whatever game or lobby
|
||||
that browser last touched. The door marks its intent explicitly now (`?solitaire`), the same fix
|
||||
`?lobby` already carries for the door on the other side.
|
||||
|
||||
**0.7.5 was a client-side flow change**: a genuinely fresh visit to the solitaire page opens a setup
|
||||
screen and asks for the game's options before dealing, the same question the multiplayer lobby has
|
||||
asked before a game starts since 0.6.0.
|
||||
|
||||
**0.7.4 bundled three rules corrections** off the tracker (Gitea#13, #5, #19), all of them places
|
||||
where the code and the cards disagreed.
|
||||
|
||||
**The Yard Office is offered rather than imposed (Gitea#5).** It was implemented in a form missing
|
||||
all three of its conditions: a qualifying train was teleported onto the card, so nobody was asked, no
|
||||
route was walked — the card's printed "that can reach the yard office in one move" was unenforced —
|
||||
and nothing was ever met on the way in. The arrival now interrupts the Mainline Phase to ask the
|
||||
district's owner, reachability is the engine's own move walk, and cars on the lead collide. Where no
|
||||
route exists the offer is withheld and the history says why.
|
||||
|
||||
**Some Extras must run loaded (Gitea#13).** Circus, Campaign and Military trains take a loaded car
|
||||
while the Division Yard can supply one, an empty once it cannot, and may depart short. The per-stop
|
||||
point is earned once per Office Area rather than once per game, and only by a fully loaded train.
|
||||
Two long-standing bugs went with it: `emptiesOnly` was rendered to the player and enforced nowhere,
|
||||
and a set-up out on the Mainline paid its point to player 0 whoever was playing.
|
||||
|
||||
**Red Flags is a different card (Gitea#19).** The old rule protected a stopped train on the Mainline
|
||||
and was played 4 times in 4,212 offers across 600 games. It is now a directional flag on your own
|
||||
Limits, spent on the train it stops, playable either in phase or at the moment the engine sees a
|
||||
certain collision.
|
||||
|
||||
**GAMES IN PROGRESS MAY NOT SURVIVE AN UPDATE FROM BEFORE 0.7.4.** Unlike 0.7.3, which changed no
|
||||
rules, each of the three above can stop an older history replaying: the Red Flags intent changed
|
||||
shape, a make-up that was legal may now be refused, and a Yard Office arrival asks a question no
|
||||
older history has an answer for. It fails safe — `src/server/index.ts` refuses to resume a save the
|
||||
rules reject, logs which move it stopped at, and **leaves the file untouched**, so an operator can
|
||||
put 0.7.3 back on to finish a game that matters. A game that never meets one of the three carries on
|
||||
normally, which is why this is "may not" rather than 0.7.2's "will not". **0.7.5 carries every game
|
||||
forward without exception** — see above.
|
||||
|
||||
**Diagnosing the interruptions.** The Mainline Phase can now stop and ask three different questions,
|
||||
of three different players: §8.1's clearance goes to the Superintendent, the Yard Office offer and
|
||||
the Red Flag prompt to the owner of the district a train is arriving at. A game sitting on one is
|
||||
waiting on a person, not stuck. `Games in Progress` shows it as active with nobody to wait for,
|
||||
because an interruption is not a turn — the same reading as an extension vote.
|
||||
|
||||
0.7.0 remains the shape of the service: the lobby sets a game up from one of four game types
|
||||
(Solitaire, Co-op, Competitive, Cutthroat) plus Custom, offers the whole rule set for reading before
|
||||
a seat is taken, and lets a player leave a lobby or a running game and come back to it. None of this
|
||||
changes anything the package itself does: the submodule pin is the version, and the service is the
|
||||
same server it always was.
|
||||
|
||||
---
|
||||
**Housekeeping (0.8.5)** and **the save rule (0.8.6):** in a Competitive game the save file — which
|
||||
carries the seed, and so every rival's hand — can be downloaded once the game is over; in Co-op at
|
||||
any time.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
||||
Reference in New Issue
Block a user