A second-digit bump for a playtest read back against the save file. Nine questions were asked of one three-Day game; three were bugs, three were the rules working and undocumented, three were decisions. Every save on the test server was replayed against this build BEFORE release, which is how the cost of each rule was known before it was chosen rather than discovered after. EVERY DISTRICT OPENS ON A DEPOT. A Whistle Post has one A/D track and is not a Passenger Facility, so the opening of every game was spent unable to work a passenger and one arrival away from a collision. Two A/D tracks and passengers from Stage 1 now; "Players start with Whistle Posts, not Depots" is the harder game, set when the game is created. The deck follows the choice — starting on Depots the four Depot upgrade cards are left out, because an upgrade must be to the next tier and a Depot card at a table of Depots is a dead draw. How much easier it is showed up as a test failure rather than an argument: the cue-coverage pool needed widening from 24 seeded games to 60 before it held one collision. NO SAVE WAS STRANDED BY IT, which took care. This is the one house rule that changes how a game is DEALT rather than how it plays, so replaying a save under the wrong opening is a different railroad from intent one — silently, with no error. `withSavedOpening` fills it on the replay paths ONLY. Putting it in the resolver instead made a fresh Cutthroat game deal Whistle Posts and read as Custom, which is how the distinction was found. THREE BUGS, ALL REPORTED FROM ONE GAME AND ALL CONFIRMED ON ITS SAVE. An Office held TWO TRAINS ON ONE A/D TRACK. The capacity test passed with nothing standing, the train the Interlocking had been holding at the Limits was moved into the free slot, and the arriving train was pushed in after it without anyone asking again whether there was room — so the collision §8.3 calls for never happened. The held train keeps priority; the newcomer now takes the consequence it would have met had the held train arrived first. THE HISTORY FROZE, permanently, and the log cap was not really the cause. Each seat's "what have I sent you" bookmark was an INDEX into an array the game trims, so once a seat's bookmark reached the limit the slice returned nothing for the rest of the game — at a different moment per seat, because each holds its own. That game's log ended at exactly the cap. Lines carry a sequence number now, which survives trimming; proven by pushing twice the cap through a simulated seat. §8.1 ASKED THE WRONG QUESTION TWICE. "Trains may pass" returned `clear` before the Subdivision was looked at, so a train entering a Double Track was released however busy the rest of it was — that, not anything about Control Points, is what let Train 8 out with no ruling. And a train standing at an Office was invisible to the scan, so one about to re-enter the very Subdivision being entered counted for nothing. Capacity is the test, not presence: a Depot with a track free is not in the way; a Whistle Post with its one track taken is. THINGS THAT HAPPENED SILENTLY NOW SAY SO — a train held against a facing one, a train released from the Limits (a side effect of somebody else's arrival, so it simply appeared at the Office), and the train an Interlocking is holding, whose explanatory tooltip has existed since #99 with NO renderer ever reading the flag. WHERE A MOVE IS REFUSED, AND WHY. `exploreMoves` decides where the rails go and the pick-up restrictions are enforced afterwards in `check`, so a square the rails reached and the card forbade was reachable, un-offered, and absent from the block list with nothing said. Those squares are blocked with the rule that blocks them now, and the reasons are got by ASKING `check` rather than re-deriving: a second implementation of the rules is exactly the failure the block list exists to avoid. A train may also always recover its own caboose — X13 prints "may drop but not pick up anything", and a train needs its caboose to be made up, so one that parted with it could never legally leave again. RULES DECIDED IN SEPTEMBER AND APPLIED HERE. A Modifier must sit square against its host, no diagonals. A passenger Modifier may not be played at a Whistle Post. Both were built, measured, held back for a fortnight so a playtest could finish, and applied now. A Second Section costs its card: `SECOND_SECTION` was declared in content.ts and never dealt, so the action was free and the bot ordered 26 accidental ones in a measured round. The card is dealt and spent — gating on a card the deck never holds would have deleted the mechanic rather than fixed it. THE DOCUMENTATION IS A SET OF PAGES, not five text files served as text/plain — a card reference is mostly tables, and as plain text a table is rows of pipes. Markdown is still the one copy; the build renders it, and publishes the .md beside each page. No Markdown library: this project has no runtime dependencies and one would be a poor first. The pages add what Markdown cannot carry without drifting — a nav across the set, a contents list built from the headings actually rendered, an anchor on every heading, a 70-character measure, and tables that are tables. They print as ink on paper. The references caught up with the rules, checked rather than assumed: two statements had gone from stale to misleading (the Quickstart told a new player to "get a Depot down as soon as one appears"), and four rules nobody could look up are written down — the Office tier table, §8.1 in practice, what the Circus Train pays for, and that a Realignment can be a card with no legal target. Adding one card to the deck reshuffles every seeded deal, which broke five fixtures. Each was a seed meaning "a game like this" — TODO #84, exactly — so seeds moved and pools widened rather than assertions weakening, and the clearance fixture pins its terrain the way `enhancements.test.ts` already does. The three published replays were re-recorded. Closes TODO #40, #42a, #108, #109 and #110. 1046 fast tests and 35 sim tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUizFYCMHRWhbWwXhp7WPR
126 lines
8.7 KiB
Markdown
126 lines
8.7 KiB
Markdown
# Station Master — design index
|
||
|
||
Station Master is a tabletop railroad-operations game set in the era of timetable-and-train-order
|
||
railroading (1840–1950), being developed into a multiplayer digital game: players connect from a
|
||
web browser and an authoritative server manages lobbies and game state.
|
||
|
||
## Source of record
|
||
|
||
- `StationMasterPrototypeRules.pdf` — the original prototype rules
|
||
- `StationMaster-PrototypeTurnChart.pdf` — the 12-Stage turn chart
|
||
- `Deck cards2.xlsx` — the complete card list (115-card deck, 104 track pieces, 12 start cards)
|
||
- `Mainline Cards.pdf` — the ten Mainline card types with their speeds and entry points
|
||
- `Trains3.pdf` — all 22 train cards with names, speed class, consist and operating rules
|
||
- `tracks.png` — card art for track and industry cards
|
||
|
||
These stay as-is. Everything below is derived from them.
|
||
|
||
> **The last four arrived after the engine was built** and expand the game roughly threefold. See
|
||
> [`rules/implications.md`](rules/implications.md): much of what is recorded below as decided was a
|
||
> placeholder for exactly this material, and the balance measurements in Gap 12 were taken against a
|
||
> ruleset that does not match the design.
|
||
|
||
## For players and testers
|
||
|
||
Written to be handed to somebody who is about to play, rather than to somebody building the game.
|
||
|
||
| Document | What it is |
|
||
| --- | --- |
|
||
| [`quickstart.md`](quickstart.md) | **Start here if you have never played.** The point of the game, how a Stage runs, what is on screen, how you win, a first twenty minutes, and what to report. |
|
||
| [`rules.md`](rules.md) | **The rules in full**, as the engine actually runs them, with a FAQ. |
|
||
| [`home-deck.md`](home-deck.md) | How the Home Office deck is dealt, drawn and played out. |
|
||
| [`mainline-deck.md`](mainline-deck.md) | The Mainline cards, how the deck is dealt, and what a card does to a train crossing it. |
|
||
| [`components.md`](components.md) | Rolling stock, the two yards, Crew Trays, the Fedora, the D12. |
|
||
|
||
**None of these carry a version in the filename**, and that is deliberate (Jesse, 2026-09-21): they
|
||
are kept current with every release rather than published as editions, so the name is always the
|
||
latest and each says at the top which build it describes. Four of them were stamped `v0.4.5` until
|
||
v0.8.0.17 — the prototype rules edition they were first written against, never the version they
|
||
described — which read as though they documented a build five minor versions old.
|
||
|
||
## Rules
|
||
|
||
| Document | What it is |
|
||
| --- | --- |
|
||
| [`rules/rules-v0.1.md`](rules/rules-v0.1.md) | Faithful markdown transcription of the PDFs. No corrections. The baseline everything diffs against. |
|
||
| [`rules/rules-v0.2.md`](rules/rules-v0.2.md) | **The working ruleset.** v0.1 with all ten gaps resolved, each change marked with its gap number. |
|
||
| [`rules/card-reference.md`](rules/card-reference.md) | **⚠ SUPERSEDED** — an invented 52-card placeholder, kept for its economy summary and its history. For what is printed on every card, read the generated tables in [`home-deck.md`](home-deck.md) and [`mainline-deck.md`](mainline-deck.md). |
|
||
| [`rules/glossary.md`](rules/glossary.md) | Every defined term, alphabetized. |
|
||
| [`rules/open-questions.md`](rules/open-questions.md) | All thirteen gaps, each with the options considered, the decision, and the rationale. |
|
||
| [`rules/implications.md`](rules/implications.md) | **Read this first.** What the four recovered design files (`Deck cards2.xlsx`, `Mainline Cards.pdf`, `Trains3.pdf`, `tracks.png`) change — and which decisions they supersede. |
|
||
|
||
## Architecture
|
||
|
||
Target is **small self-hosted** scale: a handful of concurrent games, hosted on a website or as a
|
||
StartOS service. Stack is deliberately undecided; these documents describe what any implementation
|
||
must do.
|
||
|
||
| Document | What it is |
|
||
| --- | --- |
|
||
| [`architecture/components.md`](architecture/components.md) | **The project plan.** All 20 buildable pieces — where each runs, when, MVP vs finished size — plus dependency graph and build order. |
|
||
| [`architecture/overview.md`](architecture/overview.md) | Why the server is authoritative; state/intent/event split; transport; the rules-engine boundary. |
|
||
| [`architecture/game-state.md`](architecture/game-state.md) | The entity model, and the constraints that are easy to lose. |
|
||
| [`architecture/protocol.md`](architecture/protocol.md) | Intents, events, and per-player view redaction. |
|
||
| [`architecture/lobby-and-sessions.md`](architecture/lobby-and-sessions.md) | Create/join, seating, reconnection, persistence. |
|
||
| [`architecture/deployment.md`](architecture/deployment.md) | Plain web host vs StartOS service — and the one constraint that differs. |
|
||
| [`architecture/multiplayer.md`](architecture/multiplayer.md) | **The multiplayer build plan.** The authoritative server as a layer added on top, with solitaire unchanged and server-free — plus every decision taken, listed for review. |
|
||
|
||
## Current status
|
||
|
||
**v0.8.2.** Rules formalized, card faces specified, architecture documented, and the game
|
||
playable **solitaire and multiplayer** in a browser against an authoritative server. See
|
||
[`../CHANGELOG.md`](../CHANGELOG.md) for what each version changed and [`../TODO.md`](../TODO.md) for
|
||
what is open; this section is the shape of the project, not a running tally, because a
|
||
hand-maintained tally is what drifted last time.
|
||
|
||
**What is built.** The rules engine, the developer bot, the balance harness, the replay viewer, the
|
||
playable page — and the server: lobby, game codes, seating, bots, per-seat reconnection, persistence
|
||
by replaying the intent history, and an ordered replay of other players' turns on each player's own
|
||
screen. It ships as a StartOS package. **999 fast tests and 35 simulation tests.**
|
||
|
||
**What is not.** The 22 opponent-directed cards — the Action and Space-use categories — are held out
|
||
of every dealt deck until they have an implementation, along with the two defensive cards whose only
|
||
purpose is to answer them. Real audio: everything the game plays is synthesised from oscillators.
|
||
|
||
**Balance is not where it should be, and no conclusion should be read from the revenue numbers yet.**
|
||
The rebalance pass is deliberately deferred until the rules stop moving — card counts, industry counts
|
||
and the track mix all need moving together. `TODO.md` carries the standing distortions and the
|
||
measurements behind them.
|
||
|
||
**Three rules are genuinely open**, and they are the reason the README does not claim the rules are
|
||
finished: where the Local's coach stands while its engine works (a §A.4 question, not a train-card
|
||
question); 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 gaps in the prototype rules were found and closed;
|
||
these three came after.
|
||
|
||
**The event log narrates; the intents reconstruct.** Settled in v0.4.0 after the documentation had
|
||
claimed `state = fold(events)` for months. It is not true — the phase driver mutates state and then
|
||
describes it — so the canonical record is `{ seed, history: Intent[] }` and persistence will be built
|
||
on that. [`architecture/protocol.md`](architecture/protocol.md) §3 has the reasoning;
|
||
`test/events.test.ts` pins it.
|
||
|
||
**The economy, in one line:** Local Operations actions are the main currency — one per Stage, twelve
|
||
per Day — but **inbound work bypasses them**, which is where the game's variance comes from.
|
||
|
||
**Stack: TypeScript**, chosen so the engine runs in both the server and the browser — one
|
||
implementation of the movement rules, and instant affordances without a round-trip. Node 22 runs
|
||
TypeScript natively, so there is no build step during development, which also means **erasable syntax
|
||
only**: no `enum`, no parameter properties, no namespaces.
|
||
|
||
Running alongside, and independent of all of it: **print-and-play components.**
|
||
The generated tables in [`home-deck.md`](home-deck.md) and [`mainline-deck.md`](mainline-deck.md)
|
||
carry every card face as the game actually deals it, so layout and art are the only remaining work
|
||
before a table playtest — which answers the one question
|
||
simulation cannot, whether it is fun.
|
||
|
||
Run the harness with `node src/sim/harness.ts [games] [length]`.
|
||
|
||
**Watch a game:** `node src/sim/replay.ts --seed 1234 [--length standard] [--out replay.html]`
|
||
writes a self-contained HTML file — open it in any browser and step through the game. It shows the
|
||
Division, the Office Area grid, every facility's boxes and `MEN|AT|WORK` track, plain-English
|
||
narration of each event, and a **Blocked** panel explaining why nothing is moving.
|
||
|
||
Running alongside, and independent of all of it: **print-and-play components.** `card-reference.md`
|
||
specifies every card face, so layout and art are the only remaining work before a table playtest —
|
||
which answers the one question simulation cannot, whether it is fun.
|