Files
station-master/docs/design.md
T
Jesse.MarkowitzandClaude Opus 5 b90c0413d2 v0.8.0.17 — four things the game knew and the screen did not say
All four reported from a table on Day 1 of v0.8.0.16, and all the same shape.

ABS SIGNALS COULD ONLY BE PLAYED ON ONE MAINLINE CARD, while its tooltip said
"any Mainline card". The engine was never wrong: check accepts any node whose
kind is mainline and legalActions filters by check, so all of them were legal.
The failure was the LABEL — describeIntent named i.placement and never i.node,
so every placement described itself as plain "play ABS Signals", and the action
list drops duplicate labels. All but the lowest-index node were discarded before
the menu saw them. This is the THIRD time that trap has fired and the file
documents the other two three lines apart: a turnout's two rotations, and three
Department discards. Same fix — name what distinguishes them.

The card is also called what the card face calls it. prettyKey rendered
absSignals as "Abs Signals" beside a tooltip saying ABS, an acronym no
key-splitter can recover, so the authored names now win. Three of those names
were transcribed in sentence case and were CORRECTED rather than adopted: the
repository says "Yard Office" 36 times against "Yard office" twice. A lookup
that imports its own source's typos is the drift it exists to prevent.

NOTHING ON A MAINLINE CARD SHOWED WHAT WAS STANDING ON IT. Played, ABS left no
mark and you found out by hovering — the same complaint the Heavy Grade wedge
answered, and it matters more here because ABS decides whether a second train on
that card is safe. It draws a signal mast with a lit lamp now; a signal is the
literal object and needs no room for words, which is what lets it sit clear of a
name as long as "Uncontrolled Siding" on a 152px cell. The Mainline modifiers
draw as BRK, AIR and HLP. Realignment is deliberately not among them: reduce
takes the `became` branch and changes node.card, so a realigned Trestle IS an
Uncontrolled Siding afterwards. Asserted, so the absence reads as a finding.

A FREIGHT AGENT TURN SAID A CAR MOVED WHEN NONE HAD. Three faults behind one
line. It asserted an outcome, where §6.3 requires no action and the bot declines
deliberately — unjamming a healthy box destroys a load that cost a whole action
to stock. An idle Agent was then silent, which read as a dropped turn; a new
freightAgentIdled event says so and why, reducing to nothing exactly like
switchingEnded. And the work named a coordinate rather than the industry, though
a `place` helper has existed for precisely that since the switching lines moved
to it. "Loaded a loaded boxcar INTO the green Outbound box at the Freight House",
with the direction in capitals because to-or-from was the question asked.

THE LOG AND THE ACTION MENU SPELLED THE SAME SQUARE DIFFERENTLY. view.ts wrote
(col,row) — X,Y, east/west then north/south — with a comment saying why;
narrate.ts wrote the internal storage order with no comment at all. So the menu
offered a move to "(1,-1)" and the log reported it at "(-1,1)", side by side.
Pinned by a test that renders one square through BOTH describers and compares
them to each other: a test written against either file alone would have passed.

THE DOCUMENTATION IS REACHABLE FROM A RUNNING GAME, AND ALL OF IT IS PUBLISHED.
v0.8.0.16 published the Quickstart and nothing it points at — its §8 links five
documents by relative path and every one 404'd on the package, verified against
the running container. The build publishes the full set, and the test reads the
links OUT OF the guide rather than listing them. They are linked from the This
Game card, where reference already lives, rather than the header that must not
wrap; no mode awareness is needed, because solitaire and multiplayer are the
same page on the same origin.

THE REFERENCES DROPPED THE VERSION FROM THEIR NAMES. Four described v0.8.0.16
and had since the v0.8.0.15 audit; the v0.4.5 was the prototype edition they
were first written against, kept only because 36 citations pointed at it — and
it read as documentation five minor versions stale. They are quickstart.md,
rules.md, home-deck.md, mainline-deck.md and components.md now, kept current
with each release rather than published as editions. Two errors surfaced while
checking them against this release, which is the argument for doing it:
home-deck.md filed ABS Signals under Enhancements "played into your district"
that "change what a square does" — it does neither, this release's bug written
down — and mainline-deck.md, which lists everything playable onto a Mainline
card, never mentioned it at all.

1010 fast tests and 35 sim tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUizFYCMHRWhbWwXhp7WPR
2026-09-21 05:53:52 -04:00

126 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. |
| [`rules/as-built.md`](rules/as-built.md) | **Every card, GENERATED from `src/engine/content.ts`** and checked by a test, so it cannot disagree with the game. The table of record for per-card facts. |
| [`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 [`rules/as-built.md`](rules/as-built.md), which is generated from the code. |
| [`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.0.17.** 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.**
[`rules/as-built.md`](rules/as-built.md) carries 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.