v0.7.9.3 — the reference says what only the code knows, and saves get a rule
The generated card reference now carries the half TODO #15a said was the point of generating it: every Enhancement's `live` / `dormantSolo` / `unbuilt` status — whether its printed effect actually resolves yet — and the opponent-directed Action and Space-use cards, none of which is dealt in any deck. A transcription cannot carry either fact. Card counts are removed throughout, as ruled: they move with play balance so a document printing them is stale on the next retune. Where a count matters it is a yes/no "is this dealt at all", which is a fact about the design rather than the current tuning. #88 closes with it — it asked whether `card-reference.md`'s industry rows were stale, deliberately without rewriting them since Laborer counts are a balance decision. They are; nothing in the engine changed; that file is simply no longer where anyone looks. The balance question it guarded is #70. Save compatibility becomes a general rule in `README.md` § Design notes rather than a fact restated per version: a save is a list of moves and reopens by being re-played through the CURRENT rules, so any change that makes a once-legal move illegal stops an older one there — a deck change being the likeliest breaker. It fails safe every time. #40 generalised, #32's version-specific note dropped, #52 carries the ruling that versioned replays are a post-1.0 question. #94 opened: a Red Flag set out at an Office's Limits holds the next train from that side and is drawn nowhere. `DivisionView`'s office node has no `redFlag` field and `board-svg.ts` never mentions one, so after the single log line announcing it there is nothing on screen. Same class as Gitea#21 and #22, and already on the common board's step 1 list. No engine change. 897 tests pass, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3Qk7uresKCHksdZajXCLg
This commit is contained in:
co-authored by
Claude Opus 5
parent
819996faa2
commit
e734481d65
@@ -19,6 +19,37 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.7.9.3 — 2026-09-07
|
||||
|
||||
Documentation and the build script behind it. No engine change; 897 tests pass, unchanged.
|
||||
|
||||
### The generated reference covers what only the implementation knows
|
||||
|
||||
TODO #15a asked for this in 2026-08-22 and specified more than a table of card faces: every
|
||||
Enhancement carries a `live` / `dormantSolo` / `unbuilt` status in `content.ts` saying **whether its
|
||||
printed effect actually resolves yet**, and the opponent-directed Action and Space-use cards are held
|
||||
out of every deck until the attacks are implemented. A transcription cannot carry either fact. Both
|
||||
are in the generated page, which is the argument for generating it.
|
||||
|
||||
**No card counts appear, as ruled** — the counts move with play balance, so a document that prints
|
||||
them is stale on the next retune. Where a count matters it is rendered as a yes/no "is this dealt at
|
||||
all", which is a fact about the design rather than about the current tuning. #88 closes with it: it
|
||||
asked whether `card-reference.md`'s industry rows were stale, deliberately without rewriting them
|
||||
since Laborer counts are a balance decision. They are stale, nothing in the engine changed, and that
|
||||
file is simply no longer where anyone looks. The balance question it was guarding is #70.
|
||||
|
||||
### Save compatibility is a rule now, not a per-version note
|
||||
|
||||
`README.md` § Design notes carries it: a save is a list of moves and reopens by being re-played
|
||||
through the *current* rules, so any change that makes a once-legal move illegal stops an older one
|
||||
there — **a deck change being the likeliest breaker**, since a history naming a card the deck no
|
||||
longer deals has no legal answer at all. It fails safe every time. #40 is generalised to match and
|
||||
#32 closed; per-version compatibility facts are no longer tracked (Jesse, 2026-09-07). Versioned,
|
||||
migratable replays are a post-1.0 question, deliberately deferred — every migration would be written
|
||||
against rules that change again next release.
|
||||
|
||||
---
|
||||
|
||||
## 0.7.9.2 — 2026-09-07
|
||||
|
||||
Two multiplayer information leaks, and the documentation problem that let a wrong table sit in
|
||||
|
||||
@@ -131,6 +131,18 @@ is the thing this machinery exists to prevent.
|
||||
and they do not reconstruct the position. The phase driver mutates state and then describes it, so
|
||||
roughly a third of the event types are never reduced at all. Anything that needs to rebuild a game
|
||||
replays the intents.
|
||||
- **A save is only guaranteed to replay on the version that wrote it.** This is the cost of the
|
||||
property above and is not a bug to be fixed case by case: a save is a list of moves, so it reopens
|
||||
by being *re-played through the current rules*. Any rules change that makes a once-legal move
|
||||
illegal will stop an older save at that move — **a change to the deck is the likeliest breaker**,
|
||||
since a history that names a card the deck no longer deals has no legal answer at all, but any
|
||||
narrowing of what is permitted does it. It fails safe in every case: the load is declined, the
|
||||
offending move is named, and the file is left untouched, so nothing a player has is destroyed.
|
||||
Assume an older save may not open, tell players so wherever a build is announced, and read "this
|
||||
save will not load" in a bug report as this before treating it as a fault. **Versioned, migratable
|
||||
replays are a post-1.0 question** — deliberately not worth the effort while the rules are still
|
||||
moving this fast, since every migration would have to be written against rules that changed again
|
||||
next release.
|
||||
- **Never call `Math.random()`.** One ambient random call silently breaks replay.
|
||||
- **The Mainline Phase can stop and ask, and there are three questions it asks.** §8.1's clearance
|
||||
ruling goes to the Superintendent; the Yard Office offer and the Red Flag prompt go to the owner of
|
||||
|
||||
@@ -77,7 +77,7 @@ Not items. Things that are true of every change, and that have gone wrong when s
|
||||
1. **Play it at a table** — #39 #35 #42a #40
|
||||
2. **The common board, and watching play happen — Gitea#20** — #13 #15 #18 #91 #75
|
||||
3. **Multiplayer, sessions and operations** — #8 #7 #76 #77 #79
|
||||
4. **The screen** — #44 #81 #33 #36
|
||||
4. **The screen** — #94 #44 #81 #33 #36
|
||||
5. **Replays and saved games** — #14 #47 #48 #49 #50 #51 #52
|
||||
6. **Rules** — #12 #80 #82 #83 #85
|
||||
7. **Play balance** — #61 #62 #63 #64 #67 #68 #69 #70 #71 #72 #73 #66 #65 #74
|
||||
@@ -119,10 +119,10 @@ specific paths below have not been exercised at a table. **More testing is plann
|
||||
the dealt game matches what was chosen. It took three attempts to become reachable at all —
|
||||
reachable is not the same as correct. See **Reference · #42a**.
|
||||
|
||||
- [ ] **#40** — **A save from before v0.7.4 may not replay, and nobody has been told.** It fails safe
|
||||
— the server declines the save, names the move and leaves the file untouched — so this is a line
|
||||
in a release note rather than code, and worth knowing when a bug report arrives with a save that
|
||||
will not load. See **Reference · #40**.
|
||||
- [ ] **#40** — **An older save may not replay, and players are not told so anywhere they will see
|
||||
it.** Not a v0.7.4 fact and not a bug: a save is re-played through the current rules, so any
|
||||
narrowing of what is legal can stop one. The rule is written down now (`README.md` § Design
|
||||
notes); what is still owed is a line **wherever a build is announced**. See **Reference · #40**.
|
||||
|
||||
---
|
||||
|
||||
@@ -195,6 +195,12 @@ happening, and the turn structure that is still solitaire-shaped.
|
||||
What is drawn and where, for a player at the board. The three items settled on 2026-08-30 shipped in
|
||||
v0.7.9; what is left is the history panel and the end-of-game statistics.
|
||||
|
||||
- [ ] **#94** — **A Red Flag standing at an Office's Limits is drawn nowhere.** It is set out on the
|
||||
board, it holds the next train from that side, and after the one log line announcing it there is
|
||||
nothing on screen saying it is there. Confirmed in code rather than inferred. **The same class
|
||||
as Gitea#21 and #22 — the engine is right and the screen is silent — and it is already on
|
||||
Gitea#20 step 1's list, so doing it now is 0.8.0 groundwork.** See **Reference · #94**.
|
||||
|
||||
- [ ] **#44** — How much history the panel holds should be configurable. The cap is `slice(-60)` with
|
||||
no recorded reason anywhere. **Where the setting lives is an open question and the item exists
|
||||
to ask it** — a StartOS action, per game, or per browser. The replay viewer already answers the
|
||||
@@ -455,12 +461,26 @@ below); reachable is not the same as correct. Early item for the next play sessi
|
||||
|
||||
#### #40 — A save from before v0.7.4 may not replay, and nobody has been told.
|
||||
|
||||
**A save from before v0.7.4 may not replay, and nobody has been told.** 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 — the server declines the save, names
|
||||
the move and leaves the file untouched — and `WHISTLE-4086` did survive on `phoenix.local`, so it
|
||||
is "may not" rather than "will not". Worth a line wherever the build is announced, and worth
|
||||
knowing when a bug report arrives with a save that will not load.
|
||||
**An older save may not replay, and players are not told so anywhere they will see it.** A save is
|
||||
a list of moves and reopens by being re-played through the CURRENT rules, so any change that makes a
|
||||
once-legal move illegal stops it there. **A deck change is the likeliest breaker** — a history
|
||||
naming a card the deck no longer deals has no legal answer at all — but any narrowing does it. This
|
||||
is the design working, not a fault: it fails safe every time, declining the load, naming the move
|
||||
and leaving the file untouched.
|
||||
|
||||
**So this item is no longer "saves before v0.7.4"** and should never be restated per version
|
||||
(Jesse, 2026-09-07). The specific 0.7.4 breakages — the Red Flags intent changing shape, a make-up
|
||||
that was legal being refused, a Yard Office arrival asking a question no older history answers —
|
||||
are recorded here as the ORIGIN of the rule rather than as the rule, and `WHISTLE-4086` did survive
|
||||
on `phoenix.local`, which is why it is "may not" rather than "will not".
|
||||
|
||||
**What is actually owed:** the general rule is written down in `README.md` § Design notes, and a
|
||||
line belongs wherever a build is announced. Nothing in code.
|
||||
|
||||
**Versioned, migratable replays are a post-1.0 question, deliberately deferred** (Jesse,
|
||||
2026-09-07): "once we get to a solid 1.0 release we will consider a system to version the replays so
|
||||
they are not as fragile — but not worth any effort right now." The reason it would be wasted effort
|
||||
now is that every migration would be written against rules that change again next release.
|
||||
|
||||
### The common board, and watching play happen — Gitea#20
|
||||
|
||||
@@ -782,6 +802,39 @@ and whether they took it the moment their turn arrived.
|
||||
|
||||
### The screen
|
||||
|
||||
#### #94 — A RED FLAG STANDING AT THE LIMITS IS DRAWN NOWHERE.
|
||||
|
||||
**A RED FLAG STANDING AT AN OFFICE'S LIMITS IS DRAWN NOWHERE.** Found 2026-09-07 while checking
|
||||
|
||||
which of the Gitea#20 step 1 findings were still real. It is real, and it is a play bug now rather
|
||||
than a common-board one.
|
||||
|
||||
**What the engine does**, traced rather than assumed. `maneuver.redFlags` emits `redFlagsSet`, whose
|
||||
reducer sets `node.redFlag = side` on that Office's Division node (`apply.ts:2307`). From then on
|
||||
`redFlagStop` holds the next train arriving from that side — `dest.redFlag === from` → `spendFlag`,
|
||||
which removes the flag and emits `redFlagSpent` (`advance.ts:1216, 1173`). So it is a standing token
|
||||
on the board that stops a train, exactly as a flag on the table would be.
|
||||
|
||||
**What the screen does.** `narrate` announces it once — "RED FLAGS set out on the Eastern Limits" —
|
||||
and then it is gone with the scroll. The Office node in `DivisionView` carries `kind`, `label`,
|
||||
`trains`, `capacity`, `modifiers`, `gradeUp`, `seat`, `running` and `switching`, and **no `redFlag`
|
||||
field at all**; `redFlag` appears nowhere in `board-svg.ts` and nowhere in the web layer. The map
|
||||
draws the Office, its A/D tracks and the trains standing on it, and not the flag at its Limits.
|
||||
|
||||
**So a player who set a flag out three Stages ago has nothing telling them it is still there, and an
|
||||
opponent who missed the line never knew.** Then a train stops short, and the only explanation is a
|
||||
log entry that has scrolled away. That is the shape of Gitea#21 — a correct refusal with no visible
|
||||
reason — and of Gitea#22 — a board that does not show what the rules are acting on.
|
||||
|
||||
**Why it belongs before 0.8.0 rather than in it:** the plan already lists "add the Red Flag holder to
|
||||
the public player projection — it is public game state but is currently absent from `Frame`" as part
|
||||
of step 1. The field has to exist on the projection either way, so every hour spent on it is 0.8.0
|
||||
groundwork rather than a detour. **The drawing is the open question, not the data** — a flag at the
|
||||
Limits column is the obvious rendering, and `board-svg.ts` already draws those edges (`edge()`), so
|
||||
there is somewhere to hang it.
|
||||
|
||||
**Not fixed. Sized: small on the data, a judgement call on the picture.**
|
||||
|
||||
#### #44 — The history panel's 60-line cap is hard-coded, and how much history it…
|
||||
|
||||
**The history panel's 60-line cap is hard-coded, and how much history it holds should be
|
||||
@@ -995,6 +1048,17 @@ bitten once**: both published replays were dead — one got 42 intents into 360,
|
||||
carry a ruleset stamp and the page should say "this replay was recorded under an older
|
||||
ruleset and stops at Stage N" rather than presenting a truncated game as a whole one.
|
||||
|
||||
**THE FULL FIX IS DEFERRED PAST 1.0, ON JESSE'S CALL 2026-09-07:** "once we get to a solid 1.0
|
||||
release we will consider a system to version the replays so they are not as fragile — but not worth
|
||||
any effort right now." The reason is sound and worth keeping: every migration would be written
|
||||
against rules that change again the next release, so the work would be redone rather than reused.
|
||||
|
||||
**What that ruling does NOT defer is the honesty.** A truncated replay presented as a whole game is
|
||||
a wrong answer, not a missing feature, and saying "this stops at Stage N" needs no version stamp at
|
||||
all — `fromSave` already knows how many intents it replayed of how many it was given
|
||||
(`session.ts:454` reports exactly that for multiplayer). That half is small and stands alone. The
|
||||
general rule now lives in `README.md` § Design notes; #40 is the "tell the players" half.
|
||||
|
||||
### Rules
|
||||
|
||||
#### #12 — PARTLY REPRODUCED: "when I back up to collect standing cars and, furth…
|
||||
@@ -1685,9 +1749,20 @@ counts move with play balance, so a document that prints them is stale on the ne
|
||||
same rule was applied to `content.ts`'s own comments on 2026-08-22 — see the pass recorded in
|
||||
CHANGELOG for what came out and what was kept.
|
||||
|
||||
Not started. Worth deciding first whether this is a build step writing Markdown into `docs/`,
|
||||
or a page on the site beside the replay viewer — the site can render it from the same view-model
|
||||
the game uses, which argues for the second.
|
||||
**BUILT 2026-09-07 in v0.7.9.2, completed in v0.7.9.3, as the first of the two options** — a build step writing Markdown,
|
||||
`scripts/build-card-reference.ts` → `docs/rules/as-built.md`, with `test/card-reference.test.ts`
|
||||
re-running the generator and failing when the checked-in file disagrees. All six sections are
|
||||
there, and so is the honesty column: every Enhancement carries its `live` / `dormantSolo` /
|
||||
`unbuilt` status, and the opponent-directed cards say plainly that none of them is dealt.
|
||||
**The counts are omitted as ruled** — where a count matters it is rendered as a yes/no "is this
|
||||
dealt at all", which is a fact about the design rather than about the current tuning.
|
||||
|
||||
**WHAT REMAINS, and it is the second option rather than a gap in the first:** a page on the site,
|
||||
beside the replay viewer, rendered from the same view-model the game uses. The argument for it is
|
||||
unchanged — a player cannot read a file in the repo — and it is now cheap, because the projection
|
||||
work is done and only the presentation is missing. **Do it with Gitea#20 step 3**, which builds a
|
||||
renderer for exactly this kind of read-only public page; building a second one first would be the
|
||||
waste.
|
||||
|
||||
#### #86 — Real audio, as committed assets.
|
||||
|
||||
@@ -1719,37 +1794,29 @@ placeholder, not the finished sound. Sound therefore defaults to OFF.
|
||||
- Keep the synthesised versions as the fallback for anything not sourced, so a missing file is
|
||||
a quieter game rather than a broken one.
|
||||
|
||||
#### #88 — card-reference.md's industry table may still be stale beyond Grocer's…
|
||||
#### #88 — CLOSED — card-reference.md's industry table is no longer anybody's source.
|
||||
|
||||
**`card-reference.md`'s industry table may still be stale beyond Grocer's Warehouse, the Oil
|
||||
**CLOSED 2026-09-07 in v0.7.9.3, by removing the question rather than answering it.** This asked
|
||||
|
||||
Refinery and Freight House (corrected v0.5.0) — Mine Tipple, Produce Shed and Power Plant were
|
||||
NOT re-verified.** The v0.5.0 pass corrected three rows (and the "Freight House is not a card"
|
||||
claim across `card-reference.md`, `glossary.md`, `rules-v0.2.md` and `open-questions.md`) on
|
||||
Jesse's explicit call. Checking `content.ts` while making that change turned up that
|
||||
`mineTipple` and `powerPlant` are ALSO base 1 out/in + 1 Laborer in the engine — the same
|
||||
uniform model as the three that were corrected — while `card-reference.md` still prints Mine
|
||||
Tipple 3/3/4 and Power Plant 3/3/4, and the "Throughput — why these Laborer counts" section
|
||||
right below the table is built entirely on those higher numbers. Flagged inline in
|
||||
`card-reference.md` rather than silently rewritten — this needs the same kind of decision Jesse
|
||||
made for the other three, not an assumption that the same correction applies, since raising or
|
||||
lowering Laborer counts is also a balance question, not only a docs one.
|
||||
whether `card-reference.md`'s Mine Tipple, Produce Shed and Power Plant rows were stale — it prints
|
||||
3/3/4 for two of them while `content.ts` has every industry at base 1 out / 1 in / 1 Laborer — and
|
||||
deliberately did NOT rewrite them, on the grounds that changing a Laborer count is a balance
|
||||
decision rather than a documentation one. **That reasoning still stands and nothing was changed in
|
||||
the engine.**
|
||||
|
||||
**v0.4.9e narrows it.** The Direction column for the Grocer's Warehouse and the Oil Refinery was
|
||||
the wrong half of that v0.5.0 pass and has been put back to one-way each, from gameplay testing
|
||||
and Jesse's confirmation. The *numbers* in those two rows are still the card reference's own
|
||||
(1 Laborer, 3–4 track) and still unverified against the engine, so this entry stands as written
|
||||
for all five industries — what changed is only that the two rows the v0.5.0 pass claimed to have
|
||||
re-verified turn out to have been re-verified against a premise rather than against a card.
|
||||
|
||||
---
|
||||
What changed is that `card-reference.md` is no longer where anyone looks. `docs/rules/as-built.md`
|
||||
is generated from `content.ts` and carries the industry table the game actually runs; the old file
|
||||
keeps its SUPERSEDED banner, now pointing forward, and its numbers are read as what the v0.4.5
|
||||
placeholder said. **The balance question the entry was really guarding is #70** (the rolling stock
|
||||
supply and the uniform 1/1/1 industry model, both marked provisional in `content.ts`) — that is
|
||||
where it belongs, and it is still open.
|
||||
|
||||
## Done
|
||||
|
||||
Closed items, kept because several are the only record of a ruling or a lesson. Newest first within
|
||||
each group.
|
||||
|
||||
### Shipped through v0.7.9.2, from the queue
|
||||
### Shipped through v0.7.9.3, from the queue
|
||||
|
||||
Closed items, newest first. Kept because several of them are the only record of a ruling or a lesson;
|
||||
the numbers stay so cross-references above and below still resolve.
|
||||
@@ -1757,12 +1824,11 @@ the numbers stay so cross-references above and below still resolve.
|
||||
32. ~~**Tell the 0.4.9 playtesters their saves are dead, before they find out.**~~ — done
|
||||
2026-09-07. `PLAYTEST-0.7.4.md` was written for exactly this and did its job; Jesse, 2026-09-07:
|
||||
"a temporary document to help some of the playtesters out on making the big jump, but that is no
|
||||
longer needed." The jump is made, so the note is retired rather than committed. **The durable
|
||||
fact, which is why this entry stays:** a save written by v0.4.9h does not replay on 0.7.x — the
|
||||
deck changed, so it stops at its first `card.play` — and that includes the saves attached to
|
||||
Gitea#15 and #17. It fails safe, naming the move and leaving the file untouched, so a bug report
|
||||
arriving with a save that will not load is this and not a new fault. #40 is the same shape on the
|
||||
main line and is still open.
|
||||
longer needed." The jump is made, so the note is retired rather than committed. **No per-version
|
||||
save-compatibility fact is tracked from here**, on Jesse's call 2026-09-07: a save replays
|
||||
through the current rules, so an older one breaking is the design working rather than an event
|
||||
to log each time. The general rule lives in `README.md` § Design notes; #40 is the same shape and
|
||||
has been generalised to match.
|
||||
|
||||
92. ~~**Two multiplayer information leaks in the shared narration log.**~~ — done 2026-09-07 in
|
||||
v0.7.9.2, found while planning Gitea#20 step 1. The **seed** was announced in the opening line of
|
||||
|
||||
+127
-44
@@ -14,6 +14,11 @@ reasoning; read this for the numbers.
|
||||
The engine instantiates from the same constants this is emitted from, so a disagreement between
|
||||
this page and the game is a bug in the generator, not a stale table.
|
||||
|
||||
**No card counts appear here, deliberately** (TODO #15a, Jesse 2026-08-22): the counts move with
|
||||
play balance, so a document that prints them is answering a question that will have a different
|
||||
answer next retune. Where a count matters it is a yes/no — whether the deck deals the card at
|
||||
all — which is a fact about the design rather than about the current tuning.
|
||||
|
||||
---
|
||||
|
||||
## Trains
|
||||
@@ -97,12 +102,12 @@ Upgrades are strictly sequential — no skipping a tier — so a Terminal needs
|
||||
order. Green slots are outbound passengers, red are inbound, and the design gives slots **equal**
|
||||
to Porters rather than one more.
|
||||
|
||||
| Office | Control point | Passenger facility | A/D tracks | Porters | Green | Red | In deck |
|
||||
| --- | :---: | :---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Whistle Post | — | — | 1 | 0 | 0 | 0 | — |
|
||||
| Depot | yes | yes | 2 | 1 | 1 | 1 | 4 |
|
||||
| Station | yes | yes | 3 | 2 | 2 | 2 | 2 |
|
||||
| Terminal | yes | yes | 4 | 3 | 3 | 3 | 1 |
|
||||
| Office | Control point | Passenger facility | A/D tracks | Porters | Green | Red |
|
||||
| --- | :---: | :---: | ---: | ---: | ---: | ---: |
|
||||
| Whistle Post | — | — | 1 | 0 | 0 | 0 |
|
||||
| Depot | yes | yes | 2 | 1 | 1 | 1 |
|
||||
| Station | yes | yes | 3 | 2 | 2 | 2 |
|
||||
| Terminal | yes | yes | 4 | 3 | 3 | 3 |
|
||||
|
||||
Whistle Posts are a fixed supply of 4 outside the deck, and Limits signs a
|
||||
supply of 8.
|
||||
@@ -117,14 +122,14 @@ build one end of a chain or the other, never both, which is what forces traffic
|
||||
districts rather than in circles inside one. No two of the same industry may share an Office Area,
|
||||
and that rule is enforced for every kind rather than repeated in each row.
|
||||
|
||||
| Industry | Cars | Flow | Green | Red | Laborers | Locked out with | Copies |
|
||||
| --- | --- | --- | ---: | ---: | ---: | --- | ---: |
|
||||
| Freight House | boxcar | both | 1 | 1 | 1 | Grocer's Warehouse | 2 |
|
||||
| Mine Tipple | hopper | outbound | 1 | 0 | 1 | Power Plant | 2 |
|
||||
| Refinery | tank | outbound | 1 | 0 | 1 | Power Plant | 1 |
|
||||
| Power Plant | hopper, tank | inbound | 0 | 1 | 1 | Mine Tipple, Refinery | 2 |
|
||||
| Packing Sheds | reefer | outbound | 1 | 0 | 1 | Grocer's Warehouse | 1 |
|
||||
| Grocer's Warehouse | boxcar, reefer | inbound | 0 | 1 | 1 | Packing Sheds, Freight House | 1 |
|
||||
| Industry | Cars | Flow | Green | Red | Laborers | Locked out with |
|
||||
| --- | --- | --- | ---: | ---: | ---: | --- |
|
||||
| Freight House | boxcar | both | 1 | 1 | 1 | Grocer's Warehouse |
|
||||
| Mine Tipple | hopper | outbound | 1 | 0 | 1 | Power Plant |
|
||||
| Refinery | tank | outbound | 1 | 0 | 1 | Power Plant |
|
||||
| Power Plant | hopper, tank | inbound | 0 | 1 | 1 | Mine Tipple, Refinery |
|
||||
| Packing Sheds | reefer | outbound | 1 | 0 | 1 | Grocer's Warehouse |
|
||||
| Grocer's Warehouse | boxcar, reefer | inbound | 0 | 1 | 1 | Packing Sheds, Freight House |
|
||||
|
||||
---
|
||||
|
||||
@@ -134,25 +139,25 @@ Each sits beside a host and raises one of its capacities. `office` as a host mea
|
||||
Facility — so a modifier that adds a green slot to an Office adds nothing to a Whistle Post, which
|
||||
is not one.
|
||||
|
||||
| Modifier | Hosts | +Green | +Red | +Laborers | +Porters | Copies |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| Waiting area | any Passenger Facility | 1 | — | — | 1 | 3 |
|
||||
| Restaurant | any Passenger Facility | 1 | — | — | 1 | 2 |
|
||||
| Hotel | any Passenger Facility | 1 | — | — | 1 | 1 |
|
||||
| Truck dock | Freight House, Packing Sheds, Grocer's Warehouse | — | 1 | — | — | 2 |
|
||||
| Railroad Express Agency | Freight House | 1 | — | 1 | — | 1 |
|
||||
| Forklifts | Freight House, Packing Sheds | 1 | — | 1 | — | 2 |
|
||||
| Prep Plant | Mine Tipple | 1 | — | 1 | — | 1 |
|
||||
| Coal Piles | Mine Tipple | 1 | — | 1 | — | 1 |
|
||||
| Conveyor Belts | Mine Tipple | 1 | — | 1 | — | 1 |
|
||||
| Pipelines | Refinery | 1 | — | 1 | — | 1 |
|
||||
| Oil Depot | Refinery | 1 | — | 1 | — | 1 |
|
||||
| Viscosity breakers | Refinery | 1 | — | 1 | — | 1 |
|
||||
| Transmission lines | Power Plant | — | — | 1 | — | 1 |
|
||||
| Rotary Dumps | Power Plant | — | — | 1 | — | 1 |
|
||||
| Steam Turbines | Power Plant | — | — | 1 | — | 1 |
|
||||
| Ice House | Packing Sheds, Grocer's Warehouse | 1 | — | 1 | — | 2 |
|
||||
| Local small groceries | Grocer's Warehouse | — | — | 1 | — | 1 |
|
||||
| Modifier | Hosts | +Green | +Red | +Laborers | +Porters |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| Waiting area | any Passenger Facility | 1 | — | — | 1 |
|
||||
| Restaurant | any Passenger Facility | 1 | — | — | 1 |
|
||||
| Hotel | any Passenger Facility | 1 | — | — | 1 |
|
||||
| Truck dock | Freight House, Packing Sheds, Grocer's Warehouse | — | 1 | — | — |
|
||||
| Railroad Express Agency | Freight House | 1 | — | 1 | — |
|
||||
| Forklifts | Freight House, Packing Sheds | 1 | — | 1 | — |
|
||||
| Prep Plant | Mine Tipple | 1 | — | 1 | — |
|
||||
| Coal Piles | Mine Tipple | 1 | — | 1 | — |
|
||||
| Conveyor Belts | Mine Tipple | 1 | — | 1 | — |
|
||||
| Pipelines | Refinery | 1 | — | 1 | — |
|
||||
| Oil Depot | Refinery | 1 | — | 1 | — |
|
||||
| Viscosity breakers | Refinery | 1 | — | 1 | — |
|
||||
| Transmission lines | Power Plant | — | — | 1 | — |
|
||||
| Rotary Dumps | Power Plant | — | — | 1 | — |
|
||||
| Steam Turbines | Power Plant | — | — | 1 | — |
|
||||
| Ice House | Packing Sheds, Grocer's Warehouse | 1 | — | 1 | — |
|
||||
| Local small groceries | Grocer's Warehouse | — | — | 1 | — |
|
||||
|
||||
---
|
||||
|
||||
@@ -162,16 +167,94 @@ Track is **in the Home Office deck** and is drawn and played like any other card
|
||||
per-player supply. Operational Rail is the flag that says a train may stop on the card; a turnout
|
||||
may be run through but not stopped on.
|
||||
|
||||
| Track | Geometry | Hand | Operational rail | Move cost | In deck |
|
||||
| --- | --- | --- | :---: | ---: | ---: |
|
||||
| Straight track | straight | none | yes | 1 | 16 |
|
||||
| Curved track (right) | curved | right | yes | 1 | 8 |
|
||||
| Curved track (left) | curved | left | yes | 1 | 8 |
|
||||
| Sharp Curved Track (right) | sharpCurved | right | yes | 2 | — |
|
||||
| Sharp Curved Track (left) | sharpCurved | left | yes | 2 | — |
|
||||
| Turnout (right) | turnout | right | — | 1 | 8 |
|
||||
| Turnout (left) | turnout | left | — | 1 | 8 |
|
||||
| Track | Geometry | Hand | Operational rail | Move cost | Dealt |
|
||||
| --- | --- | --- | :---: | ---: | :---: |
|
||||
| Straight track | straight | none | yes | 1 | yes |
|
||||
| Curved track (right) | curved | right | yes | 1 | yes |
|
||||
| Curved track (left) | curved | left | yes | 1 | yes |
|
||||
| Sharp Curved Track (right) | sharpCurved | right | yes | 2 | no |
|
||||
| Sharp Curved Track (left) | sharpCurved | left | yes | 2 | no |
|
||||
| Turnout (right) | turnout | right | — | 1 | yes |
|
||||
| Turnout (left) | turnout | left | — | 1 | yes |
|
||||
|
||||
48 track cards are dealt in total. Rows showing no copies are shapes the engine
|
||||
understands but the deck does not currently print.
|
||||
A row marked "no" is a shape the engine understands but the deck does not currently print.
|
||||
|
||||
---
|
||||
|
||||
## Enhancements
|
||||
|
||||
The column that only the implementation can fill in: **whether the printed effect actually
|
||||
resolves yet.** `live` is read during play; `dormantSolo` is implemented at the point of attack
|
||||
but the attack is an opponent-directed card held out of every deck, so nothing reaches it in a
|
||||
solitaire game; `unbuilt` means the effect is recorded and nothing reads it. A transcription
|
||||
cannot carry this column, which is the argument for generating the page rather than writing it.
|
||||
|
||||
| Enhancement | Placement | Requires | Effect resolves |
|
||||
| --- | --- | --- | :---: |
|
||||
| Interlocking | runningTrackStraight | — | **live** |
|
||||
| Facing Point Locks | onCard | interlocking in the district | **dormantSolo** |
|
||||
| Yard office | secondaryTrackStraight | — | **live** |
|
||||
| Small yard | secondaryTrackStraight | — | **live** |
|
||||
| Water column | runningTrackStraight | — | **dormantSolo** |
|
||||
| Overpass | onCard | — | **unbuilt** |
|
||||
| Telegraph | runningTrackStraight | — | **live** |
|
||||
| Telephone | onCard | telegraph on the same card | **live** |
|
||||
| Radio | onCard | telephone on the same card | **live** |
|
||||
| ABS Signals | mainlineCard | — | **live** |
|
||||
|
||||
---
|
||||
|
||||
## Opponent-directed cards, and what answers them
|
||||
|
||||
**None of these is dealt in any deck today.** A card that can only be played at another player
|
||||
has no legal target in a solitaire game, and a defence with nothing to defend against is as dead
|
||||
a draw as the attack — so both halves are held out until the attacks are implemented. They are
|
||||
listed because they are the design, and because what a defence answers is the only record of why
|
||||
it exists.
|
||||
|
||||
### Action cards — opponent-directed
|
||||
|
||||
| Card | Played on | Effect | Answers |
|
||||
| --- | --- | --- | --- |
|
||||
| Derail | a moving train in the Local Phase | That train must stop for the remainder of the turn. | — |
|
||||
| Broken coupler | a moving train in the Mainline Phase | That train must stop and not move. | — |
|
||||
| Railroad crossing | any Secondary Track Straight | May not be used as a stop point for switching. May not become an Industry. | — |
|
||||
| Per Diem inventory | another player | Lose one point per 2 empty cars on Secondary Tracks. | — |
|
||||
| Demurrage charge | another player | Lose one point per 2 loaded freight cars on Secondary Tracks. | — |
|
||||
| Customer complaints | another player | Lose one point per 2 coaches in loading boxes. | — |
|
||||
| Vandalism | another player | A train passing a Hobo Jungle has a boxcar looted (converted to empty). | — |
|
||||
| Hotbox | another player | A train just arrived must set one car (chooser’s pick) onto Secondary Track until it departs. | — |
|
||||
| Outlawed | another player | A train just arrived may not depart for one turn — the crew’s hours have expired. | — |
|
||||
|
||||
### Space-use cards — opponent-directed
|
||||
|
||||
| Card | Played on | Effect | Answers |
|
||||
| --- | --- | --- | --- |
|
||||
| Bean house | adjacent to any straight, curve, turnout, Limit | Burns tablespace. | — |
|
||||
| Flop house | adjacent to any straight, curve, turnout | Burns tablespace. | — |
|
||||
| Watertower | adjacent to any straight, turnout on Running Track | Burns tablespace. | — |
|
||||
| Hobo Jungle | adjacent to any straight, turnout, Limit on Running Track | Burns tablespace. Vandalism can loot a boxcar passing it. | — |
|
||||
| Section House | adjacent to any straight, curve, turnout | Burns tablespace. | — |
|
||||
| City blocks | adjacent to any straight, curve, turnout, Limit | Burns tablespace. | — |
|
||||
| Engine Shops | adjacent to any straight, curve, turnout | Burns tablespace. | — |
|
||||
| Tenderloin District | adjacent to any straight, curve, turnout, Limit | Burns tablespace. | — |
|
||||
| Engineer cemetery | adjacent to any straight, curve, turnout, Limit | Burns tablespace. | — |
|
||||
|
||||
### Maneuver cards
|
||||
|
||||
| Card | Played on | Effect | Answers |
|
||||
| --- | --- | --- | --- |
|
||||
| Red Flags | any time | A stopped train is prevented from being hit; the approaching train is prevented from moving. | — |
|
||||
| Flying Switch | any time | Break a cut of cars away from behind the engine and roll them into an industry. | — |
|
||||
| Poling | any time | TBD in the source. | — |
|
||||
|
||||
Mainline modifier cards, for completeness — these ARE dealt:
|
||||
|
||||
| Card | Played on | Effect | Answers |
|
||||
| --- | --- | --- | --- |
|
||||
| Brakeman | a GRADE Mainline card | Faster passage downhill. | — |
|
||||
| Airbrakes | a GRADE Mainline card | Faster passage downhill. Brakeman must be in effect. | — |
|
||||
| Helpers | a GRADE Mainline card | Faster passage uphill. | — |
|
||||
| Realignment | a Mainline card | Convert one Mainline type to another. Not while a train is on it. | — |
|
||||
| Facing Point Locks | adjacent to Interlocking | Prevents Derail being played on you. | Derail |
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "station-master",
|
||||
"version": "0.7.9.2",
|
||||
"version": "0.7.9.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Station Master — a railroad operations game",
|
||||
|
||||
@@ -30,11 +30,12 @@ import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
ALL_TRAINS, EXTRA_TRAINS, INDUSTRY_PROFILES, LIMITS_SUPPLY, MAINLINE_DECK, MAINLINE_PROFILES,
|
||||
MODIFIER_PROFILES, OFFICE_PROFILES, TIMETABLED_TRAINS, TRACK_CARDS, TRACK_IN_DECK,
|
||||
WHISTLE_POST_SUPPLY, consistSize, mainlineDescription,
|
||||
ACTION_CARDS, ALL_TRAINS, ENHANCEMENT_CARDS, ENHANCEMENT_RULES, EXTRA_TRAINS, INDUSTRY_PROFILES,
|
||||
LIMITS_SUPPLY, MAINLINE_DECK, MAINLINE_MODIFIER_CARDS, MAINLINE_PROFILES, MANEUVER_CARDS,
|
||||
MODIFIER_PROFILES, OFFICE_PROFILES, SPACE_USE_CARDS, TIMETABLED_TRAINS, TRACK_CARDS,
|
||||
WHISTLE_POST_SUPPLY, consistSize, isOpponentOnly, mainlineDescription,
|
||||
} from '../src/engine/content.ts';
|
||||
import type { ConsistSpec, TrainProfile, TrainRules } from '../src/engine/content.ts';
|
||||
import type { ConsistSpec, SimpleCard, TrainProfile, TrainRules } from '../src/engine/content.ts';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
@@ -88,6 +89,11 @@ w();
|
||||
w('The engine instantiates from the same constants this is emitted from, so a disagreement between');
|
||||
w('this page and the game is a bug in the generator, not a stale table.');
|
||||
w();
|
||||
w('**No card counts appear here, deliberately** (TODO #15a, Jesse 2026-08-22): the counts move with');
|
||||
w('play balance, so a document that prints them is answering a question that will have a different');
|
||||
w('answer next retune. Where a count matters it is a yes/no — whether the deck deals the card at');
|
||||
w('all — which is a fact about the design rather than about the current tuning.');
|
||||
w();
|
||||
w('---');
|
||||
w();
|
||||
|
||||
@@ -141,11 +147,11 @@ w('Upgrades are strictly sequential — no skipping a tier — so a Terminal nee
|
||||
w('order. Green slots are outbound passengers, red are inbound, and the design gives slots **equal**');
|
||||
w('to Porters rather than one more.');
|
||||
w();
|
||||
w('| Office | Control point | Passenger facility | A/D tracks | Porters | Green | Red | In deck |');
|
||||
w('| --- | :---: | :---: | ---: | ---: | ---: | ---: | ---: |');
|
||||
w('| Office | Control point | Passenger facility | A/D tracks | Porters | Green | Red |');
|
||||
w('| --- | :---: | :---: | ---: | ---: | ---: | ---: |');
|
||||
for (const o of OFFICE_PROFILES) {
|
||||
w(`| ${o.name} | ${o.isControlPoint ? 'yes' : '—'} | ${o.isPassengerFacility ? 'yes' : '—'} | ` +
|
||||
`${o.adTracks} | ${o.porters} | ${o.passengerOut} | ${o.passengerIn} | ${o.copiesInDeck || '—'} |`);
|
||||
`${o.adTracks} | ${o.porters} | ${o.passengerOut} | ${o.passengerIn} |`);
|
||||
}
|
||||
w();
|
||||
w(`Whistle Posts are a fixed supply of ${WHISTLE_POST_SUPPLY} outside the deck, and Limits signs a`);
|
||||
@@ -162,13 +168,13 @@ w('build one end of a chain or the other, never both, which is what forces traff
|
||||
w('districts rather than in circles inside one. No two of the same industry may share an Office Area,');
|
||||
w('and that rule is enforced for every kind rather than repeated in each row.');
|
||||
w();
|
||||
w('| Industry | Cars | Flow | Green | Red | Laborers | Locked out with | Copies |');
|
||||
w('| --- | --- | --- | ---: | ---: | ---: | --- | ---: |');
|
||||
w('| Industry | Cars | Flow | Green | Red | Laborers | Locked out with |');
|
||||
w('| --- | --- | --- | ---: | ---: | ---: | --- |');
|
||||
for (const f of INDUSTRY_PROFILES) {
|
||||
const lo = f.lockouts.length
|
||||
? f.lockouts.map((k) => INDUSTRY_PROFILES.find((p) => p.kind === k)?.name ?? k).join(', ')
|
||||
: '—';
|
||||
w(`| ${f.name} | ${f.carTypes.join(', ')} | ${f.flow} | ${f.baseOut} | ${f.baseIn} | ${f.baseLoaders} | ${lo} | ${f.copies} |`);
|
||||
w(`| ${f.name} | ${f.carTypes.join(', ')} | ${f.flow} | ${f.baseOut} | ${f.baseIn} | ${f.baseLoaders} | ${lo} |`);
|
||||
}
|
||||
w();
|
||||
w('---');
|
||||
@@ -180,13 +186,13 @@ w('Each sits beside a host and raises one of its capacities. `office` as a host
|
||||
w('Facility — so a modifier that adds a green slot to an Office adds nothing to a Whistle Post, which');
|
||||
w('is not one.');
|
||||
w();
|
||||
w('| Modifier | Hosts | +Green | +Red | +Laborers | +Porters | Copies |');
|
||||
w('| --- | --- | ---: | ---: | ---: | ---: | ---: |');
|
||||
w('| Modifier | Hosts | +Green | +Red | +Laborers | +Porters |');
|
||||
w('| --- | --- | ---: | ---: | ---: | ---: |');
|
||||
for (const m of MODIFIER_PROFILES) {
|
||||
const hosts = m.hosts
|
||||
.map((h) => (h === 'office' ? 'any Passenger Facility' : INDUSTRY_PROFILES.find((p) => p.kind === h)?.name ?? h))
|
||||
.join(', ');
|
||||
w(`| ${m.name} | ${hosts} | ${m.addOut || '—'} | ${m.addIn || '—'} | ${m.addLoaders || '—'} | ${m.addPorters || '—'} | ${m.copies} |`);
|
||||
w(`| ${m.name} | ${hosts} | ${m.addOut || '—'} | ${m.addIn || '—'} | ${m.addLoaders || '—'} | ${m.addPorters || '—'} |`);
|
||||
}
|
||||
w();
|
||||
w('---');
|
||||
@@ -198,14 +204,64 @@ w('Track is **in the Home Office deck** and is drawn and played like any other c
|
||||
w('per-player supply. Operational Rail is the flag that says a train may stop on the card; a turnout');
|
||||
w('may be run through but not stopped on.');
|
||||
w();
|
||||
w('| Track | Geometry | Hand | Operational rail | Move cost | In deck |');
|
||||
w('| --- | --- | --- | :---: | ---: | ---: |');
|
||||
w('| Track | Geometry | Hand | Operational rail | Move cost | Dealt |');
|
||||
w('| --- | --- | --- | :---: | ---: | :---: |');
|
||||
for (const t of TRACK_CARDS) {
|
||||
w(`| ${t.name} | ${t.geometry} | ${t.hand} | ${t.isOperationalRail ? 'yes' : '—'} | ${t.moveCost} | ${t.copiesInDeck || '—'} |`);
|
||||
w(`| ${t.name} | ${t.geometry} | ${t.hand} | ${t.isOperationalRail ? 'yes' : '—'} | ${t.moveCost} | ${t.copiesInDeck ? 'yes' : 'no'} |`);
|
||||
}
|
||||
w();
|
||||
w(`${TRACK_IN_DECK} track cards are dealt in total. Rows showing no copies are shapes the engine`);
|
||||
w('understands but the deck does not currently print.');
|
||||
w('A row marked "no" is a shape the engine understands but the deck does not currently print.');
|
||||
w();
|
||||
w('---');
|
||||
w();
|
||||
|
||||
w('## Enhancements');
|
||||
w();
|
||||
w('The column that only the implementation can fill in: **whether the printed effect actually');
|
||||
w('resolves yet.** `live` is read during play; `dormantSolo` is implemented at the point of attack');
|
||||
w('but the attack is an opponent-directed card held out of every deck, so nothing reaches it in a');
|
||||
w('solitaire game; `unbuilt` means the effect is recorded and nothing reads it. A transcription');
|
||||
w('cannot carry this column, which is the argument for generating the page rather than writing it.');
|
||||
w();
|
||||
w('| Enhancement | Placement | Requires | Effect resolves |');
|
||||
w('| --- | --- | --- | :---: |');
|
||||
for (const r of ENHANCEMENT_RULES) {
|
||||
const card = ENHANCEMENT_CARDS.find((c) => c.key === r.key);
|
||||
const needs = r.requiresOnSameCard
|
||||
? `${r.requiresOnSameCard} on the same card`
|
||||
: r.requiresInDistrict
|
||||
? `${r.requiresInDistrict} in the district`
|
||||
: '—';
|
||||
w(`| ${card?.name ?? r.key} | ${r.placement} | ${needs} | **${r.effect}** |`);
|
||||
}
|
||||
w();
|
||||
w('---');
|
||||
w();
|
||||
|
||||
w('## Opponent-directed cards, and what answers them');
|
||||
w();
|
||||
w('**None of these is dealt in any deck today.** A card that can only be played at another player');
|
||||
w('has no legal target in a solitaire game, and a defence with nothing to defend against is as dead');
|
||||
w('a draw as the attack — so both halves are held out until the attacks are implemented. They are');
|
||||
w('listed because they are the design, and because what a defence answers is the only record of why');
|
||||
w('it exists.');
|
||||
w();
|
||||
const pvp = (title: string, cards: readonly SimpleCard[], category: string): void => {
|
||||
w(`### ${title}${isOpponentOnly(category) ? ' — opponent-directed' : ''}`);
|
||||
w();
|
||||
w('| Card | Played on | Effect | Answers |');
|
||||
w('| --- | --- | --- | --- |');
|
||||
for (const c of cards) w(`| ${c.name} | ${c.placement} | ${c.effect} | ${c.answers ?? '—'} |`);
|
||||
w();
|
||||
};
|
||||
pvp('Action cards', ACTION_CARDS, 'action');
|
||||
pvp('Space-use cards', SPACE_USE_CARDS, 'spaceUse');
|
||||
pvp('Maneuver cards', MANEUVER_CARDS, 'maneuver');
|
||||
w('Mainline modifier cards, for completeness — these ARE dealt:');
|
||||
w();
|
||||
w('| Card | Played on | Effect | Answers |');
|
||||
w('| --- | --- | --- | --- |');
|
||||
for (const c of MAINLINE_MODIFIER_CARDS) w(`| ${c.name} | ${c.placement} | ${c.effect} | ${c.answers ?? '—'} |`);
|
||||
w();
|
||||
|
||||
writeFileSync(join(root, 'docs/rules/as-built.md'), `${lines.join('\n')}\n`);
|
||||
|
||||
Reference in New Issue
Block a user