From 9f3b92d08e121b5486e9891c097bc9ad254d7b46 Mon Sep 17 00:00:00 2001 From: Jesse Date: Wed, 19 Aug 2026 12:12:30 -0400 Subject: [PATCH] =?UTF-8?q?v0.4.7=20=E2=80=94=20the=20switching=20game:=20?= =?UTF-8?q?track=20order,=20the=20cut=20on=20your=20own=20card,=20and=20fo?= =?UTF-8?q?ur=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight play reports and one design that had been written up and not built. The through-line is switching: what a card can hold, which end of a train a cut comes off, which way a train meets cars standing on the line, and what the board and the log say about all of it. TRACK ORDER FOR STANDING CARS, AND THE CUT ON YOUR OWN CARD Two reports turned out to be one root cause. `TrackCard.standing` claimed "in track order (§A.3)" and had no defined orientation at all, while `CrewTray.consist` does (nose first, relative to facing) — so every transfer between them was a conversion nothing performed. §A.3 says what it should be: cars occupy the track "in the same order they originally held, left-to-right". Left-to-right is west-to-east, and that is now the defined orientation of `standing` and of an industry track through `carsOn`. It is the board's orientation, not the train's, so it does not change when a different train touches the card. - Setting out is batch-invariant. Four cars at once, four singles and two pairs parked three different orders, one of them physically impossible. Successive cuts off the same end stack up towards the engine, so the insertion point is the train's own place in the row. - Approaching a cut from either end now mirrors. `couples` is built nearest-first along the direction of travel and reverses onto the nose, so the farthest car met ends up nose-most — which is what makes a run-around worth its Move. - A train no longer drives through its own cut. The walk began at the neighbour of the start square and never read the start card, so a crew could set cars out and pull straight away from them. Coupling is mandatory (§A.4) and your own square is no exception; the cut counts against the four-car limit. Setting out off the end you are not leaving by still works. `CrewTray.standingWest` records where a train stands among the cars on its card — a train may set out off both ends on one square, so which side a cut is on is not recoverable from the array alone. On the board, the cut is drawn split at the train — west cars left, east cars right, engine in the gap — and each car's tooltip says whether it stands ahead of or behind the engine. The history says which end a cut came off, and a move's button separates "takes your own boxcar back off this card" from cars found standing on the line. Decided: taking your own cut back on the square you are standing on is UNDOING the drop. It is exempt from trains 3/4's per-location freight budget, X13's "drop but not pick up" and X22's "empties only", and it refunds the budget the drop spent. Otherwise a legal-looking drop becomes silently one-way. Measured, 200 paired seeds, developer bot: -0.55 revenue (t = -3.63), freight revenue 1.11 -> 0.56. That cost is the bot's, not the rule's — its trains run engine-first, so at a stub industry it sets a car out between itself and the only way out, and the correct play is §A.5's facing-point move, which is the cross-turn planning TODO.md already records as out of reach of any bot. Filtering self-recoupling moves out of its options took recoupling from 625 of 1,029 set-outs to 101 of 677, and all 101 that remain are that case. Read the number as a bot measurement, not a balance one. THE SUPERINTENDENT'S RULING NAMES THE TRAINS IT IS ABOUT Reported: the Superintendent could not tell which train he was clearing. The heading asks the question now — "may Train 6 follow Train 4 onto the same Mainline card?" — and the trains moved to the FRONT of each button, because the button splits its label at the first em-dash and showed only the head. AN INDUSTRY TRACK HOLDS FOUR CARS, LIKE EVERY OTHER CARD Reported at undo 188: "we wanted to drop two cars, but were only allowed to drop one." An industry track was built as long as its box count, so a one-box industry had room for one car. Box count is how much WORK an industry can hold, not how much RAIL it has. Ordinary track was the other exception, unbounded; both are gone and every card holds four. THE FREIGHT AGENT MAY STAGE A LOAD BEFORE THE CAR IS THERE §6.3 asks nothing of the industry track — the empty car belongs to §9.3's Load the car, which is the Laborer's action. The gate now lives only there, so cargo can wait on the dock while the car to ship it in is still being switched in. Nothing can jam: a load in a green box is waiting, not stuck. THE TRUCK DOCK UNLOADS, AND BRINGS NOBODY +1 inbound, no Laborer. It printed +1 outbound and +1 Laborer, which made it a longer-host-list copy of Forklifts. Beside Packing Sheds it now does nothing at all, and the hand tooltip says so before it is played. Also in this release, from the days before: Mainline card tooltips computed from the crossing rule; an Extra starts from the Division Point its number sends it to; a modifier's suppressed grant comes back when a Whistle Post is upgraded; the Oil Refinery and the Grocer's Warehouse ship as well as receive, per the card reference; and the dormant defences name the attack they answer. `.claude/` is now gitignored — it holds Claude Code's worktrees, i.e. a second checkout of this repository. 570 tests, typecheck clean. The three published replays were re-recorded twice — legality changed, so bot play changed. Full detail in CHANGELOG.md. --- .gitignore | 7 + CHANGELOG.md | 333 +- TODO.md | 33 +- docs/DeckCards3.pdf | Bin 0 -> 210041 bytes docs/StationMaster-Components-v0.4.5.md | 58 + docs/StationMaster-Home-Deck-v0.4.5.md | 155 + docs/StationMaster-Mainline-Deck-v0.4.5.md | 71 + docs/StationMaster-Rules-v0.4.5.md | 290 ++ docs/design/cut-ordering-plan.md | 233 ++ docs/rules/implications.md | 21 + .../station-master-seed58228926-day6.json | 2420 ++++++++------ node_modules | 1 + package.json | 2 +- .../{seed-309841.json => seed-2701379.json} | 2327 ++++++------- .../{seed-143542.json => seed-2717217.json} | 2868 ++++++++--------- public/replays/seed-404869.json | 2758 ++++++++++++++++ src/engine/advance.ts | 43 +- src/engine/apply.ts | 334 +- src/engine/content.ts | 165 +- src/engine/events.ts | 24 +- src/engine/intents.ts | 25 +- src/engine/legal.ts | 11 + src/engine/setup.ts | 19 +- src/engine/state.ts | 133 +- src/engine/track.ts | 46 +- src/sim/board-svg.ts | 72 +- src/sim/bot.ts | 125 +- src/sim/narrate.ts | 58 +- src/sim/replay.ts | 27 +- src/sim/view.ts | 91 +- src/web/game.ts | 53 +- src/web/panels.ts | 23 +- test/advance.test.ts | 171 +- test/apply.test.ts | 223 +- test/cut-ordering.test.ts | 365 +++ test/enhancements.test.ts | 15 +- test/mainline-cards.test.ts | 2 +- test/multiplayer.test.ts | 4 +- test/replay.test.ts | 12 +- test/setup.test.ts | 34 +- test/sim.test.ts | 55 +- test/track.test.ts | 63 +- test/train-rules.test.ts | 2 +- test/web.test.ts | 97 +- 44 files changed, 9739 insertions(+), 4130 deletions(-) create mode 100644 docs/DeckCards3.pdf create mode 100644 docs/StationMaster-Components-v0.4.5.md create mode 100644 docs/StationMaster-Home-Deck-v0.4.5.md create mode 100644 docs/StationMaster-Mainline-Deck-v0.4.5.md create mode 100644 docs/StationMaster-Rules-v0.4.5.md create mode 100644 docs/design/cut-ordering-plan.md rename public/replays/seed-286084.json => docs/station-master-seed58228926-day6.json (63%) create mode 120000 node_modules rename public/replays/{seed-309841.json => seed-2701379.json} (62%) rename public/replays/{seed-143542.json => seed-2717217.json} (75%) create mode 100644 public/replays/seed-404869.json create mode 100644 test/cut-ordering.test.ts diff --git a/.gitignore b/.gitignore index 69c1768..159674e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,13 @@ target/ __pycache__/ *.pyc +# Claude Code's own state — session checkpoints and the git worktrees it works in. +# +# NOT ignored until now, so `git add -A` in the repo root would have staged +# `.claude/worktrees/`: a whole second checkout of this repository, committed into itself. Nothing +# in here is a source file and none of it should travel with the package. +.claude/ + # Env .env .env.local diff --git a/CHANGELOG.md b/CHANGELOG.md index e83107c..9a05aab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,338 @@ page as `v0.1.0 · · `, so what is deployed can always be identifie --- -## Unreleased +## 0.4.7 — 2026-08-19 + +Eight play reports and one design that had been written up and not built. The through-line is the +switching game: what a card can hold, which end of a train a cut comes off, which way a train meets +cars standing on the line, and what the board and the log say about all of it. + +### Track order for standing cars, and the cut you left on your own card + +**Reported, twice, and it turned out to be one bug wearing two faces:** + +> "If I put cars off the nose on a given track, and my next move is go forward, I need to couple +> those cars right back on. If I drop cars off the back and I move back, then I will automatically +> recouple the cars onto the back of my train. Right after dropping my cars I need to be able to see +> if those cars are ahead or behind the train." + +> "When dropping all 4 cars, order was reversed. It worked properly if we dropped cars individually. +> Also, when adding four cars, again the order was reversed." + +**The single root cause.** `TrackCard.standing` was a bare array whose doc comment claimed "in track +order (§A.3)" and which in fact had **no defined orientation at all**. `CrewTray.consist` *is* +oriented — nose first, relative to `facing` — so every transfer between the two is a conversion that +nothing performed. §A.3 says what the orientation should be outright: cars "occupy the track, in the +same order they originally held, **left-to-right**". Left-to-right is west-to-east. + +So `standing` (and an industry track through `carsOn`) now runs **west to east**, which is the +board's own orientation rather than whichever train last touched the card. Three consequences, each +of which was one of the reports: + +- **Setting out is batch-invariant.** A drop costs no Moves, so one drop of four and four drops of + one are the same turn played two ways — and they parked three different orders, one of them + physically impossible (`2+2` gave `reefer tank boxcar hopper`). Successive cuts off the same end + stack up *towards* the engine, so the insertion point is the train's own place in the row, and all + three now park identically. +- **Approaching a cut from either end mirrors.** `couples` was accumulated in path order with no + reference to the direction of travel, so running onto a parked cut eastbound and westbound gave the + **identical** consist. It is built nearest-first along the direction of travel now, and reverses on + its way onto the nose — the farthest car met ends up nose-most, which is what makes a run-around + worth the Move it costs. +- **A train no longer drives through its own cut.** The movement walk began at the *neighbour* of the + start square and never read the start card at all, so a crew could set cars out and pull straight + away from them in either direction. Coupling is mandatory (§A.4) and your own square is no + exception: pulling out through the end the cut sits at picks it back up, and the cut counts against + the four-car limit. Setting out off the end you are *not* leaving by still works, which is the whole + reason the choice of end is a decision. + +**`CrewTray.standingWest`** records where a train stands among the cars on its card — a train may set +out off both ends on one square, so which side a cut is on is not recoverable from the array alone. +It answers all three questions that needed it: which cut a departing train must couple, which cars are +ahead of the engine and which behind, and which side of the chip the board draws them on. + +**On the board.** The cut used to be drawn as one strip along the bottom of the card whether a train +was there or not, so "are those cars ahead or behind" had no answer in the picture. The row is split +at the train now — west cars left, east cars right, the engine in the gap — and every car's tooltip +says *"standing AHEAD of the engine — it would couple onto the nose pulling forward"* or the reverse. +The history says which end a cut came off, and a move's button distinguishes *"takes your own empty +boxcar back off this card"* from cars found standing on the line. + +**The printed-rule interaction, decided.** Trains 3/4 spend a per-location freight budget on setting +out, so recoupling would have been refused with `FREIGHT_WORKED_HERE` and a legal-looking drop would +have become silently one-way — same shape for X13's "drop but not pick up" and X22's "empties only". +Taking your own cut back on the square you are standing on is **undoing the drop**: exempt from those +restrictions, and the budget is refunded. The alternative — treat it as an ordinary pick-up — never +strands a train, since backing up stays legal, but it makes a legal-looking move a trap, which is +exactly what the "why can't I move" panel exists to prevent. + +**Measured**, 200 paired seeds, developer bot: **-0.55 revenue** (SE 0.15, t = -3.63), 3 seeds better, +27 worse, 170 identical, with freight revenue 1.11 → 0.56. This is a real cost and it is the bot's, +not the rule's. The bot's trains run engine-first with every car behind, so at a stub industry it sets +a car out *between itself and the only way out* — and the correct play is §A.5's "facing point" move, +shoving the car in ahead of the engine and backing out, which is the same cross-turn planning +`TODO.md` already records as out of reach of any bot. What the bot could be taught, it was: moves that +drag its own cut back on are filtered out of its options before any heuristic sees them, which took +recoupling from **625 of 1,029 set-outs in 60 games to 101 of 677** — and all 101 that remain are the +stub-industry case above. Read the revenue as a bot measurement, not a balance one. + +Twenty-one tests in `test/cut-ordering.test.ts`: batch-invariance off both ends at both facings, the +mirror property, the nearest-first coupling order in both directions, the round trip at every batch +size, §A.5's trailing-point step 1, the own cut counting against the four-car limit, and the freight +budget refunded for a 3/4 Express but still charged for a genuine pick-up. The published replays were +re-recorded twice — legality changed, so bot play changed. + +### The Superintendent's ruling names the trains it is about + +**Reported:** the Superintendent could not tell which train he was clearing without hovering the +button. The §8.1 clearance is the sharpest decision in the game and it was posed as "Superintendent — +rule on this train", over buttons reading "ALLOW" and "HOLD". + +Which train is exactly what the ruling turns on, and it was the one thing not said. Two changes, and +the second is the one that was actually broken: + +- **The heading asks the question.** The pending decision holds both trays, so it now reads *"may + Train 6 follow Train 4 onto the same Mainline card?"* instead of naming neither. +- **The train moved to the FRONT of each button.** They already carried the trains and the + consequences — but `actionButton` splits a label at the first em-dash and shows only the head, so + "ALLOW — Train 6 follows Train 4…" put the whole point behind a hover. They read + *"ALLOW Train 6 to follow Train 4 — onto the same Mainline card, closing up behind it"* and + *"HOLD Train 6 — it waits where it is, losing the Stage but keeping the line clear"*, so the + visible half of each button is now the half that decides it. + +### An industry track holds four cars, like every other card + +**Fixed:** a crew standing at an industry could set out fewer cars than it was carrying. Reported +from a playtest at undo 188 — *"we wanted to drop two cars, but were only allowed to drop one."* + +The industry track was built `length: baseOut + baseIn` cars long, so its capacity for ROLLING STOCK +was silently its capacity for WORK. A Mine Tipple prints one green box and no red one, so it had +room for exactly one car and refused the second; the same arithmetic gave a Power Plant one and a +Freight House two. Nothing in the rules says this. An industry track is ordinary Operating Rail, and +what actually limits a set-out is the same thing that limits it anywhere else: a consist may not +exceed four cars, so four is all that can ever be shoved onto a card. + +`industryTrack.length` is gone rather than corrected. Leaving a number there invites the next reader +to derive it from something, which is exactly how this happened. `spaceOn` is now one line for every +card — `MAX_CONSIST - carsOn(card).length` — and `carsOn` picks the industry track by asking whether +the facility is freight, which is what the length was standing in for. + +**Also fixed, in the same place:** ordinary track was *unbounded*. It could be piled with five or +more cars, and once it was, no train could legally couple them — mandatory coupling would exceed +four — so the pile was unrecoverable. Both exceptions are gone; there is one rule now. + +**Changed:** the siding graphic is off the industry cards. Every renderer drew one empty square per +unit of capacity, along the bottom of the card and labelled *siding* in the panels. It asserted two +things that are not true: that an industry card prints a siding, and that the siding is as long as +the box count. No industry card prints one. Industry cards now draw what is standing on them and +nothing more, exactly as a plain straight does, and the panel row is relabelled *spotted*. + +A Modifier still adds its box and never adds room for a car. A Grocer's Warehouse receives into one +red box; set a Truck Dock beside it and it has two. Neither number has anything to do with how many +cars fit in front of it, which was four before the Truck Dock arrived and is four after. + +**Test note:** `spots cars on industry tracks` went red on this change while measuring an +improvement. It summed cars left spotted across eight seeds, and across those eight the old engine +managed exactly ONE — a coin-flip standing in for the claim "structurally zero". With longer tracks +the bot leaves whole cuts instead of single cars, which lands on fewer seeds and delivers more cars +(22 against 19 over forty games). Widened to twenty seeds so it fails for the reason it names. + +### The Truck Dock unloads, and brings nobody + +**Changed:** the Truck Dock is now **+1 inbound slot, no Laborer**. It printed +1 outbound and +1 +Laborer, which made it a longer-host-list copy of Forklifts — three of the seventeen Modifiers were +the same card with different names on them. A dock is where a truck backs up to take delivery, so it +adds the red box rather than the green one, and pays for the only inbound grant in the deck by +bringing no man to work it. + +Two consequences, both intended and both visible before the card is played: + +- **Beside Packing Sheds it now does nothing at all.** Packing Sheds is `flow: 'outbound'`, and + `usableGrant` drops a grant on a direction its host cannot use — the same rule that used to swallow + the Ice House's outbound slot at a Grocer's, running the other way. The hand tooltip reads + *"+1 in · goes beside Freight House or Packing Sheds or Grocer's Warehouse · Packing Sheds only + ships, so the inbound slot does nothing there"*, which is the whole decision stated while the card + is still in hand. +- **It is the first Modifier that grants no worker.** A facility's Laborer count no longer rises with + every card set beside it, so an unloading industry can now be capacity-rich and man-poor — which is + a queue at the industry track, not a bug. + +Every tooltip, the panel's grant lines and the card reference are computed from `MODIFIER_PROFILES`, +so the one data change carries all of them; nothing prints the old numbers. + +**Measured**, 200 paired seeds, developer bot: **-0.03 revenue**, 0 seeds better, 4 worse, 196 +identical, with a Truck Dock standing at the end of 16 games either way. Neutral for the bot, which +places one in 8% of games and does not plan around unloading capacity. A human building toward a +Power Plant or a Grocer's is the player who feels this, and the harness cannot see that. + +Both published replays that depended on the old grant went dead and were re-recorded +(`node src/sim/save-replay.ts 400 --top 3`). They went dead twice more before this release shipped — +the cut-ordering work above changed what is legal — so the three the site publishes are +`seed-2717217`, `seed-404869` and `seed-2701379`, all recorded against the final rules. + +### Mainline cards say what they do + +**Reported:** "mainline cards need a tooltip stating what they do. Hilly and Uncontrolled Siding — I +have no idea the impact they have on game play." + +Both are invisible without one: **Hilly** charges freight double what it charges passengers, and +**Uncontrolled Siding** is one of only two cards where a following train is not stuck behind a slower +one. The tip carried the card's name and its modifiers and nothing else. + +Crossing times are **computed by `crossingStages`** rather than written out, so a tooltip cannot +drift from the rule it describes — including the Hilly split, which is decided by whether the train +carries a coach: + +> **Hilly** — P60 / F30 — a train carrying ANY coach crosses as a 60 (1 Stage for a fast train), and +> a freight-only train as a 30 (2 Stages). A slow train adds one Stage either way. · One train at a +> time — anything following has to wait for it to clear. + +> **Uncontrolled Siding** — 60 — 1 Stage for a fast train, 2 Stages for a slow one. · TRAINS MAY +> PASS — two trains may stand on this card at once, so a following train is not held behind a slower +> one. The Double Track and the Uncontrolled Siding are the only cards that allow it. + +### An Extra starts where its number sends it + +**Reported:** "Extras should start at Eastern or Western Division point based on their numbers. Even +trains run to the east (start at western DP), odd run to the west (start at eastern DP). They can +also start at a control point (any office except whistlepost) at player's choice." + +Every Extra used to launch **eastbound from the West Division Point**, hardcoded, with the +simplification flagged in a comment — so half of them ran the wrong way and the Control Point option +did not exist. §2.3's "odd runs west, even runs east" now governs an Extra exactly as it governs a +timetabled train, and the New Train phase **stops for the decision** the same way it stops to have +cars placed. Measured over 60 deals: 32 Extras started at the West Division Point and 29 at the East, +where before it was 61 and 0. + +A Control Point is any Office above a Whistle Post, so upgrading is what buys the option — `check` +refuses a Whistle Post, and an Extra starting at an Office takes an A/D track like any other arrival. + +### A modifier's grant comes back when the Office can use it + +**Reported:** "Restaurant attached to a whistle stop, then upgrade to depot — depot only shows one +green / one red box. I expected two, because Restaurant increases outbound by one." + +Exactly right. `hosts: ['office']` includes a Whistle Post, which is **not** a Passenger Facility, so +`usableGrant` correctly dropped the +1 outbound when the card was played — and it was gone for good, +because the upgrade only ever applied the difference between two tiers and knew nothing about what +had been discarded. The porter landed, because porters have no direction gate, which is why the +Restaurant looked half-applied rather than suppressed. + +`TrackCard.modifiers` already records which Modifiers served a facility, so what was dropped is +recoverable: when the Office becomes a Passenger Facility, each of them is granted the capacity it +always printed. Keyed on the **transition**, so a Depot → Station upgrade does not pay them twice. + +### The Grocer's Warehouse ships as well as receives + +**Reported:** "grocer's warehouse didn't get extra outbound slot for truck dock." + +It could not — and the reason was in the card data, not the modifier code. `card-reference.md`: + +| Facility | Car | Direction | Laborers | Out | In | Track | +| --- | --- | --- | ---: | ---: | ---: | ---: | +| Grocer's Warehouse | Boxcar | **Both** | 2 | 2 | 2 | 3 | +| Oil Refinery | Tank car | **Both** | 3 | 2 | 2 | 4 | + +and in prose: *"'Freight House' is not a card. It is the collective term for a freight facility that +loads **and** unloads — the Grocer's Warehouse and the Oil Refinery."* The engine had the Grocer's +inbound-only and the Refinery outbound-only, so §9.3's "Passenger Facilities and Freight Houses +permit cars to move each direction" named **neither of them**, and every Modifier grant on the +missing direction was silently dropped — Truck Dock, Ice House and Forklifts at the Grocer's, and +every inbound grant at the Refinery. + +`TODO.md` had recorded this in v0.4.2 as *"checked, and there is no bug"*, on the reasoning that a +Grocer's is inbound-only. **That premise was the bug**, and the note is corrected. + +Base capacities stay at the engine's own scale — 1 per direction a facility allows — rather than the +card reference's 2/2. Every industry here is scaled down the same way, Mine Tipple included, so +raising one alone would be a balance change rather than a correction. Two things left for Jesse in +`TODO.md`: those numbers, and the fact that the engine deals a **Freight House card** (6 copies) that +the rules say is not a card at all. + +### A defence goes out with the attack it answers + +**Reported:** "just like the opponent directed cards are removed from the solitaire game, remove any +of the defensive cards whose only purpose is to answer them (Facing Point Locks and Water Column and +???). No need to have them in the deck when they can never be used." + +The third is the **Overpass**, and there is a fourth: Facing Point Locks exists twice, once as an +Enhancement and once as a Mainline modifier. Seven cards in all, and every one of them answers a card +that is already held out of the deck: + +| Card | Copies | Answers | Which is a… | +| --- | ---: | --- | --- | +| Facing Point Locks (Enhancement) | 2 | Derail | Action card | +| Facing Point Locks (Mainline modifier) | 2 | Derail | Action card | +| Water column | 2 | Watertower | Space-use card | +| Overpass | 1 | Railroad crossing | Action card | + +The engine already knew two of them were dead — `ENHANCEMENT_RULES` marks Facing Point Locks and the +Water Column `dormantSolo`, and the Overpass is the one card with no code path at all — and dealt +them anyway. The pairing now lives **on the card**, as `SimpleCard.answers`, so it is visible where +the card is defined and they come back automatically the moment `opponentCardsInDeck` does. + +The dealt deck drops 213 → **206**. The catalogue is unchanged, and so are the rules: the tests that +exercise Facing Point Locks and the Water Column now mint the card directly rather than fishing it +out of a deck that deliberately no longer contains it — a rule nobody exercises is a rule that rots, +and these fire the moment their attacker returns. + +*Also:* a pending Extra's placement now gets its own heading naming the Extra and its card. It had +landed in the "Making up the train" group, which — with no tray being filled — had no train to name. + +### Measured drift + +- **Deals producing a completed unload: 12 in 40 → 6 in 40.** The Grocer's can ship now, so the bot + often loads there instead of unloading. Unloads themselves are unharmed — 30 completed across the + 40 deals measured after the change — and the test that needed one widened its sample rather than + lowering its bar. +- **Moves per productive act: ~8 → 9.9**, with the crew doing *more* work (1.48 → 1.84 acts a game), + not less. Westbound Extras exist for the first time and the Grocer's gives more switching worth + doing; a crew shuttling for its own sake would show this ratio climbing while the work stood still. +- Both published replays were re-recorded: the rules genuinely moved. +- **Three reachability canaries were under-powered and are now sampled properly**, not relaxed. The + anomaly check ran 60 games while its rarest subject, Red Flags, fires in about 4 games in 200 — so + it reported "unreachable" on the luck of the draw, which is the opposite of what a canary is for; + it runs 200 now. The sound test pooled three deals while coupling happens in 39 games in 200, and + on this seed stride the first game that couples anything is index 13 — twelve seeds still contained + none, so it pools 24. The action-list width bound went 13 → 14, re-measured across five seeds in + all three opening deals. +### The Freight Agent may stage a load before the car is there + +**Reported:** "A freight agent should be able to load an outbound green box prior to having the car +there that matches the load that he's putting in there. But in order for the laborers to move it +from the green box into the men at work track, they would need to have an empty car of the +appropriate type waiting there." + +Correct on both halves, and the engine had the requirement one step too early. §6.3 lists what the +Freight Agent does — *"select one Rolling Stock from the Division Yard pile and place it onto a +Facility's green Outbound box"* — and asks for nothing on the industry track. The empty car belongs +to §9.3's **Load the car**: *"a load in the Green Loading Box and an empty car of the required type +on the industry's track"*. That is the Laborer action, the one that walks the load Green → MEN → AT → +WORK. + +`freightAgent.stockOutbound` was rejecting with `NO_EMPTY_CAR_SPOTTED` when no matching empty was +spotted, which made the ordinary sequence illegal: you could not have the cargo waiting on the dock +while the car to ship it in was still being switched in. Now the gate lives only on +`laborer.startLoad`, where `startableLoad` already enforced it — including the type match and the +count against loads already staged or on the sign, so two loads can never walk toward one car. + +**Nothing can jam as a result.** A load in a green box is *waiting*, not stuck; only a load on +MEN | AT | WORK locks the industry track (§9.3). The staged load simply sits there until a crew sets +an empty car out. + +Also updated so nothing still says the old order: + +- **Impediments** told players to "bring one in with a crew FIRST, then the Freight Agent can stage a + load onto it". They are not bound to that order — it now reports the empty siding as the second + errand rather than a reason to hold the Freight Agent back. +- **The bot** ranks a stock whose load a Laborer can start next Stage above one that must wait; it + falls back to staging ahead rather than wasting the option. Previously "first legal stock" was + enough because only ready facilities were offered. 40 games: revenue and trains unchanged, cards + played 16.8 → 16.9 — the bot barely exercises the new freedom, which is a human's to use. + +Four tests: stocking over a bare industry track, stocking still refused with no matching loaded car +in the Division Yard, a staged load held in the box until an empty is spotted and started once it is, +and a spotted car of the wrong type not counting. ## 0.4.6 — 2026-08-16 diff --git a/TODO.md b/TODO.md index b548d6b..636eecf 100644 --- a/TODO.md +++ b/TODO.md @@ -9,6 +9,20 @@ Ordered within each section by how much it is currently costing us. ## Next +- [ ] **THE BOT CANNOT SPOT A CAR AT A STUB INDUSTRY, and the cut-ordering rules made that visible.** + Coupling is mandatory on your own square now (v0.4.7), so a crew that sets a car out *between + itself and the only way out* picks it straight back up. At a stub industry that is every + set-out the bot makes: its trains run engine-first with all four cars behind, so the tail cut + always lands on the exit side. The correct play is §A.5's **facing point** move — couple the car + onto the nose, shove it into the stub, set out off the nose, back away — which is the same + cross-turn planning already recorded as out of reach of any bot two items below. + + Measured over 200 paired seeds: **-0.55 revenue** (t = -3.63) and freight revenue 1.11 → 0.56. + Filtering self-recoupling moves out of the bot's options took recoupling from **625 of 1,029 + set-outs in 60 games to 101 of 677**, and all 101 that remain are this case. Nothing is broken — + the game models the difficulty correctly and the bot cannot yet play it — but **every revenue + figure in this file measured before v0.4.7 is now low by roughly half a point** and the rebalance + pass should not read the drop as a deck problem. - [ ] **WHERE THE LOCAL'S COACH STANDS WHILE ITS ENGINE WORKS (§A.4) — now hit in play, still open.** Trains 7/8 print "coach must remain on station track if switching", read as "the coach is never set out". A cut comes off an OUTER end, so a coach on one outer end with the engine on the @@ -345,7 +359,24 @@ and what went wrong on the way are in `CHANGELOG.md`. are wired but dormant in solitaire; **Overpass alone has no code path at all**. The shipped tooltip briefly told players four working cards did nothing, which is worse than the bare label it replaced — `enhancements.test.ts` had passing tests for all four the whole time. -- [x] **A modifier's grant can land on a direction its host cannot use, and nothing says so.** +- [ ] **THE INDUSTRY TABLE STILL DISAGREES WITH THE CARD REFERENCE — two items left, Jesse's call.** + v0.4.7 corrected the DIRECTIONS: the Grocer's Warehouse and the Oil Refinery are `flow: 'both'`, + as `card-reference.md` always said, which is what let an Ice House finally give a Grocer's its + outbound slot. Two discrepancies remain and both are deliberate for now. + **(a) Base capacities.** The reference prints Grocer's 2/2 with 2 Laborers and the Refinery 2/2 + with 3; the engine gives every industry 1 per direction it allows, Mine Tipple included. Raising + one alone would be a balance change rather than a correction. + **(b) The Freight House card.** The reference is explicit — "'Freight House' is not a card. It is + the collective term for a freight facility that loads *and* unloads" — and the engine deals 6 + copies of one. Removing them is a deck-composition change worth measuring, not a quiet delete. +- [x] **~~A modifier's grant can land on a direction its host cannot use, and nothing says so.~~ + THE "NO BUG" VERDICT BELOW WAS WRONG, and v0.4.7 corrected it.** The reasoning was that a + Grocer's Warehouse is inbound-only. It is not — the card reference says "Both" — so the grant + was being dropped on a direction the facility should have had. Reported again in play as + "grocer's warehouse didn't get extra outbound slot for truck dock". The suppression machinery + itself was right and is kept: it still fires for a passenger Modifier beside a Whistle Post, + which is not a Passenger Facility — and v0.4.7 gives that grant BACK when the Office is + upgraded, which it never used to. Original note follows. Reported as "Ice House added the laborer but not the outbound slot" — **checked, and there is no bug**: Ice House prints +1 *outbound*, a Grocer's Warehouse is `flow: 'inbound'` so `allows.outbound` is false, and the capacity was raised on a direction that can never render or diff --git a/docs/DeckCards3.pdf b/docs/DeckCards3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..81c9e21d9d2dfb2b7630124684c2b62c2b6bf024 GIT binary patch literal 210041 zcmdSBWl)_6qv_;GoGG0Y7liOoWVtc7~R4(7e11DjxPG3?c^3 z2G(|F3n)C`5EL)oDGZt1__xNIoW=Ts#%!0nK&vqnwXk6n%Elsa>2;?+X_EF z9JGn8@$X=Mx&22}zoTIgv2(F?CS>Adkg+g!(k5j6Ws#8ScZ`Hg|1tY*3n26R88hRb zmp>xRe?*x7h%o;VVfiD%@<)W_j|j^j5tct9tbasU|A?^u7SSdAd%P@vi&x0S+1$>N zQpwKH#L<~h$-u+L&ej-^IuS<`17{06TTugN6G~BTW=3XqMouO+Mh<2!HYPeoRtiQ& z3V@iLo$>#Rgo2};v5S$3BjwvWkQ|T&kP(n0kR6Z{;MWw$8K??~5J(Kj9q_IRkoE5w zGY}&X6A=4fApm0g|El=+rT$v|e=7}Ow*ac?{2$vz$;8Rd#Su_jzMWxS7`uyMBL8S8Sqe?iIC&hI}GB?giOB^4tNGws*$pZvo-@@+cKz_ zxH~gQ*#P#X@ShivKQB_hY5~}4#uf&`cJA5$BaDO`T+D>*Ow77~v;)ioLUSVgv;QO< z?Of~u?tbs}-xCI91{FsGTPOQpOEB_a5K(3jHF32tGEtHc2G~_GFmwWl{Mq<_mgz*u z`1g$bGW=IY#4W6yO&l4-tpO_*H8HX?Herx8u{CoxCuCvb;QXcKAv!orFJ)S$zfs}FuAyLm| zOTyq3VF>(u@qqk^LhObax^O#rxdg}-s=MO%=1^`f-?9x=%=77zM4e=kNN2Um$TY`) zJiNVxt)E}wz5P&2Kp1=NfL?`r0{T?;wNaD_zq5!fE}Z$I)A@GpZIj^Hv(H*xg;J#3 z-el(^4XS3t4->j8o;*-f3!8%rKE@htb=>DrYnX{V4kb`FOcN07oYZV8u~W!wx8;$Q zF%(Dl28{{3Vs#qF7sq2`Mi35E7f=kGjr9<=#M1@>WI4XI%+CX&YiWh5>r2;UvTshu zaR$!^Q8==XF$NNxSkM;{7LHF?0#DnCN<2l3H*0U3pCBj_%y#v;6(L|=c0S(-I6ghU zZLXLIV1FuLDlOfQmdiVF6*Q7oNVO&7TyTv-57i?Y?i0oLdh=YgkE35QYEy5R9T^V1 zI9lGqbN(oArk-GSvaM_&+n*qdQBAtQ3(?3WBj^@sz1)z=yO;g5OprP!P(@I7hFaq1 zq3ew-H$8`Yy_BZPiBt_~d8r9k1;zuTsSo!h#o&llBdz^+iXEkR<>UiL!>*7StqEvW zQ@H{zQC$Xau>*QW?0j=Nr|gHrPQ_)*CXHR*_`VUAjXm!eB7IJuR*UrB{uj9SW>QMR z2N8>=vqS3J-k1jfpk~ z{Y>{ZHP_iYEU{De?S4pgM7^qz>K-iu0+R-3$>nQUJu998zUu|YDdivpGAB`SUu2g! zts%{k`8D$;m9`|I;hCgOLcZVM9x=)uQJ9fsQf@e?Dpz6Sle0GRY-`x_fF^;cuyXfs zrEU0iA)zK`RN&kkpsNVH%ImH@;XU?xvE^(UE`QXycF-4BCJUXfTr{d-5Om<_NcVP) zd$AQ8!GeSWi=qF_6B3g`U^Z4_3evSe&EbZD;D!SCgtd+^bO0JXLdHzj4P#*;T%bco z$JwdD;@=(2GODGlcrL^U1Cab7D-d zN?m^OB;0}JbS#-zz_kA;JCfG<=qeGbnj$<|({%a?D|@>=(S7}MeQLU<5=&&UA+|al z7qvEsRCwSFVY?Ei3|4G&3Wmf{Zu!<5Xm}T`Vj8JE80}?5n}Pu^hNy>54NYCW@T38bAY8w={FV5V@7R`(7+_FJIUl-OvM^?PiVe=v&J;-eEvc_5Z- zK>+Es_9PkD=lBTHJly-_r0?EbTq5L^~|GEf! zle$u^I@xHSO;YJ&i?=ydXY~SZ+qLL#cPcBw1VOe?AB%p}QhIK{#7s-H+X+`p2xi5c zG^jBgsv&5k==b$4TPL0NJc9J#RznzkZ`?zG2zW&jKEeKe$#0viurL`vVxF{kJoh|4 zmD87+Qn3f5Z{O^uhmLfhXKEj8v^GvFjA3OQsijgMoBy%Ct}j2NO*-|StO4NpQDFj<4`a80cR#KGcu4>e$;i%2Fi1+NQ zWc}GuYuZS*mik1$u%aL61%uAlU;+0deY;i`A44+a#P#;b?;%^In%5$%N~oh|*AALe zeN!CdwoyH}xADw%l;&^CpW%7JCy z)50G>jg7&6v=EUceKKd2B)Ra3HQ1F`nl#7mfmJ1!nETrHr63+mbNgEC0-o~`79o?h zV9z}3Ah{_6?BdRH$MW)Ew%WZf-VD#|h|kkFhEz==NHiu+Q0?b75vuJ{aStd<5q2t-So#{@ zLa6L_5lI16rocihPf`gKp`TpAVzI31F@Y8!k^+9T{br9jJ0gAN%W<;)|a(X6o zqjoaUf{qmJy>G8uaqx7zKDlq371k>*>ej$c9SlD(kEct^*R%ZVJW_(`XnFIwvU=txH(fJl+sbt_MLgesUXdt(Fv4OHdX2yPRw3XTF=w< zsH`KIGPb|hS4+b8TIX_F-)uiDob2JDA*{`+Vwxdm-y?X<*zX%I@7WK^gvVtNJ1-YM zd+^vbcXC@n(VQMw^X?E8$gFGzIq~gQ?i&OtpE3J%uZ{CJ0#;U1JX3y3OG-K7grQn? z!YY*>DqnMTtnM1L1s6CDA-Hi$5|swF#Xg>~yNpB>+^VC@x$$1cbvW{_hnVrt?e5D%vrU?672=1&WjqX9)S((s6@Glc539gG?}DsA|}F-yy=JE42+t4=I^bEc7xsLd=| zbR<1WUQ70+V#9_|5=DnP7^21vo_?>*WoAJ5NBsBKl)f>epQ@Tfc7DC94@2(DBK(wgvsISKX*Zk~h2P)550eh}dAQd&Kx>GbE1Cguq9uFB} z^&xvFXo0Y^bh?!qcSKR)y48)sQgzAY0h!lGaY375=a^#Lr6*99$AaG#{b!Z=^RW>s zBRE>PNJ=D{ZQnLMq^m&@4D>4j4~(~ZgZCX%a8$YtP&$jwqjeXY)G2pl z!C?8b0Q_F6-pr4gAu1a_*dZQAfXFyHK{~9R$PiX8 zr8{GgO)uDS(vE^HzLY0r3-CjI-M z8)u#e-}^YErF#s}6=+8%7w^Xc4PyV{Y`f!S7;23SN9twPfutf~v0`D7A=IYpZ&u+H z#iErP-xn5D%#Txho1~L$6|`!Ih?Vp5jG))3mGftDBRU(&-aUs9*;_GEns9n=U#(b* zHyI6S$rmT1zV}1&H_a+qeD__dTE2I!8R)t{$%n<)Ukr=C8j!}4h+h_-Rxs*=v)Ayq zp{A_c!{K&q|DuJSI2D`~S0rSo$ijJUdb`l=mHgyoUOZgXh6tg-4p&hi)fKdOzbXX< zcU`FB(~u<~&eE5FWt&;ehK_A$)PvPbU3A^PC~qEm{^8^LD9TF}+n#Ss^EsW#(>Dk>A}Uy?$Uigm`JT<_tqPr=bd_Wi5qusfz2#)GfeIAQOV5#(NMoL zfL8t;;>PaF+HY9>7wq^8VgJHme}R_YSm-~28iTMLgS?%ije#|TkpUso-;vHQfc6ii zU)YU7*~QTL7jvKj!2G{W$s5?10Pq}xuz{1wZwX-$Q86(w8W96)3qwZ>I$=9&V+Ju> z06Ma;HDge>uobd(viQ&WFAY%>CnHA-duKaG!ru^_LEOU8$yvnQz>$!J89+z;$C!zk z37~H5Z2p@?_(f#^0OfzN2)|VSTe$ya`Zx0Y9qYe}4*=oJoPK$e0`MUgMnbk`)+U6E z3_?yuzkn_W8zTTE{u$FT112H{_L3$RX6An$3Avj6j*F0ug&jbNxc`{|5FpcUr|SP9 z$;il{@gG{hY$!XM*r)+ie#;2|#KE8>VK^IUsYB_=%1DSjky1z^*@!C`5&bf|F6>iA6J#>cd`DD%KD32`A1oQ>;AV` z|2K90dvO1zk^(l}-%85*r(FM0Qr3T6Qs#e@l#%7Hk^-2ee^Juk1k>M3`kUhV$BKXP zVgC-&<^LzHh3R+E82&wE3-dox@yqgGsrcWsEzImptiRO$`q*>N?8wx%!{I>xvIX}j zaAi}JZxHN;ObNX_SwzKdbK;If+w`34LLQAgbUiw1dVA>!A5x&e&5|cNBBU=<%xA`r zmrXMQ<@xCUdN0*G0Rj+GktVIeL4Gm3%(_&-b8h zT>PIO?}|*+-G(Imp%!Zf&~%pUFVm31`&@eLff=+MPWMdgd^q`k?me1oKV=vusq2Kw zR#-Vltp{77Y4lazUg`EeoZ-)2iQhjkW{-(%31N@ibMS+CzPv>8@oLf@*_o!uxc>Bd zhFVccO$?`5+L8vRKO;I6Vuk+@9UevS#l)42{d^R1;(qB_Dtqhv4utkJ%vji7VSE6m z-52!zgHyIPR01(B_WS_EG)r5!DP}$v``P#FnJ#jS^Ep-(-oJ}^3jfdu`OCh|Xu(lPh~wSvb;ss~OWtbIY+)50m>@ndaXZkc-$ z_yXD$PMv1tyPU6pFRQmcdCb)1URCg_sP7uZ$Kua7XJ-yV_df-}bM=QKn9@_mRvh0P zehPlHh2}jJ*&So%o;bUC)%Bae51p_isB})1L!(?L5Bd>Iv!&ea z7T0STM`9m?e#G<*Fidhtt@oCB+E0>peKU zRq&LHE30f|>sohYOH|EL67>7vox8B+g;hnXrmvwGevcY>y54J$|mi~onIzx z&nw}b-^iMj=QEOKiU70MNS3mkPoab5M|m)*xeO+{i|qOjBV`LH6=Hn>tCOJeqxgi9 z+zW$_MJ#oLb}3hA2#A`j=9|-q4x5{F4xOesK-Nqmm2@)ZCko~rPVfaI$PiMTjktiJ z(>zy6-sH+mWu%XG%OpqRoQ@a5QboZTpOZ!qTDBCk@%g@`K?{6HgKs)}p0Gk&g2$sE zoGSyIUbEB>7}+_Aiic!H53!(`OihUeo&!eJ(l>NglSP9&ey&Kxg9JxH1MwEEl=8IG%?7WQZvRUvE`@n}g2q`8PtmB~3IPh|&4WE4WD?EY z>Y$T?BSsaf&(!fTR$NMzY`u!AR#Z};<`PRmg?QUKM_Be^xp31knRQl_O?ve!R*+Jc zt1=a>H9x{#yq(?I+n$*5sc32|_T`z1(9{jufbL+R1c5LGfJCRb#1}9XE;pO1A&C1s zwOCueax)LaS@sx)&QghXNHdApV6dRleW9+bMKIZNT+on}IH_xkx0C=Mlhcp=Ztq|g z03N3wa9B_UEyEshg;?6cWCPV+WnAbe!D6En#Lw ziq(^%-y6sm5ur%^wN`VG8{tBW)mN))UJLI> z2@&onoyEDr*uqeyL6zZp{s8EpiF%_ddO73wmcq)x+XW>H99<_L7&|*5^`)Yhl96a3 zIs=04V5kV-$h-SST#HtlK%=7rc0BX5lVMke+Fi?xd62gH%cgZTnYGHMsILxP3+y6u z9tXx&L=Gedfj0xxg+3QesNcFj^NBpp>ZRwkb|BZq5cS0tVH!$WVh{~pY#4=xnN?p2 zCg*ZdQ(Mh!)+LHI&mw)Vuv$^N!!)2d2_?CtTsD?Y~KwtF+ zjRQWZkU1b$z2Y(i4p*4oP_#P~E2}lJ$a6rbJwqXNsWCAF7)m64)cUrSeM}AiRjdS6 z&!KH;bfigMb7aNyAQ!pWMOr4_@V?M;rxu<&b4NR!Qc%;rr`@>{xL2?3g;CnDO&*>;m7T8J_87 zb|3fps({68MW*tGhR#Gg13@lS!>YQGgYGFPt0wznjjWgh-Y$G(_tv!T`|S@ooiPiH zA~JQEIa4x40=(cb>2!xeiSh$UxF^V=Nw`)NR%U18B)HaC(_@W=4z~Tq23-+;Q+-di z{pjM!(Ghuo7@5tmorAJ*@X%CB#RsWdQ!--$7HYnLeL0Av$SSV)i&p>DEto51%P+d$ z186+XT~eH+6&K=MrnKr%dBlgqv57UmCX=Y5li;c*1r>M}hgCUFp}O^_$9-S3V#r69 zVZ&eBK+3Yo3Bw(UYOMci5Q@ zBJc@>Bl&Cog{(E?&_fLq`>BY+Z=CiN)ct#mX{1W3+cZi52;= zDRPqceWR2TrMhA!Vpg=!mqU$aE4~AEa19(4{rz~c1vx3{k#%2PYGcaE-+uTwpAf^5 ziQXC#_!<85Yk(&42OVI7XB>pA942AS|$V2=?scpwm z`7c=|Jx4QX|C$%))?8bCx!C#jZ?X!kXK+H-=E0*Miu`n9VV`=MH?cXuu)-(|S%fya z(+M#xWy8Mm1ja+1fG>U+@iKE2MXY-HsUu-Qh(Rjc$-{CpvumkJPx01HK4^(A%^4e) zH8J1}YUQZXNzWb}0A_uG3rK}R0>>bU=;J4f$MuRiL3;C7U%zG2@DaH9^-nx8UikkK zIsZOr{;LE1jd%au&G>(wh5nmQK$-rXN1(sk>%Z89-i3;a+^J4EQwzZ1r7@uc9 zZ*Kl?4|nr*ZYjTe^4bZWma@G`rVTe8twT5rd6w_3H9K|90=NXPfTmpF?lgDQeDfZ{ z-&KcZirdx`LV)aLoE;I5!*E%U##VO>e3AN#Yd8G1Y>o|K)c)r-KL;IY?-rdx6&*bK z6lR+pHubU=V9%FbqBb{Qr$(PP-Y!D~y)!F*j*C)S$)oKIB_QZE=dac!vYmeRI)N~i zCRfiIC_C92hnW7H;5y&WYgk^+bee7hSgxWekX?7OOwH^Rt?*t*G`1uTXhz6*o*_wB z+P0_Hq6BXE`_>?vHJy% z!a+SdwK0V936mg0Uu{$fh|FS*TR}H%R&a#O;+cK_>=u2}X14o|Rx%7!KGWkhw;pk= z02uiQYhMD?LP#sseE(C~xS7IM0YN8(o3iwFLN25+{-t=79ZQYbj^ISz(y81A0d+}- zKE2(Lh=URo?I>J-(m}Q85rz`3QEa$)ENzS2PnO!b*JmjSKG%>3_fyJ@jrHp9RoD3z zOh_3y=zN!+#d!0|tlK$yHp%8Tm)lzp-DfB8BC6SX-a$c2HQhd+wn4Zh74B6ZJs;7Q zMypnb(W$j4`%8)*PiVBzZDHW8K#nj^*vD=Wx^m`4(~A_q?#(H{<0o$>C&%*QVN~P4 z!xW-oR{7UVE`I6+x{O%BqplPQlk~$A7t(o#U{4>y>FBmp|KYC{jmoNS4mal% z;2?v8NV((Cq?xZnFOZK`&A_mfBTyUhwZ_uC$fG@O7qO|BN=@v*E<&P=0-87d1Zrhw z8*OC*9zx9?_y-)}VXi-j#+=loWa0^S-#$ty_U(;}`t3lzol^2vKqf z~NfIb}e^St5 z%WudxC*>9mwa9OTB)Ye`#w6}%jcI~3Fb@np(;^8xxhj6EgPaj}4Mm)b&qN_I@3mE_ z3f_b~c<5;NE51`}mP-;V;D~Ln%9dec>4tqgF@dgDT2J;zJoP zm$6VT)Ir#NxeFh@KkHf@wNtQwC&@g(!m!4nO0X$(_XU0cL!5egJh+qYuDN76sd`g| zOzg0)@?k|D3bY}wb$&i> z+E>zKxd7O68DZ~lP$fu5SUZ0xBn z#;S=LRM?cAGv`58WmOY%oOymYLZ329RMUC7w8x(trT$3+xqAOx(#=B~2&@YY3gMun z@Xn4JG)gzOb2_@swif_j<&q8AnGWPdT*y1|Fd zT`fP3-8SSu;MIIln`Co_4`aw%;mMlp920i%f?fsl0xe93pgIb1b1v)E?xm#vk-g^G z@)h0{VHIvPL%kD=S;5XFPNl2y8+9qnm4R_Jpk1_P4nlxHYF6II{)p_5zmy&t)E_Ya z6xQOg78?p9oR$_!mj=;rLd{0nTt*`^`oRE>-wOn{wjoo|;Kw!;w(JO%twq+A7Y_AQ z3I=WVX9fm(MncqhC!V-$PYvY7kG$XkMCRI973CuKGG$4k1q>ubX3Uc9))Qm ztTDxLVMSA8WUiG6wdj$@S9ww_3>4F5B|+@eY-Vb{$^4ipoENiOn|yi|^A9)Ob1{## z`+gxT4(s7%EkCXVm`fqctlTllUDAc!<~|lp9JA8lDU+&2LCIr!?~$k-d|3KsBWpib zmzuF{&NTZqzsJW;X(m#BE}jakbEAQ7QB?Ofp8p~UUiv|D{H|%&Qx3-BNj4Lg;*@c^Ev>>E+Ri5k8X2N=HR_h4hcP5ZnxmpPs) z+=F~eUdi{RQIhY)-34Vh$d1|`q#LO2XGJ*3CUnLHnPF?yrjli~;U5`i8adh}jcttM zP+2DOe`aHal}FLWYZYrIHVEsnFJ=eG306svj$E^n#EyOfLr;HAi%l~YU|D`a06z%Y z0Y8BbKp1C~^>7fP?CUVb7?LOnLwN%Rt%goA9O}5<44DJ3^^|*&6y&24cXu!d46)LM zH))ecq_n`r_c+g;?S+J1DkKn*0x4=AsBgwo(ZUqk(K55-_?mZ!P8}c(Lqe%@`V*Um z<{HsH1XUpBi#-+Q{Dfe~l)-0TxDT0|r`J)MARt9W;_Q4r4|~^FDxqB21o32q`3>NQ=m*(XojArX> zLo4*9rNqRP5k1D@t|r!+yi*iTW`o@fY_ByM#P^0v@{zYU_JSP9OKkptbygVO5B$`w z5NUI0RH*j|DR@Fp43GAb8ybGt1|9`NoRi!4)`~IhRPYh8dE9Te7gBP+IuaGLl#_B9 zxF`r$lB=7YjnHdWdSB4N;Dhl^hy+Y?@JA765^=zkl+Y>-S|N35zc>Nikx=KP%0dpA zkmNdT0hW?J@{6nwC83M_!X1PXn_&lslTB}y z2=I91zO{fUKs{ z7ys>C?*AHa@#psDzlZW?{|Dv&_hZNZ0bH;#ar_fpG-m4B;WncMUt&!AZXZy8E#2XO z`T$Hj6lO$ffn6@?mydztLC51_?AB;G?Yq^7r9}K--#;@04!*x(blx^~MfS;&|G56B z?KQ+LV~mgIeFyA$YGf1uEN-*ggji_!IMOXnTW77C`7~c|z}omVv%H@N>&i0{7P^xh zthZgSuI6a^T-9@d8Tb+=9qdwPgL#Gmk7nhuFFQFkUu`bxS7~fz-)bO(A$%VVejsbC ztb9BhW0>-Bzus7~1~iMVZ!SG{4ZEvmKEUx!-2z=-L>|SNi8yih2ig*n^g&;cU^^#! zF|ce}6V7(sa*fsZu7kh3uQXGX`&LbS?^+%>O;{LYiwtp7cBv^KGXYbn4tc^HX0%mvv7IVS?8Ou)!ngTc4A?eQW5 zW6xF0h_jwn8dA5!0pS}7jYrQxf{lWn8JWTn@J5So;5+SPj>aLYxDoQQiic&}ttyF) zu65>!>*JjBZT7heC-D|_Vsh!(BG6)VX(Zb7`x^U*;o1jiZvMQ%iOsy@T=Q*#xeD7# z7V$zXthylzi%ML85pD#ouwS|LHt{r`W4smHp2DT2zi^sN(Fi=iC4623Am6`S;tGeA zM;J88q=IkVXP~8>~c#)nN6b6 z00pBs5l)94t9}$U=r4uXG(j_Ywqx7lWV3*9SU1+-IBmjk0rEr0A+XXqShO5>N&c{Y z&x6ti&R^YxdUD5$W=TU#&HtyrpXkR|t6fT7cJp?XgeQ@)Z=-XlF4Hzi$Cy5(% z@@t!Xa&?;6%sII)nKLu>_AFcESi{Fk#|ky}ux}js762m$Eegw(>K?EyjK%( zyngFso_)Djp2F8;7#<5aMqtiVNPd>6;o|r~2eX0nP?;m2^Br&uqGPvmcwVac4$e^h zFg>>a8Nufp+nUZhLQEG0^7re$L)*tBQI6-0!#=d!_K@P$cayWLlx80D-goiEbCs2? ziz)-ib(hx12#Fw--u@rywayJ!eV2Pvd}eqIbm^vHud*WRR=3-NUiV-9%&!Y&NLU0- z%`#43(7$#wc61s%AMYNjyT3Yk-g^c0I|S8yMNc04j9kGjl0DS|sWXq4 z+H1c`0kK@|qAi1A#k~Z0_I{YpX{x*B&anHvg-#9~HkNxG1@d961qwf(@wB6$#z+xe zp8~!1bgLf(oQ$9qwtAEA%2{)qT{FSjZH$fes4fD07IXZLQg_47=cnCi9q~g+;JHJ> zg9sGLPskguXI-f9AmHKwUlL%P&P-6!Mtb+b2p&XElhtSp6tW{1uFqX?2L-kUuaQ6b zv;=?D8(TKM}#!EZf0g(h&Ol9(jeqOb(k5|V~ODJa_2G5 z6Kzp5;m#&};>UIUR1{Jk5t1V@hE6ajVNK8h*_ez&StPME-T3YN;H~fV z%tOxz`Ry8DJZ5|Sv2SeEm9 zvn;{S4YavGD;*@haV?K(ug0( zqP&hn!ZRs;a5y4!E|a26zR?>W?q#jWRx$87;X!oLLdz;emU=zbGqpBF+VY%_fi5jo zkdb@+N*MlntU&FS!SlK+H zO9}1+4X0XD9CieaPb3!nG)X}$N=;^B1cN=ZU`k2ko>y|6yUN|p6hf#S0>09!77G`F zjPa2WsDwRZ$;4fI7ZJS%fLvSpGoTds0`87-Ap)(EKtskusZ^e)fQj!!(lv?6yc$BPUE1FjM z8ptb{j<4u-qnsqHoQGU@fU@W$d<`t72o0#&a3TW86dtw)PhtQo8a0tBe|5UZnSx}x z`*j22FTb<|<(VYmGOcB`CBkH+qb2XAfrBAtql5B6N|X~52n1SQ^CC$$iO@+3k(bjX z;Sbt0`K1a_R09DgP(3Tj<)MHauqJZ2nFS7xr36jhbtWR!=zM2WXouDx%gX_*(tiDW zw1UnlLXiS>qVWB@lmjIz{k%cXoA1nLaO{8GH2=4YUjNrL&A-0{^>4Z-^Sk@~Pe%Ul z*TDXVrkR8Nf4wK8ZDY5_iTs97`0ht=K9G#IB?_%UJ6*WZLEp5t$tGCa(t%KRO1epI zJEf*|x3vW;HIhh`#=M7*p1~c-`k57%e`-9|=cn~tMb)X82)&+e{Cjo|mAIHxV)1Nj zJWd&DoT`|r6oCG$cN2)ce_EOQw3WPX>gl7VkjF}8u8H|*5lEbY!w5_OhlG7$C@Lw| zNItGWUm12vFgf+c{m1(3PR{7lHkg=G%G<^&b<&U(Q5v;I{sP)X zIA4TgfzN*G!37#<`D2@bmrA?`)#S6a@cD%F z(v)0oB>I65bQ0b!*Fa7X^<-w+v)ibPmpiu`j?b;ZVcN6_7c>tB(gk0SLot2ogylh? zRr284Q<)kfNiGL|f(u(V1}|%`5_)>B1uvTkrr#9vW66HBAYs%53E{$m|7c0VStnT; zE;&))G5}_eUX7#&l??Tgu`>{v0OqXyKv%?HA z;@yO6C?wIXI-4Xz@&};!CW23A2Oh47i$=H7_(hVvsEM`c6!TNG$)JQ(SUsrjMU3&# zDItu3Bdg!rDl0#GSOzVG>MMWeidh9#o`##W0rsd4UK6V57j)hb)?YXt1e>hS2JR+p zk^BMnqI{g|N;%#TOkU5`s*rUlY|p@SjDvA!L*W!;(Jcq9UwGFF)Ik~rv`>ZW7tf*r zR@1Ic2zGG5B-o<+0c1PXPg7LxCj{r=3V{<#o7Xk9VdxNlmZ}C$bW0Ea8}&g%%i8vg zbJD8{V^O!ULqCQe$SNz+-ar_B;8YjBf|30&@`)V9LB{862OYZ`d+4uQ=Nkd?o)Kk` zQxdRhn96v^oLwW479*MO^6kf>v3rt)BKjGV4qvV?zvSwF!_L@Nhb$d@`he{sHDSgv zWE{l%1K;O+U1*azVN{06T}u?CXU;3?#LvBMnrWqp!S=Ov`jN{f`NqsQ6t_@vd#E;%Ra-~JfnYR>*Y!N{I)Bh9;~b&1S`ZdmymM zv=vH1&Xg0G!Gs@emPRtI4_q+R?!buV-KMjgv}#IuZ`#g~09UP~qGmMt%~6U0D8g}O zzAizLJ|1r+o{HMpa-~CDI=khpFcf)a(l2!k5437l1sNHu4WYARbiEBVf$3|VYR%u()dw=yVz_djD zl;qrFwM5W@X=<4J{wUcFZA&?T3DzF`UAFnwcz)6dOrq#I@`I?Z{5UA8iMwd2Lav;{ ziqEI5jX3&!@9QO=_XMtetoKW` zKY3W?^N>JWqo%NnWbnh|GW0Wt(a|iJdiV&(B1lF`@fy=_#y6$o6W{`3t zi(%X8;Vyv1<3CR8sy*2o_Sj~9u|&>=B;?yXS?5777Ov`Rk=H-yclHt{Nk zwdN`~cgx{2#*2#{{f$Fr6d9&~O8j!IfT z?j3PKEv;~QRDYCCHC)~33(H?6)M*X?YG z3Gs0a_N>@*Eyj>sxAeeSqoGClrMEk&PR^pUM0H%13n)8iurOa!(_65yzk65P3`4*w zxy9O03g=w-!bz)zN>SmYgWwv)e4@c8q`6}Kq5dw~J7V`i@KtUyRmDB8m!vj$I}D8D z%aLRtq1%y}9iQ4u0PQ&erw>M%k}B9YxN_Cgi)VR3k64!bvsS-sK3p1MWfR4kVN?ip z7_`(IP(EeO!SYTj{4`|MFbZuc%LUJbW1Ea%`>`jPKgI1SQYg=smTPrW?m|y0k&;UZ^f=6)|9D?%&J=LP13M%Y;SfgI(Mc*${-X{@{>&FD&@jWN6h-a-IFaY87#EK}<3P z5Qsw;6U&Kgms=lc|-z;*t@YW*ozYV4{!!k3PF=UK=X)-!RVAHO=BC?`F`m_28SmS zkw)k-rcZFF zS5E#1tiZzh`zrq5?$m2(+pP_w`C#O}GcKn{uid9QQTPpjxb7BfvQDzIPlx0qM9Qw6 zP&+0YE?#wAacVNp=5bgXU()s~8h4opI7a zob_XAyePC(8mS3W&g-c}UDnj(*1GvkO~w>5bs`bef)uoXa-QT95aJaRS*FQ6W<(w^ zW6`?M`a5UNs}nzo`W3I^8_v_y(XCtcW+(XwLs#tBl7v%BI~cz2ULaGtq@$-m4pV%$ zfVeF5Rv=GguBNoD0I=MRzLZ%ss|QQYtLOFG)IpjS>cNp06?)sojllkorKhu#rX0MC zaJ)GzG$Njl;Zf7u5ZJUb7QMib6_sJ~luPl2>2-IU#JmP7NNN6_?%jJQpQ$i>A^LQ5 z80H4b%iV1BlzH)cMGEZ&G^*aOn@O4wA++t26ss(m0q3{b{R8H(3&7ehO}q&aa-Gx{ z!ze!M{+g1V-b7<8wU4pY9Nmpxy%8_){PEo?Tf_3CR1^JQiu773E?rOQc%KdWjq7j* z8lQDAa|EURp9`@GU9>DqL8;QD(?bJ_zAQ);GK+zcNc;1M2>4L)(@l-V=vXZ;Ig3q#PuNTSjp_!XkCFpWW8rv`@hiNzdLjc)GY4B4 zWww3Rqo?#R>$+j)G5{0$x{bl25>nS#UQrv%SsDw>s5jajeL9NGEAj2e06B-1DWymq z$U)0k9Z>bDSZ!sezK-p5A%i4(?o9gRS-C$i$%xDSzhR`+PQM<1E7a8Zyu%SF(mEd^GO{kDVPRv!9iILmb2gLs3EO=q;)Z%W@D;PYd=1i zWLb!Ym7Qv8Ea73PbA8;-2MhY)+>QE);q((Dj00qYs-_KDv;4AJ7lQgDXSP4t#f4tR zhKjsw9H_Js+hQqrl=Ha^#>~)dJmHUJ^!r#Nxd)D)O&^Z9Qky72=o8 zguAWxBqt_sHwkn+Q!xXs8YFw6!?FtS%;A<+&@h|G6#5S4@nbu^EZ0(q1LsyI3tn#DiihNa<1RCIO|>wL)%8-z z5WM0W2q4BCsZsO7QC5cZp+%~G1o?SPYkZ@NDzIuQe*?i>L-O=pG@d|i(c(=Ia?sp| zJE`y|70MK|rLZMI1<}$+Bj>s>G@mb%slB(P3hvn!Wp&GG!TRyxj409QkH;lE-V!W@ zp!X)STOKuK+6i2Zs)Nt*)}gW@n;#by@EhoBamhGTU>$jxBU>R!H*Ukuc7jlcEup1i zNR?=~ms!}}3B+R9G~*R4f|;vGj$wVT!c_Q@Yf$d}Vj~b;@GJD4a!Kn$vxmMc9&Bi1`CAh>kt z+(I1s-VT+V$eY!tG*N3U8*U{Hy0~23xddzjc2=XZs%eg`As&7RE}UFPB*SHErL(kJ zkz$+BcTG}#gY06&`s5!?&Hc?GP9?QxA8?(|E!GP`#>M*3#6Y)f!rd{%?jR}SgL;?$ zh(en7fZc_a2Gu}W=Vn||ebJU^H(xN^3csp5TSZl^ta4C7te-w@L_06d4^O%#;1P9K zb)!#Ul&#*h>*SINSNo#1K(OE1T0I*)8@QCzn%Y;>UF@ELIyLxqenf3FX!OSLX+C03#(Ei1pbC?+BuGV;DRVkC$Ku4i2U}Lf_#da59e2whqh$n3DD`gmaaa#vhZGqzGrP5)EIM}yZ0V=6wK_J$_i6HAm z5i7zcHblGGr6YY{dwGY@>*&x+9{`-5VD(nfUNjdaEHvy?o6a$%yB%CsqEoWvl6y&WjczEO!0+6Gfm2Hu}ZQI+WaEY^_ROl zuNs1hNhqj*#oQy(em9JG$w_(fT$%rey0?suBw5l##mp>~m{}<*F*B5ysl-%bW@ct) zW|m5fC1z%3<`PS*?%BILb6@Y9yJvdNJMUk3c)EFHWSY5|n|+@jabREL8Ipz>(Ghoe zMb98D+ut!#ll}ltB~&+>^c`fEF$;oieap}|hk9JVpRzmDiWQ4K1SIjw3ca)x|S6w?mO!1;YW3ICV?#DMb`b&WjIif7lsPX0cPjlk@aP(cT0cB zn78e^+TS~7Xc{Qu;jil^+&n5QX)(9}{{C&cF57{)(ub`K;FGw_Btm#Pt)$1!Yo4 z9Wca0N|`jNd(-Cxl80_p-QEZg#&WjAylRB(CF&}ORw5iivwYqNMxZw?5v6Secwn8& zSH#-x)5Qv)vBs?Ne;F2;_CC%y(MMfjgBtmo&|dE|Pr8&En@ z0>STxdnCq)dsUk3yndoK07C>y@>AVrAoI>d1OUGPne{Kk9JwQyHy=d8@;Y`fXSc-0 z*g1jP;wK!!i`GygN2evVvdm49o_Oqb=v@d6_M8JM0^qtA&kEo@$TWTsNDY1$gtSN?Mgu%opc~L{ zgt(G-A(0mFRy#|l7}EBNwc+|Sq)Hw$dlXt6!q3-Ks#623PF58LPMh?;C~a_dQR+60;FPG6BUb{Q@VYm@H1Ou`Jsn#rmG z5*e)5-Cw=?7%pD~8p-N42_>uW&gW*yF9-Aa$h)NXNx!e}{-{lTmoV;%5<9Ga+k4dA zYp!K!L~iL0eYisMzebW)G-`Fxl8)Zn+v`)31emw3gHe=$r+@^4T1p}bENySJB zotHmi3!!Ue*_jLfy8L+@n^sXr!Nh|S-*z0ERiQ^9AsTc1GD&f@4d=bT_9l2}E^f~u zRI_k#Lz>G?%rhC*uk=kk6b#oau2RMM@i>-nwcJASAW#!D$f#-AaM4D9m;Ku?*|+$r zaqpDpm(A{veHa~;29wo!UA@wPoYD*Z-)RZ+mNE*AX+@ec85B7v3_A$`>Hxn}babYj zJB~?}(bLlCZq2m;5AEMJ%>K5pb~8PnUa2_5F7jT1|6s12J_!djnCGvqpzQ6rB4LRN zc;v3xFXHtU@C-zoKDwgxHp~yB5FwHo;|aYcD~aWh%)x>qJ{-ZP(w}@=l)1oUTD_Hw zKx{)|;Eo)9mus5R#|$aM9I4w6B40l^+2zv}z=c!0=b}Thc+ocP?v(xY)8DT)6z4vF z7E;PciMP(IO(N`eXLWW-8l`p*0s7=Cdta31p!F^*=!Y$C#Z&s=1u0VG$@&q!k0DWV z5KXpHx^E6=_1JE?Lu_Qmz*i-%``FG1-xTZu-+FX7(4fk;EfKHB}=fxBhGF%hGG27M7&azWnOZt$iKv)yN|p**fVV`y3ccHqL;WW z?+im{hdbMKjp*)XOy zPHYtWWHdJIBcX`KapK-$Cl#jsn)!=0%`iFG8SezV^R%4G^Bg03AT}%X9D4)00TE`8 zBA0w2(-{l&cE1VW*60*&yK!Ijp%!@a`r{H89UcJ5d^pgUf;aZUNkG3UEL>KyfR1cx zqOO{VqrzNbI&6g6yq9oO4STmPW>(f$q~=HMMA{PvOig;r6`YDL85 z5sfi6O(jcI-i^a{9BZB`7UXM5K82XkT7rz6?aF9pF5_mRZ&hYaCWsrMYNGkwg}txF z-e#{uqpkjNS{`lyK39aBX$;M#TeF^pX&#QGS#Mh&ux7mnW_2Sm42M&2?si!>>P-1_ z%Z6Ok081=Z8M`mKO%9}T*s2a`>1;W0<_=w|uRpG(y9h7WHnzY1u9FPbM=yFp zP-(ri0dNZPBn125e?7j7%0A%xcE0!e@R8wtVbE{DbKBr?(vg9FlIk~-3m$bd$r^lb z1)zCDR5IOsY4J63J*|t2i?<47k0_EPYl;D(kGD0bqawOQ9lY9*741|7Z&Y*B7P^%= z{T(T@IgIG$hxS`dJR@TDHNz>$pZ11q6TKJZzJIrvxedgQ~n{S0=#3*xw zCn$s9dRJ;5s2#88k3NQ0Ayb^a0VG-s@!;1!K?5dj;iGnYT_0?DUJvX|c^#4?lD2r7 z>-*UPH#ND%HdN80SG|dH6w3IsTNa(EY+UM~6InH4NW!?SCiTIe5`pr&hC=!h3c;sW zbboI_ZNY=!kmnDZbGO2AU&9)$VFJ3DOu-b+K0S1j&2s|e2=+1EcHcvIcz_i(E=Pm zUg_pb2x$(GSOJlsEwVMxBL|tLk732Y0rh;-z^y!>X(N#|MC@G3BZygh|8KI4Auyr4)ezo5JkCR zH2?2wwZP`mnAbxHxsZHb!iWq!>9{6-4_sCZV`3!JuZbo7$lY?LP5wk+aX-YT8@P;} zfae8!_|%b_f>7Wb(E{7dPARd#=m;O61QvY!auEW^V;&%4OhLDy6D5D}_sBuQgf@ZN z0Z&0l!a?Z44}HPmufddj0N$om{jLFE@ktq2a>Iu@#VNN|2h91V$wI8~+Ra0k&3ypx z7x&-)wZHe@#+v#M0+0VL%plX>i5X=44^$q1ivEWa{Qt7;$jJUr#Hsyh+Sa>YKZ#TS zpz@fZ#}R#vqtunt?bIkpId=Nu>^6N7c(N3#IVeF^SpW3kjvyhCK&c)z&=%gq{*|uF zOE~lP=1MKO`qu8}_yU>h?KR!g4_?KHu#}?^(OOc*MI{O1_>`iwz4WKEn#7}gl-HPzpD34TgAqK?y)D0{+tJMef-V-K!JRT%1ud(fJ zj!)O;^IRyNxA3mF6Zvvi(=1h#TZ}hE8x=r%nlf1Vv83ha9s}ZFA6V0IPzsZtF?`TO&N|vcHpbD$~BKoCr`W>@;z73gp znJf;P@<*KH$QD;%9&sEniWUwwA@s=Bu9-BdWegTEBiFaZQ;+)gt?)_1QjAHdkP?a+ za;`7@c`rZ<>YeQ&m~8z+9woZGYSPM%zDk1Fd~_{n`?%b`g;-qcC9c5|jl=cpUSJfw zs8OQ@z_`4Eu^FL$%r)&UAv^b)S4s2pd|p_&)9{gCh~WW!dLTAB{3MQovk60La>(6U zmct|g$Gq-eZQVgl!L$I35aLXJX5Qa5cQ+p`oPCx$0}*3UHUrCHC&#zjgs{%O2O%uy zZuiwiIc9gXuf`x^od;QFo`*7CbMX&{jV}guw2zMX+^)rBP>U4ABn$!#>lqt3K(Vm%Q|-%G{5{qzX-6HOyG5IA7m!0zo9e_YLitVP zq&N(2y#kRUX@FmN%oI21F#Q#{dOiD#2g{2#gfLj zUz{FO$ZZO6j3Q6ab+;j|JgN!>h(slofpCG`2iXT(cB%{kNt4e?QmIby(^7${Hjx$F z17_=l;T{Vk>IMPupa%ulTK8OBA84oi@9{U|F(5U$oogk<%(+;&=xZ|wB>6yaxnI{7 zt;N@<3kY_4mV|9RrT_$8$S{zqU|fbd{}XF!i^TW0n*3t|C2d8Hva`!Wc8-^hy72M@ zJ7r+TrJUpBE7BTgAGsj(QVNW3esG2F+sj~E_r{jJh>+JYwB88J2e3qnB1W7nX64+P zA7%>stqtno8~$e6QRG4>&8Cs2anFXr74zNR0ZN|at*xu*1r8bXCiNzL5wpezlSQ|e zjU3ppOKUL|SS&fE6*b@>n<#$w>c*ll20joY%@eyq=O-9;KOpQBJO)(T>^xOn3vJ(+ zcgw`nOV1JI+xNs(1IyE>min-u zt`o!gG7(jQRYNYc*zPO>Mp-Y1ZV)=|-%0527F(a}%#7yLN?4sAGGe1|89sRSecJhk z+y32>!p$?d`c|`MVI0d=PC7pVws1ZtYMt3t+j!RuYNfJUh5_*dkIb!g2{uH)-Q`n z5u~E{gtDtUSa&wP8_AzoC&04lr|hWPi_7=Qoj0EIW3V*~5Nwx71b5RWAf`=A9Nzn^ zR@|RGImM?~6v;545&2apXd3YX8I^E885T5VrZh>W=B3>YdB;~5mt9*l@vf{%tDx8K zAO>b>vCGdBjzv=z&8+D~{*D*Sa=jPK&q7-d=A$>(it5qip->}g@S8)Y-#zrQc2IAz zV2Ie2q;88x@2Sdh)|T-Vo0|zhVV?^Xd$ev4d?LxJp~6Z_ap|2Q*=5GHiTu_a`M<`= z9QLqc?8ZH`iNzF2Ka?88$PP61h-fL!RiX{8 z0*MI)>)dRwVi|-78LmIiv29UGpTa(L%VH-2!nW~oGlCL)mSmh03n8V#GH-cvc`mWN zcCK)+(B>|ZpSwvJXLfAzM()1DKaAmHtpZ8(IVxRYs?bPPLpC_ z^BHqV?~URbNc4A09QADpL15eVbO?Rnz#*MM>Eq7Ur2%O3;a#C6RWKWud*LaIbnsQt zF#MS@rr&!I8b~d4aOD{vuxw@f;UB5lt;fF>g!NY;g-;jo5OGbR=)<5#!KM;$1ybxm zcn|^4()5SK95*n_4#7VP;?B?-SrXwx>D^9H*n!d!ks;S1Go?-=Glf=h6R~Z*M0W)U-ORa3O9!hyer#-cFMhMOi;5?6V>bYjl2)#k2{!xbdr|AFO$75vtXJUxP+{IygD`c-%o>?|-nn+QWPd#3Dh3aVzI#$Mo-g>0ymLLZd1R81e-r;mTT+H+yD&yKdo^9S-{ict4m+_KS zwu_gMp7H7xAlsbv)_vxU+Sq~f9zbsA;R8R44VxAZbfk6mR6xXP{soM{ffJ0D!SRAM zcuhra>gIGk2#kNQRa}tdbhmcfiwTqpcY82K)!rm@Ws z3o7V_W`@`H41O6|#L9A`x zjNFCVA}CHa{Iyi}f0N|VrqT0ShTYKk*B@@N(2B`eV*Lc2_%SkF1aWOUkMY1F*b(3) zQ`qhoNI~DW0*_Ez#PaigN4Zt)sfqDx zUTg;LJ#z2^_I*^D=la6FY6+K+#wIZ{*Ve0B7b+DglKyHayUaW37#w24?4$j70=BMB z#j1#{>NvtwO0B7&yH~)0oO_djUckjG)PQ1}{nw)jUy8Yzc-x`-nnF|hr5NMAgV*v9 zZEF=4IPAfT@ej}$;~b{p2Ap!%V4?Ok9W$}w2|U+KCTt^@?6jG{g#|g(Yo48vTUHq0?4Usu2-Gw4kjr2$wgJ$^T@XWV!HHk9 znsiGLSP5kxs-|cPeME4zA%w$TY5?8pRsts$%zg8t>|tOVOcBClQpVAeL^UKLwS=3; zDcqCtFqMEyiG6HTE(L8}RTzVI1|tqkP%1NAqh|qXFsA}a@>^{xju-{grIby`QlzEW zJS?_gj1s(ef?PL+KjbiG;pNpSFs&+{Y&Zu{8r})y(q4`lK4pm?ln6Ru7()^-{=3gKVfs7q1OG7@^Dp8Dm;nC-x~+PG08Nw_n5QC=XH>?^PtY`KXO?WmMQi@_z%!WqD|DAVUR@ddVCm`g=`n+0 z&}pbId2c#Dn1R02TXJc4aBp}P_GZ#4pCb|8OrtCW!%tF1X>~?4cK6=t_LOa}`ut$~ zRu$g$!?jIMmu-)S`+Rb^WJk5o@0r$z`{fcI>rh^g|agP+zjBuf_(>y?R)P*p~&nL01kE~c=LF}2&#G(Cg-h% zPaL_PzG841DT5|otDlu==`QR&vi%|2%{P8Q^L}f6g+c#)Sm+S26PKos>*?#ep2PmU z8Wyc|Ns>!u*=!O(>;glCL^xog?Ep*PE8fYa54K(X<*;Uq(PY}RB$2GE{g(jWPEM3q zKVz$Ngkf5kV+3h^!!+4ovmxgI&`C*b`5Wl2^o8m{ZvA5eGrpa?eXv#U78ya6f!cS~ zjCs-tKT|8!xF1{EddVEHS_RJDamkB=&>tB>JyT63ybRVcpa>tVFn6^(*!Bv@JE5V_ z`Y<6qQX1-n2QG|z{oK{6-+zx^#48?X!ezE0F0$&J^IXr&^#1UU!$bp?77(#*cI(*H zQiWP}w?}I?tW^LF9mBnU8l~{FdiHR3t$+sNtMtmSjYUx!k`M*O48_Ty zGUvgRcGhVon!TX6Hb49ICOSvjmfIyA8HcObOTBa>4zrQhs{TvD58o$3ijH!U$LaAY z*@Tt)WTOO2D$+qIWvrxu75}-@C5O;fXUF!6)4_Gu;K;@G-Xv1p{`;6d&^N{}U`UFr zGO2F6l2C*$6Dtvt^JJ+q)%(bxrfGx6z@$ta zI-|4@Q)u-pJ^_25;|*a)*Hk=>fS_RurQdZlk@$&c59gIUfS_qg$NOtA_=!=Cd|&Py zQXhqDzaGYm?j9IK$}%bIOp>t|%B%H8wJ7b9#v1{P)Nqi<6cjmWXBcv}r%BpC%bDG> zm$vS8QXh)@w|wgv-gf&QXqVFy*;Dy4%)+$1k2_aN0iszX!lEH!RGyQY+s@-EZ&*}I zV`j6K$69V?Sl1dCv}F-A&|kTFq3u~RJzU^Ohv!1Zq@7n!=xRh>MUx7K&EYRWUfFzX zsz9u-z7r6_`X0)dxKboxJkpvZ2DRMC&7`h9O7hFXcW@_VeBY6dfS>`KS4xm9c_~pz za4t)ftgS;niCRSW<(Qqtg(~k1V2g$R9yRnZ-%18j&4JbURw>a;=~JVZYcw?m?wqP_ z9(*2-hJdr?dz)(Eh$!oLv{hWvzxzbYM#-EgzKX4+Mgmph;N}%L5Tyg`9jC>PosF&K zWBn!FVH$(_9yu&`CNihPN4lx?u$a?pX@EM93z50g<;Ug--P*qOt((s^KM zha=GxeXP%#XI#=o8fB9<{_$VoaBU03eeAuYeKz7;qf^cL?Enu8I(P#6`e@rheLc6| ziyp3;{Gz~V6ZF|`@skTPok?j#*TC+g7~J({>PPekBWq!(ux4NICs_4-E#~66!o*Va zb7PYe3folD3)Rx}$J{?W@Z(_eyYwW_RYaW5-7f%;MxZTMwmX6fmm6 zK~T}*L{q2PCT$gTS~aWu*aIaJ$g$^&A74Sfa>VK7*aUgjC2zT;8a8!cE?X5-DAv_8E3YN5^waMA(VEM~db8z4GUr zy!&s7@xm;CAyR1Gn0@iGCt*~vYD!nlMG?}yhZL1 zsQmLoO{;M4ezHzdx6byMtLf2A96Xe69S7!i6*8 zLZLKZ>sUX&2yP|yx;RQ(;rCz^0^Da&UScyOY!KR5stY)z(}c0lE<7miZm&1~U@>1e zKai8%XVnVP5kP#EprM$_=R17a*H~Y)-g16FJ^l=(B4FqsHaI{#m@v{$8H0?JOFS7s z0XxwLYVj2u1-W}LDjb!J_`5nU3SS}I^r7;vFA(f2nJtKz5-pSF&rpUCWQBv!L1>WQ z%zv2#%n}6BQi%vAmnuML2-E<`;3T8NJLSZ=gwjx_6mb@Tc z)#*PbLNkwrk*od^=hf_8P?F?d9cpKQ2YC)2^G$P`%XUS?nv-$3j_!HXEV^qBsQqa`$8N z?J?qQ-8uQClgm30&Ih*tx3edt^b{hXxY^;3v1X0E=_CC8@}8@Y_kGdX&oe@nm#5>R zEt}2m1$MRTxDAiPYtz}n*|T450Gwh6#Il*0L-m)sxGzN>KT@=u)>}P~V)_GtS9)%2 zaUNq5ERRTi*P9+2Xr2rhagXgS8j>eeUPci&uE3OSj%wA=_x%tk8nl#c0L89&DpOA_ zi=sup&?<{LVA+!7+}avNRTHj_a+X{fSDTk>ojxq@LOgN+{*m&i01{=&oh@0*omlcA9NYI= zlq0Ze^dxt-`!{RnPj7Rzg-{6{KUCu7UYV2*!fi5Bs|!mx)OufB)5y=DN2Rok%Y3*W z^#p6Ccjx)WH*aJz)=)e_`JRNFQlrN*ld2wi<$Z%lh=RC=QmFQXCM3>%H7j?H40(Y; zY+gl!LoOvHcaWoVV`O|a{XBJJEnt9)AiHd|!LD$Gx2G|u&S2iCo95z+Xk3HRmf4o} z_vP9XKqanODLK0egEH%e|MWHYqT3kIuB#BWsz_I=0o>GQHS=|88jv_nca7(I0#sMl zm~tanPOGuDyH0Rd1s~ps`HuBIKd@d01^aKzP0RZO%_g4s^m1631uY*#<$TQ^1^6_O z^lE`R>e&VP(;pl(2#j|8con=DbYas^{l$}An;jolsoFYX#QS~sLGz&@t$BqcpJ+Qo z4?lnXk771PLu;(**B(x~<~$zB+g#NMn;?@PEhP1PFBjUxaBeYldaxILWIkA%HV+k- zoH=rjTZV%6_Q?X2TBhtO^v=+(j=E?JI{;=$L92q2b{Y>jiZxBWsQ7?hkwaB4MRq;$(frQ_ z)U8wV5$JLXnl%Sq)+?2hrUQW=ODH06H7-A@@s2-f7YeZbk7vIU^htO(_!^SlVUd*pS z4bSYmRea0pYj#%QwYO6lb%Ry1{3-cgFzZar|3n5*nQHw5mknuPli|#HYP3!6l;xQL z3V5}p;G0A|rz#4ir@W-4Mw;L{f0vgS&H1^hqq}<>Bk<k*S19hTKC7FmSCy_j6IcCkDt_8 z_yjRN-+4f4$Z?jpK~Tf?U`)?yeuX&T4lkR(($rt@=r^2^eDFM{=o zu{XOVWY$O&T6YKD>M%XRBA>_f;XQ?q8on92bMIftUPqWLktU>7t8+bHq z_tMj&eaDpiUE3sQv;6hF9LA^-m>o5wKM|!2%y)01THVW%r2v?ZK1JX&eRHBPLUW

g4rS~X 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

>q#iq;SyWWWm4(2b+mG`3wC=)1TtC9=zpXI3 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 literal 0 HcmV?d00001 diff --git a/docs/StationMaster-Components-v0.4.5.md b/docs/StationMaster-Components-v0.4.5.md new file mode 100644 index 0000000..d51258c --- /dev/null +++ b/docs/StationMaster-Components-v0.4.5.md @@ -0,0 +1,58 @@ +# Station Master — Components and Markers + +**Rules implementation reference: v0.4.5** +**Scope:** non-card physical components and supplies modeled by the v0.4.5 game. Card-created facilities, workers, deck piles, hand state, timetable state, and other markers are documented with their cards or in the rules book. + +## Rolling stock + +Rolling stock has a type and a load state. In the interface, coloured cars are loaded and white cars are empty. The Division Yard starts with the following supply. + +| Type | Loaded | Empty | Total | Use | +| --- | ---: | ---: | ---: | --- | +| Coach | 8 | 8 | 16 | Passenger work. | +| Boxcar | 10 | 10 | 20 | Freight House outbound/inbound; Grocer’s Warehouse inbound. | +| Hopper | 8 | 8 | 16 | Mine Tipple outbound; Power Plant inbound. | +| Reefer | 5 | 5 | 10 | Packing Sheds outbound; Grocer’s Warehouse inbound. | +| Tank car | 6 | 6 | 12 | Refinery outbound; Power Plant inbound. | +| Caboose | 6 | 0 | 6 | Required or permitted at the rear of the applicable train cards. | +| **Total** | **43** | **35** | **78** | | + +The engine is not rolling stock and does not count against the four-car Crew Tray limit. + +## The two yards + +### Division Yard + +The Division Yard is the active supply. It provides: + +- loaded cars to a Freight Agent stocking a green outbound box; +- empty cars for building up trains; +- an empty coach when passengers detrain; and +- an empty matching freight car when a loaded freight car begins unloading. + +When a train completes a run or is destroyed, its caboose returns to the Division Yard. When a car is taken from the Division Yard and that yard has become entirely empty, every car from the Classification Yard moves back into the Division Yard immediately. + +### Classification Yard + +The Classification Yard collects used rolling stock: cars displaced by boarding passengers, cars cleared from inbound red boxes, cars removed by unjamming a facility, ordinary cars from completed or destroyed trains, and empty cars replaced by a completed outbound freight load. It is not a player-selectable source. It returns to service only when the Division Yard is empty. + +## Crew Trays and trains + +Crew Trays represent an engine and its coupled consist. The game creates **players + 3** free trays at setup. A tray is scarce: a due train without a free tray is held for that Stage, and Extras wait until one becomes free. + +Within a tray, the engine can pull, push, or be between cars while switching. To depart an Office for the Mainline, it must be made up with the engine at an outer end; if it has a caboose, the caboose must be at the opposite outer end. A train may have at most four cars. A scheduled train can run under-strength but never with more than its card allows. + +## Fedora + +The **Fedora** is the physical marker for the Superintendent. The player with it resolves following-train clearance decisions and takes the 5-Revenue penalty for a Mainline collision caused by an unsafe clearance. The initial holder is the first player tied for the highest Superintendent setup D12 roll. The Fedora passes to the next seat to the left at each third-stage shift change. + +## D12 and seeded randomness + +The D12 is used by the engine for: + +- the initial seating roll in a multi-player engine game; +- the initial Superintendent roll; +- a timetabled train’s timetable slot; and +- 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. diff --git a/docs/StationMaster-Home-Deck-v0.4.5.md b/docs/StationMaster-Home-Deck-v0.4.5.md new file mode 100644 index 0000000..2d1cd48 --- /dev/null +++ b/docs/StationMaster-Home-Deck-v0.4.5.md @@ -0,0 +1,155 @@ +# Station Master — Home Deck + +**Rules implementation reference: v0.4.5** +**Scope:** every card associated with the Home Office deck, including cards catalogued in the source but deliberately excluded from the dealt deck. + +## Dealt card catalogue + +The live Home Office deck contains **213 cards** in every currently supported mode. The rules for its setup, drawing, discarding, reshuffling, and hand limit are in the rules book, section 4.2. + +| Active category | Cards | +| --- | ---: | +| Track | 96 | +| Office upgrades | 14 | +| Freight facilities | 27 | +| Facility modifiers | 23 | +| Train cards | 22 | +| Enhancements | 18 | +| Mainline modifiers | 7 | +| Maneuvers | 6 | +| **Total dealt** | **213** | + +The catalogue also contains 12 space-use cards and 10 action cards. All 22 are excluded from every v0.4.5 dealt deck because their opponent-directed play rules are not implemented. They are listed at the end of this document for completeness. + +## Track cards — 96 + +Track cards are ordinary Home Office cards, not a separate personal supply. A placed card must connect to existing rail. The Running Track is the horizontal row from Limit to Limit; cards there must carry an east–west through route. Placing track on a Limit extends the Running Track and moves that Limit outward. + +| Card | Copies | Implemented placement facts | +| --- | ---: | --- | +| Straight track | 32 | East–west Operational Rail. | +| Curved track, right | 16 | A 45° curve; can rotate 180°, but cannot flip. Right-hand geometry is fixed to the `ne_sw` diagonal. | +| Curved track, left | 16 | A 45° curve; can rotate 180°, but cannot flip. Left-hand geometry is fixed to the `nw_se` diagonal. | +| Turnout, right | 16 | East–west through route plus one 45° branch. It may be passed through but is not Operational Rail, so a train cannot end a Move on it. | +| Turnout, left | 16 | Same operational rules; opposite fixed diagonal. | +| Sharp curved track, right | 0 | Catalogued but not dealt. | +| Sharp curved track, left | 0 | Catalogued but not dealt. | + +A turnout may upgrade an existing straight, or a curve whose arc is exactly the turnout’s diverging arc. An upgrade is forbidden if the existing card holds standing cars or an enhancement. All other occupied squares are unavailable. + +## Offices — 14 + +Every player begins at a Whistle Post, which is not drawn from the deck. Office cards are upgrades and must be played in sequence; they upgrade the existing Office rather than replacing its card or attached track. + +| Card | Copies | A/D tracks | Porters | Passenger outbound/inbound slots | Other effect | +| --- | ---: | ---: | ---: | ---: | --- | +| Depot | 8 | 2 | 1 | 1 / 1 | Becomes a Control Point and Passenger Facility. | +| Station | 4 | 3 | 2 | 2 / 2 | Upgrade Depot only; Control Point and Passenger Facility. | +| Terminal | 2 | 4 | 3 | 3 / 3 | Upgrade Station only; Control Point and Passenger Facility. | + +The Whistle Post has one A/D track, no porters, and no passenger slots. An Office upgrade preserves modifiers already applied to it. + +## Freight facilities — 27 + +An industry may be placed only on a connected straight stub off the Running Track. Each facility begins with one Laborer and a three-box MEN | AT | WORK freight pipeline. Its industry track has capacity equal to its base outbound plus inbound capacity, with a minimum of one car. + +No Office Area may contain a duplicate industry, or both ends of a listed lockout pair. + +| Card | Copies | Cars handled | Flow | Base boxes | Lockout in same Office Area | +| --- | ---: | --- | --- | --- | --- | +| Freight House | 6 | Boxcar | Outbound and inbound | 1 out / 1 in | Freight House; Grocer’s Warehouse | +| Mine Tipple | 6 | Hopper | Outbound | 1 out / 0 in | Power Plant | +| Refinery | 3 | Tank car | Outbound | 1 out / 0 in | Power Plant | +| Power Plant | 6 | Hopper or tank car | Inbound | 0 out / 1 in | Mine Tipple; Refinery | +| Packing Sheds | 3 | Reefer | Outbound | 1 out / 0 in | Grocer’s Warehouse | +| Grocer’s Warehouse | 3 | Boxcar or reefer | Inbound | 0 out / 1 in | Packing Sheds; Freight House | + +## Facility modifiers — 23 + +A modifier occupies an empty square adjacent to an eligible facility. It is unique by modifier type within an Office Area. The implementation attaches it permanently to the first eligible adjacent facility found; it does not implement a per-Stage choice when one modifier touches more than one possible facility. + +`+ out` adds green outbound capacity only where the host can load; `+ in` adds red inbound capacity only where the host can unload. A capacity increase at a freight facility also lengthens its industry track by the usable number of added slots. Worker increases always apply. + +| Modifier | Copies | Eligible host | Actual grant | +| --- | ---: | --- | --- | +| Waiting Area | 3 | Any Office | +1 outbound passenger slot; +1 Porter | +| Restaurant | 2 | Any Office | +1 outbound passenger slot; +1 Porter | +| Hotel | 1 | Any Office | +1 outbound passenger slot; +1 Porter | +| Truck Dock | 2 | Freight House, Packing Sheds, Grocer’s Warehouse | +1 **inbound** slot; no Laborer | +| Railroad Express Agency | 1 | Freight House | +1 outbound slot; +1 Laborer | +| Forklifts | 2 | Freight House, Packing Sheds | +1 outbound slot; +1 Laborer | +| Prep Plant | 1 | Mine Tipple | +1 outbound slot; +1 Laborer | +| Coal Piles | 1 | Mine Tipple | +1 outbound slot; +1 Laborer | +| Conveyor Belts | 1 | Mine Tipple | +1 outbound slot; +1 Laborer | +| Pipelines | 1 | Refinery | +1 outbound slot; +1 Laborer | +| Oil Depot | 1 | Refinery | +1 outbound slot; +1 Laborer | +| Viscosity Breakers | 1 | Refinery | +1 outbound slot; +1 Laborer | +| Transmission Lines | 1 | Power Plant | +1 Laborer | +| Rotary Dumps | 1 | Power Plant | +1 Laborer | +| Steam Turbines | 1 | Power Plant | +1 Laborer | +| Ice House | 2 | Packing Sheds or Grocer’s Warehouse | +1 outbound slot; +1 Laborer | +| Local Small Groceries | 1 | Grocer’s Warehouse | +1 Laborer | + +A bonus beside a facility that cannot use its direction is not usable and does not create a slot or lengthen the track — an outbound bonus beside an inbound-only facility, or the Truck Dock's inbound bonus beside the outbound-only Packing Sheds, which leaves that card with no effect at all. Likewise, passenger modifiers beside a Whistle Post add Porters but do not create an outbound slot until the Office becomes a Passenger Facility. + +## Train cards — 22 + +Playing a timetabled train card rolls the seeded D12 and places its number in the first open timetable slot at or after the result, wrapping around the 12-slot chart. It then runs at that Stage every Day. An Extra is queued and made up when a Crew Tray becomes available; v0.4.5 automatically launches Extras eastbound from the Western Division Point. + +The listed consist is a maximum, not a minimum: a train may depart with fewer cars, but must not exceed the listed categories, put a car behind a caboose, or leave with the engine buried among cars. A Crew Tray holds no more than four rolling-stock cars. + +| Train | Speed | Direction | Listed maximum consist | Implemented special rule | +| --- | --- | --- | --- | --- | +| 1/2 Crack Limited | Fast | 1 west / 2 east | 3 coaches | No switching; passenger work only at Terminals; expedited. | +| 3/4 Express | Fast | 3 west / 4 east | 2 freight | May exchange at most one freight car at each grid location during its switching turn; expedited. | +| 5/6 The Sparrow | Fast | 5 west / 6 east | 2 coaches | No switching; expedited. | +| 7/8 Local | Slow | 7 west / 8 east | 1 freight, 1 coach | Its coach may not be set out during switching. | +| 9/10 Heavy Freight | Slow | 9 west / 10 east | 3 freight, 1 caboose | — | +| 11/12 Drag Freight | Slow | 11 west / 12 east | 2 freight, 1 caboose | — | +| X13 Appleseed Extra | Slow | Player choice printed; v0.4.5 launches east | 3 empty freight, 1 caboose | May drop cars but cannot pick up. The engine enforces no pickup, but does not enforce the printed empties-only consist restriction at make-up. | +| X14 Fruit Growers Express | Fast | Player choice printed; v0.4.5 launches east | 2 reefers, 1 caboose | Expedited. The code enforces reefers-only; the printed extra loaded-reefer pickup is not a separate rule. | +| X15 Yard Xfer | Slow | Player choice printed; v0.4.5 launches east | 2 freight, 1 caboose | — | +| X16 Light Engine Move | Fast | Player choice printed; v0.4.5 launches east | No cars | No switching. | +| X17 Campaign Train | Fast | Player choice printed; v0.4.5 launches east | 1 coach | No switching. First Office arrival lays over for speeches; later arrivals are expedited. | +| X18 Circus Train | Slow | Player choice printed; v0.4.5 launches east | 2 freight, 1 coach, 1 caboose | No switching. The first Mainline Phase in which it remains stopped awards its current Office’s player 1 Revenue. | +| X19 Military Train | Slow | Player choice printed; v0.4.5 launches east | 1 freight, 2 coaches | No switching; no passenger work; expedited. | +| X20 Director’s Private Car | Slow | Player choice printed; v0.4.5 launches east | 2 freight, 1 coach | No passenger work. | +| X21 Freight Extra | Slow | Player choice printed; v0.4.5 launches east | 3 freight, 1 caboose | — | +| X22 Pee-Dee | Slow | Player choice printed; v0.4.5 launches east | 1 caboose | May pick up empty cars only. | + +## Enhancements — 18 + +| Card | Copies | Placement | v0.4.5 behavior | +| --- | ---: | --- | --- | +| Interlocking | 2 | Bare Running Track straight | When the Office is full, an inbound train is held at the Limits instead of colliding. | +| Facing Point Locks | 2 | Any card; requires an Interlocking somewhere in the district | Blocks Derail. Derail is unavailable in v0.4.5, so this remains dormant. | +| Yard Office | 1 | Bare Secondary Track straight | A coachless inbound train is diverted to this track instead of occupying an A/D track. | +| Small Yard | 1 | Bare Secondary Track straight | A train may spend one switching Move here to reorder its entire consist and put the engine at the nose. | +| Water Column | 2 | Bare Running Track straight | Removes a Watertower. Watertower cards are unavailable, so this remains dormant. | +| Overpass | 1 | Any card | No implemented effect. | +| Telegraph | 3 | Bare Running Track straight | Once per Day, the Superintendent may add 4 to an opposing train’s number when that makes a facing clearance safe. | +| Telephone | 2 | On a Telegraph | Same dispatch mechanism, +8 once per Day. | +| Radio | 2 | On a Telephone | Same dispatch mechanism, +12 once per Day. | +| ABS Signals | 2 | Any Mainline card | Prevents rear-end collisions and holds a following train short. | + +An enhancement requiring a bare straight cannot share that straight with another such enhancement. Telephone and Radio are the explicit stackable chain. + +## Maneuvers — 6 + +| Card | Copies | Behavior | +| --- | ---: | --- | +| Red Flags | 5 | May be played at any time on a train stopped on a Mainline card. An approaching following train is held instead of moving into it. | +| Flying Switch | 1 | During the owner’s switching option, spend one Move to roll a tail-end cut into a track-connected freight facility without moving the engine there. | +| Poling | 0 | Catalogued but not dealt; no rule is implemented. | + +## Catalogued but not dealt — 22 opponent-directed cards + +### Space-use cards — 12 + +Bean House (1), Flop House (1), Watertower (1), Hobo Jungle (1), Section House (1), City Blocks (4), Engine Shops (1), Tenderloin District (1), and Engineer Cemetery (1) are excluded. Their source descriptions say they consume table space; Hobo Jungle additionally describes vandalism looting a passing boxcar. No placement or effect is available in v0.4.5. + +### Action cards — 10 + +Derail (2), Broken Coupler (1), Railroad Crossing (1), Per Diem Inventory (1), Demurrage Charge (1), Customer Complaints (1), Vandalism (1), Hotbox (1), and Outlawed (1) are excluded. Their printed target/effect text is catalogued in the code, but `card.play` rejects the categories as not implemented. Consequently, no card can currently be played on another player. + +There are also two **Facing Point Locks** listed among Mainline modifiers. They are treated as the same grid enhancement as Facing Point Locks above, require an Interlocking, and are included in the active 213-card total. diff --git a/docs/StationMaster-Mainline-Deck-v0.4.5.md b/docs/StationMaster-Mainline-Deck-v0.4.5.md new file mode 100644 index 0000000..af4b6fe --- /dev/null +++ b/docs/StationMaster-Mainline-Deck-v0.4.5.md @@ -0,0 +1,71 @@ +# Station Master — Mainline Deck + +**Rules implementation reference: v0.4.5** +**Scope:** the tarot-sized Mainline cards placed between Offices. This is an implementation reference, not a transcription of earlier prototype rules. + +## How Mainline cards work + +At setup the game places one randomly selected Mainline card between each neighbouring pair of Offices and one beyond each end Office, between it and a Division Point. Thus, a game with *N* players has *N + 1* Mainline cards. The implementation selects types with replacement; it does not deal them from a shuffled finite deck. + +A train crossing a Mainline card spends Stages, rather than moving through its printed cells. A 60 mph card costs one Stage and a 30 mph card costs two. A Slow train adds one Stage to every crossing. On Hilly terrain, any train carrying at least one coach uses the passenger rate; a train carrying no coach uses the freight rate. No crossing can take less than one Stage. + +The card does not itself determine which player owns the adjacent Office. It is part of the shared Division. + +## Physical card inventory in `Mainline Cards.pdf` + +The supplied PDF has **ten** tarot-sized terrain cards. Plains appears twice; the other terrain types appear once each. It also includes two Division Point cards. + +| Physical card | Copies in the PDF | +| --- | ---: | +| Plains | 2 | +| Curves | 1 | +| Hilly | 1 | +| Heavy Grade | 1 | +| Double Track | 1 | +| Uncontrolled Siding | 1 | +| Tunnel | 1 | +| Trestle | 1 | +| Interchange | 1 | +| East Division Point | 1 | +| West Division Point | 1 | + +The PDF art labels this card “Yard”; this reference uses the implementation’s correct name, **Interchange**, to distinguish it from the Division Yard, Classification Yard, Yard Office, and Small Yard. + +## Implemented Mainline card reference + +| Card | Implemented crossing time and feature | +| --- | --- | +| Plains | 60 mph; one Stage for Fast, two for Slow. The implementation has one Plains *type* rather than the PDF’s two physical copies. | +| Curves | 30 mph; two Stages for Fast, three for Slow. | +| Hilly | Passenger train: 60 mph. Freight-only train: 30 mph. Add one Stage if Slow. | +| Heavy Grade | Starts at 30 mph; two Stages for Fast, three for Slow. The card requires the player to select its uphill direction; v0.4.5 instead selects that direction from the game seed during setup. Grade modifiers can reduce the time, to a minimum of one Stage. | +| Double Track | 60 mph. Printed capability: trains may pass. The traffic-resolution rule is in Rules §4.5. | +| Uncontrolled Siding | 60 mph. Printed capability: trains may pass. The traffic-resolution rule is in Rules §4.5. | +| Tunnel | 30 mph. | +| Trestle | 60 mph. | +| Interchange | 60 mph. A train may be reordered there only through the card’s printed “sort cars” concept; the current engine does **not** provide a Mainline sorting action for it. | + +### PDF/code mismatch requiring correction + +`src/engine/content.ts` defines nine `MAINLINE_PROFILES` types: one Plains entry plus the eight other terrain types above. `setup.ts` selects uniformly from that nine-type list. The second Plains card shown in `Mainline Cards.pdf` is therefore not represented as a duplicate card or as extra Plains weight in setup. If the PDF inventory is authoritative, the setup selection needs a second Plains entry (or an equivalent weighted selection). + +The executable state represents East and West Division Points as fixed end nodes, not as card records. They are functionally present at the ends of the Division, but are not represented as the two PDF cards in the deck/state model. + +## Heavy Grade modifiers + +These cards come from the Home Office deck and are played onto a Mainline card during a player’s Draw option. + +| Card | Copies | Placement and actual effect | +| --- | ---: | --- | +| Brakeman | 1 | Heavy Grade only. Reduces a downhill crossing by one Stage. | +| Airbrakes | 1 | Heavy Grade only, and Brakeman must already be on that card. Reduces a downhill crossing by one additional Stage. | +| Helpers | 1 | Heavy Grade only. Reduces an uphill crossing by one Stage. | +| Realignment | 2 | May be played only onto an unoccupied Mainline card. Changes Plains → Double Track, Curves → Plains, Uncontrolled Siding → Double Track, or Trestle → Uncontrolled Siding. It cannot be played on any other card type. | + +For the grade cards, “uphill” should be the direction selected by the player when the card is placed. In v0.4.5 it is the seeded `gradeUp` direction because setup has no player-choice step. + +## What is not implemented + +- There is no finite draw pile, player choice, or physical placement interaction for Mainline cards; setup selects their types automatically from the seeded random stream. +- Interchange is catalogued as a “sort cars” card, but v0.4.5 has no operation that reorders a train on the Interchange. The Small Yard in an Office Area is the implemented sorting mechanism. +- Heavy Grade orientation is seeded automatically rather than chosen by a player. The implementation needs a player-selection step to match the card. diff --git a/docs/StationMaster-Rules-v0.4.5.md b/docs/StationMaster-Rules-v0.4.5.md new file mode 100644 index 0000000..bbc0abe --- /dev/null +++ b/docs/StationMaster-Rules-v0.4.5.md @@ -0,0 +1,290 @@ +# Station Master — Rules + +**First-draft rules reference for v0.4.5** +**Authority:** observed v0.4.5 code paths and tests. Where a card face, prototype document, and executable behavior differ, this document reports executable behavior and marks unimplemented material. + +## 1. Overview and background + +Station Master is a railroad-operations game set in the timetable-and-train-order era. Each player operates an Office Area on a shared east–west Division. The local work is building rail, switching cars, moving passengers and freight through facilities, and keeping the Office clear. The shared work is dispatching scheduled trains across the Division. The player holding the Superintendent’s Fedora decides whether to clear a following train into an occupied Subdivision; an unsafe decision can cause a collision. + +Revenue is the score. Passenger boarding and detraining, freight loading and unloading, completed runs, collisions, and victory conditions are all resolved by the rules configured when the game is dealt. + +This book is divided as follows: + +1. overview and scope; +2. definitions; +3. creating, saving, replaying, and configuring games; +4. the shared basic game mechanics; +5. the implemented solitaire game; +6. the implemented multiplayer/engine status; and +7. FAQs and implementation limits. + +The companion references are [Mainline deck](StationMaster-Mainline-Deck-v0.4.5.md), [Home deck](StationMaster-Home-Deck-v0.4.5.md), and [components](StationMaster-Components-v0.4.5.md). + +## 2. Definitions + +| Term | Meaning in v0.4.5 | +| --- | --- | +| A/D track | An Office arrival/departure capacity. A Whistle Post has 1; Depot, Station, and Terminal have 2, 3, and 4. | +| Card location | A square in an Office Area grid. A train may end a switching Move only on Operational Rail. | +| Classification Yard | The holding yard for used rolling stock. It refills the Division Yard only when that yard is completely empty. | +| Control Point | A Depot, Station, or Terminal. It breaks the shared Mainline into Subdivisions. A Whistle Post is not one. | +| Crew Tray | An engine plus up to four coupled rolling-stock cars. The engine itself does not count as a car. | +| Department pile | One of three shared face-up card stacks. Draw and discard affect only the top card. | +| Division Point | The western or eastern end of the shared Division, where trains enter and leave it. | +| Division Yard | The active source of rolling stock. | +| Expedited train | A train with the expedite rule. It performs Load/Unload at an Office after arriving, then attempts to depart at the end of that same Stage. | +| Facility | A Passenger Facility at an upgraded Office or a Freight Facility at an industry. | +| Freight Agent | The Local Operations option that stocks one outbound box, clears one inbound box, unjams one load, or ends without acting. | +| Limits | The two signs defining an Office Area’s Running Track. They move outward when that track is extended. | +| Local Operations | The first player-driven phase of a Stage. A player chooses one: switch, draw, or Freight Agent. | +| Mainline | The shared cards between Offices and Division Points. | +| MEN \| AT \| WORK | A three-box freight pipeline. Any load on it locks its industry track. | +| Operational Rail | A card on which a train may finish a Move. Turnouts are pass-through only; a locked industry is not usable at all. | +| Running Track | The east–west track between an Office Area’s Limits, including its Office. | +| Secondary Track | All local rail inside the Limits that is not Running Track. | +| Stage | One of twelve turns in a Day. Its phases are Local Operations, New Train, Mainline, Load/Unload, and Shift Change. | +| Subdivision | Mainline between Division Points or Control Points. Clearance checks look through the whole next Subdivision. | +| Superintendent | The player with the Fedora. The role decides same-direction clearances and rotates every three Stages. | +| Timetabled train | A numbered train card scheduled to one of the 12 Stage slots, then due at that slot each Day. Odd numbers go west; even numbers go east. | + +## 3. Create, save, replay, and game configuration + +### 3.1 Starting a new game in the shipped client + +The v0.4.5 browser page creates a **one-player solitaire Standard game**. Select **New game**, then choose: + +1. a numeric seed, or leave it blank for a fresh browser-generated seed; +2. a starting hand; +3. passenger, freight, and train-transit Revenue rates. + +The browser writes those choices into the URL. A particular game is defined by the **seed plus these house rules**, not the seed alone. + +The three starting-hand choices are: + +| Choice | Setup result | +| --- | --- | +| Three random cards | Default. Deal three cards from one shuffled deck. | +| Six random cards | Deal six cards from one shuffled deck. The player begins over the normal hand limit and must play/discard down before ending the first Draw turn. | +| Three random track and three random non-track cards | Shuffle track and non-track piles separately for the opening deal; then recombine and shuffle their remainders for all later draws. | + +The three Revenue values are independently rounded and clamped to 0–5: + +- passenger Revenue per coach, paid on boarding and again on detraining; +- freight Revenue per load, paid on completion of an outbound load and again on completion of an inbound unload; and +- train Revenue per transit, paid to every player when a train leaves the Division. + +Defaults are 1 / 1 / 0. Zero turns that revenue source off; it does not prevent the underlying operation. + +### 3.2 Deterministic setup and the seed + +The engine uses one seeded random stream for shuffles, Mainline selection, opening D12 rolls, timetable rolls, and automatic Heavy Grade orientation. Given the same seed, configuration, player names/order, and accepted action history, it reconstructs the same state. + +At setup it creates each Office Area as `Limit — Whistle Post — Limit`, makes the Division Yard, creates players + 3 Crew Trays, turns up three Department cards, and constructs the shared Division. + +In a multi-player engine game, every player receives two seeded D12 rolls: + +- The **division roll** orders seats from low west to high east; equal results put the lower player index farther east. +- The **Superintendent roll** gives the initial Fedora to the first player tied for highest. + +The opening deal starts at the Superintendent’s seat and proceeds left (eastward in the engine’s seat ordering). + +### 3.3 Saving, resuming, and replaying solitaire + +The local save format is JSON containing the seed, the accepted intent history, and the house rules. It does not serialize mutable game state. Loading recreates the game from the seed and replays every action through the rules engine. If an old history reaches an action that is no longer legal, replay stops there instead of forcing an invalid state. + +The browser also stores the current local game and resumes it automatically when the page opens without an explicit seed in the URL. A downloaded save is named `station-master-seed-day.json`. + +**Undo is solitaire-only.** It removes the final accepted intent and rebuilds the game from the earlier history. Random outcomes are not rerolled: replaying the same action consumes the same seeded result. Undo can therefore change the player’s decision after seeing an outcome, but cannot fish for a different timetable die roll. + +### 3.4 Engine game modes and endings + +The engine defines Solitaire, Competitive, and Co-op modes. The browser exposes only Solitaire. + +| Length | Target | Days | +| --- | ---: | ---: | +| Short | 10 | 3 | +| Standard | 20 | 5 | +| Campaign | 45 | 10 | + +With `firstToTarget`, competitive mode ends when any individual reaches the target; co-op uses target × player count and total Revenue. With `highestAfterDays`, competitive mode requires the table’s combined Revenue to reach `3 × players × days`; otherwise everyone loses. If that floor is met, the highest individual score wins. Solitaire and co-op win only if their score reaches their target at the end of the length. + +Competitive mode also ends in a collective loss after three collisions in one Day. + +The configuration contains four optional-rule flags. Only two have engine effects: **Reduced Visibility** gives five rather than six switching Moves in Stages 1, 2, 3, 11, and 12; **Emergency Toolbox** initially sets the Red Flags hand-limit status, allowing four cards. Sister Trains and Employee Rotation are represented in configuration/state design but are not executed by v0.4.5. + +### 3.5 Multiplayer setup status + +There is no multiplayer lobby, room creation flow, remote server, invitation flow, or network session in v0.4.5. The engine can be called with multiple player names (and rejects a Solitaire configuration with more than one), but the delivered page always calls it for one local player. See section 6. + +## 4. Basic game mechanics + +### 4.1 The Stage sequence + +Each of 12 Stages follows this sequence: + +```text +1. Local Operations — each player, starting with the Superintendent and proceeding left +2. New Train — make up due timetabled trains, then queued second sections and Extras +3. Mainline — automatic train movement in numeric order +4. Load/Unload — each player, starting with the Superintendent and proceeding left +5. Shift Change — expedited departures, clocks/workers, and possibly the Fedora +``` + +At a Shift Change, Laborers and Porters reset. The Fedora moves after Stages 3, 6, 9, and 12. At Day end, dispatch-device use resets, collision count resets, the Day and Stage roll over, and victory is checked. + +### 4.2 Local Operations: choose one option + +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 can reorder a consist for one Move. Flying Switch spends one Move to roll a tail cut into a connected Freight Facility. + +**Draw.** Take one card from the face-down Home Office or the exposed top of one Department pile. During this option, play eligible cards and/or discard cards to Department piles, then finish at the hand limit. Track, facilities, offices, modifiers, enhancements, and train cards have the placement or scheduling rules in the deck references. Mainline modifiers are played from this option as well. + +**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. + +Implementation note: `card.discard` is accepted by the v0.4.5 engine during Local Operations without checking that Draw was chosen. This unusual implementation behavior is not a separate published turn option. + +### 4.3 Track, switching, and local safety + +The Running Track cannot be broken by a curve or other card lacking its east–west through route. Local branches use turnouts and 45° curves. Cards that touch vertically connect only if their 45° legs share the correct diagonal; mere adjacency is not rail connection. + +An industry’s track is locked while any load is on its MEN | AT | WORK pipeline. Trains cannot enter, cross, pick up from, or set cars out there. When it is unlocked, an industry track is ordinary Operating Rail and holds up to four cars, the same as any other non-turnout, non-Limits card: a crew may set out one, two, three or four cars there, whatever it is carrying. An industry’s green and red boxes are how much work it can hold, not how much rail it has, and a Modifier beside it adds a box without adding room for a car. + +Train-card restrictions also apply while switching. No-switching trains cannot move, set out, or sort locally. Trains 3/4 share a one-freight-car budget for pickup or setout at each grid location. Trains 7/8 cannot set out their coach. X13 cannot pick up, and X22 may pick up empty cars only. + +### 4.4 New Train phase + +At the Stage shown on the timetable, the engine makes up the matching timetabled train if a Crew Tray is free. It starts at the Division Point appropriate to its direction. A train may receive matching loaded or empty cars from the Division Yard until its listed maximum consist is reached or no suitable car remains. It is permitted to leave under-strength. + +A Second Section order on the due train creates another identical timetabled train behind it when a free tray is available. A played Extra is made up after timetabled trains and Second Sections when a tray is free; v0.4.5 automatically sends every Extra east from the Western Division Point. + +### 4.5 Mainline movement and Office arrival + +Mainline movement is automatic and processes lower train numbers first; a timetabled train outranks an Extra with the same number. A train at a Division Point, at an Office A/D track, or already crossing a Mainline card attempts its applicable movement. + +Crossing time comes from the Mainline card and train speed. On a normal card, a train must check the entire next Subdivision before entering it: + +- an opposing train normally blocks entry; +- a following same-direction train asks the Superintendent to allow or deny clearance; +- Red Flags or ABS Signals hold the follower automatically; and +- passing cards allow entry without this occupancy check. + +An Office arrival normally takes a free A/D track. If the Office is full, the inbound train collides and the local Office player loses 5 Revenue; Interlocking instead holds it at the Limits. A coachless inbound train may divert to a Yard Office. Cars fouling the Running Track at the Office also cause a collision. + +An expedited train remains available for this Stage’s Load/Unload work after arriving, then attempts to depart at Shift Change. A non-expedited arrival waits until a later Mainline phase. A train departing an Office must be correctly made up: engine at one end, caboose at the far end if present. + +When a train leaves the far Division Point, its cars are returned to yards, its Crew Tray becomes free, and each player receives the configured train-transit Revenue (zero by default). + +### 4.6 Passenger work + +Passenger work occurs during Load/Unload, at a Depot, Station, or Terminal. Each Porter may take one action per Stage. + +- **Board:** replace an empty coach on an eligible train at the Office with a loaded coach from a green outbound box. The removed empty coach goes to the Classification Yard. Earn configured passenger Revenue. +- **Detrain:** replace a loaded coach on an eligible train with an empty coach from the Division Yard, placing the loaded coach into an available red inbound box. Earn configured passenger Revenue. + +Crack Limited trains permit passenger work at Terminals only. Military Train and Director’s Private Car permit no passenger work. A Whistle Post has no Porters. + +### 4.7 Freight work + +Freight work also occurs during Load/Unload. Each Laborer can take one action per Stage. Every freight facility has MEN, AT, and WORK boxes. + +For an **outbound load**, first use the Freight Agent to put a matching loaded car into the green box, with an unclaimed empty matching car spotted on the industry track. A Laborer starts the load on MEN, then advances it to AT, then WORK, then onto the empty car. Completing the last step changes that car to loaded, sends the replaced empty car to the Classification Yard, and earns configured freight Revenue. + +For an **inbound unload**, a matching loaded car must be spotted at an inbound-capable industry and the Division Yard must contain a matching empty car. A Laborer replaces the spotted loaded car with that empty car and starts the load on WORK. Laborers move it backward through AT and MEN into a free red inbound box. That final placement earns configured freight Revenue. Clearing the red box later with the Freight Agent pays no additional Revenue. + +## 5. Solitaire + +The implemented game is solitaire: one named player, one Office Area, local browser execution, Standard length, highest-after-days victory, and the default house rules unless changed in New Game. The game has no AI opponent. “Multiplayer” gameplay does not occur locally by simulating other players. + +Solitaire-specific features are: + +- a local browser save, automatic resume, and JSON download/load; +- unlimited step-by-step Undo back through accepted action history; and +- a seed/rules URL suitable for sharing or reproducing a game. + +The active deck removes all 22 opponent-directed cards. Therefore, the defensive cards whose only purpose is to answer them (Facing Point Locks and Water Column) can be placed but have no opportunity to fire; Overpass has no effect at all. The game still includes shared-rail mechanics such as Mainline clearance, but with one player no other player can occupy the Division. + +To win the default game, finish Day 5 with at least 20 Revenue. A result below 20 is a loss. Train-transit Revenue defaults to zero, so the default score must principally come from passenger and freight work. + +## 6. Multiplayer + +### 6.1 What the engine supports + +The engine has player, seat, score, Office Area, Director/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 left, and models the following multiplayer-specific outcomes: + +- the D12 seating and Superintendent rolls described in section 3; +- individual Revenue in competitive play and shared total Revenue in co-op; +- a competitive collective loss after three collisions in one Day; +- a collective Revenue floor for competitive highest-after-days games; and +- train-transit Revenue awarded to every player, if that revenue setting is nonzero. + +The engine’s setup checks only that there is at least one player and that Solitaire has exactly one player. It does not enforce a maximum player count, although the test and design material exercise two through four players. + +### 6.2 What is not delivered in v0.4.5 + +There is no implemented multiplayer game setup for end users: no server, lobby, invitation, room code, player join flow, authoritative remote state, or remote Session. The browser page creates a local single-player session only. Accordingly, there is no supported procedure for resuming a multiplayer game, and no multiplayer Undo. + +The 12 space-use cards and 10 action cards are not dealt in competitive or co-op either. The engine rejects attempts to play either category. Thus, **no card can currently be played on another player**. This includes Derail, Broken Coupler, Railroad Crossing, score-penalty cards, Vandalism, Hotbox, Outlawed, and all table-space cards. Facing Point Locks and Water Column are implemented only as dormant defences for these unavailable effects. + +### 6.3 Difference from solitaire, if a multi-player engine session is created + +Players have separate local districts, hands, and scores, but they share the Home Office deck, Department piles, yards, timetable, Mainline, and traffic consequences. Turn order is sequential; one current actor acts at a time. The Superintendent role is attached to a player while Offices are attached to fixed seats. Employee Rotation is not active, so players do not actually change seats in v0.4.5. + +The deck is still 213 cards and still excludes opponent-directed content. Therefore multi-player engine mode changes shared traffic, scores, turns, and win/loss evaluation—not card attacks or a remote user experience. + +## 7. Frequently asked questions + +### Is the same seed always the same game? + +Only with the same house rules, player configuration/order, and action history. The seed drives all engine randomness, but the starting hand and Revenue settings also change the game. + +### Why can’t I end my draw turn? + +Your hand exceeds three cards, or four when the Red Flags hand-limit status is active. Play or discard down to the limit. + +### Why can’t my train move onto a turnout? + +It may move through a turnout but cannot end a Move there because turnouts are not Operational Rail. + +### Why can’t a train enter my industry? + +Its MEN | AT | WORK pipeline contains a load, which locks the track; the rail geometry does not connect; another train occupies it; mandatory coupling would exceed four cars; or four cars are already standing there, which is as many as any card holds. + +### Why won’t an industry accept a new outbound load? + +The green box may be full, the Division Yard may lack a matching loaded car, or there is no unclaimed empty matching car already spotted on that industry’s track. + +### Why does an inbound freight unload require an empty car in the Division Yard? + +The engine immediately replaces the spotted loaded car with a matching empty car. It will not create a new car from nothing. + +### Why did an arriving train collide instead of wait? + +The Office had no free A/D track and no Interlocking. A full Office is an automatic local collision. An Interlocking holds the arrival at the Limits. + +### Why is a train held at an Office even though the Mainline is clear? + +It may be on Secondary Track rather than the Office, or be badly made up: its engine is between cars or its caboose is not at the far end. + +### Why did an expedited train leave after passenger/freight work? + +Expedite means it departs in the Stage it arrived, but v0.4.5 waits until Shift Change so it remains present for that Stage’s Load/Unload phase. + +### Can I choose the direction of an Extra or a Heavy Grade? + +Not in v0.4.5. The engine launches Extras eastbound from the Western Division Point. Heavy Grade orientation is seeded automatically at setup. + +### Can I use an Interchange to reorder a train? + +No. The card is catalogued with a sorting concept, but there is no implemented Interchange sorting action. Small Yard is the available local sorting mechanism. + +### Can I play attack cards on another player? + +No. All action and space-use cards are excluded from every dealt deck in v0.4.5 and their play is rejected. + +### Is multiplayer playable? + +No. Multi-player state and rules-engine support exist, but the lobby, server, remote client, and opponent-directed card mechanics are not implemented. diff --git a/docs/design/cut-ordering-plan.md b/docs/design/cut-ordering-plan.md new file mode 100644 index 0000000..0a9a87e --- /dev/null +++ b/docs/design/cut-ordering-plan.md @@ -0,0 +1,233 @@ +# Track order for standing cars, and the cut you left on your own card + +> **BUILT IN v0.4.7.** Kept as the record of why the model is what it is — the reproductions below +> are the evidence for west-to-east, and none of them is re-derivable from the code that fixed them. +> +> Two departures from the plan as written, both recorded here so the document does not read as a +> description of the code: +> +> - **Step 3 does not flip the card's cut.** The plan asked for `board-svg` to apply the tray strip's +> `facing === 'e' ? reverse : as-is` to the standing cars too. Once `standing` is defined west to +> east that flip would be wrong — the board is a map and draws west on the left, so the array is +> already in drawing order. What the renderer got instead is the half that actually answers the +> report: the row is split at the train, west cars left and east cars right, with the engine in the +> gap, and each car's tooltip says whether it is ahead of or behind the engine. +> - **One new field after all.** The plan argued a cut's end is *derivable* from position in the +> array. It is not, in the one case that matters: a train may set out off both ends on the same +> square, and nothing in the array records where the train itself stands among them. That is +> `CrewTray.standingWest`, kept on the tray rather than the card because it describes a +> relationship — with no train there, a cut has no near or far side. +> +> **Step 5 was decided (i)**: taking your own cut back on the square you are standing on is undoing +> the drop, exempt from the printed pick-up restrictions, and it refunds the freight budget. + +Plan for the next session. Two reported problems that turned out to be one problem. + +--- + +## The reports + +1. **Drops.** "If I put cars off the nose on a given track, and my next move is go forward, I need to + couple those cars right back on. If I drop cars off the back and I move back, then I will + automatically recouple the cars onto the back of my train. Right after dropping my cars I need to + be able to see if those cars are ahead or behind the train." +2. **Ordering.** "When dropping all 4 cars, order was reversed. It worked properly if we dropped cars + individually. Also, when adding four cars, again the order was reversed." + +--- + +## The single root cause + +`TrackCard.standing` is a bare `RollingStock[]` whose doc comment claims "in track order (§A.3)" and +which in fact has **no defined orientation at all**. `CrewTray.consist` *is* oriented — nose first, +relative to `facing` — so every transfer between the two is an orientation change that nothing +performs. + +§A.3 states the requirement plainly: + +> It is critical that cars are loaded into and unloaded from the Crew Tray, and occupy the track, in +> the same order they originally held, **left-to-right**. + +Left-to-right is west-to-east. That is the orientation `standing` is missing. + +### Reproduced, against the working tree + +Scratch scripts only; reproduce with a tray on a plain straight, consist `[boxcar hopper reefer +tank]`, `engineAt = 0`. + +**(a) Batch size changes the parked order.** Tail drops: + +| drops | `card.standing` afterwards | +| --- | --- | +| `count: 4` | `[boxcar hopper reefer tank]` | +| `count: 1` ×4 | `[tank reefer hopper boxcar]` | +| `count: 2` ×2 | `[reefer tank boxcar hopper]` — physically impossible | + +`carsDropped` always does `carsOn(card).push(...stock)` (`apply.ts:1657`). Each successive tail cut is +left *outboard* of the previous one on the track, so it belongs at the other end of the array. Nose +drops are batch-invariant by luck, so only tail drops show it. + +**(b) Coupling is orientation-blind.** A train running **east** onto a parked cut and a train running +**west** onto the same cut yield the **identical** consist. They must mirror. `couples` is +accumulated in path order in `exploreMoves` (`track.ts:448`) and `unshift`ed wholesale in +`carsCoupled` (`apply.ts:1618`), with no reference to which way the engine met the cars. + +**(c) The display mismatch — what was actually seen on screen.** `board-svg.ts:736` draws the tray +strip as `t.facing === 'e' ? [...items].reverse() : items`, so the nose lands on the right for an +east-facing train (a deliberate fix, documented in the comment above it). The card's standing cars at +`board-svg.ts:~633` are drawn in raw array order with no such flip. The same physical cut therefore +reads one way in the tray and the other way on the card — and which one looks "reversed" flips with +batch size *and* with train facing. + +The round trip (drop, run away, back up, collect) is internally self-consistent today, which is why +this survived: whatever order got parked comes back in that order. Only the picture disagreed. + +### Why this settles the earlier design question + +The previous session offered two models for recording which end a cut sits at: + +- **Option A** — one cut per card plus a `standingEnd?: Port` marker. Cheap. +- **Option B** — give `standing` a real orientation. + +Option A leaves (a), (b) and (c) in place. **Do Option B.** With orientation defined, "which end is +the cut at" is *derivable* from position in the array and needs no new field — so B is also the +smaller change once the ordering bugs are counted as in scope, which they now are. + +--- + +## The model + +**`TrackCard.standing` (and `Facility.industryTrack.cars`, via `carsOn`) is ordered WEST to EAST.** + +That is the printed rule, it is the convention the board already draws in, and it is the only +orientation that is a property of the board rather than of whichever train last touched the card. + +Two derived quantities, both cheap: + +- **A cut's end.** With a train on the card, cars before the train in track order are at its west + end, cars after it are at its east end. Combined with `railFacing`, that is "ahead" or "behind". +- **Approach order.** A train travelling east meets a card's cars in array order; travelling west, in + reverse array order. + +The nose/tail blocking rule then reads: **a move that leaves the card through the end the cut sits at +must couple that cut first** — onto the nose when running forward, onto the tail when reversing, +which is what the existing `toNose: !i.reverse` already gives. + +### The two conversions, stated once + +- **Consist → track.** `consist` is nose-first; nose is `facing`. So an east-facing train's consist + reads east-to-west and must be **reversed** on the way onto the card; a west-facing train's reads + west-to-east and goes on **as-is**. +- **Track → consist.** Build `couples` **near-to-far along the direction of travel** (reverse each + card's array when travelling west, and visit cards in path order). Then a nose coupling is + `unshift(...[...couples].reverse())` and a tail coupling is `push(...couples)` unchanged. + +Check the tail case by hand: backing east into a cut, the nearest car couples closest to the existing +tail, and `consist` runs nose-to-tail, so near-before-far pushed in order is right. Check the nose +case: running east into a cut, the *farthest* car ends up nose-most, so the reverse is right. + +--- + +## Work, in order + +Each step should land green on its own. + +### 1. Define the orientation and fix the drop + +- `state.ts` — rewrite the `standing` doc comment to state west-to-east outright, and say `carsOn` + inherits it. That comment is currently the thing that is wrong. +- `apply.ts` `carsDropped` reducer (~1649) — insert at the correct end for the dropping train's + `facing`, reversing the cut where the conversion above says to. A tail cut from an east-facing + train goes at the **west** end of whatever is already on the card; a nose cut from the same train + goes at the **east** end. +- Tests: batch-invariance. `count: 4`, `1×4` and `2×2` must all produce the same `standing`, from + both ends, at both facings. Direct regression for report (2). + +### 2. Fix the coupling + +- `track.ts` `exploreMoves` (~448) — build `couples` near-to-far along travel, per the conversion + above. Needs the exit port at each hop, which the walk already has in `MoveStep`. +- `apply.ts` `carsCoupled` (~1612) — reverse on `toNose`. +- Tests: the mirror property. Approaching the same parked cut eastbound and westbound must give + mirrored consists, never identical ones. +- Tests: round trip at both facings and all batch sizes — drop, run clear, back up, collect; the + consist must come back identical. + +### 3. Fix the picture + +- `view.ts` `CellView` — `cars: string[]` is flat and carries no relation to `train`. It needs enough + for the renderer to place the cut relative to the engine. Preferred: keep `cars` in track order + (west-to-east, matching state) and let the renderer flip, exactly as it already does for the tray + strip. +- `board-svg.ts` (~633) — apply the same `facing === 'e' ? reverse : as-is` flip to the cut that line + 736 applies to the tray, and lay the cut on the correct **side** of the train chip rather than in + the fixed slot row. This is the real UI work and answers "I need to see if those cars are ahead or + behind". +- Tooltips: "standing here" becomes "standing ahead of the engine" / "behind the engine" when a train + shares the card. + +### 4. Layer on the blocking rule + +- `track.ts` `exploreMoves` — seed the walk's `couples` with the start card's cut when the cut sits at + the exit end. Today the walk begins at `neighbour(start, initialExit)` and never reads + `carsOn(startCard)` at all, so a train drives through its own cut in both directions. Regularise + the loop-back case at the same time: `couples` already picks up the start card if the walk circles + back to it, because only `results.push`/`block` are guarded by `!sameCoord(node.coord, start)`. +- `apply.ts` `switch.move` build (~1230) — `lifted` is `[...dest.path.map(s => s.coord), i.to]` and can + never contain `start`. It must, or the recoupled cut is not cleared off the card. +- `ctx.consistSize` — the start-card cut must count toward `MAX_CONSIST`, so `tooManyCars` bites at + the start square. Its message should name the cut on your own card or it will read as nonsense. +- Tests: encode §A.5's two worked examples move-for-move. Both must remain playable; the trailing + example's step 1 ("drop the back two on Card F, move train to Card B") is the specific case that + must *not* recouple, and it is the strongest single check that the rule is right. + +### 5. Decide the printed-rule interaction — Jesse's call + +Dropping a freight car spends the square's budget for trains 3/4 (`oneFreightPerLocation`). Recoupling +it on the way out would then be refused with `FREIGHT_WORKED_HERE`, so the train could only ever back +away from its own cut. Same shape for X13 `dropOnly` and X22 `pickUpEmptiesOnly`. + +Two defensible answers: + +- **(i)** Recoupling your own cut on the square you are standing on is *undoing the drop* — exempt + from those restrictions, and it refunds the freight budget. +- **(ii)** It is a pickup like any other; a train that sets out off its nose is committed to + reversing. Harsher, but never strands a train permanently, since backing up stays legal. + +Leaning (i): (ii) makes a legal-looking drop silently one-way, which is exactly the kind of trap the +"why can't I move" panel exists to prevent. + +### 6. Bot and fixtures + +- `bot.ts` — the "drop the nose cut to un-bury the engine" heuristic (~1268) and "spot the back car at + this facility" (~1352) now have direction consequences. The bot cannot produce an illegal move + (legality is enforced upstream) but it will silently get worse until its scoring knows about the + cut. +- `public/replays/seed-*.json` will diverge — legality changes, so bot play changes. Regenerate, as + was done last release. +- **`docs/station-master-seed58228926-day6.json` no longer replays at all.** It stops at intent #2, + `mainline.modify c108` → `NO_SUCH_CARD`, because uncommitted deck changes shifted card ids. It + cannot serve as the regression case for these reports. Capture a fresh save against current code + reproducing the same switching sequence, and keep it. + +--- + +## Files expected to change + +| File | Why | +| --- | --- | +| `src/engine/state.ts` | `standing` orientation, doc comment | +| `src/engine/track.ts` | direction-aware `couples`; start-card cut; consist limit | +| `src/engine/apply.ts` | `carsDropped`, `carsCoupled`, `switch.move` `lifted`, budget decision | +| `src/sim/view.ts` | `CellView.cars` orientation and its relation to `train` | +| `src/sim/board-svg.ts` | flip the cut, place it beside the chip, tooltips | +| `src/sim/narrate.ts` | say which end was set out; distinguish recoupling own cut (~138) | +| `src/sim/bot.ts` | direction-aware set-out heuristics | +| `test/apply.test.ts`, `test/train-rules.test.ts`, `test/web.test.ts`, `test/replay.test.ts` | drop/couple batteries, §A.5 examples | +| `public/replays/seed-*.json` | regenerate | + +## Not in scope + +- Leaving a coach at the Office (§A.4 / trains 7/8) — separate open item in `TODO.md`. +- Any change to `maneuver.flyingSwitch`'s target rules. It drops onto a card with no train on it, so + the cut has no "end" to sit at; it just needs to land in west-to-east order like any other cut. diff --git a/docs/rules/implications.md b/docs/rules/implications.md index 527cf4e..56b55da 100644 --- a/docs/rules/implications.md +++ b/docs/rules/implications.md @@ -241,6 +241,27 @@ Freeing the Laborers freed the whole turn economy — passenger revenue doubled passenger rule changing. **Still 0 wins against a target of 20**, and freight is still only 13% of gross, so the loop runs now but does not yet carry the game. +### Where the empty-car requirement belongs + +The rule above was later copied onto **stocking** as well, so `freightAgent.stockOutbound` refused a +green box with no matching empty spotted. That is one step too early, and the designer's ruling is +that it should never have been there: + +> A freight agent should be able to load an outbound green box prior to having the car there that +> matches the load that he's putting in there. But in order for the laborers to move it from the +> green box into the men at work track, they would need to have an empty car of the appropriate type +> waiting there. + +Which is what §6.3 and §9.3 say between them. §6.3 lists the Freight Agent's act with no reference to +the industry track at all; §9.3's **Load the car** is what requires "*a load in the Green Loading Box +and an empty car of the required type on the industry's track*". So the two errands — fetch the +cargo, spot the car — may be run in either order, and only the Laborer needs both done. + +**Staging ahead cannot jam anything**, which is why the early gate bought nothing: a load in a green +box is waiting, not stuck, and the lockout in §9.3 applies to loads on MEN | AT | WORK. `startableLoad` +holds the real gate, counting cars against loads already staged or on the sign so two loads can never +walk toward one car. + ### Track was unplayable (found while wiring up Enhancements) Moving track out of the deck into a per-player supply (§12.2) left **no way to play it**. Districts diff --git a/public/replays/seed-286084.json b/docs/station-master-seed58228926-day6.json similarity index 63% rename from public/replays/seed-286084.json rename to docs/station-master-seed58228926-day6.json index cbb14c9..02a6ff5 100644 --- a/public/replays/seed-286084.json +++ b/docs/station-master-seed58228926-day6.json @@ -1,15 +1,5 @@ { - "seed": 286084, - "title": "13 Revenue · seed 286084", - "note": "13 Revenue over 5 Days · 2 train(s) on the timetable · 25 cards down · no collisions · played by developer", - "rules": { - "startingHand": "threeRandom", - "revenue": { - "passengerPerCoach": 1, - "freightPerLoad": 1, - "trainPerTransit": 0 - } - }, + "seed": 58228926, "history": [ { "type": "localOps.choose", @@ -18,9 +8,14 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "mainline.modify", + "cardId": "c108", + "node": 3 + }, { "type": "card.play", - "cardId": "c130", + "cardId": "c136", "placement": { "row": 0, "col": 1 @@ -29,16 +24,69 @@ }, { "type": "card.play", - "cardId": "c68", + "cardId": "c19" + }, + { + "type": "card.discard", + "cardId": "c98", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c66", "placement": { "row": 1, - "col": 0 + "col": -1 }, "variant": 0 }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, { "type": "card.play", - "cardId": "c210", + "cardId": "c131", "placement": { "row": 0, "col": -1 @@ -58,119 +106,9 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.play", - "cardId": "c184", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c153", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 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" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c66", - "placement": { - "row": -1, - "col": 0 - }, - "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": "card.discard", - "cardId": "c71", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c147", + "cardId": "c193", "placement": { "row": 0, "col": -2 @@ -190,26 +128,9 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.play", - "cardId": "c4" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.discard", - "cardId": "c70", + "cardId": "c109", "toSlot": 1 }, { @@ -227,241 +148,14 @@ }, { "type": "card.play", - "cardId": "c164", - "placement": { - "row": -1, - "col": 2 - }, - "variant": 0 + "cardId": "c21" }, { "type": "draw.end" }, { "type": "newTrain.placeCar", - "trayId": "tray3", - "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": "card.play", - "cardId": "c159", - "placement": { - "row": -2, - "col": 2 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c24" - }, - { - "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.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c31" - }, - { - "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.play", - "cardId": "c145", - "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": "c97", - "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": "c41", - "placement": { - "row": -2, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c155", - "placement": { - "row": -2, - "col": 4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c17" - }, - { - "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": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", + "trayId": "tray2", "carType": "caboose", "loaded": true }, @@ -475,12 +169,147 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "card.discard", + "cardId": "c116", + "toSlot": 1 + }, + { + "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": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, { "type": "card.play", "cardId": "c162", "placement": { - "row": -3, - "col": 4 + "row": -1, + "col": -2 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c60", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c138", + "placement": { + "row": -1, + "col": 0 + }, + "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": "card.play", + "cardId": "c30" + }, + { + "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": "c0" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c191", + "placement": { + "row": 0, + "col": 2 }, "variant": 1 }, @@ -499,147 +328,12 @@ }, { "type": "card.play", - "cardId": "c3" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "boxcar", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 + "cardId": "c182", + "placement": { + "row": 1, + "col": 2 }, - "index": 0 - }, - { - "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": "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": 2 + "variant": 0 }, { "type": "draw.end" @@ -656,9 +350,9 @@ }, { "type": "card.play", - "cardId": "c134", + "cardId": "c48", "placement": { - "row": 0, + "row": 1, "col": 3 }, "variant": 0 @@ -676,12 +370,209 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "card.discard", + "cardId": "c49", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c38", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c115", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c133", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, { "type": "card.play", - "cardId": "c135", + "cardId": "c10" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c32", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c25", + "toSlot": 2 + }, + { + "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": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c124", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c100", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c69", "placement": { - "row": 0, - "col": -3 + "row": -2, + "col": -2 }, "variant": 0 }, @@ -698,9 +589,62 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "card.play", + "cardId": "c1" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, { "type": "card.discard", - "cardId": "c110", + "cardId": "c43", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c179", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c46", "toSlot": 0 }, { @@ -718,12 +662,42 @@ }, { "type": "card.play", - "cardId": "c143", - "placement": { - "row": 0, - "col": -4 - }, - "variant": 0 + "cardId": "c11" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "reefer", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "reefer", + "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": "c5" }, { "type": "draw.end" @@ -739,13 +713,9 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c156", - "placement": { - "row": -3, - "col": 5 - }, - "variant": 0 + "type": "card.discard", + "cardId": "c121", + "toSlot": 1 }, { "type": "draw.end" @@ -753,6 +723,87 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "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": "draw" @@ -762,13 +813,22 @@ }, { "type": "card.play", - "cardId": "c188", + "cardId": "c192", "placement": { "row": 0, - "col": 4 + "col": 1 }, "variant": 0 }, + { + "type": "card.play", + "cardId": "c187", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 1 + }, { "type": "draw.end" }, @@ -784,63 +844,27 @@ "carType": "coach", "loaded": false }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c83", - "toSlot": 1 - }, - { - "type": "draw.end" - }, { "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "boxcar", + "trayId": "tray3", + "carType": "coach", "loaded": false }, { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "boxcar", - "loaded": false + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 }, { - "type": "porter.board", + "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } + "row": -1, + "col": -1 + }, + "box": 0 }, { "type": "loadUnload.end" @@ -852,11 +876,112 @@ { "type": "freightAgent.clearInbound", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": -1 }, "index": 0 }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.play", + "cardId": "c121", + "placement": { + "row": -1, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.play", + "cardId": "c46", + "placement": { + "row": -1, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.play", + "cardId": "c124", + "placement": { + "row": 1, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.end" + }, { "type": "loadUnload.end" }, @@ -869,7 +994,12 @@ }, { "type": "card.play", - "cardId": "c20" + "cardId": "c76", + "placement": { + "row": -1, + "col": 4 + }, + "variant": 0 }, { "type": "draw.end" @@ -877,19 +1007,13 @@ { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", + "carType": "hopper", "loaded": false }, { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", + "carType": "hopper", "loaded": false }, { @@ -909,12 +1033,23 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c88", - "toSlot": 2 + "type": "draw.end" }, { - "type": "draw.end" + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false }, { "type": "loadUnload.end" @@ -927,13 +1062,58 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.discard", - "cardId": "c39", - "toSlot": 0 + "type": "card.play", + "cardId": "c96", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 }, { "type": "draw.end" }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "tank", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "tank", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "mainline.clearance", + "allow": false + }, { "type": "loadUnload.end" }, @@ -945,10 +1125,10 @@ "type": "switch.move", "trayId": "tray3", "to": { - "row": -1, - "col": 1 + "row": 0, + "col": -3 }, - "reverse": false + "reverse": true }, { "type": "switch.dropCars", @@ -959,8 +1139,35 @@ "type": "switch.move", "trayId": "tray3", "to": { - "row": -2, - "col": 3 + "row": -1, + "col": 2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -1 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 }, "reverse": false }, @@ -974,6 +1181,239 @@ "trayId": "tray3", "count": 1 }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 2 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 2 + }, + "reverse": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 1, + "col": 3 + }, + "carType": "tank" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "coach", + "loaded": false + }, + { + "type": "laborer.startLoad", + "at": { + "row": 1, + "col": 3 + } + }, + { + "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": -3 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1, + "fromNose": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1, + "fromNose": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 3 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 2, + "fromNose": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 2 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": 1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, { "type": "switch.move", "trayId": "tray3", @@ -983,9 +1423,109 @@ }, "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": 2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, { "type": "switch.end" }, + { + "type": "switch.end" + }, + { + "type": "switch.end" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": 1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "hopper" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 3 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": 1, + "col": 3 + }, + "box": 2 + }, { "type": "loadUnload.end" }, @@ -1001,6 +1541,22 @@ }, "index": 0 }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, { "type": "loadUnload.end" }, @@ -1011,311 +1567,22 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": -2, + "row": -1, "col": 3 }, - "carType": "boxcar" + "carType": "hopper" }, { "type": "laborer.startLoad", "at": { - "row": -2, + "row": -1, "col": 3 } }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -2, - "col": 3 - }, - "carType": "boxcar" - }, { "type": "laborer.advanceLoad", "at": { - "row": -2, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c55", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c36", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.discard", - "cardId": "c113", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.startLoad", - "at": { - "row": -2, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "boxcar", - "loaded": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c65", - "placement": { - "row": 1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -2, - "col": 3 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -2, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c129", - "placement": { - "row": 0, - "col": 5 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c182", - "placement": { "row": -1, - "col": 4 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c55", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, "col": 3 }, "box": 0 @@ -1330,68 +1597,41 @@ { "type": "freightAgent.clearInbound", "at": { - "row": -2, - "col": 3 + "row": 0, + "col": 0 }, "index": 0 }, { - "type": "laborer.startLoad", - "at": { - "row": -2, - "col": 3 - } + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "reefer", + "loaded": true }, { - "type": "loadUnload.end" + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "boxcar", + "loaded": true }, { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "caboose", + "loaded": true }, { "type": "laborer.advanceLoad", "at": { - "row": -2, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -2, + "row": -1, "col": 3 }, "box": 1 }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, { "type": "laborer.advanceLoad", "at": { - "row": -2, + "row": -1, "col": 3 }, "box": 2 @@ -1407,17 +1647,17 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c161", - "placement": { - "row": -4, - "col": 5 - }, - "variant": 1 + "type": "card.discard", + "cardId": "c73", + "toSlot": 0 }, { "type": "draw.end" }, + { + "type": "mainline.clearance", + "allow": false + }, { "type": "loadUnload.end" }, @@ -1428,12 +1668,17 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "card.discard", + "cardId": "c113", + "toSlot": 0 + }, { "type": "card.play", - "cardId": "c99", + "cardId": "c155", "placement": { - "row": 0, - "col": 2 + "row": 1, + "col": 4 }, "variant": 0 }, @@ -1442,18 +1687,277 @@ }, { "type": "newTrain.placeCar", - "trayId": "tray3", + "trayId": "tray1", "carType": "hopper", - "loaded": true + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", + "trayId": "tray1", + "carType": "tank", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", "loaded": true }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.play", + "cardId": "c133", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c183", + "placement": { + "row": 0, + "col": 4 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "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": 1 + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": -1, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray0", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": -1, + "col": 2 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray0", + "to": { + "row": 0, + "col": -3 + }, + "reverse": true + }, + { + "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.move", + "trayId": "tray0", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 5 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 1, + "col": 4 + }, + "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": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 5 + }, + "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": 1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 5 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 1, + "col": 4 + }, + "reverse": false + }, + { + "type": "switch.end" + }, { "type": "loadUnload.end" } - ] + ], + "rules": { + "startingHand": "threeTrackThreeOther", + "revenue": { + "passengerPerCoach": 1, + "freightPerLoad": 1, + "trainPerTransit": 1 + } + } } \ No newline at end of file diff --git a/node_modules b/node_modules new file mode 120000 index 0000000..6c57164 --- /dev/null +++ b/node_modules @@ -0,0 +1 @@ +../../../node_modules \ No newline at end of file diff --git a/package.json b/package.json index 89a2c05..8bcbb0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "station-master", - "version": "0.4.6", + "version": "0.4.7", "private": true, "type": "module", "description": "Station Master — a railroad operations game", diff --git a/public/replays/seed-309841.json b/public/replays/seed-2701379.json similarity index 62% rename from public/replays/seed-309841.json rename to public/replays/seed-2701379.json index 85542f9..88c64bf 100644 --- a/public/replays/seed-309841.json +++ b/public/replays/seed-2701379.json @@ -1,7 +1,7 @@ { - "seed": 309841, - "title": "16 Revenue · seed 309841", - "note": "16 Revenue over 5 Days · 1 train(s) on the timetable · 23 cards down · no collisions · played by developer", + "seed": 2701379, + "title": "14 Revenue · seed 2701379", + "note": "14 Revenue over 5 Days · 0 train(s) on the timetable · 8 cards down · no collisions · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -16,15 +16,48 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" + "type": "draw.fromDepartment", + "slot": 2 }, { "type": "card.play", "cardId": "c20" }, + { + "type": "card.play", + "cardId": "c115", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c197", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c169", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 1 + }, { "type": "draw.end" }, + { + "type": "newTrain.startExtra", + "trainNumber": 21, + "atSeat": null + }, { "type": "newTrain.placeCar", "trayId": "tray3", @@ -57,1067 +90,100 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" + "type": "draw.fromDepartment", + "slot": 2 }, { "type": "card.play", - "cardId": "c195", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c160", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c39", - "placement": { - "row": -1, - "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": "card.play", - "cardId": "c186", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "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, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c171", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c63", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.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.startLoad", - "at": { - "row": -1, - "col": 2 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c151", - "placement": { - "row": -1, - "col": -1 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c119", - "placement": { - "row": -1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.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.startLoad", - "at": { - "row": -1, - "col": 2 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c32", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c102", - "node": 1 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "mainline.modify", - "cardId": "c107", - "node": 1 - }, - { - "type": "draw.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.startLoad", - "at": { - "row": -1, - "col": 2 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c71", - "placement": { - "row": 0, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c76", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c203", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c175", + "cardId": "c60", "placement": { "row": -1, "col": -2 }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c159", - "placement": { - "row": -1, - "col": -3 - }, - "variant": 1 + "variant": 0 }, { "type": "draw.end" }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c44", + "placement": { + "row": -1, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "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, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.end" + }, { "type": "laborer.beginUnload", "at": { "row": -1, - "col": 2 + "col": -2 }, "carIndex": 0 }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 - }, { "type": "loadUnload.end" }, @@ -1129,7 +195,7 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": 2 + "col": -2 }, "carType": "boxcar" }, @@ -1137,15 +203,43 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, - "box": 1 + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "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 }, @@ -1160,7 +254,7 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": 2 + "col": -2 }, "index": 0 }, @@ -1168,14 +262,29 @@ "type": "laborer.startLoad", "at": { "row": -1, - "col": 2 + "col": -2 } }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "boxcar" + }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 0 }, @@ -1184,36 +293,34 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "switch" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c207", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" + "type": "switch.end" }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 1 }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 2 }, @@ -1222,27 +329,34 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "switch" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" + "type": "switch.end" }, { "type": "laborer.beginUnload", "at": { "row": -1, - "col": 2 + "col": -2 }, "carIndex": 0 }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 2 }, @@ -1251,31 +365,34 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "switch" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c7" - }, - { - "type": "draw.end" + "type": "switch.end" }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 1 }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 0 }, @@ -1290,23 +407,109 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": 2 + "col": -2 }, "index": 0 }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "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": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, { "type": "laborer.beginUnload", "at": { "row": -1, - "col": 2 + "col": -2 }, - "carIndex": 1 + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 2 }, @@ -1315,54 +518,34 @@ }, { "type": "localOps.choose", - "option": "draw" + "option": "switch" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c183", - "placement": { - "row": 0, - "col": 3 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c149", - "placement": { - "row": 1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c48", - "placement": { - "row": 1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" + "type": "switch.end" }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 1 }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -2 }, "box": 0 }, @@ -1377,231 +560,39 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": 2 + "col": -2 }, "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": "card.play", - "cardId": "c69", - "placement": { - "row": -2, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c141", - "placement": { - "row": 0, - "col": -3 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c93", - "placement": { - "row": 0, - "col": -3 - }, - "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": "card.play", - "cardId": "c165", - "placement": { - "row": 1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c77", - "placement": { - "row": 2, - "col": 3 - }, - "variant": 0 - }, - { - "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": "coach", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c184", - "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": "c80", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "laborer.startLoad", + "at": { "row": -1, - "col": 1 - }, - "reverse": false + "col": -2 + } }, { - "type": "switch.move", - "trayId": "tray3", - "to": { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { "row": -1, - "col": 3 + "col": -2 }, - "reverse": false + "carType": "boxcar" }, { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 2, - "fromNose": true - }, - { - "type": "switch.end" + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 }, { "type": "loadUnload.end" @@ -1613,6 +604,662 @@ { "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": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -2 + }, + "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.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "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": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -2 + }, + "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.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "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": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -2 + }, + "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.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "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": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -2 + }, + "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.startLoad", + "at": { + "row": -1, + "col": -2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -2 + }, + "carType": "reefer" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -2 + }, + "box": 0 + }, + { + "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": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -2 + }, + "carIndex": 0 + }, { "type": "loadUnload.end" } diff --git a/public/replays/seed-143542.json b/public/replays/seed-2717217.json similarity index 75% rename from public/replays/seed-143542.json rename to public/replays/seed-2717217.json index 08f98b9..9fa5364 100644 --- a/public/replays/seed-143542.json +++ b/public/replays/seed-2717217.json @@ -1,7 +1,7 @@ { - "seed": 143542, - "title": "24 Revenue · seed 143542", - "note": "24 Revenue over 5 Days · 1 train(s) on the timetable · 14 cards down · no collisions · played by developer", + "seed": 2717217, + "title": "18 Revenue · seed 2717217", + "note": "18 Revenue over 5 Days · 2 train(s) on the timetable · 25 cards down · 2 collision(s) · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -16,11 +16,16 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" + "type": "draw.fromDepartment", + "slot": 1 }, { "type": "card.play", - "cardId": "c123", + "cardId": "c7" + }, + { + "type": "card.play", + "cardId": "c197", "placement": { "row": 0, "col": 1 @@ -29,7 +34,90 @@ }, { "type": "card.play", - "cardId": "c205", + "cardId": "c166", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c165", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "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": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c117", "placement": { "row": 0, "col": -1 @@ -50,25 +138,28 @@ "type": "draw.fromHomeOffice" }, { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" + "type": "card.play", + "cardId": "c24" }, { "type": "card.play", - "cardId": "c23" + "cardId": "c5" }, { "type": "draw.end" }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, { "type": "loadUnload.end" }, @@ -84,6 +175,59 @@ }, "carType": "coach" }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "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": "card.discard", + "cardId": "c151", + "toSlot": 0 + }, + { + "type": "draw.end" + }, { "type": "loadUnload.end" }, @@ -96,28 +240,10 @@ }, { "type": "card.play", - "cardId": "c195", + "cardId": "c90", "placement": { "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c164", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c45", - "placement": { - "row": -1, - "col": 2 + "col": -1 }, "variant": 0 }, @@ -136,7 +262,548 @@ }, { "type": "card.play", - "cardId": "c76", + "cardId": "c163", + "placement": { + "row": -2, + "col": 0 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c72", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c105", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c200", + "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": "c66", + "placement": { + "row": 1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "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": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "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": "card.play", + "cardId": "c173", + "placement": { + "row": -1, + "col": -2 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c33" + }, + { + "type": "card.play", + "cardId": "c34" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c17" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.startExtra", + "trainNumber": 18, + "atSeat": null + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "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": "c145", + "placement": { + "row": -2, + "col": -1 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c177", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c184", + "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": "boxcar", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c43", + "placement": { + "row": -1, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c37", + "placement": { + "row": -1, + "col": -4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -4 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -4 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -4 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c119", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c86", "placement": { "row": 0, "col": 3 @@ -146,6 +813,82 @@ { "type": "draw.end" }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": -4 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -4 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -3 + }, + "carType": "hopper" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -4 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 1 + }, { "type": "loadUnload.end" }, @@ -158,11 +901,22 @@ }, { "type": "card.play", - "cardId": "c14" + "cardId": "c20" }, { "type": "draw.end" }, + { + "type": "newTrain.startExtra", + "trainNumber": 21, + "atSeat": null + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": false + }, { "type": "newTrain.placeCar", "trayId": "tray3", @@ -182,17 +936,12 @@ "loaded": true }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 2 }, { "type": "loadUnload.end" @@ -206,33 +955,90 @@ }, { "type": "card.play", - "cardId": "c5" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c175", + "cardId": "c169", "placement": { "row": -1, - "col": -1 + "col": -5 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -4 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c186", + "placement": { + "row": -1, + "col": 2 }, "variant": 1 }, { "type": "draw.end" }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c181", + "placement": { + "row": 0, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 1 + }, { "type": "loadUnload.end" }, @@ -245,7 +1051,7 @@ "trayId": "tray3", "to": { "row": -1, - "col": 1 + "col": -2 }, "reverse": false }, @@ -259,7 +1065,7 @@ "trayId": "tray3", "to": { "row": -1, - "col": 2 + "col": -3 }, "reverse": false }, @@ -273,6 +1079,11 @@ "trayId": "tray3", "count": 1 }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, { "type": "switch.move", "trayId": "tray3", @@ -282,20 +1093,107 @@ }, "reverse": true }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -3 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, { "type": "switch.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -3 + } }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -4 + } }, { "type": "loadUnload.end" @@ -305,25 +1203,26 @@ "option": "freightAgent" }, { - "type": "freightAgent.stockOutbound", + "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": 2 + "col": -4 }, - "carType": "hopper" - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 2 - } + "index": 0 }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -3 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 }, "box": 0 }, @@ -338,22 +1237,15 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": 2 + "col": -3 }, "carType": "hopper" }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -3 }, "box": 1 }, @@ -361,10 +1253,360 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -4 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -4 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -3 }, "box": 2 }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -4 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c75", + "placement": { + "row": -2, + "col": -2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c153", + "placement": { + "row": -1, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": -4 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -4 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -3 + }, + "carType": "hopper" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "hopper", + "loaded": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -4 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c118", + "placement": { + "row": 0, + "col": -4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "maneuver.flyingSwitch", + "cardId": "c109", + "trayId": "tray1", + "count": 1, + "to": { + "row": -1, + "col": -3 + } + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -3 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -3 + }, + "box": 0 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -4 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -3 + }, + "carType": "hopper" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -3 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -3 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c190", + "placement": { + "row": 0, + "col": 4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -4 + }, + "box": 1 + }, { "type": "loadUnload.end" }, @@ -379,25 +1621,11 @@ { "type": "switch.end" }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 2 - } - }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": 2 + "col": -4 }, "box": 0 }, @@ -412,20 +1640,11 @@ "type": "switch.end" }, { - "type": "laborer.advanceLoad", + "type": "laborer.startLoad", "at": { "row": -1, - "col": 2 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 2 - }, - "box": 2 + "col": -4 + } }, { "type": "loadUnload.end" @@ -437,258 +1656,18 @@ { "type": "freightAgent.clearInbound", "at": { - "row": 0, - "col": 0 + "row": -1, + "col": -4 }, "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": "c19" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c36", - "placement": { - "row": -1, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c72", - "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": "c119", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": -2 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": -2 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": -2 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": -4 }, - "box": 2 + "box": 0 }, { "type": "loadUnload.end" @@ -701,7 +1680,7 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": -2 + "col": -4 }, "carType": "boxcar" }, @@ -709,207 +1688,10 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": -4 }, "box": 1 }, - { - "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": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "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.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c90", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 2 - }, { "type": "loadUnload.end" }, @@ -922,759 +1704,12 @@ }, { "type": "card.play", - "cardId": "c117", + "cardId": "c148", "placement": { - "row": 0, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c87", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c187", - "placement": { - "row": 0, + "row": -2, "col": 3 }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c114", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c146", - "placement": { - "row": 0, - "col": -3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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.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.end" - }, - { - "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": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c57", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "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": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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.startLoad", - "at": { - "row": -1, - "col": -2 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c81", - "toSlot": 2 + "variant": 1 }, { "type": "draw.end" @@ -1683,199 +1718,10 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 + "col": -4 }, "box": 2 }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c92", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c30" - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c27", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "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": "newTrain.placeCar", - "trayId": "tray3", - "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": "card.discard", - "cardId": "c60", - "toSlot": 1 - }, - { - "type": "draw.end" - }, { "type": "loadUnload.end" } diff --git a/public/replays/seed-404869.json b/public/replays/seed-404869.json new file mode 100644 index 0000000..71a256c --- /dev/null +++ b/public/replays/seed-404869.json @@ -0,0 +1,2758 @@ +{ + "seed": 404869, + "title": "16 Revenue · seed 404869", + "note": "16 Revenue over 5 Days · 1 train(s) on the timetable · 9 cards down · no collisions · played by developer", + "rules": { + "startingHand": "threeRandom", + "revenue": { + "passengerPerCoach": 1, + "freightPerLoad": 1, + "trainPerTransit": 0 + } + }, + "history": [ + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c22" + }, + { + "type": "card.play", + "cardId": "c6" + }, + { + "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.fromDepartment", + "slot": 0 + }, + { + "type": "card.play", + "cardId": "c14" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.startExtra", + "trainNumber": 15, + "atSeat": null + }, + { + "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.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c15" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.startExtra", + "trainNumber": 16, + "atSeat": null + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c181", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c163", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c74", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c32" + }, + { + "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.play", + "cardId": "c151", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c53", + "placement": { + "row": -1, + "col": 2 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c37", + "placement": { + "row": -1, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": false + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 3 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "boxcar" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c166", + "placement": { + "row": -1, + "col": 4 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c202", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 3 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 3 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 3 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 3 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 3 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 3 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 3 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + } + ] +} \ No newline at end of file diff --git a/src/engine/advance.ts b/src/engine/advance.ts index fa03b95..42ac6a4 100644 --- a/src/engine/advance.ts +++ b/src/engine/advance.ts @@ -253,34 +253,23 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult { } } - // §7 — "Once all timetabled trains are created, if there is a played Extra Train card and an - // available Crew Tray, the player who played the card may place the Crew Tray in either division - // point for immediate departure." An Extra runs once and is gone. - // - // SIMPLIFICATION, flagged: the rules let the player choose the direction. This launches eastbound - // from the West Division Point. Giving the player the choice needs its own intent. + /** + * §7 — "if there is a played Extra Train card and an available Crew Tray, the player who played + * the card may place the Crew Tray in either division point for immediate departure." + * + * THIS USED TO LAUNCH EVERY EXTRA EASTBOUND FROM THE WEST DIVISION POINT, with the simplification + * flagged in a comment. Jesse's rule: an Extra runs the way its NUMBER says, exactly as a + * timetabled train does — odd runs west, even runs east (§2.3) — so it starts at the Division + * Point it runs FROM. It may instead start at any Control Point, which is any Office above a + * Whistle Post, at the player's choice. + * + * That is a decision, so the phase stops for it the same way it stops to have cars placed. The + * chooser is the acting player; §7 says the player who PLAYED the card, which is the same person + * in solitaire and needs `pendingExtras` to carry a player before it is not. + */ if (s.pendingExtras.length > 0 && s.freeTrays.length > 0) { - const number = s.pendingExtras.shift()!; - const trayId = s.freeTrays.pop()!; - s.trays.set(trayId, { - id: trayId, - trainNumber: number, - trainIsExtra: true, - engineAt: 0, - consist: [], - direction: 'east', - position: { at: 'divisionPoint', side: 'west' }, - movesUsed: 0, - }); - const dp = s.division.nodes.find((n) => n.kind === 'divisionPoint' && n.side === 'west'); - if (dp?.kind === 'divisionPoint') dp.holding.push(trayId); - events.push({ - type: 'trainMadeUp', - trainNumber: number, - isExtra: true, - at: 'West Division Point', - direction: 'east', - }); + s.clock.currentActor = actorAt(s, s.clock.actorOffset % s.players.length); + return { events, needsInput: true }; } // Gap 9 — the car-placement round REPEATS until the consist is full or no suitable car remains. diff --git a/src/engine/apply.ts b/src/engine/apply.ts index 39f8d88..3f1c294 100644 --- a/src/engine/apply.ts +++ b/src/engine/apply.ts @@ -26,6 +26,8 @@ import { mainlineModifierRule, mainlineProfile, houseRules, + runDirection, + startingDivisionPoint, modifierProfile, nextOfficeTier, officeProfile, @@ -51,7 +53,19 @@ import type { TurnoutOrientation, } from './state.ts'; import { createRng } from './rng.ts'; -import { carsOn, coordKey, isOperationalRail, playerAtSeat, seatOf, spaceOn, turnOf } from './state.ts'; +import { + carsOn, + coordKey, + cutTowards, + isOperationalRail, + playerAtSeat, + railFacingOf, + seatOf, + spaceOn, + standingSides, + trackOrder, + turnOf, +} from './state.ts'; import type { MoveBlock, Occupancy, Port } from './track.ts'; import { canDropCarsAt, @@ -330,35 +344,25 @@ function workTrack(f: Facility): [Load | null, Load | null, Load | null] { return f.menAtWork; } -/** - * HOW MANY EMPTY CARS OF THIS TYPE ARE SPOTTED AND NOT ALREADY PROMISED TO A LOAD. - * - * §9.3 — a load has to have somewhere to go. You cannot start the MEN | AT | WORK moves until an - * empty car of the right type is standing on the industry's track, "otherwise you are just dropping - * cargo onto the tracks — pointless waste". The final swap needs a car of the LOAD's type, so a - * hopper load cannot be swapped onto a tank: strict matching, not "any empty car". - * - * Counted rather than merely present, because green boxes and industry tracks both grow past one - * slot with Modifiers (measured: capacity > 1 on 24.5% of freight facilities and a track longer than - * one on 41%). Two loads staged against one spotted car would put the second one on the sign with - * nowhere to land, which is the jam this rule exists to prevent — just one step later. - * - * Loads already in the green box and already walking the sign both count as claims on a car. - */ -export function unclaimedEmpties(f: Facility, type: CarType): number { - const spotted = f.industryTrack.cars.filter((c) => !c.loaded && c.type === type).length; - const staged = f.outboundBox.filter((c) => c.type === type).length; - const working = (f.menAtWork ?? []).filter((l) => l?.dir === 'out' && l.type === type).length; - return spotted - staged - working; -} - /** * The green-box load that may start down the sign, or null if none may. * + * §9.3 — a load has to have somewhere to go before the Laborers touch it. It may not start the + * MEN | AT | WORK moves until an empty car of the right type is standing on the industry's track, + * "otherwise you are just dropping cargo onto the tracks — pointless waste". The final swap needs a + * car of the LOAD's type, so a hopper load cannot be swapped onto a tank: strict matching, not "any + * empty car". THIS IS THE ONLY PLACE THAT RULE APPLIES — stocking the green box is free of it + * (§6.3), so cargo may wait on the dock for a car that has not been switched in yet. + * * Not simply `outboundBox[0]`. A Power Plant takes hoppers AND tanks, so a green box holding a * hopper load and a tank load against one spotted empty tank must start the TANK — taking the first * entry regardless would report the whole facility as blocked while a perfectly legal load sat * beside it. + * + * Cars are COUNTED against claims rather than merely looked for, because green boxes and industry + * tracks both grow past one slot with Modifiers (measured: capacity > 1 on 24.5% of freight + * facilities and a track longer than one on 41%). Two loads walking the sign toward one spotted car + * would strand the second on WORK, which is the jam this rule exists to prevent. */ export function startableLoad(f: Facility): number | null { const claims = new Map(); @@ -513,6 +517,28 @@ function spendFreightBudget( turn.freightWorked[key] = (turn.freightWorked[key] ?? 0) + n; } +/** + * Give back what a set-out spent, when the train picks its own cut straight back up. + * + * The mirror of `spendFreightBudget` and deliberately its own function: "undo the drop" is a rule, + * not an arithmetic detail, and it applies on the square the cars were LEFT on rather than the one + * the train ends up at. + */ +function refundFreightBudget( + s: GameState, + player: PlayerIndex, + tray: CrewTray, + at: GridCoord, + stock: readonly RollingStock[], +): void { + if (!rulesOf(tray).oneFreightPerLocation) return; + const n = stock.filter(isFreight).length; + if (n === 0) return; + const turn = turnOf(s, player); + const key = freightWorkedKey(tray.id, at); + turn.freightWorked[key] = Math.max(0, (turn.freightWorked[key] ?? 0) - n); +} + /** Trains that may not be worked by Porters at all (§7): the Military train and the Director's car. */ function refusesPassengers(tray: CrewTray): boolean { return rulesOf(tray).noPassengerWork === true; @@ -613,10 +639,21 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo * on the move or it cannot bite at all. */ const rules = rulesOf(tray); - if (dest.couples.length > 0) { + /** + * THE TRAIN'S OWN CUT IS NOT A PICK-UP. Taking back the cars you just set out on the square you + * are standing on undoes the drop; it is not fresh work, and none of the three restrictions + * that gate picking cars up should bite on it. + * + * Left in and the rule reads absurdly: X13 prints "may drop but not pick up", so a legal drop + * off the nose would leave the train forbidden to pull forward past its own cars for the rest + * of the turn — a one-way move nothing warned about. Only the cars that were ALREADY there when + * the crew arrived are a pick-up, so the own cut is subtracted before the tests run. + */ + const fresh = dest.couples.slice(ownCutFor(s, player, i.trayId, i.reverse).length); + if (fresh.length > 0) { if (rules.dropOnly) return 'PICKUP_NOT_ALLOWED'; - if (rules.pickUpEmptiesOnly && dest.couples.some((c) => c.loaded)) return 'EMPTIES_ONLY'; - const freight = dest.couples.filter(isFreight).length; + if (rules.pickUpEmptiesOnly && fresh.some((c) => c.loaded)) return 'EMPTIES_ONLY'; + const freight = fresh.filter(isFreight).length; if (freight > 0 && !freightBudgetLeft(s, player, tray, i.to, freight)) return 'FREIGHT_WORKED_HERE'; } return null; @@ -820,14 +857,21 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo return 'NO_SUITABLE_CAR'; } /** - * §9.3 — FREIGHT NEEDS A CAR AT THE DOCK BEFORE THE CARGO IS FETCHED. Staging a load against - * no car is fetching cargo you cannot ship. + * STOCKING DOES NOT NEED A CAR SPOTTED — THE LABORERS DO. * - * PASSENGERS ARE DELIBERATELY DIFFERENT and this is not an oversight: people can wait on the - * platform for a train that has not arrived, so a Passenger Facility stocks freely. Freight - * cannot — a crate on the ground is not a shipment. + * §6.3 asks for nothing but a car in the Division Yard and room in the box: the Freight Agent + * "select[s] one Rolling Stock from the Division Yard pile and place[s] it onto a Facility's + * green Outbound box". The empty-car requirement belongs one step later, to §9.3's "Load the + * car" — "*a load in the Green Loading Box AND an empty car of the required type on the + * industry's track*" — which is the action that walks the load down MEN | AT | WORK. + * + * The engine used to hoist that requirement forward onto stocking, and it made the ordinary + * play illegal: an agent may perfectly well have the cargo waiting on the dock while the car + * to ship it in is still being switched in. Nothing jams as a result — a load sitting in a + * green box is waiting, not stuck, and only a load on MEN | AT | WORK locks the industry + * track. `laborer.startLoad` holds the real gate (`startableLoad`), so a load can be staged + * early but still cannot start down the sign until its car is standing there. */ - if (f.kind === 'freight' && unclaimedEmpties(f, i.carType) < 1) return 'NO_EMPTY_CAR_SPOTTED'; return null; } @@ -859,6 +903,20 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo } // -- New Train ------------------------------------------------------------ + case 'newTrain.startExtra': { + if (!inPhase(s, 'newTrain')) return 'WRONG_PHASE'; + if (!s.pendingExtras.includes(i.trainNumber)) return 'NO_EXTRA_PENDING'; + if (s.freeTrays.length === 0) return 'NO_FREE_TRAY'; + if (i.atSeat === null) return null; + // A Control Point is any Office above a Whistle Post (§8). A Whistle Post is not one, which is + // the whole reason upgrading buys a place for an Extra to start. + const area = s.officeAreas.get(i.atSeat); + if (!area) return 'NO_SUCH_FACILITY'; + if (!officeProfile(area.tier).isControlPoint) return 'NOT_A_CONTROL_POINT'; + // It still has to fit: an Extra starting here takes an A/D track like any other arrival. + return area.adOccupancy.length >= officeProfile(area.tier).adTracks ? 'NO_FREE_AD_TRACK' : null; + } + case 'newTrain.placeCar': { if (!inPhase(s, 'newTrain')) return 'WRONG_PHASE'; const tray = s.trays.get(i.trayId); @@ -934,9 +992,12 @@ export function check(s: GameState, player: PlayerIndex, i: Intent): RejectionCo if (!f.menAtWork) return 'NO_SUCH_FACILITY'; if (f.outboundBox.length === 0) return 'BOX_EMPTY'; if (f.menAtWork[0] !== null) return 'BOX_FULL'; - // The car can be coupled away after the cargo was staged — running over an industry track - // couples whatever stands on it, mandatorily (§A.4) — so the Freight Agent's check is not - // enough on its own. Both gates are required. + // §9.3's "Load the car" requires "*an empty car of the required type on the industry's + // track*", and THIS is where that is enforced — stocking the green box (§6.3) is deliberately + // free of it. Cargo may be staged against a car that is still being switched in; it simply + // cannot leave the box until the car is standing there. The check also has to be made at this + // moment rather than earlier because a car can be coupled away after the cargo was staged: + // running over an industry track couples whatever stands on it, mandatorily (§A.4). return startableLoad(f) !== null ? null : 'NO_EMPTY_CAR_SPOTTED'; } @@ -1142,12 +1203,30 @@ function destinationsFor( occupancy: occupancyFor(s, player, trayId), consistSize: tray.consist.length, self: trayId, + ...(tray.standingWest === undefined ? {} : { standingWest: tray.standingWest }), }, from, exit, ); } +/** + * The cars this crew would take back off its OWN card by pulling out this way. + * + * Exported because three places need the same answer and none of them should re-derive the exit port + * to get it: the move check exempts these cars from a train's pick-up restrictions, the move label + * names them separately from cars found on the line, and the walk counts them against the four-car + * limit. Empty for a train with nothing standing beside it, which is almost every move. + */ +export function ownCutFor(s: GameState, player: PlayerIndex, trayId: TrayId, reverse: boolean): RollingStock[] { + const tray = s.trays.get(trayId); + if (!tray || tray.position.at !== 'grid') return []; + const here = tray.position.coord; + const facing = facingPort(s, trayId); + const exit: Port = reverse ? reversePort(s, player, here, facing) : facing; + return cutTowards(tray, carsOn(areaOf(s, player).grid.get(coordKey(here)) ?? emptyCard()), exit); +} + /** * WHERE THIS CREW MAY GO, AND WHY IT MAY NOT GO ELSEWHERE. * @@ -1171,6 +1250,7 @@ export function movesFor( occupancy: occupancyFor(s, player, trayId), consistSize: tray.consist.length, self: trayId, + ...(tray.standingWest === undefined ? {} : { standingWest: tray.standingWest }), }; const facing = facingPort(s, trayId); const forward = exploreMoves(ctx, from, facing); @@ -1204,6 +1284,11 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] { const from = trayCoord(s, i.trayId)!; const dests = destinationsFor(s, player, i.trayId, from, i.reverse); const dest = dests.find((d) => d.coord.row === i.to.row && d.coord.col === i.to.col)!; + const tray = s.trays.get(i.trayId)!; + // The port the crew pulls out THROUGH — the same one `destinationsFor` explored from, so the + // cut it recouples on the way out is the cut the walk counted. + const facingNow = facingPort(s, i.trayId); + const exitPort: Port = i.reverse ? reversePort(s, player, from, facingNow) : facingNow; // The crew leaves by the port opposite the one it entered through, which is what it will be // facing when it stops. Without this the facing stays 'e'/'w' forever and a crew that turns // onto a north-south spur can never move again. @@ -1241,14 +1326,28 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] { }, ]; if (dest.couples.length > 0) { - // Name the cards the cars came from. The reducer used to clear every card in the district - // instead, so one coupling anywhere deleted every standing car and every industry track in - // the Office Area — loads worked over several Stages vanished when a crew picked up a single - // boxcar somewhere else entirely. + /** + * Name the cards the cars came from. The reducer used to clear every card in the district + * instead, so one coupling anywhere deleted every standing car and every industry track in + * the Office Area — loads worked over several Stages vanished when a crew picked up a single + * boxcar somewhere else entirely. + * + * `from` NOW BEGINS WITH THE SQUARE THE CREW LEFT. It could not before — the list was built + * from `dest.path` plus the destination, neither of which can ever contain the start — so a + * cut recoupled off your own card was added to the consist and left standing on the board at + * the same time, one car becoming two. The cars that stay behind (a cut set out off the + * OTHER end) ride along on `leaves`, because by the time this is reduced the tray has moved + * and `standingWest` no longer describes this card. + */ + const grid = areaOf(s, player).grid; + const startCut = cutTowards(tray, carsOn(grid.get(coordKey(from)) ?? emptyCard()), exitPort); const lifted = [ + ...(startCut.length > 0 ? [from] : []), ...dest.path.map((step) => step.coord), i.to, - ].filter((c) => carsOn(areaOf(s, player).grid.get(coordKey(c)) ?? emptyCard()).length > 0); + ].filter((c) => carsOn(grid.get(coordKey(c)) ?? emptyCard()).length > 0); + const sides = standingSides(tray, carsOn(grid.get(coordKey(from)) ?? emptyCard())); + const stayed = exitPort === 'e' ? sides.west : exitPort === 'w' ? sides.east : []; // §A.3 — "engines also have couplers on the front end, so a train can pick cars up onto // its nose". Running forward the engine meets cars head-on and takes them in front; backing // up, they couple behind. Which end they land on is the whole point of a run-around: it @@ -1260,6 +1359,8 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] { stock: dest.couples, from: lifted, toNose: !i.reverse, + ...(startCut.length > 0 && stayed.length > 0 ? { leaves: { at: from, stock: stayed } } : {}), + ...(startCut.length > 0 ? { recoupled: { at: from, stock: startCut } } : {}), }); } return events; @@ -1436,6 +1537,9 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] { return [{ type: 'facilityUnjammed', player, at: i.at, from: i.from, stock }]; } + case 'newTrain.startExtra': + return [{ type: 'extraStarted', player, trainNumber: i.trainNumber, atSeat: i.atSeat }]; + case 'newTrain.placeCar': return [ { @@ -1580,6 +1684,11 @@ export function reduce(s: GameState, e: GameEvent): void { // their turn. The event carries no player of its own. turnOf(s, playerAtSeat(s, seat)).movesRemaining = e.movesRemaining; + // A MOVE ENDS ON AN EMPTY CARD, always: coupling is mandatory, so anything standing on the + // destination has just been lifted into the tray. Whatever this crew had beside it belonged to + // the square it left, so its place in that row is gone with it. + tray.standingWest = 0; + // An A/D track is held only while the train is actually standing at the Office (§2.1). // Leaving it out of sync means the Office looks permanently full and every arrival collides. const area = areaAtSeat(s, seat); @@ -1593,12 +1702,25 @@ export function reduce(s: GameState, e: GameEvent): void { case 'carsCoupled': { const tray = s.trays.get(e.trayId)!; const area = areaAtSeat(s, tray.position.at === 'grid' ? tray.position.seat : 0); + /** + * `stock` ARRIVES NEAREST-FIRST — the order the engine MET the cars, accumulated along the + * walk. A tray is ordered nose first, so only one of the two ends needs turning round. + * + * Behind the train, near-first is already right: the nearest car couples to the existing tail + * and each one after it goes further back, which is the order the array is in. + * + * On the nose it is exactly backwards. The FARTHEST car ends up nose-most — the engine pushes + * the first one it met ahead of it and picks up the next in front of that — so the cut reverses + * on the way into the tray. Getting this wrong is invisible in a single move and shows up as a + * run-around that changes nothing: approaching one parked cut from either end used to give the + * identical consist, when the whole point is that the two mirror. + */ if (e.toNose) { // Cars taken on the nose go AHEAD of the engine — "pushing them into the Facility" — so the // engine is no longer at the front and its index has to follow. It never did, so a crew that // shoved a cut anywhere kept a tray claiming the engine was still leading, and §8.2's // make-up rule had nothing truthful to check. - tray.consist.unshift(...e.stock); + tray.consist.unshift(...[...e.stock].reverse()); tray.engineAt += e.stock.length; } else { tray.consist.push(...e.stock); @@ -1611,7 +1733,24 @@ export function reduce(s: GameState, e: GameEvent): void { card.standing = []; if (card.facility) card.facility.industryTrack.cars = []; } - spendFreightBudget(s, playerAtSeat(s, trayySeat(tray)), tray, e.at, e.stock); + // The card the crew pulled OUT of may still be holding a cut set out off its other end, which + // the sweep above has just emptied along with everything else. Put it back. + if (e.leaves) { + const card = area.grid.get(coordKey(e.leaves.at)); + if (card) carsOn(card).push(...e.leaves.stock.map((c) => ({ ...c }))); + } + // A tray that has moved is standing on a card it has just emptied, so nothing of its own is + // left beside it. Any cut it did leave behind is on the square it came FROM. + tray.standingWest = 0; + /** + * Charge only the cars that were ALREADY standing where the crew ran — the own cut at the + * front of `stock` is a drop being undone, so it is refunded on the square it was left on + * instead of being charged again at the far end. + */ + const owner = playerAtSeat(s, trayySeat(tray)); + const taken = e.recoupled ? e.stock.slice(e.recoupled.stock.length) : e.stock; + spendFreightBudget(s, owner, tray, e.at, taken); + if (e.recoupled) refundFreightBudget(s, owner, tray, e.recoupled.at, e.recoupled.stock); break; } @@ -1640,8 +1779,35 @@ export function reduce(s: GameState, e: GameEvent): void { } tray.engineAt = Math.min(tray.engineAt, tray.consist.length); const card = area.grid.get(coordKey(e.at)); - // On a Facility card the industry track is where cars stand (§9.3). - if (card) carsOn(card).push(...e.stock); + /** + * WHERE THE CUT LANDS ON THE CARD — the fix for "when dropping all 4 cars, order was + * reversed; it worked properly if we dropped cars individually". + * + * This was `carsOn(card).push(...stock)`, which is neither of the two things it needs to be. + * The cut arrives in TRAY order (nose first) and the card is ordered WEST TO EAST, so it has + * to be turned around for an east-facing train — `trackOrder`. And it has to go at the end of + * the row the cut physically occupies, which is not always the end of the array: + * + * - a NOSE cut is set out ahead of the engine, on the `facing` side; + * - a TAIL cut is set out behind it, on the other side. + * + * Successive cuts off the same end stack up TOWARDS the engine — the first car set out is left + * furthest away, and each one after it is left in the gap between the train and the last — + * so the insertion point is the train's own position in the row, `standingWest`, from either + * side. That is what makes the result batch-invariant: four cars at once, four singles, or two + * pairs all park in the same order, which is the reported bug and the regression test. + */ + if (card) { + const cars = carsOn(card); + const facing = railFacingOf(tray); + const onWestSide = e.fromNose ? facing === 'w' : facing === 'e'; + const k = Math.max(0, Math.min(cars.length, tray.standingWest ?? 0)); + const cut = trackOrder(e.stock, facing); + cars.splice(k, 0, ...cut); + // The train has not moved, so cars set out on its WEST side push its index along the row; + // cars set out to the east leave it where it was. + tray.standingWest = onWestSide ? k + cut.length : k; + } spendFreightBudget(s, playerAtSeat(s, trayySeat(tray)), tray, e.at, e.stock); break; } @@ -1770,7 +1936,32 @@ export function reduce(s: GameState, e: GameEvent): void { }; // Becoming a Passenger Facility at all is a state change, not a delta (Gap 8): a Whistle // Post has no passenger boxes to add to. + const wasPassenger = f.allows.outbound || f.allows.inbound; f.allows = { outbound: to.isPassengerFacility, inbound: to.isPassengerFacility }; + + /** + * A GRANT SUPPRESSED AT A WHISTLE POST COMES BACK WHEN THE OFFICE CAN USE IT. + * + * Reported from play: "Restaurant attached to a whistle stop, then upgrade to depot — depot + * only shows one green / one red box. I expected two, because Restaurant increases outbound + * by one." Exactly right, and here is why it happened: `hosts: ['office']` includes a Whistle + * Post, which is NOT a Passenger Facility, so `usableGrant` dropped the +1 outbound at the + * moment the card was played. The porter landed — porters have no direction gate — and the + * slot was gone for good, because the upgrade only ever applied the difference between two + * tiers and knew nothing about what had been discarded on the way. + * + * `TrackCard.modifiers` records which Modifiers served this facility, so what was dropped is + * recoverable: when the Office becomes a Passenger Facility, every one of them is granted the + * capacity it always printed. Keyed on the TRANSITION, so a Depot → Station upgrade does not + * pay them a second time. + */ + if (!wasPassenger && to.isPassengerFacility) { + for (const kind of officeCard.modifiers) { + const m = modifierProfile(kind); + f.capacity.outbound += m.addOut; + f.capacity.inbound += m.addIn; + } + } } break; } @@ -1858,6 +2049,32 @@ export function reduce(s: GameState, e: GameEvent): void { break; } + case 'extraStarted': { + const trayId = s.freeTrays.pop()!; + s.pendingExtras = s.pendingExtras.filter((n) => n !== e.trainNumber); + const direction = runDirection(e.trainNumber); + if (e.atSeat === null) { + const side = startingDivisionPoint(e.trainNumber); + s.trays.set(trayId, { + id: trayId, trainNumber: e.trainNumber, trainIsExtra: true, engineAt: 0, consist: [], + direction, position: { at: 'divisionPoint', side }, movesUsed: 0, + }); + const dp = s.division.nodes.find((n) => n.kind === 'divisionPoint' && n.side === side); + if (dp?.kind === 'divisionPoint') dp.holding.push(trayId); + } else { + // Starting at a Control Point: it stands on the Office card and takes an A/D track, exactly + // as though it had arrived there. + const area = areaAtSeat(s, e.atSeat); + s.trays.set(trayId, { + id: trayId, trainNumber: e.trainNumber, trainIsExtra: true, engineAt: 0, consist: [], + direction, facing: direction === 'west' ? 'w' : 'e', railFacing: direction === 'west' ? 'w' : 'e', + position: { at: 'grid', seat: e.atSeat, coord: area.officeCoord }, movesUsed: 0, + }); + area.adOccupancy.push(trayId); + } + break; + } + case 'passengersBoarded': { const f = facilityAt(s, e.player, e.at)!; const area = areaOf(s, e.player); @@ -2046,7 +2263,7 @@ function buildFreightFacility(kind: FreightKind): Facility { // industry-specific modifier cards, not printed large on the industry itself. capacity: { outbound: p.baseOut, inbound: p.baseIn }, menAtWork: [null, null, null], - industryTrack: { length: Math.max(1, p.baseOut + p.baseIn), cars: [] }, + industryTrack: { cars: [] }, laborers: p.baseLoaders, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, @@ -2290,13 +2507,15 @@ function hasModifierInArea(area: OfficeArea, kind: ModifierKind): boolean { * the laborer landed because Laborers have no direction, and the slot did not because there was * nowhere for it to go. No modifier turns a receiver into a shipper, so the grant is dropped. * - * It is not one card's quirk. The same trap catches **Truck Dock** and **Forklifts**, which also - * print `addOut` and also list `grocersWarehouse` among their hosts, and **Waiting Area**, - * **Restaurant** and **Hotel**, whose `hosts: ['office']` includes a Whistle Post — not a Passenger - * Facility, so `allows.outbound` is false there too. + * It is not one card's quirk, and it cuts both ways. **Waiting Area**, **Restaurant** and **Hotel** + * print `addOut` for `hosts: ['office']`, which includes a Whistle Post — not a Passenger Facility, + * so `allows.outbound` is false there too. **Truck Dock** is the mirror image: it prints `addIn` and + * lists **Packing Sheds**, which only ships, so its one grant is dropped there and the card does + * nothing at all. Which host you set it beside is the whole decision, and the hand tooltip says so + * before it is played. * - * The industry track grows by what was actually applied, not by what was printed: a slot that does - * not exist must not lengthen the siding that would have served it. + * What is applied is what the host can actually use, not what the card printed. It affects the box + * count only — see `applyModifier` on why no Modifier lengthens an industry's track. */ function usableGrant(f: Facility, m: ModifierProfile): { out: number; in: number } { return { @@ -2328,14 +2547,13 @@ function applyModifier(area: OfficeArea, coord: GridCoord, modifier: ModifierKin f.laborers += m.addLoaders; f.porters += m.addPorters; /** - * The siding grows only where there IS one. A Passenger Facility has no industry track — passengers - * board off the platform — so a Waiting Area, whose "+1" is a passenger slot rather than a car, - * must not give the Office somewhere to spot a car. It did, and the board then drew the Depot a - * siding square: the same phantom siding the renderer was just fixed for, arriving by another door. + * 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 set out there, + * which was already four and stays four. This used to lengthen the industry track by the same + * amount, which is where the phantom siding came from: capacity and rail are different things. */ - if (f.kind === 'freight' && (use.out > 0 || use.in > 0)) { - f.industryTrack.length += use.out + use.in; - } } /** §2.1, Gap 4a — extending the Running Track pushes the Limits sign outward. */ diff --git a/src/engine/content.ts b/src/engine/content.ts index b21dbc5..562aa55 100644 --- a/src/engine/content.ts +++ b/src/engine/content.ts @@ -230,10 +230,37 @@ export type IndustryProfile = { export const INDUSTRY_PROFILES: readonly IndustryProfile[] = [ { kind: 'freightHouse', name: 'Freight House', carTypes: ['boxcar'], flow: 'both', baseOut: 1, baseIn: 1, baseLoaders: 1, lockouts: ['grocersWarehouse'], copies: 6 }, { kind: 'mineTipple', name: 'Mine Tipple', carTypes: ['hopper'], flow: 'outbound', baseOut: 1, baseIn: 0, baseLoaders: 1, lockouts: ['powerPlant'], copies: 6 }, - { kind: 'refinery', name: 'Refinery', carTypes: ['tank'], flow: 'outbound', baseOut: 1, baseIn: 0, baseLoaders: 1, lockouts: ['powerPlant'], copies: 3 }, + /** + * BOTH DIRECTIONS, per the card reference — this was `outbound` and it contradicted the rules. + * + * `card-reference.md`: "Oil Refinery | Tank car | Both | 3 | 2 | 2 | 4", and in prose — "'Freight + * House' is not a card. It is the collective term for a freight facility that loads *and* unloads + * — the Grocer's Warehouse and the Oil Refinery." §9.3's "Passenger Facilities and Freight Houses + * permit cars to move each direction" therefore names exactly these two, and the engine had both + * of them one-way. + * + * The consequence was silent: `usableGrant` drops a Modifier's grant on a direction its host + * cannot use, so every +1 inbound beside a Refinery went nowhere. + * + * The base numbers stay at the engine's own scale (1 per direction it allows) rather than the card + * reference's 2/2 — every industry here is scaled down the same way, Mine Tipple included, and + * raising one of them alone would be a balance change rather than a correction. Flagged in TODO. + */ + { kind: 'refinery', name: 'Refinery', carTypes: ['tank'], flow: 'both', baseOut: 1, baseIn: 1, baseLoaders: 1, lockouts: ['powerPlant'], copies: 3 }, { kind: 'powerPlant', name: 'Power Plant', carTypes: ['hopper', 'tank'], flow: 'inbound', baseOut: 0, baseIn: 1, baseLoaders: 1, lockouts: ['mineTipple', 'refinery'], copies: 6 }, { kind: 'packingSheds', name: 'Packing Sheds', carTypes: ['reefer'], flow: 'outbound', baseOut: 1, baseIn: 0, baseLoaders: 1, lockouts: ['grocersWarehouse'], copies: 3 }, - { kind: 'grocersWarehouse', name: "Grocer's Warehouse", carTypes: ['boxcar', 'reefer'], flow: 'inbound', baseOut: 0, baseIn: 1, baseLoaders: 1, lockouts: ['packingSheds', 'freightHouse'], copies: 3 }, + /** + * BOTH DIRECTIONS — see the Refinery above; "Grocer's Warehouse | Boxcar | Both | 2 | 2 | 2 | 3". + * + * Reported from play: "grocer's warehouse didn't get extra outbound slot for truck dock." It could + * not: the Truck Dock printed +1 outbound at the time and this was `flow: 'inbound'`, so the grant + * was dropped on a direction the facility did not have. The same trap still swallows an Ice House + * set beside a Grocer's that has been left one-way. + * + * `TODO.md` had previously recorded this as "checked, and there is no bug" on the reasoning that a + * Grocer's is inbound-only. That premise was the bug. + */ + { kind: 'grocersWarehouse', name: "Grocer's Warehouse", carTypes: ['boxcar', 'reefer'], flow: 'both', baseOut: 1, baseIn: 1, baseLoaders: 1, lockouts: ['packingSheds', 'freightHouse'], copies: 3 }, ]; /** Legacy alias; the engine still reads FREIGHT_PROFILES in places. */ @@ -280,7 +307,20 @@ export const MODIFIER_PROFILES: readonly ModifierProfile[] = [ { kind: 'restaurant', name: 'Restaurant', hosts: ['office'], addOut: 1, addIn: 0, addLoaders: 0, addPorters: 1, copies: 2 }, { kind: 'hotel', name: 'Hotel', hosts: ['office'], addOut: 1, addIn: 0, addLoaders: 0, addPorters: 1, copies: 1 }, // Freight House / Packing Sheds / Grocer's - { kind: 'truckDock', name: 'Truck dock', hosts: ['freightHouse', 'packingSheds', 'grocersWarehouse'], addOut: 1, addIn: 0, addLoaders: 1, addPorters: 0, copies: 2 }, + /** + * INBOUND, AND NO LABORER — the one Modifier that helps a facility RECEIVE. + * + * It printed "+1 outbound, +1 Laborer" like every other freight Modifier, which made it a + * duplicate of Forklifts with a longer host list. A dock is where a truck backs up to take + * delivery, so it earns its own line in the deck by adding the red box instead of the green one — + * and pays for it by bringing no man to work it. + * + * Two consequences, both intended. `usableGrant` drops an inbound grant on a host that only ships, + * so beside **Packing Sheds** this card now does nothing at all — the hand tooltip says so before + * it is played. And it is the first Modifier that grants no worker, so a facility's laborer count + * no longer rises with every card set beside it. + */ + { kind: 'truckDock', name: 'Truck dock', hosts: ['freightHouse', 'packingSheds', 'grocersWarehouse'], addOut: 0, addIn: 1, addLoaders: 0, addPorters: 0, copies: 2 }, { kind: 'railroadExpressAgency', name: 'Railroad Express Agency', hosts: ['freightHouse'], addOut: 1, addIn: 0, addLoaders: 1, addPorters: 0, copies: 1 }, { kind: 'forklifts', name: 'Forklifts', hosts: ['freightHouse', 'packingSheds'], addOut: 1, addIn: 0, addLoaders: 1, addPorters: 0, copies: 2 }, // Mine Tipple @@ -402,6 +442,22 @@ export const EXTRA_TRAINS: readonly TrainProfile[] = [ export const ALL_TRAINS: readonly TrainProfile[] = [...TIMETABLED_TRAINS, ...EXTRA_TRAINS]; +/** + * §2.3 — ODD RUNS WEST, EVEN RUNS EAST. The number is the direction, for an Extra as much as for a + * timetabled train, and the Division Point it starts at is therefore the one it runs away from. + * + * Extras print `direction: 'playerChoice'`, which the engine read as "always eastbound from the West + * Division Point". Jesse's ruling: the number decides, like everything else on the timetable. + */ +export function runDirection(trainNumber: number): Direction { + return trainNumber % 2 === 0 ? 'east' : 'west'; +} + +/** The Division Point a train of this number starts from — the opposite end to the way it runs. */ +export function startingDivisionPoint(trainNumber: number): Direction { + return runDirection(trainNumber) === 'east' ? 'west' : 'east'; +} + export function trainProfile(number: number, isExtra: boolean): TrainProfile | null { return ALL_TRAINS.find((t) => t.number === number && t.isExtra === isExtra) ?? null; } @@ -503,6 +559,59 @@ export function crossingStages( return Math.max(1, stages - gradeReduction(profile, modifiers, direction, gradeUp)); } +/** + * WHAT THIS MAINLINE CARD DOES TO A TRAIN, in a sentence. + * + * Reported from play: "mainline cards need a tooltip stating what they do. Hilly and Uncontrolled + * Siding — I have no idea the impact they have on game play." Both are invisible without one: Hilly + * charges freight double what it charges passengers, and Uncontrolled Siding is one of only two + * cards where a following train is not stuck behind a slower one. + * + * The crossing times are COMPUTED by `crossingStages` rather than written out, so a tooltip cannot + * drift from the rule it describes — including the Hilly split, which is the whole point of the + * card and is decided by whether the train carries a coach. + */ +export function mainlineDescription(kind: MainlineKind, gradeUp: Direction = 'east'): string { + const p = mainlineProfile(kind); + const stages = (n: number): string => `${n} Stage${n === 1 ? '' : 's'}`; + const parts: string[] = []; + + if (p.speed.kind === 'byTrainType') { + // Hilly. The split is by CONSIST, not by the train's speed class: anything with a coach on it + // takes the passenger figure. + parts.push( + `P${p.speed.passenger} / F${p.speed.freight} — a train carrying ANY coach crosses as a ` + + `${p.speed.passenger} (${stages(crossingStages(kind, 'fast', true))} for a fast train), and a ` + + `freight-only train as a ${p.speed.freight} (${stages(crossingStages(kind, 'fast', false))}). ` + + `A slow train adds one Stage either way.`, + ); + } else if (p.speed.kind === 'grade') { + parts.push( + `A grade, climbing ${gradeUp === 'east' ? 'eastward' : 'westward'}. It crosses as a 30 — ` + + `${stages(crossingStages(kind, 'fast', false, [], gradeUp, gradeUp))} for a fast train, and one ` + + `more for a slow one. Brakeman and Airbrakes each take a Stage off a train running DOWNHILL; ` + + `Helpers takes one off a train running UPHILL. Never below one Stage.`, + ); + } else { + parts.push( + `${p.speed.value} — ${stages(crossingStages(kind, 'fast', false))} for a fast train, ` + + `${stages(crossingStages(kind, 'slow', false))} for a slow one.`, + ); + } + + if (p.trainsMayPass) { + parts.push( + 'TRAINS MAY PASS — two trains may stand on this card at once, so a following train is not held ' + + 'behind a slower one. Only this and the Double Track allow it.', + ); + } else { + parts.push('One train at a time — anything following has to wait for it to clear.'); + } + + if (p.sortsCars) parts.push('Cars may be sorted into any new order here.'); + return parts.join(' · '); +} + /** * Q11, answered: the Heavy Grade card prints "(Up)" and "Player sets orientation", so which way it * climbs is a property of the placed card, not a constant. `gradeUp` is the direction a train is @@ -565,7 +674,22 @@ export const REALIGNMENTS: readonly { from: MainlineKind; to: MainlineKind }[] = // The four new card categories // --------------------------------------------------------------------------- -export type SimpleCard = { key: string; name: string; copies: number; placement: string; effect: string }; +export type SimpleCard = { + key: string; + name: string; + copies: number; + placement: string; + effect: string; + /** + * The opponent-directed card this exists SOLELY to answer. + * + * A defence with nothing to defend against is a dead draw, exactly as the attack itself would be. + * The 22 Space-use and Action cards are held out of every deck until they are implemented (Q6), + * and these go with them — named here rather than in a list somewhere else so the pairing is + * visible on the card, and so they come back together when their attacker does. + */ + answers?: string; +}; /** "Burns tablespace" — occupies a grid cell and does nothing useful. Semantics pending §10 Q6. */ export const SPACE_USE_CARDS: readonly SimpleCard[] = [ @@ -672,11 +796,11 @@ export function enhancementRule(key: string): EnhancementRule | null { export const ENHANCEMENT_CARDS: readonly SimpleCard[] = [ { key: 'interlocking', name: 'Interlocking', copies: 2, placement: 'any Running Track Straight', effect: 'May stop an inbound train on the Limit Track.' }, - { key: 'facingPointLocks', name: 'Facing Point Locks', copies: 2, placement: 'adjacent to Interlocking', effect: 'Must have Interlocking. Prevents Derail being played on you.' }, + { key: 'facingPointLocks', name: 'Facing Point Locks', copies: 2, placement: 'adjacent to Interlocking', effect: 'Must have Interlocking. Prevents Derail being played on you.', answers: 'Derail' }, { key: 'yardOffice', name: 'Yard office', copies: 1, placement: 'any Secondary Track Straight', effect: 'An inbound train with no coaches that can reach the yard office in one move may arrive there instead of the Train Order Office.' }, { key: 'smallYard', name: 'Small yard', copies: 1, placement: 'any Secondary Track Straight', effect: 'A train that spends one move in the yard may sort itself into ANY order, including cars ahead of the engine.' }, - { key: 'waterColumn', name: 'Water column', copies: 2, placement: 'any Running Track Straight', effect: 'Lets you remove any Watertower in your district.' }, - { key: 'overpass', name: 'Overpass', copies: 1, placement: 'any Railroad Crossing', effect: 'Removes the restrictions of a played Railroad Crossing.' }, + { key: 'waterColumn', name: 'Water column', copies: 2, placement: 'any Running Track Straight', effect: 'Lets you remove any Watertower in your district.', answers: 'Watertower' }, + { key: 'overpass', name: 'Overpass', copies: 1, placement: 'any Railroad Crossing', effect: 'Removes the restrictions of a played Railroad Crossing.', answers: 'Railroad crossing' }, { key: 'telegraph', name: 'Telegraph', copies: 3, placement: 'any Running Track Straight', effect: 'Once a day, when dispatching facing trains, add +4 to the other train’s number.' }, { key: 'telephone', name: 'Telephone', copies: 2, placement: 'on Telegraph', effect: 'Once a day, add +8 to the other train’s number.' }, { key: 'radio', name: 'Radio', copies: 2, placement: 'on Telephone', effect: 'Once a day, add +12 to the other train’s number.' }, @@ -688,7 +812,7 @@ export const MAINLINE_MODIFIER_CARDS: readonly SimpleCard[] = [ { key: 'airbrakes', name: 'Airbrakes', copies: 1, placement: 'a GRADE Mainline card', effect: 'Faster passage downhill. Brakeman must be in effect.' }, { key: 'helpers', name: 'Helpers', copies: 1, placement: 'a GRADE Mainline card', effect: 'Faster passage uphill.' }, { key: 'realignment', name: 'Realignment', copies: 2, placement: 'a Mainline card', effect: 'Convert one Mainline type to another. Not while a train is on it.' }, - { key: 'facingPointLocksMainline', name: 'Facing Point Locks', copies: 2, placement: 'adjacent to Interlocking', effect: 'Prevents Derail being played on you.' }, + { key: 'facingPointLocksMainline', name: 'Facing Point Locks', copies: 2, placement: 'adjacent to Interlocking', effect: 'Prevents Derail being played on you.', answers: 'Derail' }, ]; /** @@ -915,6 +1039,12 @@ export function isOpponentOnly(category: string): boolean { return OPPONENT_ONLY_CATEGORIES.includes(category); } +/** How many cards `DEFENCE_ONLY_CARDS` accounts for — 7: two Facing Point Locks of each kind, two + * Water Columns and one Overpass. */ +export const DEFENCE_ONLY_COPIES = + ENHANCEMENT_CARDS.filter((c) => c.answers).reduce((n, c) => n + c.copies, 0) + + MAINLINE_MODIFIER_CARDS.filter((c) => c.answers).reduce((n, c) => n + c.copies, 0); + export function deckComposition(): { category: string; count: number }[] { const sum = (xs: readonly { copies: number }[]): number => xs.reduce((n, x) => n + x.copies, 0); return [ @@ -945,6 +1075,19 @@ export const DECK_SIZE = deckComposition().reduce((n, c) => n + c.count, 0); * implemented — `checkPlay` answers both categories NOT_IMPLEMENTED, so dealing them would make ~9% * of draws reject. See `buildDeck`. */ -export const SOLITAIRE_DECK_SIZE = deckComposition() - .filter((c) => !isOpponentOnly(c.category)) - .reduce((n, c) => n + c.count, 0); +export const SOLITAIRE_DECK_SIZE = + deckComposition() + .filter((c) => !isOpponentOnly(c.category)) + .reduce((n, c) => n + c.count, 0) - DEFENCE_ONLY_COPIES; + +/** + * Every card held back BECAUSE its attacker is held back — see `SimpleCard.answers`. + * + * Reported from play: "just like the opponent directed cards are removed from the solitaire game, + * remove any of the defensive cards whose only purpose is to answer them. No need to have them in + * the deck when they can never be used." + */ +export const DEFENCE_ONLY_CARDS: readonly SimpleCard[] = [ + ...ENHANCEMENT_CARDS.filter((c) => c.answers), + ...MAINLINE_MODIFIER_CARDS.filter((c) => c.answers), +]; diff --git a/src/engine/events.ts b/src/engine/events.ts index f7a78c0..44f0d6c 100644 --- a/src/engine/events.ts +++ b/src/engine/events.ts @@ -21,7 +21,7 @@ import type { CarType, OfficeTier } from './content.ts'; import type { LocalOpsOption } from './intents.ts'; -import type { CardId, GridCoord, PlayerIndex, RollingStock, TrayId } from './state.ts'; +import type { CardId, GridCoord, PlayerIndex, RollingStock, SeatIndex, TrayId } from './state.ts'; export type GameEvent = // -- clock @@ -43,6 +43,26 @@ export type GameEvent = * running forward: the engine meets the cars head-on and takes them on the front. */ toNose: boolean; + /** + * Cars that STAY on the card the crew pulled out of, when it coupled its own cut. + * + * A train may set out off both ends on one square, and pulling forward picks up only the cut + * at the end it leaves by — the one behind it is still standing there. `from` names the cards + * to sweep and every other one is emptied outright, so the start card needs the exception said + * explicitly rather than recomputed: the tray has already moved by the time this is reduced, + * and `standingWest` went with it. + */ + leaves?: { at: GridCoord; stock: RollingStock[] }; + /** + * The train's OWN cut, lifted back off the square it set it out on. + * + * Recoupling your own cars on the square you are standing on is UNDOING the drop, not a fresh + * pick-up (Jesse's call, of the two the plan put up). Trains 3/4 spend a per-location freight + * budget on setting out, so without this a legal-looking drop became silently one-way: the + * train could only ever back away from its own cut, never pull forward through it. The budget + * is refunded here and not charged again at the far end. + */ + recoupled?: { at: GridCoord; stock: RollingStock[] }; } | { type: 'carsDropped'; trayId: TrayId; at: GridCoord; stock: RollingStock[]; fromNose?: boolean } | { type: 'consistSorted'; trayId: TrayId; at: GridCoord; before: RollingStock[]; after: RollingStock[] } @@ -121,6 +141,8 @@ export type GameEvent = * always followed by one `revenueChanged` per player. */ | { type: 'trainCompleted'; trainNumber: number; isExtra: boolean; side: 'east' | 'west'; consist: RollingStock[] } + /** An Extra took a Crew Tray and started its run — at a Division Point, or at a Control Point. */ + | { type: 'extraStarted'; player: PlayerIndex; trainNumber: number; atSeat: SeatIndex | null } | { type: 'carPlacedOnTrain'; player: PlayerIndex; trayId: TrayId; stock: RollingStock } | { type: 'carPassed'; player: PlayerIndex; trayId: TrayId } | { type: 'dispatchBonusUsed'; key: string; bonus: number; trainNumber: number; againstTrain: number } diff --git a/src/engine/intents.ts b/src/engine/intents.ts index 00eca56..7063bb4 100644 --- a/src/engine/intents.ts +++ b/src/engine/intents.ts @@ -5,7 +5,7 @@ */ import type { CarType, Direction, Hand, OfficeTier, TrackGeometry } from './content.ts'; -import type { CardId, GridCoord, PlayerIndex, TrayId } from './state.ts'; +import type { CardId, GridCoord, PlayerIndex, SeatIndex, TrayId } from './state.ts'; // --------------------------------------------------------------------------- // Local Operations Phase (§6) — a three-way exclusive choice @@ -66,6 +66,13 @@ export type Intent = // -- New Train Phase (§7) | { type: 'newTrain.placeCar'; trayId: TrayId; carType: CarType; loaded: boolean } | { type: 'newTrain.passCar'; trayId: TrayId } + /** + * §7 — "the player who played the card may place the Crew Tray in either division point for + * immediate departure", extended by Jesse: an Extra starts at the Division Point its NUMBER sends + * it to (odd runs west, even east, exactly as a timetabled train), or at any Control Point — any + * Office above a Whistle Post — at the player's choice. `atSeat` null means the Division Point. + */ + | { type: 'newTrain.startExtra'; trainNumber: number; atSeat: SeatIndex | null } /** Q9 — run a second, identical section behind a train that is due out this Stage. */ | { type: 'newTrain.secondSection'; trainNumber: number } // -- Mainline Phase (§8.1) — the Superintendent's clearance ruling @@ -171,9 +178,13 @@ export type RejectionCode = * travel, 24 with passengers waiting and every coach on the train already full. */ /** - * §9.3 — the green box may only be stocked, and a load may only start down the sign, when an empty - * car of THAT type is standing on the industry's track and is not already promised to another - * load. Fetching cargo you cannot ship is "just dropping it onto the tracks". + * §9.3 — a load may only start down the sign when an empty car of THAT type is standing on the + * industry's track and is not already promised to another load. Walking cargo onto WORK with + * nothing to put it in is "just dropping it onto the tracks". + * + * STOCKING THE GREEN BOX IS NOT GATED ON IT. §6.3 lets the Freight Agent stage a load whenever + * the Division Yard has the car and the box has room; the empty car only has to be there before + * the Laborers can move it out of the box. */ | 'NO_EMPTY_CAR_SPOTTED' | 'NO_PORTERS_HERE' @@ -181,7 +192,11 @@ export type RejectionCode = | 'NO_EMPTY_COACH' | 'NO_LOADED_COACH' | 'INBOUND_BOX_FULL' - | 'NO_EMPTY_COACH_IN_YARD'; + | 'NO_EMPTY_COACH_IN_YARD' + | 'NOT_A_CONTROL_POINT' + | 'NO_EXTRA_PENDING' + | 'NO_FREE_TRAY' + | 'NO_FREE_AD_TRACK'; export type Rejection = { code: RejectionCode; message: string }; diff --git a/src/engine/legal.ts b/src/engine/legal.ts index 95d6c1a..ee69802 100644 --- a/src/engine/legal.ts +++ b/src/engine/legal.ts @@ -229,6 +229,17 @@ function newTrainCandidates(s: GameState): Intent[] { if (due !== null && due !== undefined) { out.push({ type: 'newTrain.secondSection', trainNumber: due }); } + /** + * Where a pending Extra starts: its own Division Point, decided by its number, or any Control + * Point. `check` refuses a Whistle Post and a full Office, so every seat is offered and the rules + * do the filtering — one implementation of "is this a Control Point", not two. + */ + for (const trainNumber of s.pendingExtras) { + out.push({ type: 'newTrain.startExtra', trainNumber, atSeat: null }); + for (const seat of s.officeAreas.keys()) { + out.push({ type: 'newTrain.startExtra', trainNumber, atSeat: seat }); + } + } return out; } diff --git a/src/engine/setup.ts b/src/engine/setup.ts index 80a7849..cc1e368 100644 --- a/src/engine/setup.ts +++ b/src/engine/setup.ts @@ -60,8 +60,9 @@ export function buildDeck(mode: GameConfig['mode'] = 'competitive'): Card[] { * Q6 took them out of solitaire because they have no legal target in a one-player game. They are * out of the competitive deck too because `checkPlay` answers both categories `NOT_IMPLEMENTED`: * leaving them in would make ~9% of draws reject outright, which is worse than not dealing them. - * Three Enhancements — Facing Point Locks, Water Column and Overpass — exist only to answer these, - * and stay dormant until they come back. Recorded in TODO.md as multiplayer work. + * + * AND SO ARE THE SEVEN CARDS THAT ANSWER THEM — see below. They used to be dealt and sit dormant, + * which is the same dead draw by another name. Recorded in TODO.md as multiplayer work. * * `mode` is still taken so the signature does not move when they return; it is deliberately unused * for these two categories today. @@ -90,10 +91,22 @@ export function buildDeck(mode: GameConfig['mode'] = 'competitive'): Card[] { for (let i = 0; i < c.copies; i++) push({ kind: 'spaceUse', key: c.key }); } } + /** + * A DEFENCE GOES OUT WITH THE ATTACK IT ANSWERS. + * + * Facing Point Locks (both kinds), the Water Column and the Overpass exist only to answer Derail, + * a Watertower and a Railroad Crossing — all of which are among the 22 opponent-directed cards + * held out above. Left in, they are dead draws for exactly the same reason the attacks would be, + * and there are 7 of them. `SimpleCard.answers` names the pairing on the card itself, so they come + * back together the moment `opponentCardsInDeck` does. + */ + const dealt = (c: { answers?: string }): boolean => opponentCardsInDeck || !c.answers; for (const c of ENHANCEMENT_CARDS) { + if (!dealt(c)) continue; for (let i = 0; i < c.copies; i++) push({ kind: 'enhancement', key: c.key }); } for (const c of MAINLINE_MODIFIER_CARDS) { + if (!dealt(c)) continue; for (let i = 0; i < c.copies; i++) push({ kind: 'mainlineModifier', key: c.key }); } for (const c of MANEUVER_CARDS) { @@ -191,7 +204,7 @@ function buildPassengerFacility(tier: Parameters[0]): NonN // WORK sign is printed "For Freight Facilities" (rules-v0.2.md:452). This was a three-slot array // of nulls, which read as "three empty boxes" to every renderer that looked. menAtWork: null, - industryTrack: { length: 0, cars: [] }, + industryTrack: { cars: [] }, laborers: 0, porters: p.porters, usedThisStage: { laborers: 0, porters: 0 }, diff --git a/src/engine/state.ts b/src/engine/state.ts index fd0709a..0f3b2cd 100644 --- a/src/engine/state.ts +++ b/src/engine/state.ts @@ -18,7 +18,7 @@ import type { OfficeTier, TrackGeometry, } from './content.ts'; -import { officeProfile } from './content.ts'; +import { MAX_CONSIST, officeProfile } from './content.ts'; // --------------------------------------------------------------------------- // Identifiers @@ -119,7 +119,26 @@ export type TrackCard = { * Operational Rail (§9.3). Use isOperationalRail() rather than reading a stored flag. */ baseOperationalRail: boolean; - /** Uncoupled cars left here, in track order (§A.3). */ + /** + * Uncoupled cars left standing here, **ordered WEST to EAST** (§A.3). + * + * §A.3 states the requirement outright: "cars are loaded into and unloaded from the Crew Tray, and + * occupy the track, in the same order they originally held, **left-to-right**". Left-to-right is + * west-to-east, and this array had no defined orientation at all — the doc comment said "in track + * order" and named no direction, so nothing performed the conversion. + * + * That is one bug wearing three faces, all reported from play. Setting out four cars at once + * parked them in a different order than setting out one car four times, because `carsDropped` + * always pushed onto the end. A train that ran onto a parked cut got the SAME consist whichever + * way it approached, when the two must mirror. And the board drew the tray strip flipped for an + * east-facing train and the card's cars unflipped, so the same cut read one way in the tray and + * the other on the card. + * + * WEST-TO-EAST IS THE BOARD'S ORIENTATION, not the train's, which is exactly why it is the right + * one: it is a property of the card, so it does not change when a different train touches it. The + * two conversions are stated once, in `apply.ts`'s `carsDropped` and `carsCoupled` reducers, and + * `CrewTray.standingWest` records where a train standing here sits among them. + */ standing: RollingStock[]; facility: Facility | null; modifiers: ModifierKind[]; @@ -179,8 +198,19 @@ export type Facility = { * reported from playtesting. Typing it away means a fourth renderer cannot reintroduce the bug. */ menAtWork: [Load | null, Load | null, Load | null] | null; - /** Where cars are spotted for loading and unloading. */ - industryTrack: { length: number; cars: RollingStock[] }; + /** + * Where cars are spotted for loading and unloading. + * + * NO LENGTH. This used to carry `length: baseOut + baseIn`, which quietly made an industry's + * SIDING as long as its box count — so a Mine Tipple (one green box) had room for exactly one + * car, and a crew standing on it with two hoppers to set out could only put down one. Reported + * from play at undo 188. The box count is how much WORK an industry can hold, not how much RAIL + * it has; conflating the two invented a printed siding that no industry card actually has. + * + * An industry track is ordinary Operating Rail and holds what any card holds — see `spaceOn`. + * Modifiers raise `capacity` (another red or green box) and never the room for cars. + */ + industryTrack: { cars: RollingStock[] }; laborers: number; porters: number; /** Resets at the start of each Stage (§9.1). */ @@ -192,25 +222,35 @@ export type Facility = { * Operational Rail status. This is why isOperationalRail is a function, not a stored field. */ /** - * The cars physically standing on a card. + * The cars physically standing on a card, **ordered WEST to EAST**. * * On a Facility card the industry track IS where cars stand — spotting a car there and leaving a * car there are the same act (§9.3). Modelling them as two separate places was a bug: cars dropped * at a facility went into `standing`, while loading looked for them on `industryTrack`, so no * freight load could ever complete and freight revenue was structurally zero. + * + * THE ORIENTATION IS THE BOARD'S, NOT THE TRAIN'S, and it is `standing`'s alone to keep — see the + * comment on that field. `industryTrack.cars` inherits it through this function, so every reader of + * either place gets the same convention and the two can never drift apart. */ export function carsOn(card: TrackCard): RollingStock[] { - return card.facility && card.facility.industryTrack.length > 0 - ? card.facility.industryTrack.cars - : card.standing; + // Gated on the facility being FREIGHT, not on a track length that no longer exists. A Passenger + // Facility has no industry track at all — passengers board off the platform — so cars left on a + // Depot stand on the card like they would anywhere else. + return card.facility && card.facility.kind === 'freight' ? card.facility.industryTrack.cars : card.standing; } -/** How many more cars this card can hold. Ordinary track is unbounded; an industry track is not. */ +/** + * How many more cars this card can hold. + * + * ONE RULE FOR EVERY OPERATING TRACK CARD, industry or not: a card holds up to `MAX_CONSIST` cars, + * because a consist may not exceed four and four is therefore all that can ever be shoved onto one. + * An industry used to be the exception — its room came from its box count, so a one-box industry + * refused a second car — and that exception was the bug. Ordinary track used to be the other + * exception, unbounded, which let a pile build up that no train could then legally couple. + */ export function spaceOn(card: TrackCard): number { - if (card.facility && card.facility.industryTrack.length > 0) { - return card.facility.industryTrack.length - card.facility.industryTrack.cars.length; - } - return Number.MAX_SAFE_INTEGER; + return MAX_CONSIST - carsOn(card).length; } export function isOperationalRail(card: TrackCard): boolean { @@ -305,6 +345,25 @@ export type CrewTray = { * puts the engine on the other end, which is the one thing the arrow exists to show. */ railFacing?: 'e' | 'w'; + /** + * HOW MANY OF THIS CARD'S STANDING CARS LIE WEST OF THIS TRAIN. + * + * `carsOn(card)` runs west to east and a train standing on the card sits somewhere IN that row: + * cars `[0, standingWest)` are west of the engine, cars `[standingWest, end)` are east of it. A + * train can set out off both ends on the same square — §A.3 lets a cut come off either outer end — + * so "which side is this cut on" is not recoverable from the array alone, and it is the question + * every remaining rule asks. + * + * It answers all three: which cars are AHEAD of the engine and which BEHIND (combine with + * `railFacing`), which cut a departing train must couple back up (the one at the end it leaves + * by), and which side of the chip the renderer draws the cut on. + * + * Kept on the TRAY rather than the card because it describes a relationship, not the card: with no + * train standing there, a card's cars are simply a cut with no near or far side, and the next + * train to arrive couples all of them regardless. Reset to 0 on every move, which is sound because + * coupling is mandatory — a train arrives on a card it has just emptied. + */ + standingWest?: number; position: NodeRef; movesUsed: number; /** @@ -570,6 +629,54 @@ export function railFacingOf(tray: Pick 0) { @@ -443,14 +464,27 @@ export function exploreMoves( continue; } - // Mandatory coupling. Rejecting rather than truncating is deliberate: a move that would - // overfill the tray is illegal, not a move that picks up fewer cars. - const couples = [...node.couples, ...carsOn(card)]; + /** + * Mandatory coupling. Rejecting rather than truncating is deliberate: a move that would + * overfill the tray is illegal, not a move that picks up fewer cars. + * + * NEAREST FIRST ALONG THE DIRECTION OF TRAVEL. `carsOn` runs west to east, so a train entering + * through the card's EAST port meets them back to front and the row has to be reversed. Without + * this the same parked cut produced an identical consist whichever way it was approached, when + * the two must mirror — which is the difference between a run-around being worth a Move and + * being pointless. + */ + const met = node.entry === 'e' ? [...carsOn(card)].reverse() : carsOn(card); + const couples = [...node.couples, ...met]; if (ctx.consistSize + couples.length > MAX_CONSIST) { + const own = + ownCut.length > 0 + ? `, ${ownCut.length} of them the cut standing at the end of your OWN card that this move pulls out through` + : ''; block( node.coord, 'tooManyCars', - `too many cars — running here couples ${couples.length} standing car(s) onto a train already holding ${ctx.consistSize}, and ${MAX_CONSIST} is the limit. Coupling is mandatory: you may not run past a car and leave it (§A.4)`, + `too many cars — running here couples ${couples.length} standing car(s)${own} onto a train already holding ${ctx.consistSize}, and ${MAX_CONSIST} is the limit. Coupling is mandatory: you may not run past a car and leave it (§A.4)`, ); continue; } diff --git a/src/sim/board-svg.ts b/src/sim/board-svg.ts index eb97189..501d376 100644 --- a/src/sim/board-svg.ts +++ b/src/sim/board-svg.ts @@ -151,7 +151,10 @@ export function divisionSvg(nodes: DivisionView[]): string { cap: n.capacity, tip: dp ? 'A Division Point — the end of the line. Trains both enter and leave the Division here (odd numbers run west, even run east), and queue without limit' - : `${n.label} — Mainline${n.gradeUp ? `, climbs ${n.gradeUp === 'east' ? 'east' : 'west'}` : ''}${n.modifiers.length ? ` · ${n.modifiers.join(' · ')}` : ''}`, + : `${n.label} — Mainline${n.gradeUp ? `, climbs ${n.gradeUp === 'east' ? 'east' : 'west'}` : ''}${n.modifiers.length ? ` · ${n.modifiers.join(' · ')}` : ''}` + + // What the card actually DOES. The name alone left Hilly and Uncontrolled Siding as + // words with no gameplay attached — reported exactly that way. + (n.what ? `\n\n${n.what}` : ''), seat: null, // A Division Point is one region — the queue trains enter and leave the Division through. regions: dp ? 1 : (n.regions ?? 0), @@ -563,7 +566,7 @@ export function officeSvg( } } // MEN | AT | WORK — one physical sign, worked left to right loading and right to left - // unloading. The spotted car sits past WORK, on the siding drawn below. + // unloading. The spotted car sits past WORK, standing on the card itself. for (let i = 0; i < f.maw.length; i++) { out += box(bx, by, f.maw[i] !== null, 'bs-maw', ['M', 'A', 'W'][i]); bx += cellW + 2; @@ -623,21 +626,45 @@ export function officeSvg( } } - // Standing room, drawn as the printed squares: filled means occupied. - // - // `Math.max(1, …)` applies only to an industry, whose track is at least one car long. A Passenger - // Facility has NO industry track — passengers board straight off the platform — so a Depot was - // being drawn a siding slot it cannot spot a car on. - const spots = cell.facility - ? cell.facility.kind === 'passenger' - ? cell.facility.trackCap - : Math.max(1, cell.facility.trackCap) - : cell.cars.length; - for (let i = 0; i < Math.min(spots, 4); i++) { - const filled = i < cell.cars.length; - const carTip = filled - ? `${cell.cars[i]} standing here` - : 'an empty space on this track — a car may be spotted here'; + /** + * THE CARS STANDING HERE — NO PRINTED SIDING BEHIND THEM. + * + * An industry card used to draw one empty square per unit of its capacity, which read as a + * siding of a particular length printed on the card. No industry card prints one. Worse, the + * length came from the box count, so a one-box industry drew a single square and looked like it + * could only ever hold one car — which is exactly what the engine was wrongly enforcing. + * + * Every card, industry or not, now draws what is actually standing on it and nothing more. Four + * is the ceiling everywhere (`MAX_CONSIST`), so the row cannot overflow the card. + * + * The 4 is written out rather than imported: this function's SOURCE is serialized into the + * standalone replay page, where module imports are not in scope. Referencing the constant threw + * `MAX_CONSIST is not defined` in the rendered HTML while typechecking perfectly here. + */ + /** + * WHICH SIDE OF THE ENGINE THE CUT IS ON. + * + * Reported from play: "right after dropping my cars I need to be able to see if those cars are + * ahead or behind the train." They were drawn as one strip starting at the left edge whether a + * train was on the card or not, so the picture could not say. + * + * `cars` runs WEST TO EAST and `standingWest` is how many of them are west of the train, so the + * row is split there: the west cars stay left-aligned, the east cars are pushed over to the + * right-hand end, and the gap between them is where the engine is standing. Whether that gap + * means "ahead" or "behind" is the engine's facing, which the tooltip says in words rather than + * leaving the player to work out from the compass. + */ + const split = cell.train ? Math.max(0, Math.min(cell.cars.length, cell.standingWest ?? 0)) : 0; + const shown = Math.min(cell.cars.length, 4); + for (let i = 0; i < shown; i++) { + // With no train on the card there is no gap to leave, so the whole cut stays left-aligned. + const westOfEngine = cell.train ? i < split : true; + const side = !cell.train + ? 'standing here' + : (cell.train.facing === 'e') === westOfEngine + ? 'standing BEHIND the engine — it would couple onto the tail backing up' + : 'standing AHEAD of the engine — it would couple onto the nose pulling forward'; + const carTip = `${cell.cars[i]} ${side}`; /** * WHICH CAR IS STANDING HERE, not just that one is. * @@ -666,7 +693,7 @@ export function officeSvg( // A caboose is never "loaded" in the freight sense (carLabel says so), so it is drawn solid: // it is a car that is doing its job, not an empty waiting for one. const loaded = /^loaded/.test(car) || /caboose/.test(car); - const cls = filled ? ` bs-occ bs-car-${type}${loaded ? ' bs-loaded' : ' bs-mt'}` : ''; + const cls = ` bs-occ bs-car-${type}${loaded ? ' bs-loaded' : ' bs-mt'}`; /** * LOADED OR EMPTY, TOLD THE SAME WAY THE TRAY TELLS IT. * @@ -678,10 +705,11 @@ export function officeSvg( * Now the card matches the tray — solid face for loaded, hollow for empty — and every slot * carries the car's own words as a tooltip, so "loaded hopper" is one hover away. */ - out += ``; - if (filled) { - out += `${esc(type)}`; - } + // West cars run rightwards from the left edge; east cars run leftwards from the right edge, so + // the engine's place in the row is the gap left between them. + const sx = westOfEngine ? 8 + i * 30 : W - 34 - (shown - 1 - i) * 30; + out += ``; + out += `${esc(type)}`; } if (cell.enhancements.length > 0) { diff --git a/src/sim/bot.ts b/src/sim/bot.ts index 813f79a..6e572cd 100644 --- a/src/sim/bot.ts +++ b/src/sim/bot.ts @@ -19,9 +19,17 @@ * complete at all. */ -import { applyIntent, areaOf, canAdvanceLoad, destinationsFor, facilityCarTypes, laborersLeft } from '../engine/apply.ts'; +import { + applyIntent, + areaOf, + canAdvanceLoad, + destinationsFor, + facilityCarTypes, + laborersLeft, + ownCutFor, +} from '../engine/apply.ts'; import { MAX_CONSIST, nextOfficeTier, officeProfile } from '../engine/content.ts'; -import type { Hand, TrackGeometry } from '../engine/content.ts'; +import type { CarType, Hand, TrackGeometry } from '../engine/content.ts'; import type { GameEvent } from '../engine/events.ts'; import type { Intent } from '../engine/intents.ts'; import { legalActions } from '../engine/legal.ts'; @@ -364,8 +372,10 @@ function chooseLocalOption( return because('the crew is away from the Office and can never depart from where it stands', can('switch')!); } - // 3. Stock a green box only when the load can actually finish — an empty car of the right type - // is already spotted. Stocking without one just fills the box. + // 3. Stock a green box when the load can actually be worked — an empty car of the right type is + // already spotted. Staging ahead of the car is legal (§6.3) and cannot jam anything, but it + // spends the Stage's one Freight Agent action on a load no Laborer can touch yet, so it is not + // worth CHOOSING the option for; it is taken only as a fallback once the option is chosen. if (can('freightAgent') && canStockProductively(s, player)) { return because('a green box can be stocked with a load that can actually finish', can('freightAgent')!); } @@ -463,9 +473,13 @@ function isTrainCard(s: GameState, cardId: string): boolean { /** * A facility with room in its green box AND an empty car OF ITS OWN TYPE already spotted. * - * The type match matters: a load that reaches WORK with no matching car to go onto is stuck, and a - * stuck load strips the industry track of Operational Rail status (§9.3), so no car can be brought - * in to rescue it. Stocking speculatively jams the facility. + * A PRIORITY TEST, NOT A LEGALITY TEST — the engine stocks either way (§6.3). What it decides is + * whether stocking is worth the Local Operations action THIS Stage: with a car spotted the load + * moves next Stage, without one it sits. + * + * The type match matters when it does move: a load that reaches WORK with no matching car to go + * onto is stuck, and a stuck load strips the industry track of Operational Rail status (§9.3), so + * no car can be brought in to rescue it. `loadCanFinish` keeps the Laborers off such a load. */ function canStockProductively(s: GameState, player: PlayerIndex): boolean { for (const card of areaOf(s, player).grid.values()) { @@ -1036,6 +1050,25 @@ function loadReason(i: Intent): string { } } +/** + * Would a load stocked here be workable straight away — is there a spotted empty car of that type + * not already promised to a load in the box or on the sign? + * + * A PREFERENCE, NOT A RULE. §6.3 stocking has no car requirement; this only ranks one legal stock + * above another, so the Stage's single Freight Agent action goes where a Laborer can use it. + */ +function stockCanBeWorked(s: GameState, player: PlayerIndex, at: GridCoord, carType: CarType): boolean { + const f = areaOf(s, player).grid.get(coordKey(at))?.facility; + if (!f) return false; + // Passengers board off the platform with no car spotted at all (§9.2), so a stocked coach is + // always immediately workable. + if (f.kind !== 'freight') return true; + const spotted = f.industryTrack.cars.filter((c) => !c.loaded && c.type === carType).length; + const staged = f.outboundBox.filter((c) => c.type === carType).length; + const working = (f.menAtWork ?? []).filter((l) => l?.dir === 'out' && l.type === carType).length; + return spotted - staged - working > 0; +} + /** * Is there already an empty car of the right type spotted to receive this load (§9.3)? Without one * the load can be started and walked across MEN|AT|WORK but can never come off. @@ -1077,10 +1110,30 @@ function consistAfterMove( (d) => d.coord.row === move.to.row && d.coord.col === move.to.col, ); if (!dest) return null; - // Forward means the engine leads and meets the cars head-on (§A.3). - return move.reverse ? [...tray.consist, ...dest.couples] : [...dest.couples, ...tray.consist]; + /** + * Forward means the engine leads and meets the cars head-on (§A.3), and `couples` is ordered + * NEAREST FIRST — so the last car met ends up nose-most and the cut reverses on its way into the + * tray. Predicting it unreversed made the bot's run-around test read the wrong car as the tail + * whenever it approached a cut of two or more forwards. + */ + return move.reverse + ? [...tray.consist, ...dest.couples] + : [...[...dest.couples].reverse(), ...tray.consist]; } +/** + * Would this move drag the cars the crew has just set out back onto the train? + * + * Coupling is mandatory, so pulling out through the end a cut is standing at picks it up again — + * which usually undoes the set-out the crew made this turn. The bot cannot make an ILLEGAL move + * (legality is enforced upstream), so this costs it nothing to ignore; it simply wastes the Move, + * and the crew arrives with the car it was trying to leave behind. + */ +function recouplesOwnCut(s: GameState, player: PlayerIndex, i: Intent): boolean { + return i.type === 'switch.move' && ownCutFor(s, player, i.trayId, i.reverse).length > 0; +} + + /** Is this player's crew sitting somewhere it can never depart from? */ function strandedFromOffice(s: GameState, player: PlayerIndex): boolean { const area = areaOf(s, player); @@ -1238,6 +1291,26 @@ function followThrough( } case 'switch': { + /** + * A MOVE THAT DRAGS THE CREW'S OWN CUT BACK ON IS A WASTED MOVE, so take those off the table + * before any heuristic gets to choose one. + * + * Coupling is mandatory (§A.4), so pulling out through the end a cut is standing at picks it + * straight back up — which undoes the set-out the crew just made. Measured before this filter: + * **625 of 1,029 set-outs across 60 games were recoupled by the very next move**, 219 of them + * cars that had just been spotted at an industry. The bot was not doing anything illegal; it + * was spending two Moves to end up where it started, over and over. + * + * Filtered here rather than at each `find` below because there are six of them and a rule that + * has to be remembered at six call sites is a rule that will be forgotten at the seventh. The + * originals come back if the filter would leave the crew with no move at all — backed into a + * stub behind its own cars, the only way out IS through them, and standing still is worse. + */ + const clear = options.filter((i) => !recouplesOwnCut(s, player, i)); + if (options.some((i) => i.type === 'switch.move') && clear.some((i) => i.type === 'switch.move')) { + options = clear; + } + // Re-check every Move, not just when choosing the option. Conditions change mid-turn — a car // gets coupled, a siding fills — and once there is nothing left to do the fall-through would // pick an arbitrary legal move and burn the remaining Moves shuttling. @@ -1457,8 +1530,30 @@ function followThrough( } const clear = options.find((i) => i.type === 'freightAgent.clearInbound'); if (clear) return because('the red Inbound box is full and blocking further unloading', clear); - const stock = options.find((i) => i.type === 'freightAgent.stockOutbound'); - if (stock) return because('stock a green box so a Laborer has work next Stage', stock); + /** + * PREFER THE BOX WHOSE LOAD CAN ACTUALLY BE WORKED. + * + * §6.3 lets the Freight Agent stage a load with no car spotted — cargo may wait on the dock — + * so every facility with room and a matching car in the Division Yard is now offered, not just + * the ready ones. `canStockProductively` chose the option believing a ready facility existed; + * taking the first offer regardless would spend the Stage's one Freight Agent action on some + * other industry that cannot move its load for want of a car. Staging ahead is still taken as + * a fallback: it is legal, it cannot jam anything, and it saves an action later. + */ + const stockable = options.filter( + (i): i is Extract => + i.type === 'freightAgent.stockOutbound', + ); + const workable = stockable.find((i) => stockCanBeWorked(s, player, i.at, i.carType)); + const stock = workable ?? stockable[0]; + if (stock) { + return because( + workable + ? 'stock a green box whose load a Laborer can start next Stage' + : 'stage a load ahead of the car — it waits in the green box until one is spotted', + stock, + ); + } /** * NEVER UNJAM A HEALTHY BOX. * @@ -1508,7 +1603,7 @@ function wantedCars(s: GameState, player: PlayerIndex): WantedCar[] { const f = card.facility; if (!f || f.kind !== 'freight') continue; const spotted = f.industryTrack.cars.length; - if (spotted >= f.industryTrack.length) continue; + if (spotted >= MAX_CONSIST) continue; // Every commodity the industry handles, asked of its profile. A hand-written switch here had // drifted from the sheet: it named two industries that do not exist (`produceShed`, // `oilRefinery`) and omitted three that do (`freightHouse`, `refinery`, `packingSheds`), so @@ -1537,12 +1632,12 @@ function wantedCars(s: GameState, player: PlayerIndex): WantedCar[] { * Would spotting this car here do any work? * * An OUTBOUND facility needs an EMPTY car of its own commodity to load onto; an INBOUND one needs - * a LOADED car of its commodity to unload. Anything else merely consumes a slot on a finite - * industry track (§9.3). + * a LOADED car of its commodity to unload. Anything else merely consumes room on the industry's + * track, which holds four cars like any other card (§9.3). */ function facilityWants(f: Facility, car: RollingStock): boolean { if (f.kind !== 'freight') return false; - if (f.industryTrack.cars.length >= f.industryTrack.length) return false; + if (f.industryTrack.cars.length >= MAX_CONSIST) return false; if (!facilityCarTypes(f).includes(car.type)) return false; if (!car.loaded && f.allows.outbound) return true; if (car.loaded && f.allows.inbound) return true; diff --git a/src/sim/narrate.ts b/src/sim/narrate.ts index c1eb7d3..022366f 100644 --- a/src/sim/narrate.ts +++ b/src/sim/narrate.ts @@ -15,6 +15,7 @@ * panel cannot drift from the rules. */ +import { MAX_CONSIST } from '../engine/content.ts'; import { adTrackCount, coordKey, seatOf, turnOf } from '../engine/state.ts'; import type { GameState, GridCoord, PlayerIndex, RollingStock, TrayId } from '../engine/state.ts'; import { areaOf, canAdvanceLoad, canStartLoad, facilityCarType, facilityCarTypes, laborersLeft, movesFor, portersLeft } from '../engine/apply.ts'; @@ -130,14 +131,26 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration { where: e.to, text: `CREW moved ${at(e.from)} → ${at(e.to)} — ${e.movesRemaining} of 6 Moves left. The crew chip on the grid carries the whole train with it.`, }; - case 'carsCoupled': + case 'carsCoupled': { + /** + * TAKING YOUR OWN CUT BACK IS NOT THE SAME EVENT AS FINDING CARS ON THE LINE, and the history + * read as though it were — "coupled 2 cars" told a player who had just set those very cars out + * that the game had silently undone their work. It has not: they were standing at the end the + * train pulled out through, and coupling is mandatory (§A.4). + */ + const own = e.recoupled?.stock.length ?? 0; + const found = e.stock.length - own; + const parts: string[] = []; + if (own > 0) parts.push(`picked its own ${carsLabel(e.recoupled!.stock)} back up off ${at(e.recoupled!.at)} on the way out`); + if (found > 0) parts.push(`coupled ${carsLabel(e.stock.slice(own))} standing on the line`); return { tone: 'plain', where: e.at, text: `Coupled ${e.stock.length} car(s) at ${at(e.at)} ${e.toNose ? 'ONTO THE NOSE' : 'behind the train'}` + - `: ${carsLabel(e.stock)}`, + ` — ${parts.join(', and ')}`, }; + } case 'consistSorted': return { tone: 'good', @@ -145,10 +158,14 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration { text: `SMALL YARD — consist re-ordered from [${carsLabel(e.before)}] to [${carsLabel(e.after)}], so the right car is now on the end and can be spotted`, }; case 'carsDropped': + // WHICH END. A cut comes off an outer end (§A.3) and the end decides everything that follows: + // the train may pull away from cars set out behind it and must couple back up to cars set out + // in front. "Dropped 2 cars" left the one fact that matters out of the record. return { tone: 'plain', where: e.at, - text: `Dropped ${carsLabel(e.stock)} at ${at(e.at)}`, + text: + `Set out ${carsLabel(e.stock)} at ${at(e.at)}, off the ${e.fromNose ? 'NOSE — ahead of the engine, so pulling forward will couple them again' : 'TAIL — behind the engine, so it may pull away and leave them'}`, }; // -- cards @@ -226,6 +243,14 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration { tone: 'bad', text: `SECOND SECTION ordered on Train ${e.trainNumber} — an identical train will run right behind it, which forces the Superintendent to rule on a following train (§8.1)`, }; + case 'extraStarted': + return { + tone: 'good', + text: + e.atSeat === null + ? `EXTRA X${e.trainNumber} started at the ${e.trainNumber % 2 === 0 ? 'Western' : 'Eastern'} Division Point, running ${e.trainNumber % 2 === 0 ? 'east' : 'west'} — odd numbers run west and even run east (§2.3), so its number chose the end` + : `EXTRA X${e.trainNumber} started at the Control Point in seat ${e.atSeat}, running ${e.trainNumber % 2 === 0 ? 'east' : 'west'} — an Extra may begin at any Office above a Whistle Post instead of at a Division Point`, + }; case 'trainMadeUp': return { tone: 'good', @@ -493,37 +518,40 @@ export function impediments(s: GameState, player: PlayerIndex = 0): Impediment[] where: `${name} ${key}`, why: blockedByBox ? 'green box has a load but MEN is occupied' - : `no empty ${want} spotted on this industry's track — a load has nowhere to go until a ` + - 'crew sets one out here (§9.3)', + : `a load is staged but no empty ${want} is spotted on this industry's track — it stays ` + + 'in the green box until a crew sets one out here (§9.3)', severity: blockedByBox ? 'waiting' : 'stuck', }); } if (f.allows.outbound && f.outboundBox.length === 0) { /** - * WHAT TO DO NEXT, in the order you have to do it. + * WHAT TO DO NEXT — AND THE TWO STEPS CAN BE DONE IN EITHER ORDER. * - * A freight industry cannot be stocked at all until an empty car of its commodity is standing - * on its track (§9.3) — so "needs a Freight Agent action" was the second step told to a player - * who had not done the first, and the Freight Agent option simply would not be there. Measured - * before the rule was enforced, 78% of the stocking moves the menu offered had no car to load - * onto. + * §6.3 stocking needs only a car in the Division Yard; §9.3's "Load the car" is what needs an + * empty car of the commodity standing on the industry's track. So an empty siding is not a + * reason to hold the Freight Agent back — it is a second errand to run before the Laborers can + * start. Saying "bring a car in FIRST" sent players to do them in a fixed order they are not + * bound by, and wasted the Stage's one Freight Agent action. */ const spotted = f.industryTrack.cars.some((c) => !c.loaded && facilityCarTypes(f).includes(c.type)); out.push({ where: `${name} ${key}`, why: spotted ? 'green box empty — nothing to load (needs a Freight Agent action)' - : `green box empty, and no empty ${want} is spotted here — bring one in with a crew FIRST, ` + - 'then the Freight Agent can stage a load onto it (§9.3)', + : `green box empty — the Freight Agent can stage a load now, but no empty ${want} is ` + + 'spotted here, so a crew must set one out before Laborers can work it (§9.3)', severity: 'waiting', }); } - if (f.industryTrack.cars.length >= f.industryTrack.length) { + // Four cars is the whole of any track card, industry or not — the same limit that caps a + // consist. It is no longer the industry's box count, which is what used to make a one-box + // industry report itself full with a single car standing on it. + if (f.industryTrack.cars.length >= MAX_CONSIST) { out.push({ where: `${name} ${key}`, - why: `industry track full (${f.industryTrack.length} cars) — no room to spot another`, + why: `industry track full (${MAX_CONSIST} cars) — no room to spot another`, severity: 'stuck', }); } diff --git a/src/sim/replay.ts b/src/sim/replay.ts index df56219..fe0a3fd 100644 --- a/src/sim/replay.ts +++ b/src/sim/replay.ts @@ -204,7 +204,7 @@ export function compress(frames: Frame[]): Packed { const fi = c.facility ? f.facilities.indexOf(c.facility) : -1; // `train` rides whole rather than being interned: it changes almost every frame, so a table // of them would be as long as the frames are and buy nothing. - return [ci, wi, c.enhancements, c.tray, c.cars, fi, c.train, c.ad, c.enhancementsWhat]; + return [ci, wi, c.enhancements, c.tray, c.cars, fi, c.train, c.ad, c.enhancementsWhat, c.standingWest]; }); return { ...f, cells } as unknown as Frame; }); @@ -238,7 +238,7 @@ export function rehydrateCells( facs: unknown[], ): unknown[] { return packed.map((row) => { - const p = row as [number, number, string[], string | null, string[], number, unknown, unknown, string[]]; + const p = row as [number, number, string[], string | null, string[], number, unknown, unknown, string[], number]; const c = cards[p[0]] as [number, number, string, string, boolean, string[]]; return { row: c[0], col: c[1], kind: c[2], label: c[3], running: c[4], links: c[5], @@ -249,6 +249,10 @@ export function rehydrateCells( // Carried rather than recomputed: this function is emitted into the page by toString() and so // cannot reach the card catalogue that produced the text. enhancementsWhat: p[8] ?? [], + // Where the train on this card stands among the cars standing on it, so the replay draws a cut + // ahead of or behind the engine exactly as the live board does. Absent in older recordings, + // which read as 0 — the whole cut east of the engine, which is what they used to draw anyway. + standingWest: p[9] ?? 0, }; }); } @@ -453,7 +457,10 @@ const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({'&':'&','<':'<',' function miniFacility(x) { let h = '

'; if (x.greenCap > 0) h += 'grn ' + boxes(x.green, x.greenCap, 'g'); - if (x.trackCap > 0) h += '
work ' + + // Gated on the sign itself. This used to gate on trackCap, which was a stand-in for "freight + // facility" back when only a freight card had an industry track length; the length is gone and + // maw is the honest test — a Passenger Facility has no MEN | AT | WORK sign at all. + if (x.maw.length > 0) h += '
work ' + x.maw.map((m) => '' + (m ? esc(m) : '·') + '').join(''); if (x.redCap > 0) h += '
red ' + boxes(x.red, x.redCap, 'r'); return h + '
'; @@ -522,18 +529,20 @@ function render() { '
' + esc(x.name) + ' ' + esc(x.commodity) + ' · ' + esc(x.flow) + ' · laborers ' + esc(x.laborers) + ' · porters ' + esc(x.porters) + '' + '
green' + boxes(x.green, x.greenCap, 'g') + '
' + - // No sign and no siding on a Passenger Facility: maw is empty for one, but the LABEL was - // printed before the loop, so a Depot showed the caption over nothing. The mini card above - // gates on trackCap and was already right; this copy was not. + // No sign on a Passenger Facility: maw is empty for one, but the LABEL was printed before + // the loop, so a Depot showed the caption over nothing. // (No backticks in here — this whole block is inside a template literal.) (x.maw.length > 0 ? '
MEN|AT|WORK' + x.maw.map((m) => '' + (m ? esc(m) : '·') + '').join('') + '
' : '') + '
red' + boxes(x.red, x.redCap, 'r') + '
' + - // 's', not 'g': the siding is a place, not a direction, and green is reserved for outbound. - (x.trackCap > 0 || x.track.length > 0 - ? '
siding' + boxes(x.track, x.trackCap, 's') + '
' + // 's', not 'g': standing here is a place, not a direction, and green is reserved for outbound. + // Cap 0 so only the cars actually spotted are drawn — an industry card prints no siding, and + // the row of empty squares this used to draw claimed both that it did and that the siding was + // as long as the industry's box count. It holds four cars, the same as any track card. + (x.track.length > 0 + ? '
spotted' + boxes(x.track, 0, 's') + '
' : '') + '
' + (x.jammed diff --git a/src/sim/view.ts b/src/sim/view.ts index 03e5082..3ba65aa 100644 --- a/src/sim/view.ts +++ b/src/sim/view.ts @@ -17,6 +17,7 @@ import { facilityCarType, laborersLeft, movesFor, + ownCutFor, portersLeft, } from '../engine/apply.ts'; import { @@ -39,10 +40,11 @@ import { officeProfile, trainProfile, houseRules, + mainlineDescription, } from '../engine/content.ts'; import type { Intent } from '../engine/intents.ts'; import type { Facility, GameState, PlayerIndex, TrackCard, TurnoutOrientation } from '../engine/state.ts'; -import { playerAtSeat, railFacingOf, seatOf, turnOf } from '../engine/state.ts'; +import { carsOn, playerAtSeat, railFacingOf, seatOf, turnOf } from '../engine/state.ts'; import type { Hand, HouseRules, TrackGeometry } from '../engine/content.ts'; import type { Port } from '../engine/track.ts'; import { connectionsFor, slopeOfPair, variantsFor } from '../engine/track.ts'; @@ -103,6 +105,18 @@ export type CellView = { */ ad: { used: number; of: number } | null; cars: string[]; + /** + * Cars in `cars` that stand WEST of the train on this card — 0 when no train is here. + * + * `cars` runs west to east like the state it comes from, and a train standing on the card sits + * somewhere IN that row rather than beside it. Reported from play: "right after dropping my cars I + * need to be able to see if those cars are ahead or behind the train" — and the board drew the + * whole cut in one left-aligned strip at the bottom of the card, which cannot answer that at all. + * + * Combined with `train.facing` it is the answer: for an east-facing engine the cars east of it are + * the ones ahead, and the split is drawn as a gap the chip sits in. + */ + standingWest: number; facility: FacilityView | null; /** * The port pairs this card joins, as two-letter codes — 'ew' for the through track, and 'ne', @@ -125,7 +139,8 @@ export type FacilityView = { /** * Freight or passengers — the one thing the renderers could not previously ask. * - * Without it they guessed from side effects (`trackCap > 0`, `laborers` starting "0/0"), and one + * Without it they guessed from side effects (a non-empty industry track, `laborers` starting + * "0/0"), and one * of the three simply did not guess at all, so a Depot drew three MEN | AT | WORK boxes it has no * Laborer to work. Reported from playtesting. */ @@ -138,8 +153,15 @@ export type FacilityView = { maw: (string | null)[]; red: string[]; redCap: number; + /** + * Cars standing on the industry's track, and nothing about how many more will fit. + * + * `trackCap` used to ride alongside this and every renderer drew that many empty squares — a + * printed siding, as long as the industry's box count. No industry card prints one, and the + * number was wrong anyway: a card holds four cars like any other. Both the field and the squares + * are gone; the room left is `spaceOn`'s business, not a drawing's. + */ track: string[]; - trackCap: number; laborers: string; porters: string; /** @@ -258,6 +280,14 @@ export type DivisionView = { * on that grade has no visible meaning. */ gradeUp: string | null; + /** + * Mainline cards only: what the card does to a train, from `mainlineDescription`. + * + * "I have no idea the impact Hilly and Uncontrolled Siding have on game play" — and there was + * nowhere to find out: the tip carried the card's NAME and its modifiers and nothing about the + * crossing time or whether trains may pass. + */ + what?: string; /** Mainline cards only: how many regions the card is divided into (§2.1 — two). */ regions?: number; /** Office nodes only: the Running Track, Limits to Limits, west to east. */ @@ -479,7 +509,6 @@ function facilityView( red: f.inboundBox.map(carLabel), redCap: f.capacity.inbound, track: f.industryTrack.cars.map(carLabel), - trackCap: f.industryTrack.length, laborers: `${laborersLeft(f)}/${f.laborers}`, porters: `${portersLeft(f)}/${f.porters}`, canFinish: canFinishHere(f), @@ -546,6 +575,21 @@ function baseOf( } /** The train standing on a given grid square, drawn as it is seated in the Crew Tray. */ +/** + * Where the train standing on this card sits among the cars standing on it, clamped to the row. + * + * Zero when there is no train, which is also the right answer for a bare cut: with nobody standing + * there, a cut has no near or far side and the whole row simply reads west to east. + */ +function standingWestOn(s: GameState, key: string, cars: number): number { + for (const t of s.trays.values()) { + if (t.position.at !== 'grid') continue; + if (`${t.position.coord.row},${t.position.coord.col}` !== key) continue; + return Math.max(0, Math.min(cars, t.standingWest ?? 0)); + } + return 0; +} + function trainOnCard(s: GameState, key: string): CellView['train'] { for (const [id, t] of s.trays) { if (t.position.at !== 'grid') continue; @@ -687,7 +731,20 @@ export function describeIntent(s: GameState, i: Intent): string { const dest = destinationsFor(s, tray!.position.at === 'grid' ? playerAtSeat(s, tray!.position.seat) : 0, i.trayId, here, i.reverse) .find((d) => d.coord.row === i.to.row && d.coord.col === i.to.col); if (dest && dest.couples.length > 0) { - picks = ` — couples ${carsLabel(dest.couples)} on the way${i.reverse ? ' (behind)' : ' (onto the nose)'}`; + /** + * NAME THE CARS THE CREW SET OUT HERE SEPARATELY. They are at the front of `couples` — the + * walk seeds itself with the cut at the end the train pulls out through — and a button + * reading "couples 2 cars" over cars the player put down thirty seconds ago is exactly the + * surprise this label exists to prevent, running the other way. + */ + const own = ownCutFor(s, playerAtSeat(s, tray!.position.at === 'grid' ? tray!.position.seat : 0), i.trayId, i.reverse).length; + const end = i.reverse ? ' (behind)' : ' (onto the nose)'; + picks = + own > 0 + ? ` — takes your own ${carsLabel(dest.couples.slice(0, own))} back off this card` + + (dest.couples.length > own ? `, then couples ${carsLabel(dest.couples.slice(own))} on the way` : '') + + end + : ` — couples ${carsLabel(dest.couples)} on the way${end}`; } } return `move to ${at(i.to)}${i.reverse ? ' (reverse)' : ''}${picks}`; @@ -768,6 +825,16 @@ export function describeIntent(s: GameState, i: Intent): string { return `board passengers at ${at(i.at)}`; case 'porter.detrain': return `detrain passengers at ${at(i.at)}`; + case 'newTrain.startExtra': { + const runs = i.trainNumber % 2 === 0 ? 'east' : 'west'; + if (i.atSeat === null) { + const end = i.trainNumber % 2 === 0 ? 'Western' : 'Eastern'; + return `start Extra X${i.trainNumber} at the ${end} Division Point — it runs ${runs}, so that is the end it starts from`; + } + const tier = officeProfile(areaAtSeat(s, i.atSeat).tier).name; + return `start Extra X${i.trainNumber} at the ${tier} in seat ${i.atSeat} — a Control Point, so it may begin its ${runs}bound run there instead`; + } + case 'newTrain.placeCar': // carLabel knows a caboose carries the crew, not freight. Formatting it here by hand put // "add loaded caboose" on a button. @@ -815,9 +882,13 @@ export function describeIntent(s: GameState, i: Intent): string { // ABS Signals exists to prevent, but no such collision is implemented — granting clearance is // currently free. Saying otherwise invents a consequence the engine will never deliver. // See implications.md §10 Q13. + // THE TRAIN BEING RULED ON GOES ON THE BUTTON, not in the tooltip. `actionButton` splits a + // label at the first em-dash and shows only the head, so "ALLOW — Train 7 follows…" left the + // one thing the ruling is ABOUT — which train — behind a hover. Reported from play: the + // Superintendent could not tell which train he was clearing without pointing at the button. return i.allow - ? `ALLOW — ${who} follows ${ahead} onto the same Mainline card, closing up behind it` - : `HOLD — ${who} waits where it is, losing the Stage but keeping the line clear`; + ? `ALLOW ${who} to follow ${ahead} — onto the same Mainline card, closing up behind it` + : `HOLD ${who} — it waits where it is, losing the Stage but keeping the line clear`; } case 'draw.fromDepartment': { // Naming the card is the whole point of a FACE-UP pile: "Department 2" tells a player nothing, @@ -933,7 +1004,8 @@ export function snapshot( card.geometry.kind === 'office' ? { used: area.adOccupancy.length, of: officeProfile(area.tier).adTracks } : null, - cars: (card.facility?.industryTrack.length ? card.facility.industryTrack.cars : card.standing).map(carLabel), + cars: carsOn(card).map(carLabel), + standingWest: standingWestOn(s, key, carsOn(card).length), facility: fv, }); } @@ -1006,6 +1078,7 @@ export function snapshot( ...(n.absSignals ? ['ABS Signals'] : []), ], gradeUp: isGrade ? (n.gradeUp ?? 'east') : null, + what: mainlineDescription(n.card, n.gradeUp ?? 'east'), }; } const oa = areaAtSeat(s, n.seat); @@ -1458,7 +1531,7 @@ function cellDescription(card: TrackCard, officeName: string, onRunning: boolean const where = onRunning ? ' · ON THE RUNNING TRACK — industries belong on a stub; a car left standing here is hit by the next arrival' : ''; - return `${flow} ${p.carTypes.join('/')} · spot a matching car on its siding to work a load${progress}${where}`; + return `${flow} ${p.carTypes.join('/')} · spot a matching car on its track to work a load${progress}${where}`; } case 'track': switch (g.geometry) { diff --git a/src/web/game.ts b/src/web/game.ts index 8d7f3e8..8d89172 100644 --- a/src/web/game.ts +++ b/src/web/game.ts @@ -292,8 +292,44 @@ export function actionGroups(game: Game): { options: Intent[]; groups: ActionGro // The tray the phase is waiting on, so the heading names the train the yard chips will load // rather than whichever tray happened to come first out of the map. const filling = prefix === 'newTrain.' ? trainNeedingCars(game.state) : null; - const headed = filling !== null ? (consistTitle(game, filling) ?? title) : title; - groups.push({ kind: prefix, title: headed, actions }); + let headed = filling !== null ? (consistTitle(game, filling) ?? title) : title; + + /** + * THE RULING NAMES ITS TRAINS IN THE HEADING TOO. + * + * "rule on this train" was the only thing said in plain sight about the §8.1 clearance, and + * "this train" is exactly the part the Superintendent has to know. The pending decision holds + * both trays, so the heading can ask the actual question. + */ + if (prefix === 'mainline.clearance') { + const pending = game.state.clock.pendingDecision; + if (pending) { + headed = + `Superintendent — may ${trainName(game.state, pending.train)} follow ` + + `${trainName(game.state, pending.occupiedBy)} onto the same Mainline card?`; + } + } + + /** + * A PENDING EXTRA IS ITS OWN QUESTION, and its own heading. + * + * "Where does this Extra start?" arrived in the same `newTrain.` group as "which car goes on + * this train", under whichever heading the group happened to have — and with no tray being + * filled there is no train to name, so it read "Making up the train" over a choice about a + * different train entirely. Split out, and headed with the Extra's own card: it is about to + * run, and what it may carry is the thing the player needs before choosing where to put it. + */ + const extras = actions.filter((a) => options[a.index]?.type === 'newTrain.startExtra'); + const rest = actions.filter((a) => options[a.index]?.type !== 'newTrain.startExtra'); + if (extras.length > 0) { + const number = (options[extras[0]!.index] as { trainNumber: number }).trainNumber; + groups.push({ + kind: prefix, + title: `${trainCardTitle(number, true) ?? title} — choose where it starts`, + actions: extras, + }); + } + if (rest.length > 0) groups.push({ kind: prefix, title: headed, actions: rest }); } } // Anything the table above does not name still has to be offered — silently dropping a legal @@ -683,7 +719,18 @@ function joinsNote( function consistTitle(game: Game, trayId: string): string | null { const tray = game.state.trays.get(trayId); if (!tray) return null; - const p = trainProfile(tray.trainNumber ?? 0, tray.trainIsExtra); + return trainCardTitle(tray.trainNumber ?? 0, tray.trainIsExtra); +} + +/** + * "Making up Extra X22 “Pee-Dee”: its card calls for 1 caboose — Per-diem train…" + * + * Taken by NUMBER rather than by tray, because a pending Extra has no Crew Tray yet — it is waiting + * for the player to say where it starts, and that heading has to name the train just as much as the + * one over a train being loaded does. + */ +function trainCardTitle(number: number, isExtra: boolean): string | null { + const p = trainProfile(number, isExtra); if (!p) return null; const parts: string[] = []; diff --git a/src/web/panels.ts b/src/web/panels.ts index f9d813e..d41ae76 100644 --- a/src/web/panels.ts +++ b/src/web/panels.ts @@ -151,7 +151,7 @@ export function blockedHtml(f: Frame): string { .join(''); } -/** The load pipeline at each industry: green → MEN | AT | WORK → red, and the siding. */ +/** The load pipeline at each industry: green → MEN | AT | WORK → red, and the cars spotted here. */ export function facilitiesHtml(f: Frame): string { /** * One number, with what the card printed and what a Modifier added. @@ -248,12 +248,12 @@ export function facilitiesHtml(f: Frame): string { }">${x.kind === 'passenger' ? 'waiting to board' : 'waiting to load'}${boxes(x.green, x.greenCap, 'g')}
` : '') + /** - * A PASSENGER FACILITY HAS NO SIGN AND NO SIDING, so neither row is drawn for one. + * A PASSENGER FACILITY HAS NO SIGN, so the row is not drawn for one. * * `maw` comes back empty for a Depot, Station or Terminal — the pipeline is a Freight * Facility fitting (§9.2 is Porters, with no MEN | AT | WORK step). The row LABEL printed * ahead of the loop regardless, so a Depot showed the caption and three empty boxes it - * has no Laborer to work. Same for the siding: an Office has no industry track at all. + * has no Laborer to work. */ (x.maw.length > 0 ? `
MEN AT WORK` + @@ -267,14 +267,23 @@ export function facilitiesHtml(f: Frame): string { : 'Loads that have come off an arriving car and been cleared. They travel car → WORK → AT → MEN, then into a red box — the opposite direction to loading.' }">${x.kind === 'passenger' ? 'arrived' : 'cleared inbound'}${boxes(x.red, x.redCap, 'r')}
` : '') + - (x.trackCap > 0 || x.track.length > 0 - ? `
siding${boxes(x.track, x.trackCap, 's')}
` + /** + * THE CARS THAT ARE HERE — NOT A SIDING OF A FIXED LENGTH. + * + * This drew one empty box per unit of the industry's capacity and labelled the row + * "siding", which said two untrue things: that the card prints a siding, and that its + * length is the box count. It is neither. An industry track is ordinary Operating Rail + * and holds four cars like every other card, so there is no printed length worth + * drawing — only what is actually standing there. Passing cap 0 renders the cars alone. + */ + (x.track.length > 0 + ? `
spotted${boxes(x.track, 0, 's')}
` : '') + `
` + (x.jammed ? 'JAMMED' : x.canFinish ? 'ready' : 'no car spotted') + @@ -378,7 +387,7 @@ ul.blocked{margin:0;padding-left:18px} .box{display:inline-block;min-width:22px;text-align:center;border-radius:3px;padding:1px 4px;font-size:10px} .box.empty{background:#242a32;color:#5a6472} /* Green OUT, red IN, matching the board SVG's palette exactly — the panel and the map are the same - card seen twice, so they may not disagree about which colour means which way. Grey for the siding, + card seen twice, so they may not disagree about which colour means which way. Grey for spotted cars, which is a place rather than a direction. */ .box.g{background:#2f6b3d} .box.r{background:#8a4a4a} diff --git a/test/advance.test.ts b/test/advance.test.ts index 21348b9..e94a2b6 100644 --- a/test/advance.test.ts +++ b/test/advance.test.ts @@ -8,7 +8,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { advance, pump } from '../src/engine/advance.ts'; -import { applyIntent, areaOf } from '../src/engine/apply.ts'; +import { applyIntent, areaOf, check } from '../src/engine/apply.ts'; import { HAND_LIMIT, STAGES_PER_DAY, lengthProfile, TOTAL_ROLLING_STOCK } from '../src/engine/content.ts'; import { legalActions } from '../src/engine/legal.ts'; import { createGame } from '../src/engine/setup.ts'; @@ -794,48 +794,153 @@ describe('the history says WHY a train moved, and says it truthfully', () => { * EXPEDITE IS CONDITIONAL, which is most of why it feels arbitrary at the table. * * `shiftChange` says so — "an expedited train that would need a ruling simply stays, and runs - * normally next Stage" — and with ordinary traffic on the Division it happens a lot: driven by - * the developer bot on the same seed, Train 6 was held and collected FOUR Local Operations turns - * instead of none. So the log and the card must not promise that an Expedited train can never be - * switched; they say "unless the Superintendent has to hold it", and this is why. + * normally next Stage" — and it happens often: driven by the developer bot this was reached by + * ordinary traffic, and Train 6 collected FOUR Local Operations turns instead of none. Built by + * hand here rather than fished out of a bot game, because whether the bot happens to produce a + * meet depends on the deck, and the deck moves. + * + * So the log and the card must not promise that an Expedited train can never be switched. */ const s = game(7, { length: 'standard' }); for (const n of s.division.nodes) if (n.kind === 'mainline') n.card = 'plains'; - const id = 'watched'; + + // Train 6 (Expedite) standing at the Office, having just arrived: this is the state + // `arriveAtOffice` leaves behind when it sets `departsThisStage`. + const area = areaOf(s, 0); + const id = 'expedited'; s.trays.set(id, { - id, trainNumber: 6, trainIsExtra: false, engineAt: 0, - consist: [{ type: 'coach', loaded: true }] as never, + id, trainNumber: 6, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', facing: 'e', - position: { at: 'divisionPoint', side: 'west' }, movesUsed: 0, + position: { at: 'grid', seat: 0, coord: area.officeCoord }, movesUsed: 0, + departsThisStage: true, }); - const dp = s.division.nodes.find((n) => n.kind === 'divisionPoint' && n.side === 'west'); - if (dp?.kind === 'divisionPoint') dp.holding.push(id); + area.adOccupancy.push(id); - let localOps = 0; - const seen = new Set(); - let heldByClearance = false; - - for (let i = 0; i < 20_000; i++) { - if (s.trays.get(id)?.position.at === 'grid' && s.clock.phase === 'localOps') { - const k = `${s.clock.day}|${s.clock.stage}`; - if (!seen.has(k)) { seen.add(k); localOps += 1; } - } - const r = advance(s); - if (r.events.some((e) => e.type === 'clearanceRequested' && e.trainId === id)) heldByClearance = true; - if (s.status === 'finished' || s.clock.day > 3) break; - if (r.needsInput) { - const actor = s.clock.pendingDecision !== null ? s.clock.superintendent : s.clock.currentActor; - if (actor === null) break; - const options = legalActions(s, actor); - if (options.length === 0) break; - if (!applyIntent(s, actor, developerBot.choose(s, actor, options)).ok) break; - } + // A train ahead of it in the same Subdivision, running the SAME way — §8.1's fourth condition, + // which is the Superintendent's call rather than an absolute bar. + const ahead = s.division.nodes.findIndex((n) => n.kind === 'mainline'); + const node = s.division.nodes[ahead]; + assert.equal(node?.kind, 'mainline'); + s.trays.set('ahead', { + id: 'ahead', trainNumber: 12, trainIsExtra: false, engineAt: 0, consist: [], + direction: 'east', facing: 'e', position: { at: 'mainline', index: ahead }, movesUsed: 0, + }); + if (node?.kind === 'mainline') { + node.transits.push({ tray: 'ahead', stagesRemaining: 2, stagesTotal: 2, direction: 'east' }); } - assert.ok(heldByClearance, 'the Expedited train was never asked for a clearance ruling'); + s.clock.phase = 'shiftChange'; + const r = advance(s); + assert.ok( - localOps > 0, - 'an Expedited train held for a ruling should get the Local Operations turns it was denied', + r.events.some((e) => e.type === 'clearanceRequested' && e.trainId === id), + 'the Expedited departure was not put to the Superintendent', + ); + assert.equal( + s.trays.get(id)?.position.at, + 'grid', + 'the Expedited train left despite needing a ruling nobody could give', + ); + assert.ok( + r.events.some((e) => e.type === 'trainHeld' && /EXPEDITES/.test(e.reason)), + 'the log does not say why the Expedited train stayed', + ); + assert.equal( + s.trays.get(id)?.departsThisStage, + false, + 'it should run as an ordinary train from now on, not retry the expedited departure', + ); + }); +}); + +// --------------------------------------------------------------------------- + +describe('an Extra starts where its number sends it, or at a Control Point', () => { + /** + * REPORTED: "Extras should start at Eastern or Western Division point based on their numbers. Even + * trains run to the east (start at western DP), odd run to the west (start at eastern DP). They + * can also start at a control point (any office except whistlepost) at player's choice." + * + * Every Extra used to launch eastbound from the West Division Point, hardcoded, with the + * simplification flagged in a comment — so half the Extras ran the wrong way and the Control Point + * option did not exist at all. + */ + const pending = (trainNumber: number, tier?: 'depot' | 'station'): GameState => { + const s = game(11); + s.pendingExtras = [trainNumber]; + s.timetable = s.timetable.map(() => null); + if (tier) s.officeAreas.get(0)!.tier = tier; + s.clock.phase = 'newTrain'; + advance(s); + return s; + }; + + const started = (s: GameState): CrewTray => { + const tray = [...s.trays.values()].find((t) => t.trainIsExtra); + assert.ok(tray, 'the Extra never took a Crew Tray'); + return tray; + }; + + it('stops for the decision instead of launching the train itself', () => { + const s = pending(17); + assert.equal(s.clock.phase, 'newTrain'); + assert.equal(s.trays.size, 0, 'the Extra was placed without anyone choosing where'); + assert.ok( + legalActions(s, s.clock.currentActor ?? 0).some((i) => i.type === 'newTrain.startExtra'), + 'the placement was never offered', + ); + }); + + it('sends an odd Extra west from the EASTERN Division Point', () => { + // §2.3 — odd runs west. It therefore starts at the end it runs away from. + const s = pending(17); + assert.ok(applyIntent(s, 0, { type: 'newTrain.startExtra', trainNumber: 17, atSeat: null }).ok); + const tray = started(s); + assert.equal(tray.direction, 'west'); + assert.equal(tray.position.at === 'divisionPoint' && tray.position.side, 'east'); + }); + + it('sends an even Extra east from the WESTERN Division Point', () => { + const s = pending(18); + assert.ok(applyIntent(s, 0, { type: 'newTrain.startExtra', trainNumber: 18, atSeat: null }).ok); + const tray = started(s); + assert.equal(tray.direction, 'east'); + assert.equal(tray.position.at === 'divisionPoint' && tray.position.side, 'west'); + }); + + it('refuses a Whistle Post, which is not a Control Point', () => { + const s = pending(18); + assert.equal(s.officeAreas.get(0)!.tier, 'whistlePost'); + assert.equal( + check(s, 0, { type: 'newTrain.startExtra', trainNumber: 18, atSeat: 0 }), + 'NOT_A_CONTROL_POINT', + ); + }); + + it('starts at a Control Point when the player picks one, taking an A/D track', () => { + // Upgrading the Office is what buys this: a Depot is a Control Point, a Whistle Post is not. + const s = pending(18, 'depot'); + const r = applyIntent(s, 0, { type: 'newTrain.startExtra', trainNumber: 18, atSeat: 0 }); + assert.ok(r.ok, `starting at the Depot was refused: ${r.ok ? '' : r.code}`); + const tray = started(s); + const area = s.officeAreas.get(0)!; + assert.equal(tray.position.at, 'grid'); + assert.deepEqual( + tray.position.at === 'grid' ? tray.position.coord : null, + area.officeCoord, + 'the Extra did not start on the Office card', + ); + assert.ok(area.adOccupancy.includes(tray.id), 'it did not take an A/D track'); + assert.equal(tray.direction, 'east', 'an even Extra still runs east from wherever it starts'); + }); + + it('takes the Extra off the pending list once, whichever end it started from', () => { + const s = pending(17); + assert.ok(applyIntent(s, 0, { type: 'newTrain.startExtra', trainNumber: 17, atSeat: null }).ok); + assert.deepEqual(s.pendingExtras, []); + assert.equal( + check(s, 0, { type: 'newTrain.startExtra', trainNumber: 17, atSeat: null }), + 'NO_EXTRA_PENDING', ); }); }); diff --git a/test/apply.test.ts b/test/apply.test.ts index b85dac2..e2ff8a4 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -7,12 +7,12 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { applyIntent, check, areaOf, facilityCarTypes, movesFor, reduce } from '../src/engine/apply.ts'; -import { HAND_LIMIT, INDUSTRY_PROFILES, MOVES_PER_LOCAL_OPS } from '../src/engine/content.ts'; +import { HAND_LIMIT, INDUSTRY_PROFILES, MAX_CONSIST, MOVES_PER_LOCAL_OPS, officeProfile } from '../src/engine/content.ts'; import type { Intent } from '../src/engine/intents.ts'; import { legalActions } from '../src/engine/legal.ts'; import { createGame } from '../src/engine/setup.ts'; import type { CrewTray, GameConfig, GameState, GridCoord, TrackCard } from '../src/engine/state.ts'; -import { coordKey, railFacingOf, turnOf } from '../src/engine/state.ts'; +import { carsOn, coordKey, railFacingOf, spaceOn, turnOf } from '../src/engine/state.ts'; import { cardDescription, snapshot } from '../src/sim/view.ts'; const config: GameConfig = { @@ -453,10 +453,9 @@ describe('Freight Agent operations (§6.3)', () => { inboundBox: [], capacity: { outbound: 3, inbound: 0 }, menAtWork: [null, null, null], - // An empty hopper is SPOTTED. §9.3 refuses to stage cargo against no car — "otherwise you - // are just dropping cargo onto the tracks" — so a fixture with a bare track is a facility - // the Freight Agent cannot legally touch. - industryTrack: { length: 4, cars: [{ type: 'hopper', loaded: false }] }, + // An empty hopper is SPOTTED, which is what the LABORERS need (§9.3). The Freight Agent + // does not: stocking works over a bare track too, and the tests below cover that. + industryTrack: { cars: [{ type: 'hopper', loaded: false }] }, laborers: 3, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, @@ -478,6 +477,38 @@ describe('Freight Agent operations (§6.3)', () => { assert.equal(areaOf(s, 0).grid.get(coordKey(coord))!.facility!.outboundBox.length, 1); }); + it('stocks the green box before the car is spotted (§6.3)', () => { + /** + * REPORTED FROM PLAY: "A freight agent should be able to load an outbound green box prior to + * having the car there that matches the load he's putting in there." + * + * §6.3 asks for nothing but a car in the Division Yard and room in the box. The empty-car + * requirement is §9.3's, on the Laborer who moves the load out of the box — see the test below. + * The engine used to hoist it forward onto stocking and so refused the ordinary play of having + * the cargo waiting while the car is still being switched in. + */ + const s = game(); + const coord = withFacility(s); + const f = areaOf(s, 0).grid.get(coordKey(coord))!.facility!; + f.industryTrack.cars = []; + applyIntent(s, 0, { type: 'localOps.choose', option: 'freightAgent' }); + const r = applyIntent(s, 0, { type: 'freightAgent.stockOutbound', at: coord, carType: 'hopper' }); + assert.ok(r.ok, 'staging cargo against a bare industry track is legal'); + assert.equal(f.outboundBox.length, 1); + }); + + it('still refuses to stock with no matching loaded car in the Division Yard', () => { + // The requirement §6.3 DOES make: the load has to come from somewhere. + const s = game(); + const coord = withFacility(s); + s.yards.divisionYard = s.yards.divisionYard.filter((c) => !(c.type === 'hopper' && c.loaded)); + applyIntent(s, 0, { type: 'localOps.choose', option: 'freightAgent' }); + assert.equal( + check(s, 0, { type: 'freightAgent.stockOutbound', at: coord, carType: 'hopper' }), + 'NO_SUITABLE_CAR', + ); + }); + it('allows only one Freight Agent operation per Stage', () => { // This is the bottleneck the whole economy rests on (card-reference.md §7). const s = game(); @@ -553,7 +584,7 @@ describe('Load/Unload: the four-action freight pipeline (§9.3)', () => { inboundBox: [], capacity: { outbound: 3, inbound: 0 }, menAtWork: [{ type: 'hopper', dir: 'out' }, null, null], - industryTrack: { length: 4, cars: [{ type: 'hopper', loaded: false }] }, + industryTrack: { cars: [{ type: 'hopper', loaded: false }] }, laborers: 3, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, @@ -611,6 +642,46 @@ describe('Load/Unload: the four-action freight pipeline (§9.3)', () => { assert.equal(check(s, 0, { type: 'laborer.advanceLoad', at: coord, box: 2 }), 'BOX_EMPTY'); }); + it('holds a staged load in the green box until a car is spotted (§9.3)', () => { + /** + * THE OTHER HALF OF THE STOCKING RULE. The Freight Agent may stage a load with nothing on the + * industry track (§6.3), but "Load the car" requires "*a load in the Green Loading Box AND an + * empty car of the required type on the industry's track*" — so the load simply waits in the + * box, which is a wait and not a jam: only a load on MEN | AT | WORK locks the track. + */ + const s = game(); + const coord = facilityWithLoad(s); + const f = areaOf(s, 0).grid.get(coordKey(coord))!.facility!; + f.menAtWork = [null, null, null]; + f.outboundBox = [{ type: 'hopper', loaded: true }]; + f.industryTrack.cars = []; + + assert.equal( + check(s, 0, { type: 'laborer.startLoad', at: coord }), + 'NO_EMPTY_CAR_SPOTTED', + 'no car on the track — the Laborers cannot start it', + ); + + // A crew sets an empty hopper out, and the same staged load may now start down the sign. + f.industryTrack.cars = [{ type: 'hopper', loaded: false }]; + assert.equal(check(s, 0, { type: 'laborer.startLoad', at: coord }), null); + const r = applyIntent(s, 0, { type: 'laborer.startLoad', at: coord }); + assert.ok(r.ok); + assert.equal(f.outboundBox.length, 0, 'the load left the green box'); + assert.notEqual(f.menAtWork![0], null, 'and is on MEN'); + }); + + it('will not start a load onto a spotted car of the wrong type', () => { + // Strict matching: a hopper load cannot be swapped onto a tank (§9.3). + const s = game(); + const coord = facilityWithLoad(s); + const f = areaOf(s, 0).grid.get(coordKey(coord))!.facility!; + f.menAtWork = [null, null, null]; + f.outboundBox = [{ type: 'hopper', loaded: true }]; + f.industryTrack.cars = [{ type: 'tank', loaded: false }]; + assert.equal(check(s, 0, { type: 'laborer.startLoad', at: coord }), 'NO_EMPTY_CAR_SPOTTED'); + }); + it('takes four Laborer actions in total for one point', () => { // The asymmetry the whole economy is calibrated against (card-reference.md §2). const s = game(); @@ -694,7 +765,7 @@ describe('legalActions shares its rules with apply (component 6)', () => { inboundBox: [], capacity: { outbound: 3, inbound: 0 }, menAtWork: [null, null, null], - industryTrack: { length: 4, cars: [] }, + industryTrack: { cars: [] }, laborers: 3, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, @@ -752,7 +823,7 @@ describe('an industry handles every commodity its profile names (regression)', ( outboundBox: [], inboundBox: [], capacity: { outbound: p.baseOut, inbound: p.baseIn }, menAtWork: [null, null, null], - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: p.baseLoaders, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, } as never; @@ -792,7 +863,7 @@ describe('a Modifier only goes beside a host that can use it (regression)', () = allows: { outbound: true, inbound: false }, outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: [null, null, null], - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, } as never); @@ -869,37 +940,40 @@ describe("a Modifier grants only what its host's flow can use", () => { outboundBox: [], inboundBox: [], capacity: { outbound: out ? 1 : 0, inbound: into ? 1 : 0 }, menAtWork: [null, null, null], - industryTrack: { length: 1, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, }); - /** Plays an Ice House next to the facility at `at`, and returns that facility. */ - const iceHouseBeside = (kind: string, out: boolean, into: boolean) => { + /** Plays `modifier` next to the facility at `at`, and returns the facility and its card. */ + const modifierBeside = (modifier: string, kind: string, out: boolean, into: boolean) => { const s = game(); const area = areaOf(s, 0); const at = { row: -1, col: 4 }; area.grid.set(coordKey(at), withFacility(kind, out, into) as never); applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' }); - const ice = [...s.cards.entries()].find( - ([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'iceHouse', + const card = [...s.cards.entries()].find( + ([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === modifier, ); - assert.ok(ice, 'no Ice House in the deck'); - s.decks.hands.set(0, [ice![0]]); + assert.ok(card, `no ${modifier} in the deck`); + s.decks.hands.set(0, [card![0]]); const r = applyIntent(s, 0, { - type: 'card.play', cardId: ice![0], placement: { row: at.row, col: at.col + 1 }, + type: 'card.play', cardId: card![0], placement: { row: at.row, col: at.col + 1 }, }); - assert.ok(r.ok, `the Ice House should be playable beside a ${kind}`); - return area.grid.get(coordKey(at))!.facility!; + assert.ok(r.ok, `the ${modifier} should be playable beside a ${kind}`); + const host = area.grid.get(coordKey(at))!; + return { f: host.facility!, card: host }; }; + const iceHouseBeside = (kind: string, out: boolean, into: boolean) => + modifierBeside('iceHouse', kind, out, into).f; + it('gives a Packing Sheds the outbound slot its card prints', () => { // The host ships, so every printed grant lands. This is the control for the case below. const f = iceHouseBeside('packingSheds', true, false); assert.equal(f.capacity.outbound, 2, 'the outbound slot did not arrive'); assert.equal(f.laborers, 2, 'the laborer did not arrive'); - assert.equal(f.industryTrack.length, 2, 'the siding should lengthen with the slot it serves'); }); it("drops the outbound slot on a Grocer's Warehouse, which only receives", () => { @@ -910,23 +984,47 @@ describe("a Modifier grants only what its host's flow can use", () => { assert.equal(f.laborers, 2, 'the laborer should still arrive — Laborers have no direction'); assert.equal(f.capacity.outbound, 0, 'outbound capacity was credited to a facility that cannot ship'); assert.equal(f.capacity.inbound, 1, 'the modifier must not silently move the grant to the other direction'); - assert.equal(f.industryTrack.length, 1, 'the siding lengthened for a slot that does not exist'); }); - it('does not give a Passenger Facility a siding by way of a Modifier', () => { + it('gives a Truck Dock a second red box and not one more car length', () => { + /** + * The case as it was described: "if I have a Grocer's Warehouse that receives, it has one red + * box. If it has a Truck Dock next to it, that should give it a second red box. There is no + * change to the number of cars that are allowed to be dropped here." + * + * Both halves matter, and the second half is the one that used to be wrong: the Truck Dock's + * inbound grant lengthened the industry track along with the box, because the track's length WAS + * the box count. The boxes are how much work the industry can hold; the card holds four cars + * either way. + */ + const { f, card } = modifierBeside('truckDock', 'grocersWarehouse', true, true); + assert.equal(f.capacity.inbound, 2, 'the Truck Dock did not add the red box it prints'); + assert.equal(f.capacity.outbound, 1, 'the Truck Dock added a green box, which it does not print'); + + assert.equal( + spaceOn(card as never), MAX_CONSIST, + 'a Modifier changed how many cars may be set out at the industry it stands beside', + ); + }); + + it('does not give a Passenger Facility an industry track by way of a Modifier', () => { // FOUND by rendering 12,000 frames of real games rather than a fixture: a Waiting Area beside the - // Office lengthened `industryTrack`, and the board then drew the Depot a siding square to spot a - // car on. The same phantom siding the renderer had just been fixed for, arriving by another door. + // Office lengthened `industryTrack`, and the board then drew the Depot a square to spot a car on. // A Waiting Area's "+1" is a passenger slot; passengers board off the platform. + // + // The length field is gone now — an industry track holds four cars like any other card, so there + // was never a number for a Modifier to raise. What is still worth pinning down is the property + // that bug was really about: an Office keeps its cars in `standing` and has no industry track for + // a Modifier to hand it, no matter what the Modifier prints. const s = game(); const area = areaOf(s, 0); applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' }); // Forced open so this tests the freight/passenger distinction rather than the `allows` gating — // the Office starts as a Whistle Post, which is not a Passenger Facility and takes nothing. - const office = area.grid.get(coordKey(area.officeCoord))!.facility!; + const officeCard = area.grid.get(coordKey(area.officeCoord))!; + const office = officeCard.facility!; office.allows = { outbound: true, inbound: true }; - const before = office.industryTrack.length; const waiting = [...s.cards.entries()].find( ([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'waitingArea', @@ -941,37 +1039,68 @@ describe("a Modifier grants only what its host's flow can use", () => { assert.equal(office.capacity.outbound, 1, 'the passenger slot should still be granted'); assert.equal(office.porters, 1, 'the porter should still be granted'); assert.equal( - office.industryTrack.length, before, + carsOn(officeCard), officeCard.standing, 'a Modifier gave a Passenger Facility an industry track to spot cars on', ); }); - it('says which bonus was dropped, and why', () => { - // The number genuinely does not move; what was missing was the reason. + it('suppresses a grant the host cannot use — and gives it back when it can', () => { + /** + * REPORTED from play: "Restaurant attached to a whistle stop, then upgrade to depot — depot only + * shows one green / one red box. I expected two, because Restaurant increases outbound by one." + * + * `hosts: ['office']` includes a Whistle Post, which is NOT a Passenger Facility, so the +1 + * outbound is genuinely unusable while the Office is a Whistle Post — suppressing it is right, + * and saying so is what the panel is for. Losing it FOREVER was the bug: the upgrade applied + * only the difference between two tiers and knew nothing about what had been discarded. + * + * This used to be written against an Ice House on a Grocer's Warehouse. That case no longer + * suppresses anything, because the Grocer's is a both-direction facility — which was the other + * half of the same report. + */ const s = game(); const area = areaOf(s, 0); - const at = { row: -1, col: 4 }; - area.grid.set(coordKey(at), withFacility('grocersWarehouse', false, true) as never); + const office = area.grid.get(coordKey(area.officeCoord))!; + assert.equal(office.facility!.allows.outbound, false, 'a Whistle Post is not a Passenger Facility'); + applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' }); - const ice = [...s.cards.entries()].find( - ([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'iceHouse', + const restaurant = [...s.cards.entries()].find( + ([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'restaurant', )!; - - // Before it is played, the card in hand warns which of its two printed hosts cannot use it. - const what = cardDescription(s, ice[0]); - assert.match(what, /only receives/, 'the hand card does not warn about the dead half'); - - s.decks.hands.set(0, [ice[0]]); + s.decks.hands.set(0, [restaurant[0]]); assert.ok(applyIntent(s, 0, { - type: 'card.play', cardId: ice[0], placement: { row: at.row, col: at.col + 1 }, - }).ok); + type: 'card.play', + cardId: restaurant[0], + placement: { row: area.officeCoord.row - 1, col: area.officeCoord.col }, + }).ok, 'the Restaurant could not be played beside the Office'); - const fv = snapshot(s, [], null).facilities.find((v) => v.name.includes('Grocer')); - assert.ok(fv, "the Grocer's Warehouse is missing from the panel"); + const f = office.facility!; + assert.equal(f.capacity.outbound, 0, 'a Whistle Post gained a passenger slot it cannot have'); + assert.equal(f.porters, 1, 'the porter has no direction gate and should have landed'); + + const fv = snapshot(s, [], null).facilities.find((v) => v.name.includes('Whistle')); + assert.ok(fv, 'the Office is missing from the panel'); assert.equal(fv!.suppressed.length, 1, 'the dropped bonus is not reported'); - assert.match(fv!.suppressed[0]!, /Ice House/); - // And the modifier is now recorded against the host it served, which never used to happen. - assert.deepEqual(fv!.modifiers, ['Ice House'], 'the modifier is not listed against its host'); + assert.match(fv!.suppressed[0]!, /Restaurant/); + assert.deepEqual(fv!.modifiers, ['Restaurant'], 'the modifier is not listed against its host'); + + // Upgrading makes it a Passenger Facility, and the slot the Restaurant always printed arrives. + reduce(s, { type: 'officeUpgraded', player: 0, from: 'whistlePost', to: 'depot' }); + assert.equal(f.allows.outbound, true); + assert.equal( + f.capacity.outbound, + officeProfile('depot').passengerOut + 1, + "the Restaurant's slot did not come back when the Office could finally use it", + ); + + // And it is paid ONCE: a second upgrade must not grant it again. + const afterDepot = f.capacity.outbound; + reduce(s, { type: 'officeUpgraded', player: 0, from: 'depot', to: 'station' }); + assert.equal( + f.capacity.outbound, + afterDepot + (officeProfile('station').passengerOut - officeProfile('depot').passengerOut), + 'the Restaurant was paid a second time on the next upgrade', + ); }); }); diff --git a/test/cut-ordering.test.ts b/test/cut-ordering.test.ts new file mode 100644 index 0000000..7adb565 --- /dev/null +++ b/test/cut-ordering.test.ts @@ -0,0 +1,365 @@ +/** + * §A.3 — cars occupy the track "in the same order they originally held, left-to-right". + * + * Left-to-right is west-to-east, and `TrackCard.standing` had no defined orientation at all until + * this release. Three reports came out of that one omission and each gets its own battery here: + * a cut parked in a different order depending on how many cars were set out at once; a train that + * got the identical consist whichever end it approached a cut from; and a train that drove straight + * through the cars it had just set out. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { applyIntent, areaOf, check } from '../src/engine/apply.ts'; +import { createGame } from '../src/engine/setup.ts'; +import type { CrewTray, GameConfig, GameState, GridCoord, RollingStock, TrackCard } from '../src/engine/state.ts'; +import { carsOn, coordKey, turnOf } from '../src/engine/state.ts'; + +const config: GameConfig = { + mode: 'solitaire', + victory: 'highestAfterDays', + length: 'standard', + optionalRules: { + reducedVisibility: false, + sisterTrains: false, + employeeRotation: false, + emergencyToolbox: false, + }, +}; + +const game = (seed = 77): GameState => createGame({ id: 'g', seed, config, playerNames: ['Jesse'] }); +const at = (row: number, col: number): GridCoord => ({ row, col }); +const car = (type: string, loaded = false): RollingStock => ({ type, loaded }) as RollingStock; + +const straight = (standing: RollingStock[] = []): TrackCard => ({ + geometry: { kind: 'track', geometry: 'straight' }, + baseOperationalRail: true, + standing, + facility: null, + modifiers: [], + enhancements: [], +}); + +function addCard(s: GameState, coord: GridCoord, card: TrackCard): void { + areaOf(s, 0).grid.set(coordKey(coord), card); +} + +/** A row of plain straights, on row 1 — row 0 is the Running Track and holds the Office. */ +function row(s: GameState, n: number): void { + for (let c = 0; c < n; c++) addCard(s, at(1, c), straight()); +} + +function placeTray( + s: GameState, + coord: GridCoord, + consist: RollingStock[], + facing: 'e' | 'w', + engineAt = 0, +): string { + const id = s.freeTrays.pop()!; + s.trays.set(id, { + id, + trainNumber: null, + trainIsExtra: false, + engineAt, + consist, + direction: facing === 'w' ? 'west' : 'east', + facing, + railFacing: facing, + position: { at: 'grid', seat: 0, coord }, + movesUsed: 0, + } as CrewTray); + return id; +} + +/** Puts the game in the one phase switching intents are legal in. */ +function switching(s: GameState): void { + s.clock.phase = 'localOps'; + s.clock.currentActor = 0; + turnOf(s, 0).option = 'switch'; +} + +const types = (cars: readonly RollingStock[]): string[] => cars.map((c) => c.type); + +const FOUR = (): RollingStock[] => [car('boxcar'), car('hopper'), car('reefer'), car('tank')]; + +const standingAt = (s: GameState, coord: GridCoord): string[] => + types(carsOn(areaOf(s, 0).grid.get(coordKey(coord))!)); + +// --------------------------------------------------------------------------- + +describe('a cut parks in the same order however many cars come off at once', () => { + /** + * Reported: "when dropping all 4 cars, order was reversed. It worked properly if we dropped cars + * individually." Both halves are the same bug — `carsDropped` pushed onto the end of the array + * whatever end the cut came off, so the answer depended on how the player chose to spend the drop. + * + * A drop costs no Moves, so one drop of four and four drops of one are the SAME turn played two + * ways. They must park the same cars in the same order, and that is the whole property. + */ + function park(facing: 'e' | 'w', fromNose: boolean, batches: number[]): string[] { + const s = game(); + row(s, 1); + // The engine goes at whichever end lets the cut come off the one being tested: cars ahead of it + // for a nose drop, behind it for a tail drop. + const id = placeTray(s, at(1, 0), FOUR(), facing, fromNose ? 4 : 0); + switching(s); + for (const count of batches) { + const r = applyIntent(s, 0, { + type: 'switch.dropCars', + trayId: id, + count, + ...(fromNose ? { fromNose: true } : {}), + }); + assert.ok(r.ok, `drop of ${count} refused: ${r.ok ? '' : r.code}`); + } + return standingAt(s, at(1, 0)); + } + + for (const facing of ['e', 'w'] as const) { + for (const fromNose of [false, true]) { + const end = fromNose ? 'nose' : 'tail'; + it(`is batch-invariant off the ${end}, facing ${facing}`, () => { + const all = park(facing, fromNose, [4]); + assert.equal(all.length, 4, 'all four cars should be standing on the card'); + assert.deepEqual(park(facing, fromNose, [1, 1, 1, 1]), all, 'four singles parked a different order than one four'); + assert.deepEqual(park(facing, fromNose, [2, 2]), all, 'two pairs parked a different order again'); + }); + } + } + + it('parks west-to-east, so an east-facing train sets its tail cut out behind itself', () => { + // Consist is nose first and the nose points east, so `[boxcar hopper reefer tank]` stands on the + // ground as tank, reefer, hopper, boxcar reading west to east. The tail car is the westmost. + assert.deepEqual(park('e', false, [4]), ['tank', 'reefer', 'hopper', 'boxcar']); + // Facing west the tray already reads west-to-east, so it goes down as it stands. + assert.deepEqual(park('w', false, [4]), ['boxcar', 'hopper', 'reefer', 'tank']); + }); + + it('keeps the two cuts apart when a train sets out off both ends on one square', () => { + // §A.3 lets a cut come off either outer end, so both can happen on one square. The engine ends + // up standing between them, which is the thing `standingWest` exists to record. + const s = game(); + row(s, 1); + const id = placeTray(s, at(1, 0), FOUR(), 'e', 2); + switching(s); + assert.ok(applyIntent(s, 0, { type: 'switch.dropCars', trayId: id, count: 1, fromNose: true }).ok); + assert.ok(applyIntent(s, 0, { type: 'switch.dropCars', trayId: id, count: 1 }).ok); + + // Facing east: the nose cut (boxcar) is set out east of the engine, the tail cut (tank) west. + assert.deepEqual(standingAt(s, at(1, 0)), ['tank', 'boxcar']); + assert.equal(s.trays.get(id)!.standingWest, 1, 'the engine should be standing between the two cuts'); + }); +}); + +// --------------------------------------------------------------------------- + +describe('a parked cut mirrors depending on which end the train comes from', () => { + /** + * The consist a train ends up with must depend on which way it ran, or a run-around buys nothing. + * `couples` was accumulated in path order with no reference to the direction of travel, so both + * approaches produced the identical tray. + */ + function collect(from: 'west' | 'east'): string[] { + const s = game(); + row(s, 3); + // The cut stands on the middle square, boxcar west of hopper. + addCard(s, at(1, 1), straight([car('boxcar'), car('hopper')])); + const start = from === 'west' ? at(1, 0) : at(1, 2); + const to = from === 'west' ? at(1, 2) : at(1, 0); + const id = placeTray(s, start, [], from === 'west' ? 'e' : 'w'); + switching(s); + const r = applyIntent(s, 0, { type: 'switch.move', trayId: id, to, reverse: false }); + assert.ok(r.ok, `the run was refused: ${r.ok ? '' : r.code}`); + return types(s.trays.get(id)!.consist); + } + + it('gives mirrored consists, never the same one twice', () => { + const eastbound = collect('west'); + const westbound = collect('east'); + assert.notDeepEqual(eastbound, westbound, 'approaching from either end gave the identical consist'); + assert.deepEqual(eastbound, [...westbound].reverse(), 'the two approaches are not mirror images'); + }); + + it('puts the car the engine met LAST on the nose, running forward', () => { + // Running east onto boxcar-then-hopper, the engine pushes the boxcar ahead of it and picks the + // hopper up in front of that. Nose first, the tray reads hopper, boxcar. + assert.deepEqual(collect('west'), ['hopper', 'boxcar']); + assert.deepEqual(collect('east'), ['boxcar', 'hopper']); + }); + + it('couples behind the train nearest-car-first when backing up', () => { + const s = game(); + row(s, 3); + addCard(s, at(1, 1), straight([car('boxcar'), car('hopper')])); + // Facing east at the east end, so backing up runs WEST into the cut: it meets the hopper first. + const id = placeTray(s, at(1, 2), [car('caboose')], 'e'); + switching(s); + const r = applyIntent(s, 0, { type: 'switch.move', trayId: id, to: at(1, 0), reverse: true }); + assert.ok(r.ok, `the reverse move was refused: ${r.ok ? '' : r.code}`); + assert.deepEqual(types(s.trays.get(id)!.consist), ['caboose', 'hopper', 'boxcar']); + }); +}); + +// --------------------------------------------------------------------------- + +describe('a train may not drive through the cut on its own card (§A.4)', () => { + /** + * Reported: "if I put cars off the nose on a given track, and my next move is go forward, I need + * to couple those cars right back on." The walk started at the NEIGHBOUR of the start square and + * never read the start card at all, so the cut was simply left behind. + * + * The rule is direction-sensitive, which is the point: only the cut at the end the train pulls out + * through is in the way. That is what makes setting out off one particular end a decision. + */ + function runOut(fromNose: boolean, reverse: boolean): { consist: string[]; left: string[] } { + const s = game(); + row(s, 3); + const id = placeTray(s, at(1, 1), [car('boxcar'), car('hopper')], 'e', fromNose ? 2 : 0); + switching(s); + assert.ok( + applyIntent(s, 0, { + type: 'switch.dropCars', + trayId: id, + count: 1, + ...(fromNose ? { fromNose: true } : {}), + }).ok, + ); + const to = reverse ? at(1, 0) : at(1, 2); + const r = applyIntent(s, 0, { type: 'switch.move', trayId: id, to, reverse }); + assert.ok(r.ok, `the move was refused: ${r.ok ? '' : r.code}`); + return { consist: types(s.trays.get(id)!.consist), left: standingAt(s, at(1, 1)) }; + } + + it('picks a nose cut back up when it pulls forward through it', () => { + const out = runOut(true, false); + assert.deepEqual(out.left, [], 'the cut was left standing on the card the train pulled out of'); + assert.equal(out.consist.length, 2, 'the train should have both cars back'); + }); + + it('leaves a nose cut standing when it backs away from it instead', () => { + const out = runOut(true, true); + assert.deepEqual(out.left, ['boxcar'], 'backing away from a nose cut should leave it where it is'); + assert.deepEqual(out.consist, ['hopper']); + }); + + it('encodes §A.5’s trailing-point step 1 — drop the back cars, then pull away from them', () => { + // "Drop the back two cars (blue and red) on Card F. Move train to Card B." The whole example + // collapses if that first move drags them along again. + const s = game(); + row(s, 3); + const id = placeTray(s, at(1, 2), FOUR(), 'w'); + switching(s); + assert.ok(applyIntent(s, 0, { type: 'switch.dropCars', trayId: id, count: 2 }).ok); + const r = applyIntent(s, 0, { type: 'switch.move', trayId: id, to: at(1, 0), reverse: false }); + assert.ok(r.ok, `pulling away from the set-out cars was refused: ${r.ok ? '' : r.code}`); + assert.deepEqual(types(s.trays.get(id)!.consist), ['boxcar', 'hopper'], 'the train kept cars it had set out'); + assert.deepEqual( + standingAt(s, at(1, 2)), + ['reefer', 'tank'], + 'the two cars set out should still be standing on the square the train left', + ); + }); + + it('counts the own cut against the four-car limit', () => { + // A full tray that sets out one car cannot simply pull forward through it again — that would be + // five cars, and coupling is mandatory, so the MOVE is what has to be refused. + const s = game(); + row(s, 3); + const id = placeTray(s, at(1, 1), FOUR(), 'e', 4); + switching(s); + assert.ok(applyIntent(s, 0, { type: 'switch.dropCars', trayId: id, count: 1, fromNose: true }).ok); + // Refill the tray so the recoupled car would be a fifth. + s.trays.get(id)!.consist = FOUR(); + assert.equal(check(s, 0, { type: 'switch.move', trayId: id, to: at(1, 2), reverse: false }), 'ILLEGAL_MOVE'); + // Backing away from it is still legal — the cut is not at that end. + assert.equal(check(s, 0, { type: 'switch.move', trayId: id, to: at(1, 0), reverse: true }), null); + }); +}); + +// --------------------------------------------------------------------------- + +describe('the round trip comes back with the train it started with', () => { + /** + * Drop, run clear, back up, collect. This was already self-consistent before the fix — whatever + * order got parked came back — and it has to STAY that way now that both halves have changed, at + * both facings and every batch size. + */ + for (const facing of ['e', 'w'] as const) { + for (const batches of [[4], [1, 1, 1, 1], [2, 2]]) { + it(`survives ${batches.join('+')} cars set out, facing ${facing}`, () => { + const s = game(); + row(s, 3); + const start = facing === 'e' ? at(1, 0) : at(1, 2); + const away = facing === 'e' ? at(1, 2) : at(1, 0); + const before = types(FOUR()); + const id = placeTray(s, start, FOUR(), facing); + switching(s); + turnOf(s, 0).movesRemaining = 6; + + for (const count of batches) { + assert.ok(applyIntent(s, 0, { type: 'switch.dropCars', trayId: id, count }).ok); + } + assert.equal(s.trays.get(id)!.consist.length, 0, 'the whole train should be on the ground'); + + // Pull forward clear of the cut, then back up onto it again. + assert.ok(applyIntent(s, 0, { type: 'switch.move', trayId: id, to: away, reverse: false }).ok); + const back = applyIntent(s, 0, { type: 'switch.move', trayId: id, to: start, reverse: true }); + assert.ok(back.ok, `backing onto the cut was refused: ${back.ok ? '' : back.code}`); + + assert.deepEqual(types(s.trays.get(id)!.consist), before, 'the train came back in a different order'); + assert.deepEqual(standingAt(s, start), [], 'cars were left behind'); + }); + } + } +}); + +// --------------------------------------------------------------------------- + +describe('taking your own cut back is undoing the drop, not a fresh pick-up', () => { + /** + * Jesse's call, of the two answers the plan put up. Trains 3/4 print "may drop OR pick up one + * freight car at every location" and the two share one budget, so a drop off the nose would + * otherwise leave the train forbidden to pull forward past its own cars — a legal-looking move + * that is silently one-way. Same shape for X13's "drop but not pick up". + */ + it('refunds the 3/4 Express its per-location freight budget', () => { + const s = game(); + row(s, 3); + const id = placeTray(s, at(1, 1), [car('boxcar'), car('hopper')], 'e', 2); + s.trays.get(id)!.trainNumber = 3; + switching(s); + turnOf(s, 0).movesRemaining = 6; + + assert.ok(applyIntent(s, 0, { type: 'switch.dropCars', trayId: id, count: 1, fromNose: true }).ok); + const r = applyIntent(s, 0, { type: 'switch.move', trayId: id, to: at(1, 2), reverse: false }); + assert.ok(r.ok, `the Express could not take its own car back: ${r.ok ? '' : r.code}`); + assert.equal(s.trays.get(id)!.consist.length, 2, 'the car it set out should be back on the train'); + assert.deepEqual( + Object.values(turnOf(s, 0).freightWorked).filter((n) => n > 0), + [], + 'the drop it undid is still charged against the square it was made on', + ); + }); + + it('still charges a genuine pick-up', () => { + // The exemption is for YOUR OWN cut and nothing else: a car that was already standing there when + // the crew arrived is an ordinary pick-up and the budget still bites. + const s = game(); + row(s, 3); + addCard(s, at(1, 2), straight([car('reefer')])); + const id = placeTray(s, at(1, 1), [car('boxcar'), car('hopper')], 'e', 2); + s.trays.get(id)!.trainNumber = 3; + switching(s); + turnOf(s, 0).movesRemaining = 6; + + assert.ok(applyIntent(s, 0, { type: 'switch.dropCars', trayId: id, count: 1, fromNose: true }).ok); + assert.ok(applyIntent(s, 0, { type: 'switch.move', trayId: id, to: at(1, 2), reverse: false }).ok); + assert.equal( + turnOf(s, 0).freightWorked[`${id}@${coordKey(at(1, 2))}`], + 1, + 'only the car that was already standing there should be charged', + ); + }); +}); diff --git a/test/enhancements.test.ts b/test/enhancements.test.ts index f0833ba..2f08e3e 100644 --- a/test/enhancements.test.ts +++ b/test/enhancements.test.ts @@ -38,7 +38,15 @@ function addCard(s: GameState, coord: GridCoord, card: TrackCard): void { areaOf(s, 0).grid.set(coordKey(coord), card); } -/** Puts an enhancement card in hand and returns its id. */ +/** + * An Enhancement in hand — minting one if the deck does not deal it. + * + * Facing Point Locks, the Water Column and the Overpass answer opponent-directed cards, which are + * held out of every deck until they are implemented, so those three are no longer dealt either + * (v0.4.7). Their RULES are still live and still worth testing: they fire the moment their attacker + * returns, and a rule nobody exercises is a rule that rots. So the card is put in hand directly + * rather than fished out of a deck that deliberately no longer contains it. + */ function handEnhancement(s: GameState, key: string): string { for (const [id, card] of s.cards) { if (card.kind.kind === 'enhancement' && card.kind.key === key) { @@ -46,7 +54,10 @@ function handEnhancement(s: GameState, key: string): string { return id; } } - throw new Error(`no enhancement card: ${key}`); + const id = `mint-${key}-${s.cards.size}`; + s.cards.set(id, { id, kind: { kind: 'enhancement', key } } as never); + s.decks.hands.set(0, [id]); + return id; } /** Puts a specific track card in hand and returns its id. */ diff --git a/test/mainline-cards.test.ts b/test/mainline-cards.test.ts index 544d8f9..f53a1ac 100644 --- a/test/mainline-cards.test.ts +++ b/test/mainline-cards.test.ts @@ -332,7 +332,7 @@ describe('Flying Switch rolls a cut into an industry', () => { outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: [null, null, null], - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, diff --git a/test/multiplayer.test.ts b/test/multiplayer.test.ts index bef4e87..751fba6 100644 --- a/test/multiplayer.test.ts +++ b/test/multiplayer.test.ts @@ -63,7 +63,7 @@ const idleIndustry = () => inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: [null, null, null], - industryTrack: { length: 1, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, @@ -504,7 +504,7 @@ describe('the view shows one seat at a time', () => { inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: [null, null, null], - industryTrack: { length: 1, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, diff --git a/test/replay.test.ts b/test/replay.test.ts index 432566c..c40b729 100644 --- a/test/replay.test.ts +++ b/test/replay.test.ts @@ -395,7 +395,17 @@ describe('the replay behaves like the game it is replaying', () => { // seed and passed on luck — the moment the opening deal changed, that seed's game happened to // contain no switching at all and four of the six cues vanished. What is under test is the // plumbing, not one game, so the frames are pooled. - const recs = [5, 4321, 880009].map((seed) => record(seed, 'standard', 4000)); + /** + * TWELVE SEEDS, not three — the same lesson as the comment above, learnt again at v0.4.7. + * Coupling happens in about 39 games in 200, so three deals expect 0.6 of one: the assertion was + * riding on luck exactly as the single seed had been, and the deck losing seven cards was enough + * to tip it. + * + * TWENTY-FOUR, not twelve, and the difference is instructive: on this stride the first game that + * couples anything is index 13, so a twelve-seed pool still contained none. The rate is what + * matters, not the count — measured 39 in 200, with couplers at indices 13, 19, 22, 24, 28 … + */ + const recs = Array.from({ length: 24 }, (_, i) => record(1000 + i * 7919, 'standard', 4000)); const withCues = recs.flatMap((rec) => rec.frames.filter((f) => (f.cues?.length ?? 0) > 0)); assert.ok(withCues.length > 20, `only ${withCues.length} frames carry a cue`); diff --git a/test/setup.test.ts b/test/setup.test.ts index e6dfdd9..1601cf0 100644 --- a/test/setup.test.ts +++ b/test/setup.test.ts @@ -7,7 +7,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import type { CarType } from '../src/engine/content.ts'; -import { MODIFIER_PROFILES, OPENING_OTHER, OPENING_TRACK, SOLITAIRE_DECK_SIZE, TRACK_CARDS, TRACK_IN_DECK } from '../src/engine/content.ts'; +import { DEFENCE_ONLY_CARDS, DEFENCE_ONLY_COPIES, MODIFIER_PROFILES, OPENING_OTHER, OPENING_TRACK, SOLITAIRE_DECK_SIZE, TRACK_CARDS, TRACK_IN_DECK } from '../src/engine/content.ts'; import { DECK_SIZE, EXTRA_TRAINS, @@ -104,7 +104,19 @@ describe('card catalogue (component 1)', () => { // Q6 took Space-use and Action cards out of solitaire, where they have no legal target. They are // now out of the COMPETITIVE deck too, until they are implemented: `checkPlay` answers both // categories NOT_IMPLEMENTED, so dealing them would make 22 of 235 draws (9%) reject outright. - assert.equal(SOLITAIRE_DECK_SIZE, 213); + // 206, not 213: the 22 opponent-directed cards come out, and so do the SEVEN that exist only to + // answer them — Facing Point Locks (both the Enhancement and the Mainline modifier, 2 each), two + // Water Columns and one Overpass. A defence with nothing to defend against is the same dead draw + // as the attack would be. `SimpleCard.answers` names the pairing, so they return together. + assert.equal(SOLITAIRE_DECK_SIZE, 206); + assert.equal(DEFENCE_ONLY_COPIES, 7); + for (const c of DEFENCE_ONLY_CARDS) { + assert.ok(c.answers, `${c.name} is held back without saying what it answers`); + assert.ok( + !buildDeck().some((x) => (x.kind as { key?: string }).key === c.key), + `${c.name} is still dealt`, + ); + } for (const mode of ['solitaire', 'competitive'] as const) { const deck = buildDeck(mode); assert.equal(deck.length, SOLITAIRE_DECK_SIZE, `${mode} deck size`); @@ -169,9 +181,23 @@ describe('card catalogue (component 1)', () => { } }); - it('identifies the both-direction industry', () => { + it('identifies the both-direction industries the card reference names', () => { + /** + * `card-reference.md`: "'Freight House' is not a card. It is the collective term for a freight + * facility that loads *and* unloads — the Grocer's Warehouse and the Oil Refinery." The table + * agrees: both are "Both", and only the Power Plant is inbound-only. + * + * The engine had the Refinery as outbound-only and the Grocer's as inbound-only, so §9.3's + * "Passenger Facilities and Freight Houses permit cars to move each direction" named neither of + * them — and every Modifier grant on the missing direction was silently dropped, which is how + * "grocer's warehouse didn't get extra outbound slot for truck dock" was reported. + * + * `freightHouse` is still in this list because the engine deals it as a CARD, which the rules say + * it is not. That is a deck-composition question, recorded in TODO.md, not something to quietly + * delete six cards over. + */ const houses = FREIGHT_PROFILES.filter(isFreightHouse).map((f) => f.kind); - assert.deepEqual(houses.sort(), ['freightHouse']); + assert.deepEqual(houses.sort(), ['freightHouse', 'grocersWarehouse', 'refinery']); }); it('starts every industry at one car out and one loader', () => { diff --git a/test/sim.test.ts b/test/sim.test.ts index f934fa6..a12a7a3 100644 --- a/test/sim.test.ts +++ b/test/sim.test.ts @@ -146,15 +146,22 @@ describe('the revenue chain works end to end (regression)', () => { // REGRESSION. Cars dropped at a facility went into the card's `standing` list, while §9.3's // loading rule looked at `industryTrack`. Across 50 games ZERO cars were ever spotted and // freight revenue was structurally zero. Win rate went 0% -> ~35% when these were unified. + // + // TWENTY SEEDS, NOT EIGHT. The claim is "structurally zero", and eight games was far too thin a + // sample to stand for it: across the original eight, exactly ONE car was ever left spotted, so + // the test passed on a single coin-flip. It went red on a change that measurably IMPROVED + // spotting — once an industry track stopped being as short as its box count, the bot began + // leaving whole cuts (2-5 cars) instead of single cars, which lands on fewer seeds but delivers + // more cars in total (22 vs 19 over forty games). Widened so it fails for the reason it names. let spotted = 0; - for (const seed of [1, 2, 3, 4, 5, 6, 7, 8]) { + for (const seed of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) { const s = createGame({ id: 'g', seed, config: { ...config, length: 'standard' }, playerNames: ['bot'] }); playGame(s, developerBot, pump); for (const card of s.officeAreas.get(0)!.grid.values()) { if (card.facility?.kind === 'freight') spotted += card.facility.industryTrack.cars.length; } } - assert.ok(spotted > 0, 'no car ever reached an industry track across eight games'); + assert.ok(spotted > 0, 'no car ever reached an industry track across twenty games'); }); it('instantiates a real Facility when a freight card is placed', () => { @@ -315,11 +322,19 @@ describe('end-of-game statistics', () => { }); it('reports no anomalies — every rule in the engine is reachable', () => { + /** + * 200 GAMES, not 60. This is a reachability canary, and the rarest thing it watches for is Red + * Flags: measured at 200 games it fires in 4 of them, about 2%, so a 60-game sample expects 1.2 + * and reports "unreachable" on the luck of the draw — which is the opposite of what a canary is + * for. Coupling is the next rarest at 39 in 200. Both are genuinely reachable; the sample was + * simply too small to say so, and it got smaller in effect when v0.4.7 took seven cards out of + * the deck and moved which seeds reach which districts. + */ // THE POINT OF THIS MODULE. Every serious bug so far was an unreachable rule: a pipeline with // no entry point, a facility that could not be worked, a car that could not be spotted. If an // expected event stops firing, something has become unreachable again. const report = simulate({ - games: 60, length: 'standard', mode: 'solitaire', players: ['bot'], policy: developerBot, + games: 200, length: 'standard', mode: 'solitaire', players: ['bot'], policy: developerBot, }); /** * ONE NAMED EXEMPTION, not a blanket one. @@ -451,8 +466,22 @@ describe('switching accomplishes something (regression)', () => { * drift, and it is logged in `TODO.md` with the rest of it rather than quietly absorbed here. */ assert.ok(work > 1.2, `only ${work.toFixed(2)} productive acts a game — the crew is doing nothing`); + /** + * RAISED 9 -> 11, and the crew is doing MORE work rather than less. + * + * Measured at 400 games after v0.4.7: 18 Moves and 1.84 productive acts, a ratio of 9.9 — where + * the work figure was 1.48 when the floor above was written. Both halves grew and the numerator + * grew faster, which is traffic rather than aimlessness, and there are two new sources of it: + * Extras now start at the Division Point their NUMBER sends them to, so westbound Extras exist + * at all (measured 32 west / 29 east across 60 deals, against every single one launching + * eastbound from the West Division Point before); and the Grocer's Warehouse ships as well as + * receives, so there is more switching worth doing. + * + * A crew that shuttles for its own sake would show this ratio climbing while `work` stood still. + * Logged in TODO.md with the rest of the bot drift rather than quietly absorbed. + */ assert.ok( - moves / work < 9, + moves / work < 11, `${(moves / work).toFixed(1)} Moves per drop or coupling suggests aimless shuttling ` + `(${moves.toFixed(0)} Moves, ${work.toFixed(2)} productive acts)`, ); @@ -734,7 +763,7 @@ describe('the bot does not throw away its own freight (regression)', () => { outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, // Stranded on WORK with no spotted car to receive it — the definition of a jam. menAtWork: [null, null, { type: 'hopper', dir: 'out' }], - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, } as never); @@ -1040,8 +1069,20 @@ describe('the freight figures count both halves (regression)', () => { // the precondition that makes the comparison mean anything. Three deals is a thin thread to hang // that on: an unload needs an inbound industry built, reachable, and a loaded car spotted at it, // and whether the bot manages all three on a given deal is luck, not the thing under test. - // Measured across these twelve: 34 unloads earned, on 12 of 40 deals sampled more widely. - for (const seed of [1000, 8919, 16838, 24757, 32676, 40595, 48514, 56433, 64352, 72271, 80190, 88109]) { + /** + * FORTY DEALS, up from twelve, and the reason is a rules correction rather than flakiness. + * + * The Grocer's Warehouse is a BOTH-direction facility now — `card-reference.md` always said so — + * where the engine had it inbound-only. So the bot can ship from it as well as receive, and it + * often does: deals producing at least one completed unload went from 12 in 40 to 6 in 40, while + * unloads themselves are unharmed (30 completed across the 40 measured after the change). + * + * The subject here is the INSTRUMENT — does `freightUnload` count Revenue earned rather than + * unloads started — and `unloads > 0` is only the precondition that makes the comparison mean + * anything. Widening the sample restores the precondition without weakening the assertion. + */ + for (let i = 0; i < 40; i++) { + const seed = 1000 + i * 7919; const s = createGame({ id: `fu-${seed}`, seed, config: { ...config, length: 'standard' }, diff --git a/test/track.test.ts b/test/track.test.ts index 2aeaa2d..8972805 100644 --- a/test/track.test.ts +++ b/test/track.test.ts @@ -98,7 +98,7 @@ const lockedFacility = (): TrackCard => ({ inboundBox: [], capacity: { outbound: 3, inbound: 0 }, menAtWork: [{ type: 'hopper', dir: 'out' }, null, null], - industryTrack: { length: 4, cars: [] }, + industryTrack: { cars: [] }, laborers: 3, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, @@ -565,6 +565,63 @@ describe('placement and drop-off', () => { ); assert.ok(!canDropCarsAt(area, at(0, 1))); }); + + it('lets a crew set out as many cars at an industry as anywhere else (regression)', () => { + /** + * REPORTED from a playtest, at undo 188: "we wanted to drop two cars, but were only allowed to + * drop one." + * + * An industry track used to be `length: baseOut + baseIn` cars long, so a Mine Tipple — one + * green box, no red one — had room for exactly ONE car and refused the second. The box count is + * how much WORK an industry can hold; it has nothing to do with how much rail is in front of it. + * No industry card prints a siding of a fixed length, and an industry track is ordinary + * Operating Rail: four cars fit, the same as on the plain straight beside it. + */ + const oneBoxIndustry = (): TrackCard => { + const c = lockedFacility(); + // Unlocked — §9.3's lock is tested above and is not what this is about. + c.facility!.menAtWork = [null, null, null]; + c.facility!.capacity = { outbound: 1, inbound: 0 }; + return c; + }; + + const area = areaFrom( + { + [coordKey(at(0, 0))]: officeCard(), + [coordKey(at(0, 1))]: oneBoxIndustry(), + [coordKey(at(0, 2))]: straight(), + }, + at(0, 0), + ); + + for (const count of [1, 2, 3, 4]) { + assert.ok( + canDropCarsAt(area, at(0, 1), count), + `a one-box industry refused a cut of ${count} — the box count is not the siding length`, + ); + assert.equal( + canDropCarsAt(area, at(0, 1), count), + canDropCarsAt(area, at(0, 2), count), + `an industry and a plain straight disagreed about a cut of ${count}`, + ); + } + + // Four is the ceiling everywhere, because a consist may not exceed four. + assert.ok(!canDropCarsAt(area, at(0, 1), 5), 'a fifth car fitted on an industry track'); + assert.ok(!canDropCarsAt(area, at(0, 2), 5), 'a fifth car fitted on a plain straight'); + }); + + it('counts the cars already spotted at an industry against the room left', () => { + const busy = lockedFacility(); + busy.facility!.menAtWork = [null, null, null]; + busy.facility!.industryTrack.cars = [car(), car(), car()]; + const area = areaFrom( + { [coordKey(at(0, 0))]: officeCard(), [coordKey(at(0, 1))]: busy }, + at(0, 0), + ); + assert.ok(canDropCarsAt(area, at(0, 1), 1), 'the fourth car should still fit'); + assert.ok(!canDropCarsAt(area, at(0, 1), 2), 'a fifth car fitted'); + }); }); describe('curves are arcs, and arcs make sidings possible', () => { @@ -724,7 +781,7 @@ describe('Operational Rail — where a train may stop and leave cars (Appendix A allows: { outbound: true, inbound: false }, outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: menAtWork as never, - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, modifiers: [], @@ -974,7 +1031,7 @@ describe('why a square is NOT offered (the same walk, keeping its rejections)', outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, // A load on MEN locks the track (§9.3). menAtWork: [{ type: 'hopper', dir: 'out' }, null, null] as never, - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, modifiers: [], diff --git a/test/train-rules.test.ts b/test/train-rules.test.ts index 76826d5..e4fff58 100644 --- a/test/train-rules.test.ts +++ b/test/train-rules.test.ts @@ -205,7 +205,7 @@ describe('§7 — trains Porters may not work', () => { outboundBox: [coach(true)], inboundBox: [], capacity: { outbound: 2, inbound: 2 }, menAtWork: null, - industryTrack: { length: 0, cars: [] }, + industryTrack: { cars: [] }, laborers: 0, porters: 2, usedThisStage: { laborers: 0, porters: 0 }, }; s.trays.set('t', { diff --git a/test/web.test.ts b/test/web.test.ts index 032d6c8..4aabe1c 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -30,6 +30,7 @@ import { overHandLimit, actionGroups, actionMenu, + drain, handPlayable, currentActor, fromSave, @@ -340,7 +341,7 @@ describe('the board shows freight work happening', () => { outboundBox: [{ type: 'hopper', loaded: true }], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: [null, null, null], - industryTrack: { length: 2, cars: [{ type: 'hopper', loaded: false }] }, + industryTrack: { cars: [{ type: 'hopper', loaded: false }] }, laborers: 3, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, } as never); @@ -384,7 +385,7 @@ describe('the board shows freight work happening', () => { kind: 'freight', subtype: kind, allows: { outbound: out, inbound: into }, outboundBox: [], inboundBox: [], capacity: { outbound: out ? 1 : 0, inbound: into ? 1 : 0 }, - menAtWork: [null, null, null], industryTrack: { length: 2, cars: [] }, + menAtWork: [null, null, null], industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, }); @@ -510,7 +511,7 @@ describe('the board shows freight work happening', () => { outboundBox: [{ type: 'coach', loaded: true }], inboundBox: [{ type: 'coach', loaded: true }], capacity: { outbound: n, inbound: n }, menAtWork: null, - industryTrack: { length: 0, cars: [] }, + industryTrack: { cars: [] }, laborers: 0, porters: n, usedThisStage: { laborers: 0, porters: 0 }, }; @@ -544,7 +545,7 @@ describe('the board shows freight work happening', () => { allows: { outbound: false, inbound: true }, outboundBox: [], inboundBox: [{ type: 'boxcar', loaded: true }], capacity: { outbound: 0, inbound: 1 }, - menAtWork: [null, null, null], industryTrack: { length: 1, cars: [] }, + menAtWork: [null, null, null], industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, } as never); @@ -566,7 +567,7 @@ describe('the board shows freight work happening', () => { outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: [null, { type: 'hopper', dir: 'out' }, null], - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: 2, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, } as never); @@ -621,7 +622,7 @@ describe('the board shows freight work happening', () => { outboundBox: [], inboundBox: [], capacity: { outbound: out ? 1 : 0, inbound: into ? 1 : 0 }, menAtWork: [null, null, null], - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, } as never); @@ -686,7 +687,7 @@ describe('the board shows freight work happening', () => { outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, menAtWork: [null, null, null], - industryTrack: { length: 2, cars: [] }, + industryTrack: { cars: [] }, laborers: 2, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, } as never); @@ -1065,7 +1066,7 @@ describe('the page explains itself', () => { kind: 'freight' as const, subtype: 'grocersWarehouse', allows: { outbound: false, inbound: true }, outboundBox: [], inboundBox: [], capacity: { outbound: 0, inbound: 1 }, - menAtWork: [null, null, null], industryTrack: { length: 2, cars: [] }, + menAtWork: [null, null, null], industryTrack: { cars: [] }, laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, }, }; @@ -1761,11 +1762,12 @@ describe('the static build', () => { const { options } = actionGroups(game); if (options.length === 0 || !submit(game, options[0]!)) break; } - // Raised 8 -> 10 -> 13. The widest group is Switching every time, which is a crew's reachable - // squares — the list getting longer for a good reason rather than the cross-products this test - // was written to kill. Re-measured over five seeds (430, 99, 1234, 880009, 202) in all three - // opening deals: 13 under `threeRandom` and `sixRandom`, 10 under `threeTrackThreeOther`. - assert.ok(worst <= 13, `the action list still reaches ${worst} buttons`); + // Raised 8 -> 10 -> 13 -> 14. The widest group is Switching every time, which is a crew's + // reachable squares — the list getting longer for a good reason rather than the cross-products + // this test was written to kill. Re-measured over five seeds (430, 99, 1234, 880009, 202) in all + // three opening deals after v0.4.7: 14 under `sixRandom`, 13 under the other two. The deck is + // seven cards smaller and Extras run both ways now, so which seeds reach which districts moved. + assert.ok(worst <= 14, `the action list still reaches ${worst} buttons`); }); it('makes up ONE train at a time, and names that train', () => { @@ -2916,3 +2918,72 @@ describe('two crews switching are told apart', () => { assert.equal(f.moves[0]!.label, 'Train 12'); }); }); + +describe('the Superintendent ruling names the train it is ruling on', () => { + /** + * REPORTED from play: "when the Superintendent has to rule on a train to allow or hold, it should + * indicate what train number he is ruling on in the primary display — you should not have to look + * at the tooltip." + * + * The page splits an action label at the first em-dash: the head goes on the BUTTON and the rest + * becomes the hover. "ALLOW — Train 7 follows Train 4…" therefore put the train number in the one + * place a player is not looking while making the sharpest decision in the game. Asserted on the + * head alone, exactly as `actionButton` cuts it, so a label rewritten back into the tooltip fails. + */ + const facing = (): Game => { + const game = newGame(4242); + const s = game.state; + // A train already occupying the first Mainline card, running east. + s.trays.set('ahead', { + id: 'ahead', trainNumber: 4, trainIsExtra: false, engineAt: 0, consist: [], + direction: 'east', position: { at: 'mainline', index: 1 }, movesUsed: 0, + } as never); + const ml = s.division.nodes[1]; + if (ml?.kind === 'mainline') { + // Pin the terrain: Double Track and Uncontrolled Siding set `trainsMayPass`, which legitimately + // removes the §8.1 bar this fixture exists to trip. + ml.card = 'plains'; + ml.transits.push({ tray: 'ahead', stagesRemaining: 2, stagesTotal: 2, direction: 'east' }); + } + // A second train at the Western Division Point wanting to follow it onto that card. + s.trays.set('behind', { + id: 'behind', trainNumber: 7, trainIsExtra: false, engineAt: 0, consist: [], + direction: 'east', position: { at: 'divisionPoint', side: 'west' }, movesUsed: 0, + } as never); + s.clock.phase = 'mainline'; + s.movedThisPhase = new Set(); + drain(game); + return game; + }; + + /** What the page actually prints on the button — `actionButton` cuts at the first ' — '. */ + const head = (label: string): string => { + const cut = label.indexOf(' — '); + return cut > 0 ? label.slice(0, cut) : label; + }; + + it('puts both train numbers on the ALLOW and HOLD buttons themselves', () => { + const game = facing(); + assert.notEqual(game.state.clock.pendingDecision, null, 'the phase did not stop to ask'); + const group = actionMenu(game).direct.find((g) => /^Superintendent/.test(g.title)); + assert.ok(group, 'no Superintendent group in the action list'); + const heads = group.actions.map((a) => head(a.label)); + assert.equal(heads.length, 2, `expected ALLOW and HOLD, got ${JSON.stringify(heads)}`); + assert.ok( + heads.every((h) => /Train 7/.test(h)), + `a ruling button does not name the train being ruled on: ${JSON.stringify(heads)}`, + ); + assert.ok( + heads.some((h) => /^ALLOW/.test(h) && /Train 4/.test(h)), + `ALLOW does not name the train ahead: ${JSON.stringify(heads)}`, + ); + assert.ok(heads.some((h) => /^HOLD/.test(h)), `no HOLD button: ${JSON.stringify(heads)}`); + }); + + it('asks the question in the heading, naming both trains', () => { + const title = actionMenu(facing()).direct.map((g) => g.title).find((t) => /^Superintendent/.test(t)); + assert.ok(title, 'no Superintendent heading'); + assert.match(title, /Train 7/); + assert.match(title, /Train 4/); + }); +});