diff --git a/CHANGELOG.md b/CHANGELOG.md index 09857a5..371348a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,190 @@ page as `v0.1.0 · · `, so what is deployed can always be identifie --- +## 0.8.2 — 2026-09-23 + +A playtest read back against the save file, and the rules that came out of it. Nine questions were +asked of one three-Day game; three were bugs, three were the rules working and undocumented, and +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. + +### Every district opens on a Depot + +**The single biggest change.** 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. Every district opens on a **Depot** now: two A/D tracks, passengers from Stage 1. + +**"Players start with Whistle Posts, not Depots"** is a setting for a table that wants the harder +game. 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 in a test rather than an argument: the cue-coverage pool needed +widening from 24 seeded games to 60 before it contained a single 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. Saves written before the setting existed name the rules that +existed then and cannot name this one, so `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. + +### An Office held two trains on one A/D track + +Reported from the table and confirmed on the save: at Day 2 Stage 9 a Whistle Post with one A/D +track held Trains 8 and 19 at once. + +An ordering fault in `arriveAtOffice`. The capacity test passed with nothing standing, the train the +Interlocking had been holding at the Limits was then 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 its priority, because it has been waiting. The NEWCOMER takes the consequence, +and it is the same one it would have met had the held train arrived first: held at its own Limits +where there is an Interlocking, a collision where there is not. + +### The history froze, permanently, and the log cap was not really the cause + +One player's history stopped gaining lines at Day 2 Stage 8 and the other's at Day 2 Stage 4, in a +two-player game that never reached Day 5. + +The log is trimmed to a limit — but each seat's "what have I sent you" bookmark was an **index** into +that array. Once a seat's bookmark reached the limit the array never grew past it again, so the +slice returned nothing for the rest of the game. Different moments per seat because each holds its +own bookmark. That game's log ended at exactly the cap. + +Raising the cap only delays it. Lines carry a **sequence number** now, which survives trimming, so +the bookmark stays meaningful however much is dropped. Proven by pushing twice the cap through a +simulated seat and asserting all of it arrives. The limit is also much larger, on its own merits: a +four-player game over ten Days is several times the game that first hit it. + +### §8.1 asked the wrong question twice + +**"Trains may pass" was short-circuiting the whole Subdivision.** The check returned `clear` before +the Subdivision was looked at, so a train entering a Double Track was released however busy the rest +of it was — including against a train coming the other way three cards deeper in. That is what let +Train 8 out of the Western Division Point with no ruling asked, and the reason had nothing to do +with Control Points. The card prints that TWO TRAINS MAY SHARE IT, so it excuses occupants on that +card and nothing else. + +**A train standing at an Office was invisible to it.** Train 19 was released from the Eastern +Division Point towards Train 14 and nobody was asked — because at that moment Train 14 was not in +transit at all, it was standing in a district. §8.1 was only ever reading trains on Mainline cards, +so a train about to re-enter the very Subdivision being entered counted for nothing. + +**Capacity is the test, not presence** (Jesse's reasoning exactly): at a Whistle Post, one A/D track +with a train on it means there is nowhere for the two to pass and no choice to be made. At a Depot +or a Terminal with a track still free, the train at the Office is not in the way. + +### Things that happened silently now say so + +**A train held against a facing one.** An absolute bar that returned without a word — the train +simply did not depart, Stage after Stage. Only the ABS Signals case announced itself, and it had +been given a line for exactly this reason. The line names the train that is coming and says there is +no Control Point between them to pass at. + +**A train released from the Limits.** It happened as a side effect of somebody else's arrival, so +the held train appeared at the Office with nothing said — "wasn't clear what changed and why train 8 +was suddenly released". The line names the train whose arrival freed the track, because *why now* is +the whole question. + +**A train the Interlocking is holding.** The tooltip explaining it has existed since #99 and **no +renderer ever read the flag**, so the train drew like any other crew and nothing told a player to +hover. It is drawn held now — red and dashed, the same "stopped, and not by choice" the Red Flag +means elsewhere on the map. + +### Where a move is refused, and why + +Asked directly: *"how does a user know what rule is violated and why you can't go there?"* Nowhere, +was the answer. `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 no reason given. + +Those squares are blocked with the rule that blocks them now, as `cardRule`. The reasons are got by +**asking `check`**, not by re-deriving the rules: a second implementation is exactly the failure the +block list was built to avoid, and a reason that does not match the refusal is worse than none. + +**And a train may always recover its own caboose.** X13 prints "may drop MTs but not pick up +anything", and a train needs its caboose at the far end to be made up — so a train that parted with +its caboose could never legally leave again. It stranded itself, permanently and silently. The +caboose only, not "your own cars" generally: it is the one car whose absence stops the train +departing. + +### Two Modifier rules, decided in September and applied here + +**A Modifier must sit square against its host** — north, south, east or west. No diagonals: touching +at a corner is not touching. This reverses an earlier report in the other direction, and the test +that asserted the old rule now asserts the new one on the very square that prompted it. + +**A passenger Modifier may not be played at a Whistle Post.** A Waiting Area, Restaurant or Hotel +needs an Office upgraded to at least a Depot. It reverses the ruling that let them stand dormant: a +Whistle Post allows neither direction, so the outbound slot was discarded on the spot and only the +porter landed, and a card that can be played to no effect is a trap however well it is labelled. +The grant-recovery path in `officeUpgraded` is kept and is now unreachable by play. + +Both were built, measured, backed out for a fortnight so a playtest could finish, and applied here. +The cost is known rather than guessed: of the thirteen saves on the test server, the Whistle Post +rule is what strands six of them. + +### The documentation is a set of pages now, not five text files + +They were served as `text/plain`, which is honest and unreadable: a card reference is mostly tables, +and as plain text a table is rows of pipes. That was TODO #109, taken deliberately as the short +version to get the references in front of testers for one round. + +**Markdown is still the one copy.** `docs/*.md` is what is written and reviewed; the build renders +it. A hand-written HTML twin drifts on the first edit, which is the whole lesson of #15a. The +Markdown is published beside each page too — it costs nothing, it is what a reader wanting a diff +actually wants, and it keeps every link handed out while the documents were text working. + +**No Markdown library.** `scripts/markdown.ts` covers the subset these five documents use. This +project has no runtime dependencies at all and one would be a poor first — and the renderer is +ten tests' worth of behaviour, not a general-purpose parser: it escapes unconditionally, and there +is no raw-HTML passthrough. + +What the page adds over the text, each because the Markdown cannot carry it without drifting: a nav +across the five documents; a contents list built from the headings actually rendered; an anchor on +every heading, so a section can be linked in a bug report; a ~70-character measure, because long +lines are the single biggest thing making long documents hard to read; and tables that are tables, +with numeric columns right-aligned and wide ones scrolling inside the page rather than widening it +on a phone. It prints as ink on paper, nav and contents dropped — a rules reference is a thing +people print. + +### The references caught up with the rules + +Checked against this release rather than assumed, and two statements had gone from stale to +misleading. The Quickstart told a new player they start on a **Whistle Post** and to "get a Depot +down as soon as one appears" — advice for a game that no longer exists. Components listed "Whistle +Post cards ×4" as what everyone starts on. + +Also added, because the playtest showed each was a rule nobody could look up: the **Office tier +table** (A/D tracks, Porters, passengers, Control Point) and what A/D tracks decide; **§8.1 in +practice**, including that a card printing "trains may pass" excuses only that card and that an +Office with no free A/D track occupies the Subdivision; the three conditions the **Circus Train** +pays on; and that a **Realignment can be a card with no legal target**, which is exactly what +happened at the table. + +### Smaller, from the same session + +**An industry may be built over a straight** off the Running Track, the way a turnout may upgrade +one — so rail can go down before the industry that will serve it. Only a plain straight: a Facility +carries east–west track, so the swap cannot break a neighbour's join, where a curve or turnout +could. + +**Revenue lines name the player who earned them.** The history prefixes the ACTOR, and revenue is +not always the actor's — a train completing its run pays everybody with no actor at all, so those +lines carried no name whatsoever. + +**"Waiting on" flashes when it is your turn**, amber, reading the move on screen rather than the +live one so it does not flash while your board is still catching up. + +**The Circus Train says what it pays for** on its own line while it is being made up, instead of as a +clause trailing the consist — it pays for STOPPING, once per district, and only if every car but the +caboose is loaded. The history line says the same when the point lands. + +**A Realignment says what it can convert.** "Convert one Mainline type to another" is true and +useless when only four of the nine types convert at all and the Division may have dealt none of +them — which is exactly what happened. + ## 0.8.1.0 — 2026-09-22 A second-digit bump, and a deliberate one. **0.8.1 had been reserved for the seatless display diff --git a/TODO.md b/TODO.md index e3cd047..643806b 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,29 @@ at all. One item per place now. Not items. Things that are true of every change, and that have gone wrong when skipped. +- **Update the documentation set in the same change.** `docs/quickstart.md`, `rules.md`, + `home-deck.md`, `mainline-deck.md` and `components.md` describe the game as built, and every + release restamps them — `**Version x.y.z** · date` is the second line of each. **The wrapper's + `instructions.md` is part of the set**: it is what a StartOS operator reads, so a change to how + the service is set up, run or recovered belongs there in the same commit. If a change alters what + a player does, sees or may rely on, the affected document changes with it. Four rules govern what + goes in them: + - **Version at the top**, before anything else on the page. + - **No history and no rationale.** No "this used to", no "corrected in v0.8.x", no ruling dates, + no TODO numbers. The documents say what the rules ARE. The reasoning belongs in `CHANGELOG.md` + and the argument in this file. + - **`instructions.md` is a manual, not a changelog.** It accumulated twenty "What changed in …" + blocks — 380 of its 469 lines — before they were deleted in 0.8.2. What changed in a release + goes in the wrapper's `releaseNotes`, which is what StartOS actually shows on update; the + instructions say how to run the service as it is now. + - **Card tables are generated, never typed.** `npm run build:cards` writes them into `home-deck.md` + and `mainline-deck.md` between `` markers, and + `test/card-reference.test.ts` fails if a checked-in table disagrees with `content.ts`. + - **The Markdown is the source; the pages are built.** `scripts/build-web.ts` renders each + document to `.html` through `scripts/markdown.ts`. Never edit a published page — and if a + document needs a construct the renderer does not cover, extend the renderer and test it rather + than writing HTML into the Markdown. + - **Ask Jesse what the version bump should be.** Third digit is a bug fix, second is a new set of features, 1.0 is the first release worth the name — but which one a batch deserves is a judgment about how finished it feels, and it is his. The number lives only in `package.json`; @@ -214,14 +237,13 @@ right — most of this release's defects were legible-but-wrong rather than brok laptop, are unanswered. **Needs a `days: 1` game — the timetable does not run out in five Days, so extended play fired in 0/10 measured games.** See **Reference · #35**. -- [ ] **#42a** — **Nobody has clicked through the solitaire setup screen's own fields** and confirmed - 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**. +- [x] **#42a** — **CONFIRMED at a table, 2026-09-23.** The solitaire setup screen's own fields were + clicked through and the dealt game matched what was chosen. -- [ ] **#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**. +- [x] **#40** — **DONE, 2026-09-23.** One sentence, in the four places a player meets a save: the + Quickstart's reporting section, a Rules FAQ entry ("Will an old save still replay?"), the + replay viewer's own page, and a tooltip on the **replays** link in the game — which had no + tooltip at all before. Also in the package's `instructions.md`. --- @@ -424,7 +446,9 @@ need RAR or Jesse rather than code.** to back" depends on which way the train points and the board has reversed east-facing consists since v0.8.0. Each says `MADE UP, ready to leave` or `HELD at the Office: `. -- [ ] **#108** — **The coach ratchet: every coach ends up in the Classification Yard and never comes +- [x] **#108** — **RULED AND CLOSED, 2026-09-23.** It stands: further table evidence supports + it, and part of the mid-game is players deliberately adding cars to clear the Division Yard so + the Classification refresh can happen. Originally: **The coach ratchet: every coach ends up in the Classification Yard and never comes back.** RULED 2026-09-17 — *the rule stands, the game says so loudly* — and recorded here because the ruling was made on one game's evidence and the balance question behind it is open. @@ -582,28 +606,6 @@ What the project says about itself, and what it ships alongside the code. - [ ] **#88** — `card-reference.md`'s industry table may still be stale beyond Grocer's Warehouse and the Oil Refinery. See **Reference · #88**. -- [ ] **#109** — **Render the published references instead of serving them as plain text.** - v0.8.0.17 publishes all five documents plus `rules/as-built.md`, links them from the splash - page and from the This Game card inside a running game, and serves them as `text/plain` — so a - tester reads their tables as rows of pipes and their links do not click. That was the - deliberate short version, to get the references in front of testers for this round rather than - leave them without any. - - **Now SIX documents rather than one**, which raises the value and the cost together: the - Quickstart's §8 is a table of links, and a reader following one lands on another wall of - pipes. - - **Copy the document, do not re-write it.** A hand-written HTML twin drifts from the Markdown on - the first edit, which is the whole argument of #15a. The step is a small Markdown-to-HTML - converter in `scripts/build-web.ts` writing `quickstart.html` beside the game, styled like the - splash page — headings, lists, tables, links and code spans are the whole of what the guide - uses. The `.md` MIME entry in `src/server/http.ts` and the two assertions in - `test/web.test.ts` (`the Quickstart guide reaches the site`) move to the rendered file with it. - - **Cost:** an afternoon, most of it in the converter's table and list handling. No dependency — - a Markdown library would be the only runtime dependency this project has, and the guide uses a - small enough subset that it is not worth becoming the first. - --- ## Reference — measurements, rulings and rejected approaches diff --git a/docs/components.md b/docs/components.md index 21b65dc..39f6f30 100644 --- a/docs/components.md +++ b/docs/components.md @@ -1,8 +1,6 @@ # Station Master — Components and Markers -**Describes the game as built at v0.8.1.0** (2026-09-22). These references are kept current with -every release rather than versioned as editions, so there is no version in the filename: this file -is always the latest, and the build it describes is stated here. +**Version 0.8.2** · 2026-09-23 **Scope:** non-card physical components and supplies. Card-created facilities, workers, deck piles, hand state, timetable state and other markers are documented with their cards or in the @@ -10,7 +8,7 @@ hand state, timetable state and other markers are documented with their cards or The figures below are checked against `ROLLING_STOCK_SUPPLY` and the supply constants in `src/engine/content.ts`. They are physical inventory rather than deck tuning, which is why they are -printed here at all — per-category CARD counts are deliberately not published anywhere (TODO #15a). +printed here at all — per-category CARD counts are not published, because they move with play balance. ## Rolling stock @@ -33,7 +31,7 @@ The engine is not rolling stock and does not count against the four-car Crew Tra | Component | Count | Note | | --- | ---: | --- | | Crew Trays | players + 3 | Engine and tray are one combined resource; there is no "engine without a tray". | -| Whistle Post cards | 4 | Every player starts on one; it is not drawn from the deck. | +| Office cards | 4 | Every player starts on one. The starting Office is not drawn from the deck. | | Limits signs | 8 | "2N + spares", so relocating one is never a supply question. | ## The two yards @@ -75,10 +73,10 @@ following-train clearance decisions, takes the Yard Office and Red Flag question every round round the table starts with. A Mainline collision is their fault by §10, and costs 5 Revenue. The initial holder is the first player tied for the highest Superintendent setup D12 roll. -It passes at the end of Stages 3, 6, 9 and 12 — every third Stage, at the Supervisor Shift. Since -v0.8.0.13 the handover is **announced on screen and written into the history**: it is the one thing -in the game that changes hands on the clock rather than because somebody did something, so nobody is -watching for it. Note that the Supervisor Shift refreshes every Laborer and Porter *every* Stage +It passes at the end of Stages 3, 6, 9 and 12 — every third Stage, at the Supervisor Shift. The +handover is **announced on screen and written into the history**: it is the one thing in the game +that changes hands on the clock rather than because somebody did something, so nobody is watching +for it. Note that the Supervisor Shift refreshes every Laborer and Porter *every* Stage while the Fedora moves only every third. ## D12 and seeded randomness @@ -91,3 +89,30 @@ The D12 is used by the engine for: - all shuffled deck order and automatic setup selection through the same seeded random stream. The rules engine uses a deterministic 32-bit seeded random generator. The same numeric seed, player configuration, house rules, and accepted intent sequence reconstruct the same game. A seed by itself is not enough when house rules differ. + +## The Office, tier by tier + +Every player has one Office card. It is a property of the district rather than a card that is +swapped, so an upgrade raises the numbers in place and leaves anything built beside it alone. + +| Office | A/D tracks | Porters | Passengers out / in | Control Point | +| --- | ---: | ---: | --- | :---: | +| Whistle Post | 1 | 0 | 0 / 0 | — | +| Depot | 2 | 1 | 1 / 1 | yes | +| Station | 3 | 2 | 2 / 2 | yes | +| Terminal | 4 | 3 | 3 / 3 | yes | + +**A/D tracks are what decide whether an arrival is a collision.** A train pulling in to an Office +with every track occupied collides (§8.3) unless an **Interlocking** holds it out on the Limit +Track. A train held that way takes the first track to free, ahead of anything arriving after it. + +**A Whistle Post is not a Passenger Facility.** It has no Porters and no passenger boxes, so nobody +boards or gets off there however well the district is built, and the passenger Modifiers — Waiting +Area, Restaurant, Hotel — cannot be played at one. + +**A Control Point divides the Mainline into Subdivisions**, which is what §8.1 reasons about. A +table of Whistle Posts is one Subdivision from end to end; every upgrade splits one in two and buys +the Division capacity. + +**Games open on a Depot** unless the table turns on "Players start with Whistle Posts, not Depots" +when the game is created. See the Home deck reference. diff --git a/docs/design.md b/docs/design.md index 5aa5a7b..edb68f0 100644 --- a/docs/design.md +++ b/docs/design.md @@ -28,7 +28,6 @@ Written to be handed to somebody who is about to play, rather than to somebody b | --- | --- | | [`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. | @@ -45,7 +44,7 @@ described — which read as though they documented a build five minor versions o | --- | --- | | [`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/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. | @@ -68,7 +67,7 @@ must do. ## Current status -**v0.8.1.0.** Rules formalized, card faces specified, architecture documented, and the game +**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 @@ -109,8 +108,9 @@ TypeScript natively, so there is no build step during development, which also me 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 +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]`. diff --git a/docs/home-deck.md b/docs/home-deck.md index 0b47cb6..6450f83 100644 --- a/docs/home-deck.md +++ b/docs/home-deck.md @@ -1,22 +1,18 @@ # Station Master — Home Deck -**Describes the game as built at v0.8.1.0** (2026-09-22). These references are kept current with -every release rather than versioned as editions, so there is no version in the filename: this file -is always the latest, and the build it describes is stated here. +**Version 0.8.2** · 2026-09-23 **Scope:** the Home Office deck — how it is dealt, drawn, discarded and reshuffled, and what the rules are for playing each kind of card out of it. -> **Per-card facts live in [`rules/as-built.md`](rules/as-built.md)**, which is GENERATED from -> `src/engine/content.ts` and checked by `test/card-reference.test.ts`, so it cannot disagree with -> the game. Read it for every card's name, effect, placement and whether its printed effect actually -> resolves yet. This document is how the deck WORKS; that one is what is in it. +> **The card tables in this document are GENERATED from `src/engine/content.ts`** and checked by a +> test, so they cannot disagree with the game. `npm run build:cards` rebuilds them; do not edit a +> table by hand. The prose around them is how the deck WORKS; the tables are what is in it. > -> **No card counts appear here, deliberately** (TODO #15a, Jesse's call 2026-08-22): counts move -> with play balance, so a document printing them is answering a question that has a different answer +> **Card counts are not published.** Counts move with play balance, so a printed count answers a +> question that has a different answer > after the next retune. Where a count matters it is rendered as a yes/no — whether the deck deals -> the card at all — which is a fact about the design. This page used to print a full counts table -> and it was wrong for a month before anyone noticed. +> the card at all — which is a fact about the design. ## The piles @@ -41,7 +37,7 @@ of nothing but those has exactly one way forward, which is to play one. The opening deal is a house-rule choice made when the game is dealt. The default (`threeRandom`) is three cards from one shuffled deck; `threeTrackThreeOther` deals three track and three others from -two separately shuffled piles, deliberately over the hand limit, so the first turn is spent choosing +two separately shuffled piles, over the hand limit, so the first turn is spent choosing which district you can afford to build. Drawing is one of the three Local Operations options — see [Rules](rules.md) @@ -56,16 +52,31 @@ Track cards are ordinary Home Office cards, not a separate personal supply. through route, or it breaks the main. - Placing track **on a Limit sign** extends the Running Track and moves that sign outward. The sign is a physical card, so it moves rather than being left stranded mid-track. -- **Nothing may be placed outside your Limits** — track, industries and, since v0.8.0.14, Modifiers - too. Your district ends at its sign. +- **Nothing may be placed outside your Limits** — track, industries and Modifiers alike. Your + district ends at its sign. - Curves and turnouts are printed left- or right-handed. A card may be turned 180° but never flipped over, so its 45° leg never changes diagonal. - A **turnout may upgrade** an existing straight, or a curve whose arc is exactly the turnout's - diverging arc. Not if the card holds standing cars or an enhancement. Every other occupied square - is unavailable. + diverging arc. An **industry may be built over a straight** off the Running Track, on the same + principle. Neither is allowed if the card holds standing cars or an Enhancement, and every other + occupied square is unavailable. - A turnout may be **run through but not stopped on**: it is not Operational Rail, so a Move may not end there. + +| 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 | + +A row marked "no" is a shape the engine understands but the deck does not currently print. + + ## Office cards Every player begins at a **Whistle Post**, which is not drawn from the deck: one A/D track, no @@ -78,12 +89,34 @@ passenger slot; a Depot and above is a Control Point and a Passenger Facility. An upgrade takes no placement: the Office is where it already is. + +Upgrades are strictly sequential — no skipping a tier — so a Terminal needs all three cards in +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 | +| --- | :---: | :---: | ---: | ---: | ---: | ---: | +| 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. + + ## Freight facilities An industry is placed on a connected straight **stub off the Running Track** — never on the Running Track itself, and never outside the Limits. A Facility carries its own rails, so placing one places track. +**It may also be built over a straight already laid**, in the same way a turnout may upgrade one, so +rail can go down before the industry that will serve it. Only a plain straight may be built over: a +Facility carries east–west track, so replacing a straight cannot break a neighbour's connection, +while a curve or a turnout carries rails the Facility does not. The square must be clear of standing +cars and carry no Enhancement — the replaced card leaves play. + Each begins with one Laborer and a three-box **MEN | AT | WORK** pipeline. No Office Area may hold a duplicate industry, or both ends of a lockout pair — a producer and the consumer of the same commodity cannot be built in one district. @@ -92,22 +125,67 @@ commodity cannot be built in one district. count. That distinction was a real bug: box count is how much WORK an industry can hold, not how much RAIL it has, and conflating the two invented a printed siding no industry card carries. + +Each lists the car types it works, which way its traffic flows, and the industries it may not sit +beside. **Every lockout pair is a producer and the consumer of the same commodity** — you may +build one end of a chain or the other, never both, which is what forces traffic to run between +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 | +| --- | --- | --- | ---: | ---: | ---: | --- | +| 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 | + + ## Facility modifiers -A Modifier sits on an empty square among the **nine spots around its host Facility** — and, since -v0.8.0.14, **inside your Limits**, like everything else. It may not stand in the Running Track row. -One of each kind per Office Area. +A Modifier sits on an empty square **square against its host Facility — north, south, east or +west**. It may not go on a diagonal: touching at a corner is not touching. It must be **inside your +Limits**, like everything else, and may not stand in the Running Track row. One of each kind per +Office Area. **A Modifier adds a BOX, never room for a car.** A Truck Dock beside a Grocer's Warehouse gives it a second red box — somewhere for one more arriving load to be cleared to — and changes nothing about how many cars may be spotted there. +**A passenger Modifier needs a Passenger Facility.** A Waiting Area, Restaurant or Hotel may not be +played at an Office that is still a **Whistle Post** — a Whistle Post is not a Passenger Facility, so +there is nothing for the card to add to. Upgrade the Office to a **Depot** or better first. + **A grant the host cannot use does nothing**, and the game says so rather than pretending. An outbound bonus beside an inbound-only facility, or the Truck Dock's inbound bonus beside the outbound-only Packing Sheds, is discarded — the latter leaving that card with no effect at all. -Passenger modifiers beside a **Whistle Post** add Porters but create no outbound slot until the -Office becomes a Passenger Facility; the panel reports that as DORMANT rather than claiming the -facility "only receives", which was wrong in both directions. + + +Each sits beside a host and raises one of its capacities. `office` as a host means any Passenger +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 | +| --- | --- | ---: | ---: | ---: | ---: | +| 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 | — | + ## Train cards @@ -121,9 +199,6 @@ house rule — `divisionPointsOnly`, `ownOffice`, or `anyOffice` (the default) also available, because it is the one Mainline card with a yard. An Extra runs once and its card goes to the Salvage Yard. -> The v0.4.5 behaviour of launching every Extra eastbound from the Western Division Point was -> replaced in v0.6.2. The number no longer decides an Extra's direction; the start does. - The listed consist is a **maximum, not a minimum**. A train may depart with fewer cars, but not with more, not of the wrong category, not with a car behind the caboose, and not with the engine buried among its own cars. A Crew Tray holds four pieces, and a caboose counts toward the four. @@ -131,14 +206,48 @@ among its own cars. A Crew Tray holds four pieces, and a caboose counts toward t A train made up short of what its card calls for is reported as such, with what it wanted and why the yard could not supply it — see Rules §4.4. -**Per-train consists and printed rules: [`rules/as-built.md`](rules/as-built.md) § Trains.** It +**Per-train consists and printed rules are in the table under § Train cards below.** It carries the `empties only`, `reefers only`, `drop only` and `pick up empties only` restrictions, every one of which the engine enforces. + +### Timetabled + +| # | Class | Speed | Runs | Consist | Printed rules | +| ---: | --- | --- | --- | --- | --- | +| 1 | Crack Limited | fast | west | 2 coaches (2 pieces) | terminals only; no switching; expedite; *"Stop at Terminals only."* | +| 2 | Crack Limited | fast | east | 2 coaches (2 pieces) | terminals only; no switching; expedite; *"Stop at Terminals only."* | +| 3 | Express | fast | west | 2 freight (2 pieces) | one freight per location; expedite; *"May drop or pick up one freight car at every location."* | +| 4 | Express | fast | east | 2 freight (2 pieces) | one freight per location; expedite; *"May drop or pick up one freight car at every location."* | +| 5 | The Sparrow | fast | west | 3 coaches (3 pieces) | no switching; expedite | +| 6 | The Sparrow | fast | east | 3 coaches (3 pieces) | no switching; expedite | +| 7 | Local | slow | west | 1 freight + 1 coach (2 pieces) | coach stays on station track; *"Maximum one freight, one coach."* | +| 8 | Local | slow | east | 1 freight + 1 coach (2 pieces) | coach stays on station track; *"Maximum one freight, one coach."* | +| 9 | Heavy Freight | slow | west | 3 freight + 1 caboose (4 pieces) | — | +| 10 | Heavy Freight | slow | east | 3 freight + 1 caboose (4 pieces) | — | +| 11 | Drag Freight | slow | west | 2 freight + 1 caboose (3 pieces) | — | +| 12 | Drag Freight | slow | east | 2 freight + 1 caboose (3 pieces) | — | + +### Extras + +| # | Class | Speed | Runs | Consist | Printed rules | +| ---: | --- | --- | --- | --- | --- | +| X13 | Appleseed Extra | slow | player's choice | 3 freight + 1 caboose (4 pieces), empties only | drop only; *"May drop MTs but not pick up anything."* | +| X14 | Fruit Growers Express | fast | player's choice | 2 reefers + 1 caboose (3 pieces) | expedite; *"Reefers only. May pick up one extra loaded reefer."* | +| X15 | Yard Xfer | slow | player's choice | 2 freight + 1 caboose (3 pieces) | — | +| X16 | Light Engine Move | fast | player's choice | engine only | no switching; *"No cars at all."* | +| X17 | Campaign Train | fast | player's choice | 1 coach (1 piece) | no switching; stop then expedite; stop earns point; must run loaded; *"One turn at station (speeches) then expedite. Earns a point per Office Area if the candidate is aboard."* | +| X18 | Circus Train | slow | player's choice | 2 freight + 1 coach + 1 caboose (4 pieces) | no switching; stop earns point; must run loaded; *"One turn stopped in an Office Area (circus set-up) earns 1 point, once per Area, if fully loaded."* | +| X19 | Military Train | slow | player's choice | 1 freight + 2 coaches (3 pieces) | no switching; no passenger work; expedite; must run loaded; *"Troops and materiel: runs loaded where the yard can supply it."* | +| X20 | Director's private car | slow | player's choice | 2 freight + 1 coach (3 pieces) | no passenger work | +| X21 | Freight Extra | slow | player's choice | 3 freight + 1 caboose (4 pieces) | — | +| X22 | Pee-Dee | slow | player's choice | 1 caboose (1 piece) | pick up empties only; *"Per-diem train. May only pick up MTs."* | + + ## Enhancements, Mainline modifiers and Maneuvers - **Enhancements** are played into your district and change what a square does — the **Small Yard** - (re-order a consist for one Move), **Interlocking**, **Yard Office** and the rest. `as-built.md` + (re-order a consist for one Move), **Interlocking**, **Yard Office** and the rest. The table below marks each one `live`, `dormantSolo` or `unbuilt`, which is the part only the implementation knows. - **ABS Signals is the exception, and it matters.** It is dealt as an Enhancement but is **not @@ -151,9 +260,139 @@ every one of which the engine enforces. Poling is catalogued but its effect is recorded as "TBD in the source", so there is nothing to implement. + +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 — not dealt The **Action** and **Space-use** categories are opponent-directed and are **excluded from every dealt deck**, because their play rules are not implemented. They are catalogued in -`as-built.md` so the composition is on record, and `check` refuses to play one. This is deliberate: +the table below so the composition is on record, and `check` refuses to play one. This is deliberate: silently accepting them would make a card look playable while doing nothing. + + +**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 | + + +## The Second Section card + +**Ordering a Second Section costs the card.** Played on a train that is **due out this Stage**, it +sends a second, identical train out right behind the first. That second train needs a Crew Tray of +its own, so there has to be one free. + +It is the one card that deliberately creates the **following-train** situation §8.1 makes the +Superintendent rule on — so playing it is choosing to put that question to them. + +One copy in the deck, spent when it is played. + +## Trains that pay for stopping + +Two Extras pay for **standing still** rather than for running: the **Circus Train** (X18) and the +**Campaign Train** (X17). Their card is worth nothing if it is run like an ordinary train. + +Three conditions, all of them required: + +- **Stopped.** The train spent a whole Mainline Phase without moving. Switching around inside a + district does not break it — what breaks it is leaving before a Mainline Phase passes. +- **In an Office Area.** Any square in a player's district. Standing on the Mainline or at a + Division Point pays nothing. A **Whistle Post district counts** — the rule is about the area, not + the Office's tier. +- **Fully loaded.** Every car except the caboose is carrying something. A train made up short, or + carrying an empty, earns nothing however long it stands. + +**One point per district**, and the point goes to whoever sits in the district it stopped in. A +Circus touring three districts is paid three times; one parked in the same district all game is paid +once. Neither train may switch, so it cannot load itself — it must be made up loaded before it goes. + +## The Office you start on + +Every district opens on a **Depot** unless the table chooses otherwise. A Depot has two A/D tracks +and is a Passenger Facility, so passengers earn from the first Stage and a second train can stand at +an Office without wrecking. + +**"Players start with Whistle Posts, not Depots"** is the harder game, set when the game is created. +A Whistle Post has **one** A/D track and is not a Passenger Facility: no passenger earns anything +until somebody draws and plays a Depot upgrade, and a second train arriving is a collision unless an +Interlocking holds it at the Limits. + +The deck follows the choice. Starting on Depots, the **Depot upgrade cards are left out** — an +upgrade must be to the next tier up, so a Depot card at a table that already has Depots is a dead +draw. Station and Terminal are still upgrades and stay in. + +## What a train may pick up + +Coupling is mandatory, so a train forbidden to pick cars up may not make the move that would pick +them up — there is no "move but leave them". A square the rails reach but the train's card forbids +is shown as blocked, with the rule that forbids it. + +**A train may always recover its own caboose.** A caboose at the far end is what makes a train +ready to leave, so a card that says "may drop but not pick up anything" would otherwise let a train +strand itself for good the moment it set its caboose down. diff --git a/docs/mainline-deck.md b/docs/mainline-deck.md index e07b4ad..059cadc 100644 --- a/docs/mainline-deck.md +++ b/docs/mainline-deck.md @@ -1,22 +1,19 @@ # Station Master — Mainline Deck -**Describes the game as built at v0.8.1.0** (2026-09-22). These references are kept current with -every release rather than versioned as editions, so there is no version in the filename: this file -is always the latest, and the build it describes is stated here. +**Version 0.8.2** · 2026-09-23 **Scope:** the tarot-sized Mainline cards placed between Offices — how the deck is dealt, what a card does to a train crossing it, and the Home Deck cards played onto one. -> **Per-card numbers live in [`rules/as-built.md`](rules/as-built.md)**, which is GENERATED from -> `src/engine/content.ts` and checked by `test/card-reference.test.ts`, so it cannot disagree with -> the game. This document explains how the deck is used; that one is the table of record. Where the -> two ever differ, as-built is right. +> **The card table in this document is GENERATED from `src/engine/content.ts`** and checked by a +> test, so it cannot disagree with the game. `npm run build:cards` rebuilds it; do not edit it by +> hand. The prose explains how the deck is used; the table is the record of what is in it. ## How Mainline cards work At setup the game lays one Mainline card between each neighbouring pair of Offices and one beyond each end Office, between it and a Division Point — so a game with *N* players uses **N + 1** cards. -They are **dealt from a finite deck without replacement** (since v0.6.2), so no Division can hold +They are **dealt from a finite deck without replacement**, so no Division can hold two of a card printed once. The two Division Points are the fixed ends of the Division and are not in the deck: `buildDivision` lays them itself. @@ -27,7 +24,7 @@ A card does not belong to either neighbouring Office. It is shared Division. A card is divided into **regions**, and a train advances **one region per Stage**. Crossing time is therefore `regions − startRegion`, and nothing else. **The printed mph is scenery.** -This is the part most likely to be remembered wrong, because it used to work the other way: mph set +This is the part most likely to be remembered wrong. The printed mph does not set the cost and a Slow train added a Stage to *every* card. It does not. Four things move a train's start region and nothing else does: @@ -73,23 +70,45 @@ The PDF art labels the Interchange "Yard". This reference uses **Interchange** t it apart from the Division Yard, the Classification Yard, the Yard Office and the Small Yard — five different things. -**Region counts and entry points per card are in [`rules/as-built.md`](rules/as-built.md).** +**Region counts and entry points per card are in the table under § The deck.** + + +| Card | Regions | Default entry | Fast / slow entry | Trains may pass | Extra may start | +| --- | ---: | ---: | --- | :---: | :---: | +| Plains | 1 | 0 | — | — | — | +| Curves | 2 | 0 | — | — | — | +| Hilly | 2 | 0 | 1 / 0 | — | — | +| Heavy Grade | 3 | 0 | — | — | — | +| Double Track | 1 | 0 | — | yes | — | +| Uncontrolled Siding | 2 | 1 | — | — | — | +| Tunnel | 2 | 0 | — | — | — | +| Trestle | 1 | 0 | — | — | — | +| Interchange | 2 | 1 | — | — | yes | + +The Mainline deck is 10 cards; the two Division Points are the fixed ends of +the Division and are not dealt. What each card does, in the words the game uses on screen: + +- **Plains** — 1 region — one Stage each. · 1 Stage for every train. · One train at a time — anything following has to wait for it to clear. +- **Curves** — 2 regions — one Stage each. · 2 Stages for every train. · One train at a time — anything following has to wait for it to clear. +- **Hilly** — 2 regions — one Stage each. · This card reads the train's FAST/SLOW rating: a fast train starts further along and crosses in 1 Stage, a slow one in 2 Stages. No other card cares which it is. · One train at a time — anything following has to wait for it to clear. +- **Heavy Grade** — 3 regions — one Stage each. · A grade, climbing eastward. 3 Stages to climb it and 3 Stages to run down, before help. Helpers start an UPHILL train a region further on; Brakeman does the same DOWNHILL and Airbrakes another again, and Airbrakes cannot be played without Brakeman. Never less than one Stage. · One train at a time — anything following has to wait for it to clear. +- **Double Track** — 1 region — one Stage each. · 1 Stage for every train. · TRAINS MAY PASS — two trains may stand on this card at once, so a following train is not held behind a slower one. +- **Uncontrolled Siding** — 2 regions — one Stage each. · A train with the card to itself starts past the back region and is across in 1 Stage. · UNCONTROLLED SIDING — arrive to find a train already here and you take the siding, a region behind it. You are not in the same place, so you do not run into it; it costs you the extra Stage instead. · One train at a time — anything following has to wait for it to clear. +- **Tunnel** — 2 regions — one Stage each. · 2 Stages for every train. · One train at a time — anything following has to wait for it to clear. +- **Trestle** — 1 region — one Stage each. · 1 Stage for every train. · One train at a time — anything following has to wait for it to clear. +- **Interchange** — 2 regions — one Stage each. · A train with the card to itself starts past the back region and is across in 1 Stage. · An Extra beginning its run here starts in the back region and takes the extra Stage. · One train at a time — anything following has to wait for it to clear. · This is the one Mainline card with a yard, so an Extra Train may be made up and started here. Its printed car-sorting is NOT implemented — a consist is re-ordered at a Small Yard in a district. + ## The Interchange, and what it does NOT do The Interchange prints a car-sorting capability. **It is not implemented, and never has been.** The one thing the card's `sortsCars` flag actually gates is that an **Extra Train may be made up and -started here** — it is the Mainline card with a yard, which is why §7 allows it (v0.6.2). An Extra +started here** — it is the Mainline card with a yard, which is why §7 allows it. An Extra starting here begins in the back region and takes the extra Stage. Re-ordering a consist is done at a **Small Yard** in an Office Area, for one switching Move. See Rules §4.3. -> **Corrected 2026-09-20.** Until this pass the card said "Cars may be sorted into any new order -> here" — on the board, in the tooltip a player reads, and in the generated reference. A card -> advertising an action the game will not offer sends a player hunting for a button that does not -> exist. The description now says what the card does. - ## Heavy Grade modifiers Home Office cards, played onto a Mainline card during a player's Draw option. @@ -99,11 +118,17 @@ Home Office cards, played onto a Mainline card during a player's Draw option. | Brakeman | Heavy Grade only. A **downhill** train starts one region further on. | | Airbrakes | Heavy Grade only, and **Brakeman must already be on that card**. A downhill train starts one region further again. | | Helpers | Heavy Grade only. An **uphill** train starts one region further on. | -| Realignment | Only onto an **unoccupied** Mainline card. Plains → Double Track, Curves → Plains, Uncontrolled Siding → Double Track, Trestle → Uncontrolled Siding. No other card may be realigned. | +| Realignment | Only onto an **unoccupied** Mainline card, and only these four conversions: Plains → Double Track, Curves → Plains, Uncontrolled Siding → Double Track, Trestle → Uncontrolled Siding. | A Heavy Grade is 3 regions, so it is 3 Stages to climb and 3 to run down before help. Modifiers never reduce a crossing below one Stage. +> **A Realignment can be a card with no legal target.** Only four of the nine Mainline types convert +> at all — Plains, Curves, Uncontrolled Siding and Trestle — so a Division dealt Heavy Grade, Tunnel +> and Double Track has nowhere to play one. A card already converted cannot be converted again, and +> a card with a train on it is refused while the train is there. The card says which four it can +> convert when you look at it in hand. + ### ABS Signals — an Enhancement, but it lives out here **ABS Signals is dealt from the Home Office deck as an Enhancement, and it is the one Enhancement @@ -135,9 +160,6 @@ around decides which modifiers are worth anything and which direction of traffic the whole game. Handing that to one of two neighbours advantages them over the other, and neither has a fair claim to it. -**Re-opened and closed again on 2026-08-23**, when giving the choice to the Superintendent was -considered and rejected. Jesse's call: v0.5.0's ruling stands. Rolling from the seed is -deterministic, roughly even (51/49 east/west over 400 games), identical in solitaire and -multiplayer, and keeps setup non-interactive — the game has no setup phase, so the question would -have to interrupt play before the first Local Operations, in the minority of games that deal the -card at all (20% at one player, rising to 50% at four). +So the orientation is **rolled from the game's seed**. That is deterministic, roughly even +(51/49 east/west), identical in solitaire and multiplayer, and keeps setup non-interactive: the game +has no setup phase, so asking would mean interrupting play before the first Local Operations. diff --git a/docs/quickstart.md b/docs/quickstart.md index cdf1a46..5f223e3 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,6 +1,8 @@ # Station Master — Quickstart -**For a tester who has never played. Describes the game as built at v0.8.1.0** (2026-09-22). +**Version 0.8.2** · 2026-09-23 + +For a player who has never played. Read this once before you sit down. It is about twenty minutes of reading and will save you an hour of confusion. The deeper references are listed at the end. @@ -32,21 +34,26 @@ and do not wait.** ## 2. How you win -The game runs a set number of **Days** — five by default. Each Day is **12 Stages**, which you can -think of as two-hour clock periods from midnight. +A game runs for a set number of **Days**, chosen when the game is created. Each Day is **12 +Stages**, which you can think of as two-hour clock periods from midnight. -At the end of the last Day: +At the end of the last Day, the table's **combined Revenue** is checked against a floor. **Miss it +and everybody loses**, however well you personally did — this is the number to watch. The floor is +worked out from the number of players and Days when the game is dealt, and whoever sets the game up +can raise or lower it. -1. **The table's combined Revenue is checked first**, against a floor of **3 × players × Days**. At - three players over five Days that is 45. **Miss it and everybody loses**, however well you - personally did. This is the number to watch. -2. **Co-op:** meeting the floor is the win, together. -3. **Competitive:** meeting the floor puts the game on, and the **highest individual Revenue** wins. -4. **Solitaire:** meet the floor by yourself. +Clear the floor, and how you win depends on the game type: + +- **Co-op** — clearing the floor is the win, together. +- **Competitive** — clearing the floor puts the game on, and the **highest individual Revenue** + wins. Everyone is still working towards the same floor first. +- **Cutthroat** — competitive, with the opponent-directed cards in play, so players can act against + each other directly. Those cards are not implemented yet, so this plays as Competitive today. +- **Solitaire** — clear the floor by yourself. **Collisions can end it early and badly.** Breaching the collision limit stops play at once in a -collective loss — the railroad has been declared unsafe. Default limits are 3 in one Day and 5 in -the game. +collective loss: the railroad has been declared unsafe. The limits are configurable, and can be +switched off entirely. **Falling short offers another Day** rather than just ending, so a game that misses the floor can be played on. A game stopped by collisions cannot. @@ -59,11 +66,11 @@ played on. A game stopped by collisions cannot. | A passenger getting off at your platform | 1 | | Completing an outbound freight load | 1 | | Completing an inbound freight unload | 1 | -| A train completing its run across the whole Division | 0 by default, to every player | +| A train completing its run across the whole Division | paid to every player; usually set to 0 | -Those rates are set when the game is dealt and can be changed. The default means **your score comes -almost entirely from working cars in your own district** — trains passing through pay nothing by -themselves. +Every rate is set when the game is dealt and can be changed. As they usually stand, **your score +comes almost entirely from working cars in your own district** — trains passing through pay nothing +by themselves. --- @@ -100,7 +107,13 @@ Stage not spent switching. ## 4. The screen -Left column, top to bottom: +**Across the top:** your Revenue, the target, the Day and Stage, collisions, and the game code. + +**When other players are acting**, their turns are replayed on your board a step at a time rather +than arriving already rearranged, with a `[N behind]` counter, **Pause** and **Skip**. Your own moves +are not replayed at you — they are already on your screen. + +**The main display, on the left**, top to bottom: - **The Division — west to east.** The shared main line: every Office and the Mainline cards between them, with trains drawn where they are. **West is always on the left**, and a train's engine is @@ -109,7 +122,7 @@ Left column, top to bottom: outside the phases that change it, unless you pin it open. - **History.** What has happened, most recent first. -Right column: +**The right-hand column:** - **Your Move** — the buttons. If it is not your turn this is empty, and the board tells you who is acting. @@ -121,23 +134,20 @@ Right column: - **Blocked — why nothing is moving.** *Read this panel.* When something will not work, this is where the game explains why, in rules terms. - **Facilities** — the load pipelines at each industry. - -Across the top: your Revenue, the target, the Day and Stage, collisions, and the game code. - -**When other players are acting**, their turns are replayed on your board a step at a time rather -than arriving already rearranged, with a `[N behind]` counter, **Pause** and **Skip**. Your own moves -are not replayed at you — they are already on your screen. +- **This Game** — the seed or your seat, the game code, the rules this game was dealt under, and + links to this guide and the other references. --- -## 5. Your first twenty minutes +## 5. Start with solitaire Play a **solitaire** game first. It needs no server and nobody else, and it is the same rules. 1. Open the site, choose **Play solitaire**, accept the defaults, **Deal**. -2. **Stage 1 — Draw.** You start on a **Whistle Post** with almost nothing. Play a track card or two - to extend your Running Track, and get a **Depot** down as soon as one appears: it is the upgrade - that makes you a passenger facility and gives you a second A/D track. +2. **Stage 1 — Draw.** You start on a **Depot** with almost nothing else. Play a track card or two to + extend your Running Track. A Depot already works passengers and has two A/D tracks, so a second + train can stand at your Office without wrecking; a **Station** upgrade, when one appears, buys a + third track and another Porter. 3. **Build one industry** on a stub off the main — not on the Running Track itself, which the game will not allow. 4. **Play a train card** when you get one. It rolls onto the timetable and then runs at that Stage @@ -147,8 +157,6 @@ Play a **solitaire** game first. It needs no server and nobody else, and it is t 6. Watch the **Blocked** panel whenever you are stuck. It is usually one missing thing: no empty car spotted, no loaded car in the yard, a full box, a locked industry track. -Then play a Day or two of multiplayer with bots filling the other seats, to see the table take turns. - --- ## 6. Things that surprise new players @@ -167,33 +175,37 @@ Then play a Day or two of multiplayer with bots filling the other seats, to see - **Passengers may dry up completely.** Coaches move one way — boarding sends the emptied coach to the Classification Yard, and it only comes back when the Division Yard is bare, which may never happen. When it does, the yard panel warns you and passenger trains are made up empty. **This is - the rules working as designed**, not a bug; report how it felt, not that it happened. + the rules working as designed.** - **Expedited trains leave the same Stage they arrived**, after the Cargo phase. Ordinary ones wait. ---- - -## 7. What to report - -Most useful, in order: - -1. **What you expected versus what happened**, with the Day and Stage. "Day 2 Stage 9, train 5 had - no coaches" is worth more than "passengers seem broken". -2. **Save the game** (the **Save replay** button) and send the file. A save is the seed and the moves - made, so it replays exactly and the bug can be looked at directly. -3. **Anything the screen did not explain.** If you had to guess a rule, that is a finding even when - the game was right. -4. **Anything you went looking for and could not find.** - -Bugs go to the tracker; anything unclear in this guide is also worth saying. +After playing some solitaire, play a day or two of multiplayer with bots or friends to get a feel +for how players interact. Then you are ready to run a real division. --- -## 8. Where to read more +## 7. Issues / Suggestions + +Please do report anything that looks like a bug or an area to be improved. + +- **What you expected versus what happened**, with the Day and Stage. "Day 2 Stage 9, train 5 had + no coaches" is worth more than "passengers seem broken". +- **Save the game** (the **Save replay** button) and send the file. A save is the seed and the moves + made, so it replays exactly and the problem can be looked at directly. +- **Anything the screen did not explain.** If you had to guess a rule, that is worth saying even + when the game was right. +- **Anything you went looking for and could not find.** + +**A save replays under the rules of the build that opens it.** When a rule changes, a save made +before it may stop part-way — the game says which move it stopped on and leaves your file untouched, +so the build you played on will still finish it. + +--- + +## 8. Documentation / References | For | Read | | --- | --- | | The rules in full, with the FAQ | [Rules](rules.md) | -| Every card, generated from the code | [`rules/as-built.md`](rules/as-built.md) | | How the Home Office deck is dealt and played | [Home deck](home-deck.md) | | The Mainline cards and what they do to a train | [Mainline deck](mainline-deck.md) | | Rolling stock, yards, trays, the Fedora | [Components](components.md) | diff --git a/docs/rules.md b/docs/rules.md index 78aefe6..c6f7e03 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1,12 +1,10 @@ # Station Master — Rules -**Describes the game as built at v0.8.1.0** (2026-09-22). These references are kept current with -every release rather than versioned as editions, so there is no version in the filename: this file -is always the latest, and the build it describes is stated here. +**Version 0.8.2** · 2026-09-23 **Authority:** observed code paths and tests. Where a card face, a prototype document and executable behaviour differ, this document reports **executable behaviour** and marks unimplemented material. -Per-card numbers are not repeated here — [`rules/as-built.md`](rules/as-built.md) is generated from +Per-card numbers are not repeated here — the Home deck and Mainline deck references carry tables generated from `src/engine/content.ts` and is the table of record. **New to the game? Start with the [Quickstart](quickstart.md).** @@ -30,7 +28,7 @@ This book is divided as follows: The companion references are the [Quickstart](quickstart.md) for a new player, [Mainline deck](mainline-deck.md), [Home deck](home-deck.md), [components](components.md), and the generated per-card table -[`rules/as-built.md`](rules/as-built.md). +[Home deck](home-deck.md) and [Mainline deck](mainline-deck.md). ## 2. Definitions @@ -119,9 +117,7 @@ The browser also stores the current local game and resumes it automatically when Three modes: **Solitaire**, **Competitive** and **Co-op**. All three are playable. -**Length is a free `days` count**, not a preset. The old `short`/`standard`/`campaign` presets -carried a `target` and were dropped in 2026-08; they survive only as a convenience argument for the -simulation tooling, resolving to 3, 5 and 10 Days. The default is 5. +**Length is a free `days` count**, not a preset — any number of Days may be set. The default is 5. **How a game ends and who wins:** @@ -196,7 +192,7 @@ On a player’s Local Operations turn, choose exactly one available option. **Switch.** Select any Crew Tray currently in that player’s Office Area. It gets six Moves, or five under Reduced Visibility on the listed night Stages. A Move travels any connected distance in one direction and must end on Operational Rail. Reversing is a separate Move. A train may pass through a turnout but cannot stop on it. It may not share or pass through another train except through an Office with a free A/D track. -Standing cars couple automatically when the train reaches them; it may not pass them, and the resulting consist may not exceed four cars. Coupling forward places cars ahead of the engine; coupling while backing places them behind it. Setting out cars does not spend a Move, but the cut must come from an outer end of the consist and may not be left on the Office. A **Small Yard** re-orders a consist for one Move, and since v0.8.0.14 may also place cars **ahead +Standing cars couple automatically when the train reaches them; it may not pass them, and the resulting consist may not exceed four cars. Coupling forward places cars ahead of the engine; coupling while backing places them behind it. Setting out cars does not spend a Move, but the cut must come from an outer end of the consist and may not be left on the Office. A **Small Yard** re-orders a consist for one Move, and may also place cars **ahead of the engine** — which is how a cut is set up to be shoved into a facing industry. Each option on the menu shows the train it would build, laid out west to east as the board draws it, and says whether the result may leave the Office or would be held there. Flying Switch spends one Move to @@ -211,18 +207,14 @@ roll a tail cut into a connected Freight Facility. > draw actually empties the pile; a pile with cards buried under the one taken is not refilled, or > the Departments would grow without limit and drain the deck into themselves. > -> This surprised a player at the table (2026-09-22), which is why it is written down here: the rule -> was implemented from the prototype rules and had never reached this document. - **Freight Agent.** Make one of these operations, then the turn ends: stock one green outbound box from a matching loaded Division Yard car; clear one red inbound box to the Classification Yard; unjam one outbound, inbound, or MEN | AT | WORK load to the Classification Yard; or explicitly end without acting. Freight can be stocked only when an unclaimed empty matching car is already spotted at that industry. Passengers may wait in a green Office box without a train present. > **A car cleared from a red Inbound box comes back empty.** Whatever was in it has arrived — the > passengers are out of the station, or the load is in the industry — and the Revenue for it was > paid on arrival, not on clearing. What returns to the Classification Yard is the *car*, in the -> common supply and carrying nothing. Before v0.8.1.0 it returned still marked loaded, which put -> coaches into the yards that could never be used to unload another passenger. +> common supply and carrying nothing. -Implementation note, still true at v0.8.1.0: `card.discard` is accepted during Local Operations +Implementation note: `card.discard` is accepted during Local Operations without checking that the Draw option was chosen — unlike `card.play`, which does check. This is an implementation quirk rather than a fourth published turn option. @@ -294,10 +286,9 @@ Car permit no passenger work. A Whistle Post has no Porters. > could board or detrain anywhere for the rest of the game, while trains whose cards call for coaches > were made up empty. > -> **This is the rules working as printed and the ruling is that it stands** (Jesse, 2026-09-17, the -> same ruling Gitea#2 got: running out is part of the game). What changed is that the game now says -> it — the yard panel warns while the shortage lasts, and a train made up short reports why. Whether -> the ratchet should be broken is open as TODO #108, to be decided on a second game's evidence. +> **This is the rules working as printed: running out of cars is part of the game.** The yard panel +> warns while the shortage lasts, and a train made up short reports what it wanted and why none is +> coming. ### 4.7 Freight work @@ -334,7 +325,7 @@ score has to come principally from passenger and freight work. ### 6.1 What the engine supports -The rules engine has always supported multiple named players; since v0.7 the lobby, server and +The rules engine supports multiple named players; the lobby, server and client around it are delivered too, so this section now describes a game people actually play. The engine has player, seat, score, Office Area, Division, phase-order, co-op and competitive-mode data for multiple named players. It deals each player a hand, creates one Office Area per seat, starts acting order at the Superintendent and proceeds eastward, and models the following multiplayer-specific outcomes: @@ -423,16 +414,15 @@ train is still present for that Stage's Load/Unload phase. ### Can I choose the direction of an Extra or a Heavy Grade? **An Extra, yes** — the player who played the card chooses where it starts and which way it runs, and -loads it as they choose (v0.6.2). **A Heavy Grade, no**: orientation is rolled from the seed. That is +loads it as they choose. **A Heavy Grade, no**: orientation is rolled from the seed. That is a decision rather than a gap — the card sits between two districts and belongs to neither, so handing the choice to one neighbour would advantage them permanently. See the Mainline deck reference. ### Can I use an Interchange to reorder a train? -**No, and the card used to claim otherwise.** Its printed car-sorting has never been implemented; the -description was corrected on 2026-09-20 to stop advertising it. What the Interchange actually offers -is the one Mainline card with a yard, so an **Extra may be made up and started there**. Re-ordering a -consist is done at a **Small Yard** in a district. +**No.** Its printed car-sorting is not implemented. What the Interchange offers is the one Mainline +card with a yard, so an **Extra may be made up and started there**. Re-ordering a consist is done at +a **Small Yard** in a district. ### Can I play attack cards on another player? @@ -442,8 +432,8 @@ is rejected. ### Is multiplayer playable? **Yes.** Lobby, game codes, seating, bots, an authoritative server that survives restarts, per-seat -reconnection and a replayed view of everyone else's turns are all delivered — see §3.5 and §6. The -opponent-directed cards remain unimplemented in every mode, so there are still no card attacks. +reconnection and a replayed view of everyone else's turns — see §3.5 and §6. The opponent-directed +cards are unimplemented in every mode, so there are no card attacks. ### Why did my passenger train arrive with no coaches? @@ -456,3 +446,28 @@ has happened, and a train made up short says so in the log. Because that train is **already made up**, so every re-order on offer would break it — most often by moving the caboose off the rear, which §8.2 will not let a train depart with. If the train is *not* currently fit to run, at least one option will be marked "MADE UP, ready to leave". + +### Will an old save still replay? + +Not always. A save is a seed and the list of moves, replayed through the rules of whatever build +opens it — so a change that makes a once-legal move illegal stops the replay at that move. The game +names the move and leaves the file untouched; the build the game was played on will still finish it. + +## §8.1 in practice — what holds a train at the end of the line + +A train is held, released or put to the Superintendent by what is in the **Subdivision** ahead of +it: the run of Mainline cards between two Control Points. Every Office that is not a Control Point +lies inside one, so a table of Whistle Posts is a single Subdivision from end to end. + +- A train coming **towards** it is an absolute bar. The train holds, and the history says which + train is coming and that there is no Control Point between them to pass at. +- A train going the **same way** is the Superintendent's ruling. +- A card printing **"trains may pass"** excuses only what is standing on **that card**. It does not + clear the rest of the Subdivision. +- A train **standing at an Office with no free A/D track** occupies the Subdivision too: it is about + to re-enter and there is nowhere for the two to pass. An Office with a track still free does not + hold anyone up. + +**An Office never holds more trains than it has A/D tracks.** A train the Interlocking is holding at +the Limits takes the first track to free, ahead of anything arriving afterwards — and the train that +arrives to find it taken is held at its own Limits, or collides if there is no Interlocking. diff --git a/docs/rules/as-built.md b/docs/rules/as-built.md deleted file mode 100644 index 17c7733..0000000 --- a/docs/rules/as-built.md +++ /dev/null @@ -1,260 +0,0 @@ -# Station Master — the cards as built - -> **Generated from `src/engine/content.ts` by `scripts/build-card-reference.ts`. Do not edit by -> hand** — `npm run build:cards` rewrites it, and `test/card-reference.test.ts` fails if this file -> and the code disagree. - -This is the one file in `docs/rules/` that describes the game **as it currently is**. Everything -else in this directory is a historical record and marked as such: [`rules-v0.1.md`](rules-v0.1.md) -transcribes the prototype PDFs, [`open-questions.md`](open-questions.md) tracks how the gaps in -them were decided, and [`rules-v0.2.md`](rules-v0.2.md) and -[`card-reference.md`](card-reference.md) both describe superseded card sets. Read those for the -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 - -12 timetabled and 10 Extras, 22 in all. -Odd numbers run west, even run east; a pair shares a class and is the same card face in two -directions. A Crew Tray holds four pieces and a caboose counts toward the four, which is why no -train with a caboose carries more than three revenue cars. - -### Timetabled - -| # | Class | Speed | Runs | Consist | Printed rules | -| ---: | --- | --- | --- | --- | --- | -| 1 | Crack Limited | fast | west | 2 coaches (2 pieces) | terminals only; no switching; expedite; *"Stop at Terminals only."* | -| 2 | Crack Limited | fast | east | 2 coaches (2 pieces) | terminals only; no switching; expedite; *"Stop at Terminals only."* | -| 3 | Express | fast | west | 2 freight (2 pieces) | one freight per location; expedite; *"May drop or pick up one freight car at every location."* | -| 4 | Express | fast | east | 2 freight (2 pieces) | one freight per location; expedite; *"May drop or pick up one freight car at every location."* | -| 5 | The Sparrow | fast | west | 3 coaches (3 pieces) | no switching; expedite | -| 6 | The Sparrow | fast | east | 3 coaches (3 pieces) | no switching; expedite | -| 7 | Local | slow | west | 1 freight + 1 coach (2 pieces) | coach stays on station track; *"Maximum one freight, one coach."* | -| 8 | Local | slow | east | 1 freight + 1 coach (2 pieces) | coach stays on station track; *"Maximum one freight, one coach."* | -| 9 | Heavy Freight | slow | west | 3 freight + 1 caboose (4 pieces) | — | -| 10 | Heavy Freight | slow | east | 3 freight + 1 caboose (4 pieces) | — | -| 11 | Drag Freight | slow | west | 2 freight + 1 caboose (3 pieces) | — | -| 12 | Drag Freight | slow | east | 2 freight + 1 caboose (3 pieces) | — | - -### Extras - -| # | Class | Speed | Runs | Consist | Printed rules | -| ---: | --- | --- | --- | --- | --- | -| X13 | Appleseed Extra | slow | player's choice | 3 freight + 1 caboose (4 pieces), empties only | drop only; *"May drop MTs but not pick up anything."* | -| X14 | Fruit Growers Express | fast | player's choice | 2 reefers + 1 caboose (3 pieces) | expedite; *"Reefers only. May pick up one extra loaded reefer."* | -| X15 | Yard Xfer | slow | player's choice | 2 freight + 1 caboose (3 pieces) | — | -| X16 | Light Engine Move | fast | player's choice | engine only | no switching; *"No cars at all."* | -| X17 | Campaign Train | fast | player's choice | 1 coach (1 piece) | no switching; stop then expedite; stop earns point; must run loaded; *"One turn at station (speeches) then expedite. Earns a point per Office Area if the candidate is aboard."* | -| X18 | Circus Train | slow | player's choice | 2 freight + 1 coach + 1 caboose (4 pieces) | no switching; stop earns point; must run loaded; *"One turn stopped in an Office Area (circus set-up) earns 1 point, once per Area, if fully loaded."* | -| X19 | Military Train | slow | player's choice | 1 freight + 2 coaches (3 pieces) | no switching; no passenger work; expedite; must run loaded; *"Troops and materiel: runs loaded where the yard can supply it."* | -| X20 | Director's private car | slow | player's choice | 2 freight + 1 coach (3 pieces) | no passenger work | -| X21 | Freight Extra | slow | player's choice | 3 freight + 1 caboose (4 pieces) | — | -| X22 | Pee-Dee | slow | player's choice | 1 caboose (1 piece) | pick up empties only; *"Per-diem train. May only pick up MTs."* | - ---- - -## Mainline cards - -A card is divided into **regions**, and a train advances one region per Stage — so the regions a -card prints are what it costs to cross. Where a train *enters* is what the rules move: a fast -train on Hilly, a Heavy Grade with Helpers, or a card whose back region is a siding rather than -part of the road all change the entry point rather than the card's length. - -| Card | Regions | Default entry | Fast / slow entry | Trains may pass | Extra may start | -| --- | ---: | ---: | --- | :---: | :---: | -| Plains | 1 | 0 | — | — | — | -| Curves | 2 | 0 | — | — | — | -| Hilly | 2 | 0 | 1 / 0 | — | — | -| Heavy Grade | 3 | 0 | — | — | — | -| Double Track | 1 | 0 | — | yes | — | -| Uncontrolled Siding | 2 | 1 | — | — | — | -| Tunnel | 2 | 0 | — | — | — | -| Trestle | 1 | 0 | — | — | — | -| Interchange | 2 | 1 | — | — | yes | - -The Mainline deck is 10 cards; the two Division Points are the fixed ends of -the Division and are not dealt. What each card does, in the words the game uses on screen: - -- **Plains** — 1 region — one Stage each. · 1 Stage for every train. · One train at a time — anything following has to wait for it to clear. -- **Curves** — 2 regions — one Stage each. · 2 Stages for every train. · One train at a time — anything following has to wait for it to clear. -- **Hilly** — 2 regions — one Stage each. · This card reads the train's FAST/SLOW rating: a fast train starts further along and crosses in 1 Stage, a slow one in 2 Stages. No other card cares which it is. · One train at a time — anything following has to wait for it to clear. -- **Heavy Grade** — 3 regions — one Stage each. · A grade, climbing eastward. 3 Stages to climb it and 3 Stages to run down, before help. Helpers start an UPHILL train a region further on; Brakeman does the same DOWNHILL and Airbrakes another again, and Airbrakes cannot be played without Brakeman. Never less than one Stage. · One train at a time — anything following has to wait for it to clear. -- **Double Track** — 1 region — one Stage each. · 1 Stage for every train. · TRAINS MAY PASS — two trains may stand on this card at once, so a following train is not held behind a slower one. -- **Uncontrolled Siding** — 2 regions — one Stage each. · A train with the card to itself starts past the back region and is across in 1 Stage. · UNCONTROLLED SIDING — arrive to find a train already here and you take the siding, a region behind it. You are not in the same place, so you do not run into it; it costs you the extra Stage instead. · One train at a time — anything following has to wait for it to clear. -- **Tunnel** — 2 regions — one Stage each. · 2 Stages for every train. · One train at a time — anything following has to wait for it to clear. -- **Trestle** — 1 region — one Stage each. · 1 Stage for every train. · One train at a time — anything following has to wait for it to clear. -- **Interchange** — 2 regions — one Stage each. · A train with the card to itself starts past the back region and is across in 1 Stage. · An Extra beginning its run here starts in the back region and takes the extra Stage. · One train at a time — anything following has to wait for it to clear. · This is the one Mainline card with a yard, so an Extra Train may be made up and started here. Its printed car-sorting is NOT implemented — a consist is re-ordered at a Small Yard in a district. - ---- - -## Office cards - -Upgrades are strictly sequential — no skipping a tier — so a Terminal needs all three cards in -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 | -| --- | :---: | :---: | ---: | ---: | ---: | ---: | -| 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. - ---- - -## Freight facilities - -Each lists the car types it works, which way its traffic flows, and the industries it may not sit -beside. **Every lockout pair is a producer and the consumer of the same commodity** — you may -build one end of a chain or the other, never both, which is what forces traffic to run between -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 | -| --- | --- | --- | ---: | ---: | ---: | --- | -| 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 | - ---- - -## Modifier cards - -Each sits beside a host and raises one of its capacities. `office` as a host means any Passenger -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 | -| --- | --- | ---: | ---: | ---: | ---: | -| 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 | — | - ---- - -## Track cards - -Track is **in the Home Office deck** and is drawn and played like any other card — not a separate -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 | 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 | - -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 | - diff --git a/package.json b/package.json index 72556d9..a695e93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "station-master", - "version": "0.8.1.0", + "version": "0.8.2", "private": true, "type": "module", "description": "Station Master — a railroad operations game", diff --git a/public/replays/seed-191056.json b/public/replays/seed-1798613.json similarity index 61% rename from public/replays/seed-191056.json rename to public/replays/seed-1798613.json index d429bcc..f284a2d 100644 --- a/public/replays/seed-191056.json +++ b/public/replays/seed-1798613.json @@ -1,7 +1,7 @@ { - "seed": 191056, - "title": "8 Revenue · seed 191056", - "note": "8 Revenue over 5 Days · 2 train(s) on the timetable · 18 cards down · no collisions · played by developer", + "seed": 1798613, + "title": "15 Revenue · seed 1798613", + "note": "15 Revenue over 5 Days · 3 train(s) on the timetable · 16 cards down · no collisions · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -10,34 +10,285 @@ "trainPerTransit": 0 }, "extraStart": "anyOffice", - "discardTimetabled": true + "discardTimetabled": true, + "startingOffice": "depot" }, "history": [ + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 2 + "type": "draw.fromHomeOffice" }, { "type": "card.play", - "cardId": "c24" + "cardId": "c17" }, { "type": "card.play", - "cardId": "c20" - }, - { - "type": "card.play", - "cardId": "c78", + "cardId": "c76", "placement": { "row": 0, "col": 1 }, "variant": 0 }, + { + "type": "card.play", + "cardId": "c39", + "placement": { + "row": 1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.startExtra", + "trainNumber": 18, + "start": { + "kind": "divisionPoint", + "side": "west" + } + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c61", + "node": 1 + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c16" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.startExtra", + "trainNumber": 17, + "start": { + "kind": "divisionPoint", + "side": "west" + } + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c20" + }, { "type": "draw.end" }, @@ -76,102 +327,6 @@ { "type": "loadUnload.end" }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c6" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c10" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, { "type": "localOps.choose", "option": "draw" @@ -181,7 +336,7 @@ }, { "type": "card.discard", - "cardId": "c101", + "cardId": "c27", "toSlot": 0 }, { @@ -197,14 +352,141 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "card.play", + "cardId": "c10" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c57", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c105", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c5" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, { "type": "card.discard", - "cardId": "c29", + "cardId": "c62", "toSlot": 1 }, { "type": "draw.end" }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c103", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c100", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, { "type": "loadUnload.end" }, @@ -214,12 +496,25 @@ }, { "type": "draw.fromDepartment", - "slot": 1 + "slot": 0 }, { - "type": "card.discard", - "cardId": "c31", - "toSlot": 1 + "type": "card.play", + "cardId": "c27", + "placement": { + "row": -1, + "col": -2 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c46", + "placement": { + "row": -2, + "col": -2 + }, + "variant": 0 }, { "type": "draw.end" @@ -232,17 +527,52 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 1 + "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c53", - "toSlot": 1 + "type": "card.play", + "cardId": "c72", + "placement": { + "row": 0, + "col": -2 + }, + "variant": 0 }, { "type": "draw.end" }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c22" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, { "type": "newTrain.placeCar", "trayId": "tray3", @@ -252,12 +582,401 @@ { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c86", + "placement": { + "row": -1, + "col": -3 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c6" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c59", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", "loaded": true }, { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "hopper" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "hopper" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c110", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c84", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c109", + "placement": { + "row": 0, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "draw" @@ -283,148 +1002,9 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.play", - "cardId": "c108", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c93", - "placement": { - "row": -1, - "col": -1 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c29", - "placement": { - "row": -1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c31", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.discard", - "cardId": "c69", + "cardId": "c48", "toSlot": 0 }, { @@ -435,10 +1015,37 @@ }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.end" + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c29", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c50", + "placement": { + "row": -1, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": false }, { "type": "loadUnload.end" @@ -451,16 +1058,73 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c39", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 + "type": "draw.end" }, { - "type": "draw.end" + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -4 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -2 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -4 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 }, { "type": "loadUnload.end" @@ -472,10 +1136,44 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": -2 }, - "carType": "coach" + "carType": "hopper" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 2 }, { "type": "loadUnload.end" @@ -489,23 +1187,183 @@ }, { "type": "card.discard", - "cardId": "c58", + "cardId": "c11", "toSlot": 1 }, { "type": "draw.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false + "type": "loadUnload.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -4 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -2 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 3 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -4 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "hopper" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "hopper" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "hopper" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 2 }, { "type": "loadUnload.end" @@ -518,13 +1376,37 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c45", - "placement": { - "row": -2, - "col": -1 - }, - "variant": 0 + "type": "card.discard", + "cardId": "c43", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c34", + "toSlot": 0 }, { "type": "draw.end" @@ -542,214 +1424,6 @@ { "type": "card.discard", "cardId": "c25", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c33", - "placement": { - "row": -1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c82", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c28", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c87", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c90", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c40", "toSlot": 1 }, { @@ -757,16 +1431,10 @@ }, { "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", + "trayId": "tray2", + "carType": "hopper", "loaded": false }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, { "type": "loadUnload.end" }, @@ -779,10 +1447,10 @@ }, { "type": "card.play", - "cardId": "c74", + "cardId": "c75", "placement": { "row": 0, - "col": 3 + "col": 2 }, "variant": 0 }, @@ -792,61 +1460,77 @@ { "type": "loadUnload.end" }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c67", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "loadUnload.end" - }, { "type": "localOps.choose", "option": "switch" }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -4 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, { "type": "switch.end" }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", + "type": "laborer.startLoad", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": -2 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 }, - "index": 0 + "box": 0 }, { "type": "loadUnload.end" @@ -858,39 +1542,21 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": -2 }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c55", - "toSlot": 0 - }, - { - "type": "draw.end" + "carType": "hopper" }, { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", + "carType": "hopper", "loaded": false }, { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", + "carType": "tank", "loaded": false }, { @@ -899,6 +1565,22 @@ "carType": "caboose", "loaded": true }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 2 + }, { "type": "loadUnload.end" }, @@ -910,35 +1592,9 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c91", - "placement": { - "row": -2, - "col": 3 - }, - "variant": 1 - }, - { - "type": "card.play", + "type": "card.discard", "cardId": "c98", - "placement": { - "row": -2, - "col": 4 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" + "toSlot": 2 }, { "type": "draw.end" @@ -950,141 +1606,87 @@ "type": "localOps.choose", "option": "switch" }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -4 + }, + "reverse": false, + "via": { + "row": 0, + "col": -1 + } + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -2 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 2 + }, { "type": "switch.end" }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c63", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c76", - "placement": { - "row": 0, - "col": -3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c64", - "node": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", + "type": "laborer.startLoad", "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" + "row": -1, + "col": -2 + } }, { - "type": "porter.board", + "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": -2 }, - "trayId": "tray3" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" + "box": 0 }, { "type": "loadUnload.end" @@ -1094,106 +1696,74 @@ "option": "freightAgent" }, { - "type": "freightAgent.clearInbound", + "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": 1 }, - "index": 0 + "carType": "tank" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 2 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "draw" + "option": "freightAgent" }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "hopper" }, { - "type": "card.discard", - "cardId": "c5", - "toSlot": 0 + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 }, { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c5", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 }, { "type": "loadUnload.end" @@ -1202,6 +1772,33 @@ "type": "localOps.choose", "option": "switch" }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, { "type": "switch.end" }, @@ -1213,13 +1810,12 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c5", - "toSlot": 0 + "type": "mainline.modify", + "cardId": "c66", + "node": 3 }, { "type": "draw.end" @@ -1227,102 +1823,6 @@ { "type": "loadUnload.end" }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c5", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c5", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "loadUnload.end" - }, { "type": "game.extend", "player": 0, diff --git a/public/replays/seed-1759018.json b/public/replays/seed-3073572.json similarity index 53% rename from public/replays/seed-1759018.json rename to public/replays/seed-3073572.json index 269d78f..c9535cb 100644 --- a/public/replays/seed-1759018.json +++ b/public/replays/seed-3073572.json @@ -1,7 +1,7 @@ { - "seed": 1759018, - "title": "8 Revenue · seed 1759018", - "note": "8 Revenue over 5 Days · 2 train(s) on the timetable · 18 cards down · no collisions · played by developer", + "seed": 3073572, + "title": "15 Revenue · seed 3073572", + "note": "15 Revenue over 5 Days · 3 train(s) on the timetable · 15 cards down · 6 collision(s) · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -10,33 +10,72 @@ "trainPerTransit": 0 }, "extraStart": "anyOffice", - "discardTimetabled": true + "discardTimetabled": true, + "startingOffice": "depot" }, "history": [ + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "draw" }, { "type": "draw.fromDepartment", - "slot": 2 + "slot": 0 }, { "type": "card.play", - "cardId": "c24" + "cardId": "c21" }, { "type": "card.play", - "cardId": "c78", + "cardId": "c71", "placement": { "row": 0, "col": 1 }, "variant": 0 }, + { + "type": "card.play", + "cardId": "c37", + "placement": { + "row": 1, + "col": 0 + }, + "variant": 0 + }, { "type": "draw.end" }, + { + "type": "newTrain.startExtra", + "trainNumber": 22, + "start": { + "kind": "divisionPoint", + "side": "west" + } + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, { "type": "loadUnload.end" }, @@ -60,18 +99,19 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" + "type": "draw.fromDepartment", + "slot": 0 }, { "type": "card.play", - "cardId": "c17" + "cardId": "c12" }, { "type": "draw.end" }, { "type": "newTrain.startExtra", - "trainNumber": 18, + "trainNumber": 13, "start": { "kind": "divisionPoint", "side": "west" @@ -79,28 +119,85 @@ }, { "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", + "trayId": "tray2", "carType": "boxcar", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray3", + "trayId": "tray2", "carType": "boxcar", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray3", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", "carType": "caboose", "loaded": true }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c104", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c88", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c100", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, { "type": "loadUnload.end" }, @@ -114,28 +211,27 @@ { "type": "draw.end" }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "freightAgent" + "option": "draw" }, { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c103", + "placement": { + "row": -1, + "col": -1 }, - "index": 0 + "variant": 1 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -148,9 +244,34 @@ "type": "draw.fromHomeOffice" }, { - "type": "mainline.modify", - "cardId": "c69", - "node": 1 + "type": "card.play", + "cardId": "c1" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" }, { "type": "draw.end" @@ -167,7 +288,7 @@ }, { "type": "card.discard", - "cardId": "c72", + "cardId": "c54", "toSlot": 0 }, { @@ -183,153 +304,6 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.play", - "cardId": "c107", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c89", - "placement": { - "row": -1, - "col": -1 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c29", - "placement": { - "row": -1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c104", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c73", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c43", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.play", "cardId": "c6" @@ -349,7 +323,7 @@ }, { "type": "card.discard", - "cardId": "c92", + "cardId": "c62", "toSlot": 1 }, { @@ -367,24 +341,78 @@ }, { "type": "card.play", - "cardId": "c112", + "cardId": "c91", "placement": { - "row": 0, - "col": 1 + "row": -1, + "col": -2 }, "variant": 1 }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, { "type": "card.play", - "cardId": "c94", - "placement": { - "row": 1, - "col": 1 + "cardId": "c22" + }, + { + "type": "card.play", + "cardId": "c24" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 }, - "variant": 0 + "carType": "coach" }, { - "type": "draw.end" + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true }, { "type": "loadUnload.end" @@ -396,67 +424,14 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.play", - "cardId": "c5" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c50", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c51", - "toSlot": 0 - }, { "type": "draw.end" }, { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", - "loaded": false + "carType": "coach", + "loaded": true }, { "type": "newTrain.placeCar", @@ -476,16 +451,76 @@ }, { "type": "card.play", - "cardId": "c61", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 + "cardId": "c11" }, { "type": "draw.end" }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray2" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray2" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, { "type": "porter.detrain", "at": { @@ -507,28 +542,15 @@ }, { "type": "localOps.choose", - "option": "switch" + "option": "freightAgent" }, { - "type": "switch.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 }, { "type": "loadUnload.end" @@ -545,14 +567,6 @@ }, "index": 0 }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray2" - }, { "type": "loadUnload.end" }, @@ -571,94 +585,6 @@ { "type": "loadUnload.end" }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c55", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c31", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.discard", - "cardId": "c102", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c101", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, { "type": "localOps.choose", "option": "draw" @@ -668,9 +594,27 @@ }, { "type": "card.play", - "cardId": "c77", + "cardId": "c113", "placement": { - "row": 0, + "row": -1, + "col": 0 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c33", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c55", + "placement": { + "row": -1, "col": 2 }, "variant": 0 @@ -683,87 +627,54 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "switch" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c82", - "placement": { + "type": "switch.move", + "trayId": "tray1", + "to": { "row": 0, - "col": 3 + "col": -2 }, - "variant": 0 + "reverse": true }, { - "type": "draw.end" + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c40", - "placement": { - "row": 1, - "col": -1 + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 }, - "variant": 0 + "reverse": false }, { - "type": "draw.end" + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": false + "type": "switch.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", + "type": "laborer.beginUnload", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": 1 }, - "carType": "coach" + "carIndex": 0 }, { - "type": "porter.detrain", + "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": 1 }, - "trayId": "tray2" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray2" + "box": 2 }, { "type": "loadUnload.end" @@ -772,20 +683,42 @@ "type": "localOps.choose", "option": "switch" }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": -2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, { "type": "switch.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 }, { "type": "loadUnload.end" @@ -797,111 +730,11 @@ { "type": "freightAgent.clearInbound", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": 1 }, "index": 0 }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray3" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c87", - "placement": { - "row": 0, - "col": -3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c117", - "placement": { - "row": 0, - "col": -4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c60", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c84", - "placement": { - "row": 0, - "col": 4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, { "type": "loadUnload.end" }, @@ -917,7 +750,7 @@ "cardId": "c116", "placement": { "row": 0, - "col": 5 + "col": -2 }, "variant": 0 }, @@ -935,12 +768,19 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c31", - "toSlot": 2 + "type": "draw.end" }, { - "type": "draw.end" + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": false }, { "type": "loadUnload.end" @@ -950,12 +790,205 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 2 + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c115", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray1" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray2" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" }, { "type": "card.discard", - "cardId": "c63", + "cardId": "c43", "toSlot": 2 }, { @@ -972,53 +1005,13 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c7", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c31", - "toSlot": 0 + "type": "card.play", + "cardId": "c114", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 0 }, { "type": "draw.end" @@ -1030,27 +1023,211 @@ "type": "localOps.choose", "option": "switch" }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 4 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1, + "fromNose": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, { "type": "switch.end" }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "draw" + "option": "switch" }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true }, { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false }, { - "type": "draw.end" + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 }, { "type": "loadUnload.end" @@ -1060,173 +1237,51 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c7", - "toSlot": 0 + "type": "card.play", + "cardId": "c19" }, { "type": "draw.end" }, { - "type": "loadUnload.end" + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": true }, { - "type": "localOps.choose", - "option": "draw" + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": false }, { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c31", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c7", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c31", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c7", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c31", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c3", - "toSlot": 0 - }, - { - "type": "draw.end" + "type": "newTrain.startExtra", + "trainNumber": 20, + "start": { + "kind": "divisionPoint", + "side": "west" + } }, { "type": "newTrain.placeCar", "trayId": "tray2", "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", "loaded": false }, { @@ -1237,17 +1292,176 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "draw.fromHomeOffice" }, { "type": "card.discard", - "cardId": "c7", + "cardId": "c35", "toSlot": 0 }, { "type": "draw.end" }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c31", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "reefer", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "caboose", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, { "type": "loadUnload.end" }, @@ -1261,18 +1475,45 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c31", - "toSlot": 0 + "type": "card.play", + "cardId": "c109", + "placement": { + "row": -1, + "col": -2 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c93", + "placement": { + "row": -1, + "col": -3 + }, + "variant": 1 }, { "type": "draw.end" @@ -1280,6 +1521,322 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray0", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray0", + "count": 2 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "reefer", + "loaded": true + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray0", + "count": 3 + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, { "type": "game.extend", "player": 0, diff --git a/public/replays/seed-1917398.json b/public/replays/seed-349436.json similarity index 68% rename from public/replays/seed-1917398.json rename to public/replays/seed-349436.json index 462c801..810f078 100644 --- a/public/replays/seed-1917398.json +++ b/public/replays/seed-349436.json @@ -1,7 +1,7 @@ { - "seed": 1917398, - "title": "8 Revenue · seed 1917398", - "note": "8 Revenue over 5 Days · 2 train(s) on the timetable · 13 cards down · no collisions · played by developer", + "seed": 349436, + "title": "18 Revenue · seed 349436", + "note": "18 Revenue over 5 Days · 3 train(s) on the timetable · 13 cards down · no collisions · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -10,50 +10,21 @@ "trainPerTransit": 0 }, "extraStart": "anyOffice", - "discardTimetabled": true + "discardTimetabled": true, + "startingOffice": "depot" }, "history": [ { "type": "localOps.choose", - "option": "draw" + "option": "freightAgent" }, { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c5" - }, - { - "type": "card.play", - "cardId": "c106", - "placement": { + "type": "freightAgent.stockOutbound", + "at": { "row": 0, - "col": 1 + "col": 0 }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false + "carType": "coach" }, { "type": "loadUnload.end" @@ -64,19 +35,43 @@ }, { "type": "draw.fromDepartment", - "slot": 2 + "slot": 0 }, { "type": "card.play", - "cardId": "c25" + "cardId": "c21" }, { "type": "card.play", - "cardId": "c9" + "cardId": "c39", + "placement": { + "row": 1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c61", + "node": 1 }, { "type": "draw.end" }, + { + "type": "newTrain.startExtra", + "trainNumber": 22, + "start": { + "kind": "divisionPoint", + "side": "west" + } + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, { "type": "loadUnload.end" }, @@ -99,34 +94,126 @@ "type": "localOps.choose", "option": "draw" }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c8" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c113", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c98", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, { "type": "draw.fromDepartment", - "slot": 2 + "slot": 0 + }, + { + "type": "card.play", + "cardId": "c26", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c109", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 }, { "type": "draw.end" }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "boxcar", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "boxcar", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "boxcar", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "caboose", "loaded": true }, @@ -142,9 +229,9 @@ }, { "type": "card.play", - "cardId": "c82", + "cardId": "c44", "placement": { - "row": 0, + "row": -1, "col": -1 }, "variant": 0 @@ -157,22 +244,38 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "switch" }, { - "type": "draw.fromHomeOffice" + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 2 + }, + "reverse": true }, { - "type": "card.play", - "cardId": "c42", - "placement": { - "row": 1, + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, "col": 0 }, - "variant": 0 + "reverse": false }, { - "type": "draw.end" + "type": "switch.dropCars", + "trayId": "tray3", + "count": 3 + }, + { + "type": "switch.end" }, { "type": "loadUnload.end" @@ -184,54 +287,224 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, + "row": -1, "col": 0 }, - "carType": "coach" + "carType": "boxcar" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "draw" + "option": "freightAgent" }, { - "type": "draw.fromHomeOffice" + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" }, { - "type": "draw.end" + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "draw" + "option": "freightAgent" }, { - "type": "draw.fromHomeOffice" + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" }, { - "type": "card.play", - "cardId": "c95", - "placement": { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { "row": -1, "col": 1 }, - "variant": 1 + "reverse": true }, { - "type": "card.play", - "cardId": "c37", - "placement": { + "type": "switch.move", + "trayId": "tray3", + "to": { "row": -1, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, "col": 2 }, - "variant": 0 + "reverse": true }, { - "type": "draw.end" + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.end" }, { "type": "loadUnload.end" @@ -256,27 +529,9 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.discard", - "cardId": "c51", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.play", - "cardId": "c88", + "cardId": "c75", "placement": { "row": 0, "col": -2 @@ -296,114 +551,26 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.discard", - "cardId": "c34", - "toSlot": 1 - }, { "type": "draw.end" }, { "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.discard", - "cardId": "c92", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray2" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - }, - "trayId": "tray2" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" + "trayId": "tray3", + "carType": "boxcar", + "loaded": false }, { "type": "newTrain.placeCar", "trayId": "tray3", "carType": "boxcar", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", "trayId": "tray3", "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", @@ -423,7 +590,391 @@ }, { "type": "card.discard", - "cardId": "c34", + "cardId": "c63", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 3 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 2 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c70", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c23" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c101", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c62", "toSlot": 2 }, { @@ -440,34 +991,10 @@ "type": "switch.move", "trayId": "tray3", "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, + "row": 0, "col": 2 }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 + "reverse": true }, { "type": "switch.dropCars", @@ -478,10 +1005,71 @@ "type": "switch.move", "trayId": "tray3", "to": { - "row": 0, + "row": -1, "col": 0 }, - "reverse": true + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 3 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" }, { "type": "switch.move", @@ -490,19 +1078,93 @@ "row": -1, "col": 1 }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 + "reverse": true }, { "type": "switch.move", "trayId": "tray3", "to": { "row": -1, - "col": 2 + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 2 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 }, "reverse": false }, @@ -512,14 +1174,64 @@ "count": 1 }, { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 + "type": "switch.end" }, { - "type": "switch.dropCars", + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", "trayId": "tray3", - "count": 1 + "to": { + "row": 0, + "col": 2 + }, + "reverse": true }, { "type": "switch.move", @@ -528,7 +1240,10 @@ "row": 0, "col": 0 }, - "reverse": true + "reverse": false + }, + { + "type": "switch.end" }, { "type": "loadUnload.end" @@ -538,12 +1253,47 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 2 + "type": "draw.fromHomeOffice" }, { "type": "card.discard", - "cardId": "c65", + "cardId": "c48", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c60", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c35", "toSlot": 2 }, { @@ -560,68 +1310,35 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c34", - "toSlot": 0 + "type": "card.play", + "cardId": "c4" }, { "type": "draw.end" }, { - "type": "loadUnload.end" + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false }, { - "type": "localOps.choose", - "option": "draw" + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "reefer", + "loaded": true }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "reefer", + "loaded": true }, { - "type": "card.discard", - "cardId": "c96", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c34", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.discard", - "cardId": "c93", - "toSlot": 1 - }, - { - "type": "draw.end" + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true }, { "type": "loadUnload.end" @@ -635,21 +1352,150 @@ }, { "type": "card.play", - "cardId": "c108", - "placement": { + "cardId": "c5" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { "row": 0, "col": 2 }, - "variant": 0 + "reverse": true }, { - "type": "card.play", - "cardId": "c34", - "placement": { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 3 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { "row": -1, - "col": 3 + "col": 0 }, - "variant": 0 + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c64", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c67", + "toSlot": 1 }, { "type": "draw.end" @@ -677,22 +1523,26 @@ "type": "draw.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true + "type": "loadUnload.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true + "type": "localOps.choose", + "option": "draw" }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c40", + "placement": { + "row": -2, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -705,17 +1555,75 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c56", - "placement": { - "row": -2, - "col": 2 - }, - "variant": 0 + "type": "card.discard", + "cardId": "c28", + "toSlot": 2 }, { "type": "draw.end" }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c51", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray3" + }, { "type": "porter.board", "at": { @@ -748,277 +1656,20 @@ "index": 0 }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c61", - "placement": { + "type": "porter.board", + "at": { "row": 0, - "col": -1 + "col": 0 }, - "variant": 0 + "trayId": "tray3" }, { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", + "type": "porter.detrain", "at": { - "row": -1, - "col": 2 + "row": 0, + "col": 0 }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 2 - }, - "index": 0 - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 2 - }, - "index": 0 + "trayId": "tray3" }, { "type": "porter.board", @@ -1028,118 +1679,52 @@ }, "trayId": "tray3" }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 2 - }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "switch" + "option": "freightAgent" }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "freightAgent.clearInbound", + "at": { "row": 0, "col": 0 }, - "reverse": false + "index": 0 }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "reefer", + "loaded": true }, { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "reefer", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "reefer", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "porter.detrain", + "at": { "row": 0, "col": 0 }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 0 + "trayId": "tray2" }, { "type": "loadUnload.end" @@ -1151,8 +1736,31 @@ { "type": "freightAgent.clearInbound", "at": { - "row": -1, - "col": 2 + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + }, + "trayId": "tray2" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 }, "index": 0 }, @@ -1171,221 +1779,18 @@ }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.end" + "type": "draw.fromHomeOffice" }, { - "type": "loadUnload.end" + "type": "card.discard", + "cardId": "c33", + "toSlot": 0 }, { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" + "type": "draw.end" }, { "type": "loadUnload.end" diff --git a/scripts/build-card-reference.ts b/scripts/build-card-reference.ts index b1966f8..fba7d04 100644 --- a/scripts/build-card-reference.ts +++ b/scripts/build-card-reference.ts @@ -1,31 +1,22 @@ /** - * Generate `docs/rules/as-built.md` — what the cards say, as the code actually has them. + * Generate the card tables inside `docs/home-deck.md` and `docs/mainline-deck.md`. * - * WHY THIS IS GENERATED RATHER THAN WRITTEN. + * WHY GENERATED RATHER THAN WRITTEN. Nothing fails when a hand-written table falls behind a + * constant, so the tables are emitted from the same exported catalogues the engine instantiates + * from, and `test/card-reference.test.ts` re-runs this generator and asserts the checked-in docs + * match. Change a card face and the suite goes red until the docs are regenerated. * - * Every other file in `docs/rules/` is a historical record and says so: `rules-v0.1.md` is a - * faithful transcription of the prototype PDFs, `open-questions.md` is the gap tracker, - * `rules-v0.2.md` and `card-reference.md` both carry SUPERSEDED banners. None of them describes the - * game as built, and none of them should be edited to — the record is worth more intact than - * patched. + * EACH TABLE LANDS UNDER THE SECTION IT BELONGS TO, between a marker pair the deck documents carry: * - * So there was no current reference at all, and `content.ts` spent several releases pointing at - * `card-reference.md` as "the place that now carries what the cards say" while that file's own - * banner said "do not use its numbers". A reader following the code's advice landed on the v0.4.5 - * deck: twelve numbered trains, "3 / 4 Mail-Express, 3 coaches", against a `content.ts` whose train - * 3 is the Express with two freight cars and a per-location freight rule. + * …generated… * - * A HAND-WRITTEN REPLACEMENT WOULD HAVE DRIFTED THE SAME WAY, and for the same reason: nothing - * fails when a table falls behind a constant. So the reference is emitted from the same exported - * catalogues the engine instantiates from, and `test/card-reference.test.ts` re-runs this generator - * and asserts the checked-in file matches byte for byte. Change a card face and the suite goes red - * until the doc is regenerated — which is the only mechanism this project has found that keeps a - * document honest. + * Everything around the markers is hand-written and is never touched. Only the Mainline card table + * goes to `mainline-deck.md`; every other table belongs to the Home Office deck. * - * `npm run build:cards` writes it. Nothing at runtime reads it; it is for people. + * `npm run build:cards` writes them. Nothing at runtime reads them; they are for people. */ -import { writeFileSync } from 'node:fs'; +import { readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -70,40 +61,17 @@ const trainRow = (t: TrainProfile): string => `| ${t.isExtra ? 'X' : ''}${t.number} | ${t.name} | ${t.speed} | ` + `${t.direction === 'playerChoice' ? "player's choice" : t.direction} | ${consistOf(t.consist)} | ${rulesOf(t.rules)} |`; -const lines: string[] = []; -const w = (s = ''): void => void lines.push(s); +/** Generated blocks, keyed by the marker name the deck documents wrap them in. */ +const blocks = new Map(); +let current: string[] = []; +/** Start a new generated block. Everything `w` writes lands here until the next `section`. */ +const section = (key: string): void => { + current = []; + blocks.set(key, current); +}; +const w = (s = ''): void => void current.push(s); -w('# Station Master — the cards as built'); -w(); -w('> **Generated from `src/engine/content.ts` by `scripts/build-card-reference.ts`. Do not edit by'); -w('> hand** — `npm run build:cards` rewrites it, and `test/card-reference.test.ts` fails if this file'); -w('> and the code disagree.'); -w(); -w('This is the one file in `docs/rules/` that describes the game **as it currently is**. Everything'); -w('else in this directory is a historical record and marked as such: [`rules-v0.1.md`](rules-v0.1.md)'); -w('transcribes the prototype PDFs, [`open-questions.md`](open-questions.md) tracks how the gaps in'); -w('them were decided, and [`rules-v0.2.md`](rules-v0.2.md) and'); -w('[`card-reference.md`](card-reference.md) both describe superseded card sets. Read those for the'); -w('reasoning; read this for the numbers.'); -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(); - -w('## Trains'); -w(); -w(`${TIMETABLED_TRAINS.length} timetabled and ${EXTRA_TRAINS.length} Extras, ${ALL_TRAINS.length} in all.`); -w('Odd numbers run west, even run east; a pair shares a class and is the same card face in two'); -w('directions. A Crew Tray holds four pieces and a caboose counts toward the four, which is why no'); -w('train with a caboose carries more than three revenue cars.'); -w(); +section('trains'); w('### Timetabled'); w(); w('| # | Class | Speed | Runs | Consist | Printed rules |'); @@ -116,16 +84,7 @@ w('| # | Class | Speed | Runs | Consist | Printed rules |'); w('| ---: | --- | --- | --- | --- | --- |'); for (const t of EXTRA_TRAINS) w(trainRow(t)); w(); -w('---'); -w(); - -w('## Mainline cards'); -w(); -w('A card is divided into **regions**, and a train advances one region per Stage — so the regions a'); -w('card prints are what it costs to cross. Where a train *enters* is what the rules move: a fast'); -w('train on Hilly, a Heavy Grade with Helpers, or a card whose back region is a siding rather than'); -w('part of the road all change the entry point rather than the card\'s length.'); -w(); +section('mainline'); w('| Card | Regions | Default entry | Fast / slow entry | Trains may pass | Extra may start |'); w('| --- | ---: | ---: | --- | :---: | :---: |'); for (const m of MAINLINE_PROFILES) { @@ -138,11 +97,7 @@ w('the Division and are not dealt. What each card does, in the words the game us w(); for (const m of MAINLINE_PROFILES) w(`- **${m.name}** — ${mainlineDescription(m.kind)}`); w(); -w('---'); -w(); - -w('## Office cards'); -w(); +section('office'); w('Upgrades are strictly sequential — no skipping a tier — so a Terminal needs all three cards in'); w('order. Green slots are outbound passengers, red are inbound, and the design gives slots **equal**'); w('to Porters rather than one more.'); @@ -157,11 +112,7 @@ w(); w(`Whistle Posts are a fixed supply of ${WHISTLE_POST_SUPPLY} outside the deck, and Limits signs a`); w(`supply of ${LIMITS_SUPPLY}.`); w(); -w('---'); -w(); - -w('## Freight facilities'); -w(); +section('facilities'); w('Each lists the car types it works, which way its traffic flows, and the industries it may not sit'); w('beside. **Every lockout pair is a producer and the consumer of the same commodity** — you may'); w('build one end of a chain or the other, never both, which is what forces traffic to run between'); @@ -177,11 +128,7 @@ for (const f of INDUSTRY_PROFILES) { w(`| ${f.name} | ${f.carTypes.join(', ')} | ${f.flow} | ${f.baseOut} | ${f.baseIn} | ${f.baseLoaders} | ${lo} |`); } w(); -w('---'); -w(); - -w('## Modifier cards'); -w(); +section('modifiers'); w('Each sits beside a host and raises one of its capacities. `office` as a host means any Passenger'); w('Facility — so a modifier that adds a green slot to an Office adds nothing to a Whistle Post, which'); w('is not one.'); @@ -195,15 +142,7 @@ for (const m of MODIFIER_PROFILES) { w(`| ${m.name} | ${hosts} | ${m.addOut || '—'} | ${m.addIn || '—'} | ${m.addLoaders || '—'} | ${m.addPorters || '—'} |`); } w(); -w('---'); -w(); - -w('## Track cards'); -w(); -w('Track is **in the Home Office deck** and is drawn and played like any other card — not a separate'); -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(); +section('track'); w('| Track | Geometry | Hand | Operational rail | Move cost | Dealt |'); w('| --- | --- | --- | :---: | ---: | :---: |'); for (const t of TRACK_CARDS) { @@ -212,11 +151,7 @@ for (const t of TRACK_CARDS) { w(); w('A row marked "no" is a shape the engine understands but the deck does not currently print.'); w(); -w('---'); -w(); - -w('## Enhancements'); -w(); +section('enhancements'); 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'); @@ -235,11 +170,7 @@ for (const r of ENHANCEMENT_RULES) { w(`| ${card?.name ?? r.key} | ${r.placement} | ${needs} | **${r.effect}** |`); } w(); -w('---'); -w(); - -w('## Opponent-directed cards, and what answers them'); -w(); +section('opponent'); 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'); @@ -264,5 +195,33 @@ 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`); -console.log(`built -> docs/rules/as-built.md (${lines.length} lines)`); +/** + * Splice each block into its document, between the markers that name it. + * + * Strict on purpose: a block with nowhere to go, or a marker pair with no block, is a mistake that + * would otherwise show up as a silently missing table. Both throw. + */ +const WHERE: Record = { + mainline: 'docs/mainline-deck.md', +}; +const DEFAULT_DOC = 'docs/home-deck.md'; + +const edited = new Map(); +for (const [key, body] of blocks) { + const rel = WHERE[key] ?? DEFAULT_DOC; + const text = edited.get(rel) ?? readFileSync(join(root, rel), 'utf8'); + const begin = ``; + const finish = ``; + const from = text.indexOf(begin); + const to = text.indexOf(finish); + if (from < 0 || to < 0) throw new Error(`${rel} has no markers for "${key}" — expected ${begin} … ${finish}`); + if (to < from) throw new Error(`${rel}: markers for "${key}" are the wrong way round`); + // Trailing blank lines are trimmed so the block sits the same way however the section ends. + const inner = body.join('\n').replace(/\n+$/, ''); + edited.set(rel, `${text.slice(0, from + begin.length)}\n${inner}\n${text.slice(to)}`); +} + +for (const [rel, text] of edited) { + writeFileSync(join(root, rel), text); + console.log(`built -> ${rel}`); +} diff --git a/scripts/build-web.ts b/scripts/build-web.ts index eff66ec..c3d2500 100644 --- a/scripts/build-web.ts +++ b/scripts/build-web.ts @@ -14,6 +14,9 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { DOC_PAGES, docPage } from './docs-page.ts'; +import { renderMarkdown } from './markdown.ts'; + const root = join(dirname(fileURLToPath(import.meta.url)), '..'); /** * Overridable so a test can point the build at an isolated directory instead of the shared @@ -189,41 +192,46 @@ if (existsSync(imageSrc)) { } /** - * The player-facing documentation, published beside the game so a tester can reach it from the box. + * The player-facing documentation, RENDERED and published beside the game. * - * COPIED, NOT RE-WRITTEN. The Markdown in `docs/` is the one copy; a hand-written HTML twin would - * drift from it on the first edit, which is the whole lesson of TODO #15a and of the 2026-09-20 - * documentation pass that found four references a month out of date. + * MARKDOWN IS STILL THE ONE COPY. `docs/*.md` is what is written and reviewed; this turns it into + * a page at build time. A hand-written HTML twin would drift from it on the first edit, which is + * the whole lesson of TODO #15a and of the documentation pass that found four references a month + * out of date. * - * THE WHOLE SET, NOT ONLY THE QUICKSTART. v0.8.0.16 published the guide alone, and the guide's own - * §8 "Where to read more" links five further documents by relative path — so every one of them - * 404'd on the package (verified on the box: 5 of 6 paths missing). Publishing the guide without - * what it points at is the same broken-link failure the test below was written to catch, one hop - * further out. The names are kept exactly as the guide writes them, because those links are what - * has to resolve. + * WHY RENDER AT ALL. They were served as `text/plain`, which is honest and unreadable: a card + * reference is mostly tables, and as plain text a table is rows of pipes. That was TODO #109, taken + * deliberately as the short version to get the references in front of testers for one round. * - * SERVED AS PLAIN TEXT for now, which is honest rather than good: tables render as pipes and the - * links do not click. Rendering them into styled pages needs a small Markdown converter and is - * filed as TODO #109 — this is the version that gets the references in front of testers for this - * round rather than leaving them without any. + * NO MARKDOWN LIBRARY. `scripts/markdown.ts` covers the subset these five documents use, and this + * project has no runtime dependencies at all — one would be a poor first. * - * PUBLISHED UNDER THEIR OWN NAMES, which since v0.8.0.17 carry no version: the documents are kept - * current with every release rather than published as editions, so `docs/rules.md` is served as - * `rules.md` and the splash page and the This Game card link it by that name. Four of them were - * `StationMaster--v0.4.5.md` until then — the prototype edition they were first written - * against, never the version they described. + * THE `.md` IS PUBLISHED TOO, beside the page. It costs nothing, it is what a reader who wants the + * source or a diff actually wants, and it keeps every link that was handed out while the documents + * were served as text working rather than 404ing. */ -const GUIDE_DOCS: readonly string[] = [ - 'quickstart.md', - 'rules.md', - 'home-deck.md', - 'mainline-deck.md', - 'components.md', - // Generated by `build:cards` and checked in; a test fails when it disagrees with the code, which - // is why it is the one reference that has never drifted. Its `rules/` directory is preserved - // because that is the path the Quickstart links it by. - 'rules/as-built.md', -]; +const GUIDE_DOCS: readonly string[] = DOC_PAGES.map((d) => `${d.slug}.md`); + +/** `rules.md` → `rules.html`, so a link between documents lands on the rendered page. */ +const docLink = (href: string): string => + /^https?:/.test(href) || href.startsWith('#') ? href : href.replace(/\.md(#|$)/, '.html$1'); + +/** + * The title and version line, lifted out of the Markdown body. + * + * The documents open with `# Title` then `**Version x.y.z** · date`, and the page draws both in its + * own header — so rendering them again in the body would print each twice. Taken by pattern rather + * than by line count, and the body is only trimmed where the pattern actually matched. + */ +function splitHead(src: string): { title: string; version: string; body: string } { + const m = /^#\s+(.+?)\n+\*\*Version\s+([^*]+)\*\*\s*·\s*([^\n]+)\n/.exec(src); + if (!m) return { title: 'Station Master', version: '', body: src }; + return { + title: m[1]!.replace(/^Station Master\s*[—-]\s*/, '').trim(), + version: `Version ${m[2]!.trim()} · ${m[3]!.trim()}`, + body: src.slice(m[0].length), + }; +} for (const rel of GUIDE_DOCS) { const src = join(root, 'docs', rel); @@ -233,9 +241,17 @@ for (const rel of GUIDE_DOCS) { console.error(`WARNING: docs/${rel} is missing — a published link will 404`); continue; } + const md = readFileSync(src, 'utf8'); const out = join(dist, rel); mkdirSync(dirname(out), { recursive: true }); - copyFileSync(src, out); + writeFileSync(out, md); + + const { title, version, body } = splitHead(md); + const { html, headings } = renderMarkdown(body, docLink); + writeFileSync( + join(dist, rel.replace(/\.md$/, '.html')), + docPage({ slug: rel.replace(/\.md$/, ''), title, version, body: html, headings }), + ); } // A tiny note for whoever unzips this later and wonders what it needs. diff --git a/scripts/docs-page.ts b/scripts/docs-page.ts new file mode 100644 index 0000000..f54437a --- /dev/null +++ b/scripts/docs-page.ts @@ -0,0 +1,197 @@ +/** + * The page a documentation file is rendered into. + * + * ONE TEMPLATE FOR ALL FIVE, so the guide reads as one publication rather than five files that + * happen to be linked. It carries the same dark palette, the same type and the same blue as the + * game, because a player arrives here from the board and should not feel they have left the site. + * + * WHAT THE PAGE ADDS OVER THE MARKDOWN, and why each is here rather than in the source: + * + * - a **nav** across the five documents, so the set is navigable from any one of them. The + * Markdown cannot carry this: it would have to be repeated in every file and would drift. + * - a **contents list** built from the headings actually rendered, so it cannot fall out of step + * with the document the way a hand-written one does. + * - **anchors** on every heading, so a section can be linked to in a bug report. + * - a **measure** of about 70 characters. Long lines are the single biggest thing making plain + * text hard to read, and these documents are long. + * + * PRINTS SANELY TOO: the nav and contents drop out, the palette goes to ink on paper, and tables + * keep their rules. A rules reference is a thing people print. + */ + +import type { Heading } from './markdown.ts'; + +export type DocPage = { + /** Published filename, without the extension — also the nav's identity for "you are here". */ + slug: string; + /** What the nav calls it. */ + nav: string; +}; + +export const DOC_PAGES: readonly DocPage[] = [ + { slug: 'quickstart', nav: 'Quickstart' }, + { slug: 'rules', nav: 'Rules' }, + { slug: 'home-deck', nav: 'Home deck' }, + { slug: 'mainline-deck', nav: 'Mainline deck' }, + { slug: 'components', nav: 'Components' }, +]; + +export const DOCS_CSS = ` +:root{ + --bg:#12161c; --panel:#161b22; --line:#2c333d; --fg:#cfd6e0; --dim:#8b94a3; + --head:#cfe0f5; --link:#5aa9e6; --accent:#9fb6d8; --rule:#39424e; +} +*{box-sizing:border-box} +html{scroll-behavior:smooth} +body{ + margin:0;background:var(--bg);color:var(--fg); + font:15px/1.65 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; + -webkit-text-size-adjust:100%; +} +a{color:var(--link)} +a:hover{color:#9fd0f5} + +/* THE NAV. Sticky, because these documents are long and the set has to stay reachable from the + middle of one. Horizontally scrollable on a phone rather than wrapping into three rows. */ +.docnav{ + position:sticky;top:0;z-index:5;background:var(--panel);border-bottom:1px solid var(--line); + display:flex;align-items:center;gap:4px;padding:8px 16px;overflow-x:auto; +} +.docnav .home{color:var(--head);font-weight:700;margin-right:10px;text-decoration:none;white-space:nowrap} +.docnav a.tab{ + color:var(--dim);text-decoration:none;padding:4px 10px;border-radius:6px;white-space:nowrap; + border:1px solid transparent;font-size:13px; +} +.docnav a.tab:hover{color:var(--fg);background:#1f2733} +.docnav a.tab[aria-current="page"]{color:#f2e6cf;background:#2b3444;border-color:#c8912f} + +.wrap{max-width:78ch;margin:0 auto;padding:22px 16px 72px} + +/* THE HEADER — what this document is and which build it describes, lifted out of the prose so the + version is the first thing on the page, as the process rules require. */ +.dochead{border-bottom:1px solid var(--rule);padding-bottom:12px;margin-bottom:8px} +.dochead h1{margin:0 0 6px;font-size:26px;line-height:1.25;color:var(--head);letter-spacing:.01em} +.dochead .ver{color:var(--dim);font-size:13px} +.dochead .ver b{color:#c8912f;font-weight:700} + +/* CONTENTS, built from the headings actually rendered. Collapsed by default on a phone. */ +.toc{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 14px;margin:18px 0 26px} +.toc summary{cursor:pointer;color:var(--accent);font-size:13px;font-weight:600;letter-spacing:.04em;text-transform:uppercase} +.toc ol{list-style:none;margin:10px 0 2px;padding:0;columns:2;column-gap:26px} +.toc li{margin:0 0 4px;break-inside:avoid} +.toc li.l3{padding-left:14px;font-size:13px} +.toc a{text-decoration:none;color:var(--fg)} +.toc a:hover{color:var(--link)} +@media (max-width:640px){.toc ol{columns:1}} + +h2,h3,h4{color:var(--head);line-height:1.3;margin:28px 0 8px} +h2{font-size:20px;border-bottom:1px solid var(--rule);padding-bottom:5px} +h3{font-size:16px} +h4{font-size:14px;color:var(--accent);text-transform:uppercase;letter-spacing:.05em} +p{margin:0 0 12px} +strong{color:#e8eef7} +hr{border:none;border-top:1px solid var(--rule);margin:26px 0} + +/* The anchor beside a heading: invisible until the heading is hovered, so it never competes with + the words but is always there to copy. */ +.anchor{margin-left:.45em;color:var(--rule);text-decoration:none;font-weight:400;opacity:0} +h1:hover .anchor,h2:hover .anchor,h3:hover .anchor,h4:hover .anchor{opacity:1} +.anchor:hover{color:var(--link)} + +ul,ol{margin:0 0 12px;padding-left:22px} +li{margin:0 0 5px} +li>ul,li>ol{margin-top:5px} + +code{background:#1d232c;border:1px solid var(--line);border-radius:4px;padding:1px 5px; + font:13px ui-monospace,SFMono-Regular,Menlo,monospace;color:#e0c89a} +pre{background:#1d232c;border:1px solid var(--line);border-radius:7px;padding:12px 14px;overflow-x:auto} +pre code{background:none;border:none;padding:0;color:var(--fg)} + +blockquote{ + margin:16px 0;padding:10px 14px;background:#181f28; + border-left:3px solid #c8912f;border-radius:0 7px 7px 0; +} +blockquote > :last-child{margin-bottom:0} + +/* TABLES THAT LOOK LIKE TABLES. This is the whole reason the documentation is rendered rather than + served as text: a card reference is mostly tables, and as plain text they are rows of pipes. */ +.tablewrap{overflow-x:auto;margin:0 0 16px;border:1px solid var(--line);border-radius:8px} +table{border-collapse:collapse;width:100%;font-size:14px} +thead th{ + background:#1f2733;color:var(--accent);text-align:left;font-weight:600; + padding:8px 12px;border-bottom:1px solid var(--line);white-space:nowrap; +} +td{padding:7px 12px;border-bottom:1px solid #222a34;vertical-align:top} +tbody tr:last-child td{border-bottom:none} +tbody tr:nth-child(even){background:#151a21} +tbody tr:hover{background:#1b222b} +.ta-right{text-align:right;font-variant-numeric:tabular-nums} +.ta-center{text-align:center} +th.ta-right,th.ta-center{text-align:inherit} + +footer{margin-top:40px;padding-top:14px;border-top:1px solid var(--rule);color:var(--dim);font-size:12px} +footer a{color:var(--accent)} + +@media print{ + .docnav,.toc,.anchor{display:none} + body{background:#fff;color:#111;font-size:11pt} + h1,h2,h3,h4,strong{color:#000} + a{color:#000;text-decoration:underline} + .wrap{max-width:none;padding:0} + .tablewrap{border-color:#999} + thead th{background:#eee;color:#000;border-bottom-color:#999} + td{border-bottom-color:#ccc} + tbody tr:nth-child(even){background:#f6f6f6} + blockquote{background:#f4f4f4;border-left-color:#888} + code,pre{background:#f4f4f4;border-color:#ccc;color:#111} +} +`; + +/** The contents list, from the headings the renderer actually produced. */ +function toc(headings: readonly Heading[]): string { + // h2 and h3 only: h1 is the page title, and h4 is a label inside a section rather than a place. + const items = headings.filter((h) => h.level === 2 || h.level === 3); + if (items.length < 3) return ''; + const lis = items + .map((h) => `
  • ${h.text.replace(/&/g, '&').replace(/
  • `) + .join(''); + return `
    On this page
      ${lis}
    `; +} + +export function docPage(opts: { + slug: string; + title: string; + version: string; + body: string; + headings: readonly Heading[]; +}): string { + const tabs = DOC_PAGES.map( + (d) => + `
    ${d.nav}`, + ).join(''); + return ` + + + + +${opts.title} — Station Master + + + + +
    +
    +

    ${opts.title}

    +
    ${opts.version}
    +
    + ${toc(opts.headings)} + ${opts.body} +
    + Station Master — back to the game · + these references are kept current with every release. +
    +
    + + +`; +} diff --git a/scripts/markdown.ts b/scripts/markdown.ts new file mode 100644 index 0000000..91ed816 --- /dev/null +++ b/scripts/markdown.ts @@ -0,0 +1,261 @@ +/** + * A small Markdown renderer, for the player-facing documentation only. + * + * WHY NOT A LIBRARY. This project has no runtime dependencies at all, and the guide uses a small, + * known subset of Markdown — headings, paragraphs, lists, tables, links, code spans, block quotes + * and rules. A Markdown library would be the first dependency in the tree, pulled in to render five + * files whose whole vocabulary fits below. `docs/` is written by hand, not by users, so this does + * not have to survive hostile input; it has to render what those five documents actually contain + * and fail loudly on anything else. + * + * WHY NOT HAND-WRITTEN HTML. The Markdown is the one copy (TODO #15a). An HTML twin drifts from it + * on the first edit, which is the failure the whole documentation pass was about. + * + * ESCAPING IS UNCONDITIONAL. Every scrap of text goes through `esc` before any markup is added, and + * the inline pass only ever inserts tags around already-escaped content. A `<` in the prose is a + * less-than sign, not the start of an element — there is no raw-HTML passthrough, deliberately. + */ + +/** HTML-escape. Ampersand first, or it double-escapes the entities added after it. */ +export function esc(s: string): string { + return s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +/** A heading found while rendering, for the contents list the page builds from it. */ +export type Heading = { level: number; text: string; id: string }; + +/** + * `## 3. The shape of a Stage` → `the-shape-of-a-stage`. + * + * The leading number is dropped: it is a position in the document, and a link that carries it + * breaks when a section is inserted above. Duplicate slugs get a numeric suffix rather than + * silently pointing at the first one. + */ +function slug(text: string, taken: Set): string { + const base = + text + .toLowerCase() + // A whole section number, dotted or not: "4.2 Local Operations" and "7. FAQ" both lose it. + .replace(/^\d+(?:\.\d+)*[.)]?\s+/, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'section'; + let id = base; + for (let n = 2; taken.has(id); n++) id = `${base}-${n}`; + taken.add(id); + return id; +} + +/** + * Inline markup, applied to text that is ALREADY escaped. + * + * Order matters: code spans are taken out first and put back last, so `**` inside backticks stays + * literal. That is not a corner case here — the rules reference quotes field names like + * `**Version**` when describing the page header. + */ +function inline(escaped: string, linkHref: (href: string) => string): string { + const code: string[] = []; + let s = escaped.replace(/`([^`]+)`/g, (_m, body: string) => { + code.push(`${body}`); + return `\u0000${code.length - 1}\u0000`; + }); + + // Links: [text](target). The target is rewritten so a link between documents lands on the + // rendered page rather than the Markdown source. + s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, text: string, href: string) => { + const target = linkHref(href); + const external = /^https?:/.test(target); + return `${text}`; + }); + + // Bold before italic, or `**x**` is read as an empty italic wrapping a bold. + s = s.replace(/\*\*([^*]+)\*\*/g, '$1'); + s = s.replace(/(^|[\s(])\*([^*\n]+)\*/g, '$1$2'); + + return s.replace(/\u0000(\d+)\u0000/g, (_m, i: string) => code[Number(i)]!); +} + +/** One table row's cells, from `| a | b |`. */ +function cells(line: string): string[] { + return line + .replace(/^\s*\|/, '') + .replace(/\|\s*$/, '') + .split('|') + .map((c) => c.trim()); +} + +/** `---`, `:--`, `--:` and `:-:` → the CSS alignment a column wants. */ +function alignments(sep: string): (string | null)[] { + return cells(sep).map((c) => { + const left = c.startsWith(':'); + const right = c.endsWith(':'); + if (left && right) return 'center'; + if (right) return 'right'; + if (left) return 'left'; + return null; + }); +} + +const isTableSep = (line: string): boolean => /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(line) && line.includes('-'); + +export type Rendered = { html: string; headings: Heading[] }; + +/** + * Render a Markdown document to HTML. + * + * `linkHref` rewrites link targets — the build uses it to send `rules.md` to `rules.html` — and + * defaults to leaving them alone so the function is testable on its own. + */ +export function renderMarkdown(src: string, linkHref: (href: string) => string = (h) => h): Rendered { + const lines = src.replace(/\r\n/g, '\n').split('\n'); + const out: string[] = []; + const headings: Heading[] = []; + const taken = new Set(); + const ln = (s: string): void => void out.push(s); + const text = (s: string): string => inline(esc(s), linkHref); + + let i = 0; + while (i < lines.length) { + const line = lines[i]!; + + // The generated-card markers, and any other HTML comment: structural, never shown. + if (/^\s*')) i++; + i++; + continue; + } + + if (line.trim() === '') { + i++; + continue; + } + + if (/^\s*(---|\*\*\*|___)\s*$/.test(line)) { + ln('
    '); + i++; + continue; + } + + const heading = /^(#{1,6})\s+(.*)$/.exec(line); + if (heading) { + const level = heading[1]!.length; + const raw = heading[2]!.trim(); + const id = slug(raw, taken); + headings.push({ level, text: raw.replace(/[*`]/g, ''), id }); + // The anchor is a link to itself, so a section can be pointed at without a separate widget. + ln( + `${text(raw)}` + + `#`, + ); + i++; + continue; + } + + // Fenced code. + if (/^\s*```/.test(line)) { + i++; + const body: string[] = []; + while (i < lines.length && !/^\s*```/.test(lines[i]!)) body.push(lines[i++]!); + i++; + ln(`
    ${esc(body.join('\n'))}
    `); + continue; + } + + // Tables: a header row, an alignment row, then body rows. + if (line.includes('|') && i + 1 < lines.length && isTableSep(lines[i + 1]!)) { + const head = cells(line); + const align = alignments(lines[i + 1]!); + i += 2; + const body: string[][] = []; + while (i < lines.length && lines[i]!.includes('|') && lines[i]!.trim() !== '') body.push(cells(lines[i++]!)); + const th = head + .map((c, n) => `${text(c)}`) + .join(''); + const rows = body + .map( + (r) => + '' + + r.map((c, n) => `${text(c)}`).join('') + + '', + ) + .join(''); + // Wrapped so a wide table scrolls inside the page rather than widening it on a phone. + ln(`
    ${th}${rows}
    `); + continue; + } + + // Block quote: consecutive `>` lines, rendered through this same function so a quote may hold + // a list or a table — the rules reference puts both inside one. + if (/^\s*>/.test(line)) { + const body: string[] = []; + while (i < lines.length && /^\s*>/.test(lines[i]!)) body.push(lines[i++]!.replace(/^\s*>\s?/, '')); + ln(`
    ${renderMarkdown(body.join('\n'), linkHref).html}
    `); + continue; + } + + // Lists. A bullet or a number opens one; continuation lines are indented under their item. + const bullet = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line); + if (bullet) { + const ordered = /\d/.test(bullet[2]!); + const baseIndent = bullet[1]!.length; + const items: string[] = []; + let current: string[] | null = null; + while (i < lines.length) { + const l = lines[i]!; + if (l.trim() === '') { + // A blank line ends the list unless the next line is still inside it. + const next = lines[i + 1] ?? ''; + const continues = /^(\s*)([-*+]|\d+[.)])\s+/.test(next) || /^\s{2,}\S/.test(next); + if (!continues) break; + i++; + continue; + } + const m = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(l); + if (m && m[1]!.length <= baseIndent) { + if (current) items.push(current.join(' ')); + current = [m[3]!]; + i++; + continue; + } + if (m || /^\s{2,}\S/.test(l)) { + // A nested item or a wrapped continuation. Nesting is rendered by recursion on the block. + if (!current) break; + current.push(l.trim()); + i++; + continue; + } + break; + } + if (current) items.push(current.join(' ')); + const tag = ordered ? 'ol' : 'ul'; + ln(`<${tag}>${items.map((it) => `
  • ${text(it)}
  • `).join('')}`); + continue; + } + + // Anything else is a paragraph, running to the next blank line or block opener. + const para: string[] = []; + while (i < lines.length) { + const l = lines[i]!; + if ( + l.trim() === '' || + /^(#{1,6})\s/.test(l) || + /^\s*>/.test(l) || + /^\s*```/.test(l) || + /^\s* @@ -628,6 +630,15 @@ ul.blocked li{padding:2px 0} +
    + + +
    +
    + + +