diff --git a/.gitignore b/.gitignore index f02f33f..69c1768 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,9 @@ __pycache__/ # Logs *.log -# Generated replays -replay*.html +# Generated replays — ANCHORED to the repo root on purpose. +# +# Unanchored, `replay*.html` also matched `src/web/replays.html`, which is a SOURCE file: the +# website's replay viewer. It was therefore never committed, and a fresh clone was missing a page +# the build copies unconditionally. Only the throwaway files this directory collects are ignored. +/replay*.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 910ba10..c5c9269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,237 @@ The target is 20 Revenue over 5 Days. ## Unreleased +### Q13 — a train that catches the one ahead runs into it + +Answered, and implemented as option B: **collide on catching up**, which is the version that rewards +judging the gap. + +§10 makes a Mainline collision the Superintendent's fault and removes both trains, and ABS Signals +exists to stop trains rear-ending each other — but §8.3's trigger list never named one and nothing +was implemented, so granting clearance was FREE: both trains survived, no penalty, and ABS Signals +protected against nothing. Now a following train that closes on the one ahead runs into it, and one +that never closes is fine, so clearance is a bet on relative speed rather than a formality. §2.1 +divides the card into two regions, and sharing one is what "caught up" means. ABS Signals does what +it prints instead: the follower stops short and holds. + +**Not on a card that prints "trains may pass".** The first version fired 0.41 times a game while the +bot never once granted clearance, which is the tell — those were all Double Track and Uncontrolled +Siding, cards that hold two trains because they HAVE two roads. Catching up there means going past, +which is what the card is for. + +With that corrected the mechanic is invisible to the current bot, because it always denies +clearance. That is the right shape, and the teeth are real: + +| Superintendent | revenue | rear-enders | ABS holds | +| --- | --- | --- | --- | +| always denies (the bot) | 7.34 | 0.00/game | 0 | +| **always allows** | **−5.13** | **2.20/game** | 19 | + +So the bot's always-deny policy — a deliberate choice made when clearance was free and the +arithmetic only guessed at — turns out to be correct, and is now correct for a measured reason. + +Tested deterministically rather than through the bot: two trains built onto one single-track card +with the follower closing, which collides whichever order the phase processes them in, and the same +pair again with ABS Signals to prove it holds instead. + +### The engine has a place in the train, and the yards are visible + +**The engine had a position the game recorded and never used.** `engineFront` was a boolean, written +in three places and read in none — so every consist was drawn as an anonymous row of cars. It is now +`engineAt`, an index into the consist, because a Crew Tray is an engine plus its Rolling Stock and +the engine may be PULLING (ahead of everything), PUSHING (behind everything), or in the middle doing +both at once. A boolean cannot say the third thing. Consists are drawn with `ENG` where it sits. + +The engine is deliberately NOT one of the `consist` entries: §8.2 counts the consist as Rolling +Stock, and the four-car limit (§A.4) is a limit on cars, not on the locomotive hauling them. + +**Both yards are now on the page**, by car type and split loaded / empty, with the Division Yard +outlined the moment it goes bare. This matters more than it did: the Classification Yard returns to +service only when the Division Yard is empty, so the supply genuinely runs down, and a game that +never showed either yard gave no warning at all. + +It shows the pressure immediately. In a finished game the Division Yard held 30 cars — hoppers, +tanks and cabooses — and **no boxcars, coaches or reefers at all**, while 19 of them sat in +Classification unable to come back, because the Division Yard was not bare. + +Crew Tray scarcity was already implemented and is left alone: a train with no free tray is held +(`trainHeld`), which is §7. + +### The Classification Yard rule, from the source — and my guess was worth 2.4 Revenue it should not have been + +Answered: used Rolling Stock is set out in the Classification Yard, used engines and cabooses go +straight back to the Division Yard, and **the Classification Yard empties only when the Division +Yard is bare** — then all of it returns at once. + +That is a much harder rule than the one I invented. Returning cars at every DAY boundary keeps the +yard topped up continuously; this lets it run down to nothing and refill in one go, which is the +whole of the supply pressure the game is meant to have. Measured paired over 400 seeds: + + my Day-boundary guess 9.67 the real rule 7.25 + paired change -2.42 ± 0.49 (t -9.67) 235 of 400 seeds affected + +So the +2.32 celebrated when the Classification Yard was first made readable was very largely an +artefact of getting the trigger wrong. The refill is now checked wherever a car leaves the Division +Yard, so it fires the moment the yard empties rather than at the next convenient tick. + +**Poling is dealt zero copies** rather than deleted. Its effect is "TBD in the source", so there is +nothing to implement and a card that cannot be played is worse in a hand than absent from the deck. +The catalogue entry stays so the gap remains visible. Deck 140 → 139, solitaire 118 → 117. + +**Heavy Grade orientation stays rolled from the seed**, and is now documented as temporary in the +code rather than only in TODO: the card says the player sets it, but it is dealt during setup and +setup has no decision point — `createGame` is a pure function of the seed, which is also what makes +a save portable. + +**A counting question this raised, and could not answer.** A census of every holder of rolling stock +comes to 92 against the 80 dealt. That is not proof of duplication: `outboundBox`, `inboundBox` and +`menAtWork` all hold `RollingStock`, and stocking a green box takes a LOADED CAR out of the Division +Yard — so some of those objects are cargo in transit rather than cars, and nothing distinguishes +them. An accounting test was written and then withdrawn, because it could not tell the two apart. +Logged: until a load is its own type, "is any stock being created or destroyed?" is unanswerable. + +### The freight figures were counting one half of freight + +A measurement fix, not a game fix — but it is the instrument every balance decision is read from. + +`rev.freightUnload` was assigned `eventCounts['unloadBegan']`: unloads STARTED, not Revenue EARNED, +and the two differ by every unload that never finished. `grossFreight` then used `freightLoad` +alone, so `freightShare` omitted the unload half outright. A completed load and a completed unload +each earn a point, on two distinct `revenueChanged` reasons. + +The comment that stood there claimed "an unload scores through the same event as a load completion +in the reducer". It does not — `apply.ts` emits `freightUnload` separately. A comment asserting a +fact about code a few lines away, and wrong. + + freight share of gross: 39% -> 49% + +The harness now prints both halves. `strategyBuckets` counted "a scoring game" the same wrong way, +and so did the test guarding it — so a game that scored only by unloading was bucketed but not +counted, which is how the fix first showed up as a failure. + +This matters backwards as well as forwards: the "freight is only 13-18% of gross" finding was read +from this number, and it is what drove the Gap 12 industry-density change. That decision was taken +against an instrument reading roughly 60% low. + +### Turnouts that go nowhere — reported, confirmed, and mostly not the problem + +Reported from two replays: of 8 turnouts off the Running Track, 2 formed a run-around, 2 served +industries and 4 went nowhere. Measured across 120 games, that holds exactly: + +| what a turnout leads to | | +| --- | --- | +| part of a run-around | 30% | +| a stub, but serves an industry | 12% | +| a stub ending in bare track | 44% | +| **nothing below it at all** | **15%** | + +**4.61 wasted turnouts a game.** Then three attempts to stop it, each measured paired over 400 +seeds, and each WORSE than leaving it alone: + +| attempt | paired change | | +| --- | --- | --- | +| no new way down while one leads nowhere | run-arounds → **0** | deadlocked: a run-around needs TWO ways down, and the second cannot be justified by what hangs off the first | +| first two free, gate the rest | **−0.84 ± 0.53** (t −3.08) | track spend collapsed 15.4 → 4.8 | +| forbid rail that butts an incompatible card | **−0.62 ± 0.57** (t −2.11) | | + +The reason is that a turnout is not only a way DOWN. It is also a way UP, and both the east-west +extension and the closing arc are gated on one existing beyond them — so cutting the turnouts cuts +the places a siding can rejoin, and the sidings stop forming too. The apparent waste is optionality. +This also re-confirms, with proper statistics, a note left in the code by an earlier attempt. + +### Rail that can never go anywhere + +The one that did work, and only as a tie-breaker. + +A port facing an EMPTY square is a promise: something may be built there later. A port butting an +OCCUPIED square whose card has no matching port is not — that square is taken, so the rail stops +dead and always will. Reported from seed 618682, where an arc came off a turnout with its far end +jammed into a curve that could not accept it. Measured: **28% of all pieces laid, 4.26 a game.** + +Forbidding it cost 0.62 revenue a game. Applying it as a **tie-breaker on the distance score** — +never able to veto a piece, only to choose between two the heuristics rate equally — measured +**+0.43 ± 0.49 (t 1.74)**, with 108 seeds better against 67, and cut these from 28% of pieces to 7%. +Not significant on its own, but it is the only one of four attempts pointing the right way, and the +mechanism is sound. + +**And it fixed the reported problem after all — sideways.** Re-running the turnout taxonomy: + +| what a turnout leads to | before | after | +| --- | --- | --- | +| part of a run-around | 30% | **66%** | +| a stub, but serves an industry | 12% | 13% | +| a stub ending in bare track | **44%** | **2%** | +| nothing below it at all | 15% | 18% | +| **wasted per game** | **4.61** | **1.65** | + +Bare stubs all but gone and run-arounds more than doubled, without ever refusing a turnout. Refusing +them directly had destroyed the run-arounds; declining to lay rail INTO a dead end leaves the bot +free to cut every turnout it likes and quietly stops it building the stubs. The remaining waste is +the last turnouts of a game, cut with no turns left to build beneath them. + +Also added, and honest about it: a turnout is not cut when no arc remains to hang beneath it. +Measured at **0%** today — the bot lays the arc immediately after the turnout and never runs the +supply dry — so it is a guard against the supply changing rather than a fix for anything happening +now. Its test constructs the situation by draining the arcs. + +### A source file git was hiding + +Found while checking the above: **`src/web/replays.html` had never been committed.** `.gitignore` +carried `replay*.html` to catch the throwaway files generated at the repo root, and unanchored it +also matched a source page — the website's replay viewer. `git archive HEAD` confirms it: a fresh +clone does not contain that file, and `build-web.ts` copies it unconditionally, so the build would +have failed for anyone but this working copy. + +The pattern is anchored to the root now (`/replay*.html`), which still ignores the generated files +and no longer ignores the source. A test asks GIT — not `.gitignore` — whether each source page +would survive a clone, because that is the actual question. + +### Three places draw a game, and they had drifted + +Reported from playtesting: the replay had lost its "extra slow" speed, and neither the sound nor the +auto-hide could be found. Both true, and the same cause — a game is drawn in THREE places and only +some of them had kept up: + +| | speeds | sound | auto-hide | +| --- | --- | --- | --- | +| the playable page | — | yes | yes | +| the standalone replay file | 5 | yes | yes | +| **the website's replay viewer** | **3** | **no** | **no** | + +The website viewer is its own implementation — it replays a save through the engine in the browser +rather than reading a rendered file — and it never got what the other two grew. It now has all five +speeds (extra slow through very fast), the sound, and the auto-hide, using the same shared `cuesFor` +and `playCue` as everywhere else. + +Three tests hold them together from now on: the two viewers must offer the SAME set of speeds, all +three pages must carry the sound and auto-hide controls, and `replays.ts` may not ask for an element +its page does not have — the same total check the playable page already had, and the one that would +have caught this. Verified by removing a speed and a control and watching them fail. + +### The published replays had stopped replaying + +Both saves in `public/replays/` were dead. A save is a seed plus the intents, replayed through the +real engine — so it cannot describe a position the rules could not produce, and an intent that no +longer applies stops the replay rather than being forced. That is the safe direction, but it is +silent: `seed-202` got 42 intents into 360 before halting, and `seed-430` managed 4 of 338. + +They were recorded before this run's rules work — Modifier hosts, the industry-to-car mapping, the +Classification Yard — so most of what they described is no longer legal. Replaced with three games +generated against the rules as they stand, each verified to replay every intent to the final Day: + +| | | +| --- | --- | +| A winning run | 36 Revenue, seed 1038389 | +| A strong run | 32 Revenue, seed 618682 | +| Collisions | 10 Revenue and 27 smashes, seed 919604 | + +The third is deliberately a bad game: a full Office is a collision, and it costs more than the +freight was worth. + +This is the "save/restore is not version-aware" item in TODO doing exactly what it warns about. The +saves are cheap to regenerate, so the fix is not to freeze them — it is for a stale save to say so +instead of quietly ending early. + ### The replay behaves like the game it is replaying Sound and the district auto-hide were built for the playable page and the replay had neither, which diff --git a/TODO.md b/TODO.md index ee44446..0fe7668 100644 --- a/TODO.md +++ b/TODO.md @@ -9,16 +9,45 @@ Ordered within each section by how much it is currently costing us. ## Next +- [ ] **A "load" is stored as a car, so rolling stock cannot be counted.** `outboundBox`, + `inboundBox` and `menAtWork` all hold `RollingStock`, and `freightAgent.stockOutbound` takes a + LOADED CAR out of the Division Yard to fill a green box. So a census of every holder comes to + 92 against the 80 dealt at setup — not necessarily duplication, because some of those objects + are cargo in transit rather than cars, but there is no way to tell them apart. Until a load is + its own type, "is any stock being created or destroyed?" is an unanswerable question, and the + supply numbers below cannot be tuned with confidence. +- [ ] **Engines are not a SUPPLY yet, only a position.** `engineAt` now records where the engine + sits in the tray and the consist shows it, but an engine is still conjured with the tray + rather than drawn from the Division Yard and returned to it. The rules put engines in the + Division Yard alongside the cars, with a predefined number of them, so running out of engines + should be a second way trains get held — today only the Crew Tray count does that. Needs a + number to start from, then playtesting. +- [ ] **The yards are shown on the play page but not in either replay viewer.** The Frame carries + them, so it is a rendering job, not a modelling one. +- [ ] **The rolling stock supply is a guess.** `ROLLING_STOCK_SUPPLY` (coach 8+8, boxcar 10+10, + hopper 8+8, reefer 5+5, tank 6+6, caboose 6) is marked provisional in `content.ts` and was + scaled alongside the Gap 12 industry increase. Now that the Classification Yard returns stock + only when the Division Yard empties, these numbers set the real supply pressure. Adjust from + playtesting rather than theory, and watch whether industry density feels light or heavy at the + same time. +- [ ] **Heavy Grade orientation is rolled, not chosen.** The card prints "Player sets orientation", + but it is dealt during setup and setup has no decision point at all — `createGame` is a pure + function of the seed, which is also what makes a save portable. Rolled from the seed for now. + Revisit when setup gains an interactive phase; the orientation matters, because it decides + which direction climbs and therefore what Brakeman and Helpers are worth. + - [ ] **Measure with error bars from now on.** Revenue has a standard deviation of ~9, so a 100-game run carries about ±1.0 of noise — every single-change revenue claim in the changelog before the Interlocking work is inside that. Use paired per-seed comparison (the harness deals the same seeds either way) and 400+ games before calling a heuristic change good or bad. The first attempt at the Running Track straight was read as a 0.6 REGRESSION on 100 games and is a 0.7 improvement on 400. -- [ ] **Confirm the Classification Yard rule against the source.** Cars now return to the Division - Yard at the Day boundary — an ASSUMPTION, not a recovered rule. Gap 2c says everything but - cabooses goes to Classification and never says how it empties. It is worth +2.32 revenue a - game, so if the real rule differs the balance numbers move with it. +- [x] ~~**Confirm the Classification Yard rule against the source.**~~ Confirmed, and the guess was + wrong. The rule is: used Rolling Stock to the Classification Yard, used engines and cabooses + straight back to the Division Yard, and the Classification Yard empties ONLY when the Division + Yard is bare — then all at once. The Day-boundary version I had invented was far more generous + and worth **+2.42 revenue a game the game does not actually grant**. Corrected; revenue 9.67 + → 7.25. - [x] ~~**Enhancements are placed but mostly do nothing.**~~ Measured: forbidding every Enhancement except Interlocking is worth **-0.01 ± 0.41 (t = -0.04)** over 400 paired seeds. They neither pay nor cost. Left alone. Unlocking the Running Track straight put @@ -42,11 +71,8 @@ Ordered within each section by how much it is currently costing us. needs a stocked green box AND a spotted car AND a free Laborer to line up in the same Stage. Measure how many Stages have all three before changing any heuristic — the answer may be that the economy, not the bot, is what caps freight. -- [ ] **`stats.ts` undercounts freight.** `rev.freightUnload` is assigned `eventCounts['unloadBegan']` - — unloads *begun*, not revenue *earned* — and `grossFreight` uses `freightLoad` alone, so - `freightShare` omits unload revenue entirely. The comment justifying it ("an unload scores - through the same event as a load completion") is wrong: `apply.ts:947` emits a distinct - `freightUnload` reason. This is why freight was recorded at 13–18% of gross. +- [x] ~~**`stats.ts` undercounts freight.**~~ Fixed: both halves counted, freight share 39% → 49%. + Worth revisiting the **industry density** decision below, which was taken on the old number. --- @@ -54,13 +80,9 @@ Ordered within each section by how much it is currently costing us. Blocked on a decision, not on work. -- [ ] **Q13 — rear-end collisions on a Mainline card.** §10 says a Mainline collision is the - Superintendent's fault and removes both trains, and ABS Signals exists to prevent rear-enders — - but §8.3's trigger list does not include one, and **none is implemented**. Granting clearance is - currently free: verified, both trains survive, no penalty. Three candidates: collide on entry - (clearance becomes a gamble), collide on catching up (rewards judging the gap), or accept that - clearance is safe and ABS Signals is worth less than it reads. The buttons currently describe - only what the engine does, so nothing promises a consequence that cannot happen. +- [x] ~~**Q13 — rear-end collisions on a Mainline card.**~~ Answered: collide on catching up. + Implemented, and not on cards that print "trains may pass". Invisible to a bot that always + denies clearance; a bot that always allows drops from 7.34 revenue to **-5.13**. - [ ] **Poling.** The only card in the deck with no defined behaviour — the sheet records its effect as "TBD in the source". A test asserts it stays TBD so nobody invents one. - [ ] **Heavy Grade orientation at setup.** The card says "Player sets orientation", but `createGame` @@ -165,7 +187,11 @@ target is settled and freight carries its intended share. - [ ] **No way to start a fresh game from inside the page.** `?seed=` gives a reproducible deal and "new game" only appears once a game has ended, so abandoning a bad opening means editing the URL. - [ ] **Save/restore is not version-aware.** A save from an older ruleset stops replaying rather than - failing loudly, which is the safe direction but says little about what changed. + failing loudly, which is the safe direction but says little about what changed. **This has now + bitten once**: both published replays were dead — one got 42 intents into 360, the other 4 of + 338 — and nothing said so; they simply ended early and looked like short games. A save should + carry a ruleset stamp and the page should say "this replay was recorded under an older + ruleset and stops at Stage N" rather than presenting a truncated game as a whole one. --- diff --git a/public/replays/seed-202.json b/public/replays/seed-1038389.json similarity index 68% rename from public/replays/seed-202.json rename to public/replays/seed-1038389.json index 8acb6af..56befe4 100644 --- a/public/replays/seed-202.json +++ b/public/replays/seed-1038389.json @@ -1,5 +1,5 @@ { - "seed": 202, + "seed": 1038389, "history": [ { "type": "localOps.choose", @@ -7,339 +7,111 @@ }, { "type": "draw.fromDepartment", - "slot": 0 + "slot": 2 }, { "type": "card.play", - "cardId": "c8" - }, - { - "type": "mainline.modify", - "cardId": "c106", - "node": 1 + "cardId": "c19" }, { "type": "card.play", - "cardId": "c94", + "cardId": "c6" + }, + { + "type": "track.lay", + "geometry": "straight", + "hand": "none", "placement": { "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c66", + "placement": { + "row": -1, "col": 0 }, "variant": 0 }, + { + "type": "draw.end" + }, + { + "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": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, { "type": "track.lay", "geometry": "turnout", "hand": "left", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c56", - "placement": { - "row": -1, - "col": 0 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "left", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c38", "placement": { "row": 0, "col": -1 }, "variant": 0 }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c57", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 4 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c83", - "placement": { - "row": -1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 5 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c73", - "placement": { - "row": -2, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "mainline.modify", - "cardId": "c107", - "node": 3 - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 6 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 7 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c0" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 8 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 9 - }, - "variant": 0 - }, { "type": "card.play", "cardId": "c40", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c20" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", "placement": { "row": 0, - "col": 2 + "col": -2 }, "variant": 0 }, @@ -348,8 +120,256 @@ }, { "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": -2 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c38", + "placement": { + "row": -2, + "col": -1 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", "trayId": "tray3", - "carType": "coach", + "to": { + "row": -1, + "col": -1 + }, + "reverse": true + }, + { + "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": -1 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c52", + "placement": { + "row": -1, + "col": 2 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 + }, + { + "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.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": -1 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -1 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", "loaded": false }, { @@ -359,10 +379,12 @@ "loaded": false }, { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 }, { "type": "loadUnload.end" @@ -376,48 +398,7 @@ }, { "type": "card.play", - "cardId": "c18" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "left", - "placement": { - "row": -1, - "col": 10 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" + "cardId": "c21" }, { "type": "track.lay", @@ -434,28 +415,67 @@ }, { "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", + "trayId": "tray2", "carType": "caboose", "loaded": true }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c96", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c99", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, { "type": "loadUnload.end" }, @@ -469,258 +489,24 @@ { "type": "track.lay", "geometry": "turnout", - "hand": "left", + "hand": "right", "placement": { "row": 0, "col": -3 }, "variant": 0 }, - { - "type": "card.discard", - "cardId": "c46", - "toSlot": 0 - }, { "type": "draw.end" }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "left", - "placement": { - "row": -1, - "col": -3 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c33", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": -3 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.end" - }, { "type": "laborer.beginUnload", "at": { "row": -1, - "col": 0 + "col": -1 }, "carIndex": 0 }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 2 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 0 - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 0 - }, - "carIndex": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 0 - }, - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 0 - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 0 - }, - "carIndex": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 0 - }, - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 2 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 0 - }, - "index": 0 - }, { "type": "loadUnload.end" }, @@ -733,7 +519,11 @@ }, { "type": "card.play", - "cardId": "c28" + "cardId": "c25" + }, + { + "type": "card.play", + "cardId": "c32" }, { "type": "track.lay", @@ -741,13 +531,21 @@ "hand": "left", "placement": { "row": -1, - "col": -2 + "col": -3 }, - "variant": 1 + "variant": 0 }, { "type": "draw.end" }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, { "type": "loadUnload.end" }, @@ -763,6 +561,82 @@ }, "carType": "coach" }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "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": -1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": -1 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -1 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 + }, { "type": "loadUnload.end" }, @@ -771,81 +645,24 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c33" + "type": "draw.fromHomeOffice" }, { "type": "track.lay", "geometry": "turnout", - "hand": "left", + "hand": "right", "placement": { "row": 0, "col": -4 }, "variant": 0 }, - { - "type": "draw.end" - }, - { - "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": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, { "type": "card.play", - "cardId": "c14" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "right", + "cardId": "c70", "placement": { - "row": -1, - "col": -4 + "row": -2, + "col": 0 }, "variant": 0 }, @@ -853,46 +670,12 @@ "type": "draw.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "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": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 }, { "type": "loadUnload.end" @@ -904,6 +687,46 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": -4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c93", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, { "type": "track.lay", "geometry": "turnout", @@ -915,12 +738,15 @@ "variant": 0 }, { - "type": "card.discard", - "cardId": "c60", - "toSlot": 0 + "type": "draw.end" }, { - "type": "draw.end" + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -1 + }, + "carIndex": 0 }, { "type": "loadUnload.end" @@ -930,8 +756,59 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 0 + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "right", + "placement": { + "row": -1, + "col": 4 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c58", + "placement": { + "row": -2, + "col": 2 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" }, { "type": "track.lay", @@ -944,13 +821,61 @@ "variant": 0 }, { - "type": "card.discard", - "cardId": "c115", - "toSlot": 0 + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "right", + "placement": { + "row": -1, + "col": 5 + }, + "variant": 0 }, { "type": "draw.end" }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 + }, { "type": "loadUnload.end" }, @@ -963,7 +888,7 @@ "trayId": "tray2", "to": { "row": -1, - "col": -4 + "col": -2 }, "reverse": false }, @@ -977,6 +902,15 @@ "trayId": "tray2", "to": { "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -2, "col": -1 }, "reverse": true @@ -991,15 +925,6 @@ "trayId": "tray2", "count": 1 }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 2 - }, - "reverse": false - }, { "type": "switch.dropCars", "trayId": "tray2", @@ -1020,45 +945,19 @@ { "type": "laborer.beginUnload", "at": { - "row": 0, + "row": -2, "col": -1 }, "carIndex": 0 }, - { - "type": "laborer.beginUnload", - "at": { - "row": 0, - "col": 2 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, { "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 2 }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, - "box": 2 - }, { "type": "loadUnload.end" }, @@ -1067,17 +966,17 @@ "option": "freightAgent" }, { - "type": "freightAgent.stockOutbound", + "type": "freightAgent.clearInbound", "at": { "row": 0, - "col": -1 + "col": 0 }, - "carType": "boxcar" + "index": 0 }, { "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 1 @@ -1085,42 +984,11 @@ { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 0 }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, - "box": 0 - }, { "type": "loadUnload.end" }, @@ -1131,25 +999,11 @@ { "type": "freightAgent.clearInbound", "at": { - "row": 0, + "row": -1, "col": -1 }, "index": 0 }, - { - "type": "laborer.startLoad", - "at": { - "row": 0, - "col": -1 - } - }, - { - "type": "laborer.startLoad", - "at": { - "row": 0, - "col": 2 - } - }, { "type": "loadUnload.end" }, @@ -1160,24 +1014,53 @@ { "type": "freightAgent.clearInbound", "at": { - "row": 0, - "col": 2 + "row": -2, + "col": -1 }, "index": 0 }, { - "type": "laborer.advanceLoad", + "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": -2, "col": -1 }, - "box": 0 + "carType": "boxcar" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 2 + "row": -2, + "col": -1 }, "box": 0 }, @@ -1191,7 +1074,7 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, + "row": -2, "col": -1 }, "carType": "boxcar" @@ -1199,7 +1082,7 @@ { "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 1 @@ -1207,57 +1090,69 @@ { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 2 - }, - "carType": "boxcar" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 2 }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -2, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } + }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 2 + "row": -2, + "col": -1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -2, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 }, "box": 2 }, @@ -1272,20 +1167,86 @@ "type": "draw.fromHomeOffice" }, { - "type": "track.lay", - "geometry": "turnout", - "hand": "right", - "placement": { - "row": 0, - "col": -5 + "type": "card.discard", + "cardId": "c85", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 }, - "variant": 0 + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -2, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" }, { "type": "card.play", - "cardId": "c79", + "cardId": "c65", "placement": { - "row": -2, + "row": -1, "col": 1 }, "variant": 0 @@ -1294,178 +1255,42 @@ "type": "draw.end" }, { - "type": "laborer.beginUnload", + "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": "laborer.beginUnload", + "at": { + "row": -2, "col": -1 }, "carIndex": 0 }, - { - "type": "laborer.beginUnload", - "at": { - "row": 0, - "col": 2 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "right", - "placement": { - "row": -1, - "col": -5 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c46", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, { "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 2 }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "track.lay", - "geometry": "turnout", - "hand": "right", - "placement": { - "row": 0, - "col": 6 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c113", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -1 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "right", - "placement": { - "row": 0, - "col": -6 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c60", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -1 - }, - "box": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, - "box": 0 - }, { "type": "loadUnload.end" }, @@ -1473,21 +1298,63 @@ "type": "localOps.choose", "option": "switch" }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "maneuver.flyingSwitch", + "cardId": "c116", + "trayId": "tray2", + "count": 1, + "to": { + "row": -1, + "col": -1 + } + }, { "type": "switch.end" }, { - "type": "laborer.startLoad", + "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -2, "col": -1 - } + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 }, { "type": "laborer.startLoad", "at": { - "row": 0, - "col": 2 + "row": -1, + "col": -1 } }, { @@ -1501,23 +1368,30 @@ "type": "freightAgent.clearInbound", "at": { "row": 0, - "col": -1 + "col": 0 }, "index": 0 }, { "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -1, "col": -1 }, "box": 0 }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } + }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 2 + "row": -2, + "col": -1 }, "box": 0 }, @@ -1532,14 +1406,14 @@ "type": "freightAgent.clearInbound", "at": { "row": 0, - "col": 2 + "col": 0 }, "index": 0 }, { "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -1, "col": -1 }, "box": 1 @@ -1547,11 +1421,58 @@ { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 2 + "row": -2, + "col": -1 }, "box": 1 }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -2, + "col": -1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -2, + "col": -1 + }, + "carIndex": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 + }, { "type": "loadUnload.end" }, @@ -1561,125 +1482,16 @@ }, { "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": -2 - }, - "carType": "reefer" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -1 - }, - "box": 2 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "right", - "placement": { - "row": 0, - "col": 7 - }, - "variant": 1 - }, - { - "type": "card.discard", - "cardId": "c110", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": 0, - "col": -1 - }, - "carIndex": 0 - }, - { - "type": "laborer.beginUnload", - "at": { - "row": 0, - "col": 2 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.unjam", "at": { "row": 0, "col": 0 }, - "from": "outbound", - "index": 0 + "carType": "coach" }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -1 - }, - "box": 2 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.unjam", - "at": { - "row": 0, - "col": 0 - }, - "from": "outbound", - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 1 @@ -1687,80 +1499,220 @@ { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": 2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.unjam", - "at": { - "row": 0, + "row": -2, "col": -1 }, - "from": "menAtWork", - "index": 0 - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": 2 - }, "box": 0 }, { "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": -1 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -2, + "col": -1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 + }, + { + "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": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -2, + "col": -1 + }, + "carIndex": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": -1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -1 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -2, + "col": -1 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -2, "col": -1 }, "carIndex": 1 }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 2 - }, - "index": 0 - }, { "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -2, "col": -1 }, "box": 2 @@ -1773,37 +1725,36 @@ "option": "freightAgent" }, { - "type": "freightAgent.unjam", + "type": "freightAgent.stockOutbound", "at": { - "row": 0, + "row": -1, "col": -1 }, - "from": "menAtWork", - "index": 1 + "carType": "boxcar" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": false + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": false + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 }, { "type": "loadUnload.end" @@ -1813,28 +1764,57 @@ "option": "freightAgent" }, { - "type": "freightAgent.unjam", + "type": "freightAgent.clearInbound", "at": { - "row": 0, - "col": -2 + "row": -2, + "col": -1 }, - "from": "outbound", "index": 0 }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", + "type": "porter.detrain", "at": { "row": 0, - "col": -2 + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 }, - "carType": "reefer" + "box": 2 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -2, + "col": -1 + }, + "carIndex": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 }, { "type": "loadUnload.end" @@ -1843,9 +1823,83 @@ "type": "localOps.choose", "option": "switch" }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -1 + }, + "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": true + }, { "type": "switch.end" }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -1 + } + }, { "type": "loadUnload.end" }, @@ -1854,14 +1908,67 @@ "option": "freightAgent" }, { - "type": "freightAgent.unjam", + "type": "freightAgent.clearInbound", "at": { "row": 0, - "col": -2 + "col": 0 }, - "from": "outbound", "index": 0 }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "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": -1 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -2, + "col": -1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, { "type": "loadUnload.end" }, @@ -1873,25 +1980,17 @@ "type": "freightAgent.stockOutbound", "at": { "row": 0, - "col": -2 + "col": 0 }, - "carType": "reefer" + "carType": "coach" }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.unjam", + "type": "laborer.beginUnload", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": -1 }, - "from": "outbound", - "index": 0 + "carIndex": 0 }, { "type": "loadUnload.end" @@ -1903,26 +2002,18 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": -1 }, - "carType": "reefer" + "carType": "boxcar" }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.unjam", + "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": -1 }, - "from": "outbound", - "index": 0 + "box": 2 }, { "type": "loadUnload.end" @@ -1934,10 +2025,33 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": -2 + "row": -2, + "col": -1 }, - "carType": "reefer" + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 }, { "type": "loadUnload.end" @@ -1947,14 +2061,74 @@ "option": "freightAgent" }, { - "type": "freightAgent.unjam", + "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": -2 + "row": -2, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": -1 }, - "from": "outbound", "index": 0 }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": -1 + } + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 + }, { "type": "loadUnload.end" }, @@ -1963,24 +2137,151 @@ "option": "freightAgent" }, { - "type": "card.discard", - "cardId": "c46", - "toSlot": 0 + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": -1 + }, + "carType": "boxcar" }, { - "type": "card.discard", - "cardId": "c105", - "toSlot": 0 + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": true }, { - "type": "card.discard", - "cardId": "c60", - "toSlot": 0 + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -2, + "col": -1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": -1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -2, + "col": -1 + }, + "carType": "boxcar" + }, + { + "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": "laborer.advanceLoad", + "at": { + "row": -1, + "col": -1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": -1 + }, + "box": 2 }, { "type": "loadUnload.end" } ], - "title": "A busy district", - "note": "freight worked through a siding, and a collision to inspect" + "title": "A winning run — 36 Revenue", + "note": "The bot reaches the 20-Revenue target over 5 Days. Seed 1038389." } \ No newline at end of file diff --git a/public/replays/seed-618682.json b/public/replays/seed-618682.json new file mode 100644 index 0000000..0dd289d --- /dev/null +++ b/public/replays/seed-618682.json @@ -0,0 +1,2407 @@ +{ + "seed": 618682, + "history": [ + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.play", + "cardId": "c25" + }, + { + "type": "card.play", + "cardId": "c10" + }, + { + "type": "track.lay", + "geometry": "straight", + "hand": "none", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c36", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.play", + "cardId": "c3" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c12" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c96", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", + "placement": { + "row": 0, + "col": -2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": -2 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c38", + "placement": { + "row": -1, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": -1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 2 + }, + "carType": "boxcar" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "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": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c19" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "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": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "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.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 2 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "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": 1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "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.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c30" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": 3 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 2 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "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": 1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 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": 1 + }, + "box": 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": 1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "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": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "right", + "placement": { + "row": 0, + "col": -3 + }, + "variant": 0 + }, + { + "type": "card.discard", + "cardId": "c78", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c34" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "right", + "placement": { + "row": -1, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "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": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 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": 1 + }, + "box": 0 + }, + { + "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": 1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -1 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "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": 1 + }, + "box": 2 + }, + { + "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": "c7" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "right", + "placement": { + "row": 0, + "col": -4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "right", + "placement": { + "row": -1, + "col": -4 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c71", + "placement": { + "row": -2, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "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.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 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 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray2", + "to": { + "row": -1, + "col": -1 + }, + "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": true + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "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": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "maneuver.redFlags", + "cardId": "c115", + "trayId": "tray2" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 1 + }, + "carIndex": 1 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 2 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 1 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": -2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": 1 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 2 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 1 + }, + "carType": "boxcar" + }, + { + "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": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 1 + } + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 1 + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 1 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + } + ], + "title": "A strong run — 32 Revenue", + "note": "Freight and passengers both working. Seed 618682." +} \ No newline at end of file diff --git a/public/replays/seed-430.json b/public/replays/seed-919604.json similarity index 59% rename from public/replays/seed-430.json rename to public/replays/seed-919604.json index fc90272..1d231a8 100644 --- a/public/replays/seed-430.json +++ b/public/replays/seed-919604.json @@ -1,21 +1,30 @@ { - "seed": 430, + "seed": 919604, "history": [ { "type": "localOps.choose", "option": "draw" }, { - "type": "draw.fromHomeOffice" + "type": "draw.fromDepartment", + "slot": 0 }, { "type": "card.play", - "cardId": "c25" + "cardId": "c17" + }, + { + "type": "card.play", + "cardId": "c18" + }, + { + "type": "card.play", + "cardId": "c9" }, { "type": "track.lay", - "geometry": "turnout", - "hand": "left", + "geometry": "straight", + "hand": "none", "placement": { "row": 0, "col": 1 @@ -24,214 +33,12 @@ }, { "type": "card.play", - "cardId": "c81", + "cardId": "c55", "placement": { "row": -1, "col": 0 }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c38", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c39", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "left", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c37", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c59", - "placement": { - "row": 0, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 4 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c48", - "placement": { - "row": 0, - "col": -3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 5 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c14" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 6 - }, - "variant": 0 + "variant": 1 }, { "type": "draw.end" @@ -239,15 +46,39 @@ { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", + "carType": "hopper", "loaded": true }, { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "boxcar", + "carType": "hopper", "loaded": true }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, { "type": "newTrain.placeCar", "trayId": "tray3", @@ -255,37 +86,8 @@ "loaded": true }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c16" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 7 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true + "type": "mainline.clearance", + "allow": false }, { "type": "loadUnload.end" @@ -295,23 +97,19 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 8 - }, - "variant": 0 + "type": "draw.fromDepartment", + "slot": 0 }, { "type": "card.play", - "cardId": "c85", + "cardId": "c1" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", "placement": { - "row": -1, + "row": 0, "col": -1 }, "variant": 0 @@ -320,11 +118,8 @@ "type": "draw.end" }, { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } + "type": "mainline.clearance", + "allow": false }, { "type": "loadUnload.end" @@ -334,30 +129,8 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "straight", - "hand": "none", - "placement": { - "row": -1, - "col": 9 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" + "type": "draw.fromDepartment", + "slot": 1 }, { "type": "track.lay", @@ -365,32 +138,25 @@ "hand": "left", "placement": { "row": -1, - "col": 10 + "col": -1 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c37", + "placement": { + "row": -2, + "col": 0 }, "variant": 1 }, - { - "type": "card.discard", - "cardId": "c22", - "toSlot": 0 - }, { "type": "draw.end" }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 + "type": "mainline.clearance", + "allow": false }, { "type": "loadUnload.end" @@ -400,16 +166,8 @@ "option": "draw" }, { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c103", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 + "type": "draw.fromDepartment", + "slot": 2 }, { "type": "track.lay", @@ -417,65 +175,6 @@ "hand": "left", "placement": { "row": 0, - "col": -4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c6" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "left", - "placement": { - "row": -1, - "col": -4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "turnout", - "hand": "left", - "placement": { - "row": 0, - "col": 4 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c70", - "placement": { - "row": -1, "col": -2 }, "variant": 0 @@ -486,291 +185,6 @@ { "type": "loadUnload.end" }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c31" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "left", - "placement": { - "row": -1, - "col": -3 - }, - "variant": 1 - }, - { - "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": "track.lay", - "geometry": "turnout", - "hand": "left", - "placement": { - "row": 0, - "col": 5 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c109", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "turnout", - "hand": "right", - "placement": { - "row": 0, - "col": -5 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c97", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "right", - "placement": { - "row": -1, - "col": -5 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c101", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "mainline.modify", - "cardId": "c108", - "node": 1 - }, - { - "type": "track.lay", - "geometry": "turnout", - "hand": "right", - "placement": { - "row": 0, - "col": 6 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "turnout", - "hand": "right", - "placement": { - "row": 0, - "col": -6 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c99", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c34" - }, - { - "type": "track.lay", - "geometry": "curved", - "hand": "right", - "placement": { - "row": -1, - "col": -6 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "track.lay", - "geometry": "turnout", - "hand": "right", - "placement": { - "row": 0, - "col": 7 - }, - "variant": 0 - }, - { - "type": "card.discard", - "cardId": "c79", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "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": "loadUnload.end" - }, { "type": "localOps.choose", "option": "switch" @@ -779,10 +193,29 @@ "type": "switch.move", "trayId": "tray3", "to": { - "row": 0, + "row": -1, "col": -1 }, - "reverse": false + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": true }, { "type": "switch.dropCars", @@ -804,8 +237,8 @@ { "type": "laborer.beginUnload", "at": { - "row": 0, - "col": -1 + "row": -1, + "col": 0 }, "carIndex": 0 }, @@ -814,27 +247,140 @@ }, { "type": "localOps.choose", - "option": "freightAgent" + "option": "draw" }, { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c10" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": -2 }, - "index": 0 + "variant": 0 + }, + { + "type": "draw.end" }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -1 + "row": -1, + "col": 0 }, "box": 2 }, { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c29" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "left", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "freightAgent" @@ -842,18 +388,28 @@ { "type": "freightAgent.clearInbound", "at": { - "row": 0, + "row": -1, "col": 0 }, "index": 0 }, { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -1 - }, - "box": 1 + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true }, { "type": "loadUnload.end" @@ -871,12 +427,539 @@ "carType": "coach" }, { - "type": "laborer.advanceLoad", + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c4" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -1, + "col": -2 + }, + "reverse": true + }, + { + "type": "switch.dropCars", + "trayId": "tray1", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "porter.detrain", "at": { "row": 0, - "col": -1 + "col": 0 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": -2 }, - "box": 0 + "reverse": false + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray1", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -2, + "col": 0 + }, + "reverse": true + }, + { + "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": "loadUnload.end" @@ -889,7 +972,7 @@ "type": "freightAgent.clearInbound", "at": { "row": 0, - "col": -1 + "col": 0 }, "index": 0 }, @@ -903,16 +986,16 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 }, "carType": "boxcar" }, { "type": "laborer.startLoad", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 } }, { @@ -925,16 +1008,28 @@ { "type": "freightAgent.stockOutbound", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 }, "carType": "boxcar" }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "coach", + "loaded": true + }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 }, "box": 0 }, @@ -950,20 +1045,11 @@ }, { "type": "track.lay", - "geometry": "curved", - "hand": "right", + "geometry": "turnout", + "hand": "left", "placement": { "row": 0, - "col": -7 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c40", - "placement": { - "row": 0, - "col": 8 + "col": 3 }, "variant": 0 }, @@ -973,8 +1059,167 @@ { "type": "laborer.advanceLoad", "at": { + "row": -2, + "col": 0 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "left", + "placement": { + "row": -1, + "col": 3 + }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c63", + "placement": { + "row": -1, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "right", + "placement": { "row": 0, - "col": -1 + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": 0 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c2" + }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "right", + "placement": { + "row": -1, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "mainline.modify", + "cardId": "c106", + "node": 3 + }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "right", + "placement": { + "row": 0, + "col": -4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": 0 }, "box": 1 }, @@ -993,14 +1238,14 @@ "geometry": "curved", "hand": "right", "placement": { - "row": 0, - "col": 9 + "row": -1, + "col": -4 }, - "variant": 1 + "variant": 0 }, { "type": "card.discard", - "cardId": "c56", + "cardId": "c43", "toSlot": 0 }, { @@ -1009,8 +1254,8 @@ { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 }, "box": 2 }, @@ -1025,31 +1270,29 @@ "type": "draw.fromDepartment", "slot": 0 }, + { + "type": "track.lay", + "geometry": "turnout", + "hand": "right", + "placement": { + "row": 0, + "col": 4 + }, + "variant": 0 + }, { "type": "card.discard", - "cardId": "c112", + "cardId": "c22", "toSlot": 0 }, { "type": "draw.end" }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, { "type": "laborer.beginUnload", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 }, "carIndex": 0 }, @@ -1063,9 +1306,19 @@ { "type": "draw.fromHomeOffice" }, + { + "type": "track.lay", + "geometry": "curved", + "hand": "right", + "placement": { + "row": -1, + "col": 4 + }, + "variant": 0 + }, { "type": "card.discard", - "cardId": "c117", + "cardId": "c26", "toSlot": 0 }, { @@ -1074,8 +1327,8 @@ { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 }, "box": 2 }, @@ -1090,11 +1343,21 @@ "type": "draw.fromHomeOffice" }, { - "type": "card.play", - "cardId": "c102", + "type": "track.lay", + "geometry": "turnout", + "hand": "right", "placement": { "row": 0, - "col": 1 + "col": 5 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c54", + "placement": { + "row": -1, + "col": 5 }, "variant": 0 }, @@ -1102,37 +1365,225 @@ "type": "draw.end" }, { - "type": "porter.detrain", + "type": "laborer.advanceLoad", "at": { - "row": 0, + "row": -2, "col": 0 - } + }, + "box": 1 }, { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } + "type": "loadUnload.end" }, { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c90", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -1 + "row": -2, + "col": 0 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c43", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -2, + "col": 0 + }, + "carIndex": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c23", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": 0 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c91", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "hopper", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": 0 }, "box": 1 }, { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c24", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "switch" @@ -1141,16 +1592,25 @@ "type": "switch.move", "trayId": "tray3", "to": { - "row": 0, - "col": -2 + "row": -1, + "col": -3 }, - "reverse": false + "reverse": true }, { "type": "switch.dropCars", "trayId": "tray3", "count": 1 }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 0 + }, + "reverse": true + }, { "type": "switch.dropCars", "trayId": "tray3", @@ -1169,26 +1629,36 @@ "type": "switch.end" }, { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -1 - }, - "box": 0 + "type": "mainline.clearance", + "allow": false }, { "type": "laborer.beginUnload", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": 0 }, - "carIndex": 1 + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": 0 }, "box": 2 }, @@ -1197,35 +1667,140 @@ }, { "type": "localOps.choose", - "option": "freightAgent" + "option": "switch" }, { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": 0 }, "box": 1 }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, { "type": "laborer.advanceLoad", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": 0 }, "box": 0 }, { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "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": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.unjam", + "at": { + "row": -2, + "col": 0 + }, + "from": "menAtWork", + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "tank", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "tank", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "freightAgent" @@ -1239,19 +1814,22 @@ "index": 0 }, { - "type": "loadUnload.end" + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "tank", + "loaded": true }, { - "type": "localOps.choose", - "option": "freightAgent" + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "tank", + "loaded": true }, { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": -1 - }, - "index": 0 + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "caboose", + "loaded": true }, { "type": "loadUnload.end" @@ -1263,11 +1841,62 @@ { "type": "freightAgent.clearInbound", "at": { - "row": 0, - "col": -2 + "row": -1, + "col": 0 }, "index": 0 }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -2, + "col": 0 + }, + "index": 0 + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "tank", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "tank", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "mainline.clearance", + "allow": false + }, { "type": "loadUnload.end" }, @@ -1284,208 +1913,105 @@ "carType": "coach" }, { - "type": "loadUnload.end" + "type": "mainline.clearance", + "allow": false }, { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 2 - }, - "carType": "boxcar" + "type": "mainline.clearance", + "allow": false }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "freightAgent" + "option": "switch" }, { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": -2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.startLoad", - "at": { - "row": 0, - "col": -2 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": -2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c44", - "placement": { - "row": 0, - "col": 10 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", + "type": "switch.move", "trayId": "tray3", - "carType": "hopper", - "loaded": false + "to": { + "row": -1, + "col": -4 + }, + "reverse": true }, { - "type": "newTrain.placeCar", + "type": "switch.dropCars", "trayId": "tray3", - "carType": "coach", - "loaded": false + "count": 1 }, { - "type": "laborer.startLoad", - "at": { - "row": 0, - "col": -2 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": -2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c17" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "hopper", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "hopper", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "porter.board", - "at": { + "type": "switch.move", + "trayId": "tray3", + "to": { "row": 0, "col": 0 - } + }, + "reverse": true + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "mainline.clearance", + "allow": false }, { "type": "porter.detrain", @@ -1501,22 +2027,6 @@ "col": 0 } }, - { - "type": "laborer.beginUnload", - "at": { - "row": 0, - "col": -2 - }, - "carIndex": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 2 - }, { "type": "loadUnload.end" }, @@ -1525,123 +2035,11 @@ "option": "switch" }, { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -4 - }, - "reverse": false + "type": "switch.end" }, { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -4 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -4 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 0, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } + "type": "mainline.clearance", + "allow": false }, { "type": "loadUnload.end" @@ -1650,34 +2048,6 @@ "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.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, { "type": "switch.end" }, @@ -1697,112 +2067,170 @@ "index": 0 }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": -2 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": -3 - }, - "carType": "tank" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": -3 - }, - "carType": "tank" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 3 - }, - "carType": "reefer" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 10 - }, - "carType": "hopper" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.unjam", - "at": { - "row": 0, - "col": -1 - }, - "from": "outbound", - "index": 0 + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": true }, { "type": "newTrain.placeCar", - "trayId": "tray2", + "trayId": "tray3", "carType": "hopper", - "loaded": false + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray1", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" }, { "type": "newTrain.placeCar", "trayId": "tray2", "carType": "coach", - "loaded": false + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -2, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "hopper", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray0", + "carType": "hopper", + "loaded": true + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.startLoad", + "at": { + "row": -2, + "col": 0 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -2, + "col": 0 + }, + "carType": "boxcar" + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "mainline.clearance", + "allow": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -2, + "col": 0 + }, + "box": 0 }, { "type": "loadUnload.end" } ], - "title": "A near miss", - "note": "revenue 19 against a target of 20 \u2014 one point short" + "title": "Collisions — 10 Revenue, 27 smashes", + "note": "A full Office is a collision, and it costs more than the freight was worth. Seed 919604." } \ No newline at end of file diff --git a/src/engine/advance.ts b/src/engine/advance.ts index 24162d1..6436839 100644 --- a/src/engine/advance.ts +++ b/src/engine/advance.ts @@ -30,6 +30,8 @@ import { collectiveRevenueFloor, lengthProfile, officeProfile, + REGIONS_PER_MAINLINE_CARD, + mainlineProfile, } from './content.ts'; import type { Direction } from './content.ts'; import type { GameEvent } from './events.ts'; @@ -186,7 +188,7 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult { id: trayId, trainNumber: due, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [], direction, position: { at: 'divisionPoint', side }, @@ -219,7 +221,7 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult { id: trayId, trainNumber: number, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [], direction, position: { at: 'divisionPoint', side }, @@ -250,7 +252,7 @@ function newTrainPhase(s: GameState, events: GameEvent[]): AdvanceResult { id: trayId, trainNumber: number, trainIsExtra: true, - engineFront: true, + engineAt: 0, consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, @@ -488,6 +490,53 @@ function moveTrain( // Q1/Q2 — crossing takes a whole number of Stages set by the card's speed and the train's // Fast/Slow class. Count it down rather than stepping through printed cells. if (transit.stagesRemaining > 1) { + /** + * Q13 — REAR-END COLLISIONS, answered: a train collides when it CATCHES UP. + * + * §10 makes a Mainline collision the Superintendent's fault and removes both trains, and ABS + * Signals exists to stop trains rear-ending each other — but §8.3's trigger list never named + * one and none was implemented, so granting clearance was free: both trains survived, no + * penalty, and ABS Signals protected against nothing. + * + * Colliding on CATCHING UP is the version that rewards judging the gap. A following train + * that closes on the one ahead runs into it; a following train that never closes is fine, so + * clearance becomes a bet on relative speed rather than a formality. §2.1 divides the card + * into two regions, and sharing one is what "caught up" means. + * + * ABS Signals does what it says instead: the follower stops SHORT of the collision and holds. + */ + const regionOf = (t: { stagesTotal: number; stagesRemaining: number }): number => { + const entry = REGIONS_PER_MAINLINE_CARD - t.stagesTotal; + const elapsed = t.stagesTotal - t.stagesRemaining; + return Math.min(REGIONS_PER_MAINLINE_CARD - 1, Math.max(0, entry + elapsed)); + }; + // NOT on a card that prints "trains may pass". Double Track and Uncontrolled Siding hold two + // trains because they HAVE two roads, so a train catching another there goes past it — that + // is what the card is for. Without this the mechanic fired 0.41 times a game while the bot + // never once granted clearance, which is the tell: those were all passing cards. + const mayPass = mainlineProfile(node.card).trainsMayPass; + const next = regionOf({ stagesTotal: transit.stagesTotal, stagesRemaining: transit.stagesRemaining - 1 }); + const ahead = mayPass + ? undefined + : node.transits.find( + (t) => t.tray !== id && t.direction === transit.direction && regionOf(t) === next, + ); + + if (ahead) { + if (node.absSignals) { + // "Trains on this card will not rear-end each other; they stop short of a collision." + events.push({ + type: 'trainHeld', + trainNumber: tray.trainNumber ?? 0, + reason: 'ABS Signals — held short of the train ahead', + }); + return 'held'; + } + // §10 — the Superintendent let it in behind the other, so it is the Superintendent's fault. + collide(s, s.clock.superintendent, [id, ahead.tray], events, 'ran into the train ahead', 'the Mainline'); + return 'moved'; + } + transit.stagesRemaining -= 1; return 'moved'; } @@ -756,25 +805,6 @@ function shiftChange(s: GameState, events: GameEvent[]): AdvanceResult { // Telegraph/Telephone/Radio are each usable once a Day. for (const area of s.officeAreas.values()) area.dispatchUsedToday = []; - /** - * THE CLASSIFICATION YARD GOES BACK INTO SERVICE OVERNIGHT. - * - * ASSUMPTION, flagged rather than derived: Gap 2c says engines and cabooses return to the - * Division Yard and everything else to Classification, and the recovered rules never say how - * Classification empties. It was write-only — seven places pushed into it and nothing ever read - * it — so rolling stock drained one way and left the game. Measured at 30 cars a game dead by - * the end, 37% of the 80 dealt at setup. - * - * Sorting cars for redistribution is what a classification yard is FOR, and a Day is its - * natural cycle, so they return at the Day boundary. This does not bind a 5-Day game — the - * Division Yard still held 16.6 loaded freight cars at the end and ran dry in 2 games of 100 — - * but a campaign-length game would have starved. - */ - if (s.yards.classificationYard.length > 0) { - s.yards.divisionYard.push(...s.yards.classificationYard); - s.yards.classificationYard = []; - } - s.clock.day += 1; s.clock.stage = 1; s.collisionsToday = 0; diff --git a/src/engine/apply.ts b/src/engine/apply.ts index 7a2f8ef..cc4da36 100644 --- a/src/engine/apply.ts +++ b/src/engine/apply.ts @@ -1174,6 +1174,7 @@ export function reduce(s: GameState, e: GameEvent): void { (c) => c.type === e.stock.type && c.loaded === e.stock.loaded, ); if (idx >= 0) s.yards.divisionYard.splice(idx, 1); + refillDivisionYardIfEmpty(s); f.outboundBox.push(e.stock); s.turn.freightAgentUsed = true; break; @@ -1236,6 +1237,7 @@ export function reduce(s: GameState, e: GameEvent): void { (c) => c.type === e.stock.type && c.loaded === e.stock.loaded, ); if (idx >= 0) s.yards.divisionYard.splice(idx, 1); + refillDivisionYardIfEmpty(s); tray.consist.push(e.stock); break; } @@ -1674,6 +1676,30 @@ export function acceptsCar(tray: CrewTray, carType: CarType): boolean { return true; } +/** + * §2 — WHEN THE DIVISION YARD RUNS OUT, THE CLASSIFICATION YARD GOES BACK INTO SERVICE. + * + * Used Rolling Stock is set out in the Classification Yard; used engines and cabooses go straight + * back to the Division Yard. The Classification Yard empties only when the Division Yard is bare — + * every car of every kind gone — and then all of it returns at once. + * + * Confirmed from the source after an earlier guess. The first implementation returned cars at the + * DAY boundary, which is a different rule and a much more generous one: it kept the yard topped up + * continuously, where this lets it run down to nothing and refill in one go. That difference is the + * whole of the supply pressure the game is meant to have. + * + * Called wherever a car leaves the Division Yard, so the refill happens the moment it empties + * rather than at the next convenient tick. + */ +export function refillDivisionYardIfEmpty(s: GameState): { type: 'yardRefilled'; count: number } | null { + if (s.yards.divisionYard.length > 0) return null; + if (s.yards.classificationYard.length === 0) return null; + const count = s.yards.classificationYard.length; + s.yards.divisionYard.push(...s.yards.classificationYard); + s.yards.classificationYard = []; + return { type: 'yardRefilled', count }; +} + /** A fresh Limits sign. The set is "2N + spares" (§12), so relocating one is not a supply question. */ function limitsCard(): TrackCard { return { diff --git a/src/engine/content.ts b/src/engine/content.ts index 15d464d..3324448 100644 --- a/src/engine/content.ts +++ b/src/engine/content.ts @@ -584,7 +584,12 @@ export const SECOND_SECTION = { key: 'secondSection', name: 'Second Section', co export const MANEUVER_CARDS: readonly SimpleCard[] = [ { key: 'redFlags', name: 'Red Flags', copies: 5, placement: 'any time', effect: 'A stopped train is prevented from being hit; the approaching train is prevented from moving.' }, { key: 'flyingSwitch', name: 'Flying Switch', copies: 1, placement: 'any time', effect: 'Break a cut of cars away from behind the engine and roll them into an industry.' }, - { key: 'poling', name: 'Poling', copies: 1, placement: 'any time', effect: 'TBD in the source.' }, + // POLING IS OUT OF THE DECK, at 0 copies rather than deleted. + // + // It is the one card whose effect the source records as "TBD", so there is nothing to implement + // and inventing something would be worse than leaving it out. Kept in the table with its text so + // the gap stays visible and the card can be dealt again the moment its rule is known. + { key: 'poling', name: 'Poling', copies: 0, placement: 'any time', effect: 'TBD in the source.' }, ]; /** Played AT other players. We have no player-interaction mechanic yet. */ diff --git a/src/engine/setup.ts b/src/engine/setup.ts index c95d214..755f24b 100644 --- a/src/engine/setup.ts +++ b/src/engine/setup.ts @@ -196,6 +196,13 @@ function buildDivision(players: number, rng: Rng): DivisionNode[] { // least deterministic and varies between games; it should become a real player choice when // setup gains an interactive phase. See implications.md §10 Q11. if (mainlineProfile(card).speed.kind === 'grade') { + /** + * TEMPORARY, and flagged in TODO. The card prints "(Up)" and "Player sets orientation", so + * which way a Heavy Grade climbs is the player's decision — but it is dealt during setup, and + * setup has no decision point at all. Rolled from the seed until it gains one, because the + * orientation MATTERS: it decides which direction climbs, and therefore what a Brakeman or + * Helpers card is worth. + */ node.gradeUp = rng.nextInt(2) === 0 ? 'east' : 'west'; } return node; diff --git a/src/engine/state.ts b/src/engine/state.ts index 7cb32a6..12c1c7a 100644 --- a/src/engine/state.ts +++ b/src/engine/state.ts @@ -202,8 +202,19 @@ export type CrewTray = { /** null while a local crew is switching without a train card. */ trainNumber: number | null; trainIsExtra: boolean; - /** Which end the engine occupies (§A.3). */ - engineFront: boolean; + /** + * WHERE THE ENGINE SITS IN THE TRAY, as an index into `consist`. + * + * A Crew Tray is an engine plus its Rolling Stock, and the engine may be PULLING (index 0, ahead + * of everything), PUSHING (index `consist.length`, behind everything) or somewhere in the middle + * doing both at once. That last case is why this is an index and not the boolean it replaced — + * `engineFront` was written in three places and read in none, so the engine had a position the + * game recorded and never used. + * + * The engine is NOT one of the `consist` entries: §8.2 counts the consist as Rolling Stock, and + * the four-car limit (§A.4) is a limit on cars, not on the locomotive hauling them. + */ + engineAt: number; /** ORDERED, left-to-right. Max 4 including any caboose (§A.4). */ consist: RollingStock[]; direction: Direction; diff --git a/src/sim/bot.ts b/src/sim/bot.ts index 91fed0a..c9c39fe 100644 --- a/src/sim/bot.ts +++ b/src/sim/bot.ts @@ -24,7 +24,7 @@ import { MAX_CONSIST, nextOfficeTier } from '../engine/content.ts'; import type { GameEvent } from '../engine/events.ts'; import type { Intent } from '../engine/intents.ts'; import { legalActions } from '../engine/legal.ts'; -import { exitsFrom, facilityVariants, hasPort, neighbour, opposite, variantsFor } from '../engine/track.ts'; +import { connectionsFor, exitsFrom, facilityVariants, hasPort, neighbour, opposite, variantsFor } from '../engine/track.ts'; import type { Port } from '../engine/track.ts'; import { coordKey } from '../engine/state.ts'; import type { Facility, GameState, GridCoord, OfficeArea, PlayerIndex, RollingStock, TrackCard } from '../engine/state.ts'; @@ -524,6 +524,60 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int const arcOf = (i: Extract): string | undefined => variantsFor(i.geometry)[i.variant ?? 0]?.arc; + /** + * RAIL THAT CAN NEVER GO ANYWHERE. + * + * A port facing an EMPTY square is a promise: something may be built there later. A port butting + * an OCCUPIED square whose card has no matching port is nothing of the kind — that square is + * taken, so the rail stops dead and always will. + * + * Measured, 28% of the pieces the bot laid had one, at 4.26 a game: an arc dropped off a turnout + * with its far end jammed into a curve that could not accept it, which is exactly the placement + * that prompted this. Penalised rather than forbidden — a piece can still be worth laying for the + * end that DOES connect, and the last two attempts to forbid something outright starved the + * district instead of tidying it. + */ + const deadEnds = (i: Extract): number => { + const v = variantsFor(i.geometry)[i.variant ?? 0]; + const probe = { + geometry: { + kind: 'track', + geometry: i.geometry, + ...(v?.axis ? { axis: v.axis } : {}), + ...(v?.arc ? { arc: v.arc } : {}), + ...(v?.turnout ? { turnout: v.turnout } : {}), + }, + baseOperationalRail: true, + standing: [], + facility: null, + modifiers: [], + enhancements: [], + } as unknown as TrackCard; + let dead = 0; + for (const p of new Set(connectionsFor(probe).flat())) { + const n = neighbour(i.placement, p); + const nb = at(n.row, n.col); + if (!nb) continue; // empty — may still be built on + if (hasPort(nb, opposite(p))) continue; // joins + dead++; + } + return dead; + }; + + /** + * Arcs left to build BENEATH a turnout. + * + * A turnout is a hole in the Running Track and is worth cutting only for what hangs under it, so + * cutting one with nothing left to hang there is pure loss. Measured at 0% today — the bot builds + * the arc immediately after the turnout and never runs the supply dry first — so this is a guard + * against the supply changing rather than a fix for something happening now. + */ + const arcsLeft = + (area.trackSupply.get('curved:left') ?? 0) + + (area.trackSupply.get('curved:right') ?? 0) + + (area.trackSupply.get('sharpCurved:left') ?? 0) + + (area.trackSupply.get('sharpCurved:right') ?? 0); + // Where the district already turns down off the main. const turnouts = [...area.grid.entries()] .filter(([k, c]) => Number(k.split(',')[0]) === area.runningRow && divergesSouth(c)) @@ -562,7 +616,7 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int // without competing further with the ways down. Scored below a first turnout, which is still // the most valuable single piece on the board, and below closing a run-around. bonus += 13; - } else if (row === area.runningRow && i.geometry === 'turnout') { + } else if (row === area.runningRow && i.geometry === 'turnout' && arcsLeft > 0) { // A first way down is the most valuable single piece on the board; a second closes the // run-around. Beyond that they are just holes in the Running Track — measured at 6.4 per game // when unrestrained, which consumed the whole 26-piece supply on ways down and none on the @@ -618,6 +672,11 @@ function bestTrackLay(s: GameState, player: PlayerIndex, options: Intent[]): Int if (i.geometry === 'straight') score += 2; if (dRow > 0) score += 3; + // A TIE-BREAKER, not a veto. Applied to the distance score rather than the bonus, so it can + // never push a piece below the "builds nothing" floor and refuse it outright: given two + // placements the heuristics rate the same, take the one whose rail can still grow. + score -= deadEnds(i) * 0.5; + /** * DECLINE A PIECE THAT BUILDS NOTHING. * diff --git a/src/sim/stats.ts b/src/sim/stats.ts index a561e38..f4309b9 100644 --- a/src/sim/stats.ts +++ b/src/sim/stats.ts @@ -113,6 +113,10 @@ export function summarize( const reason = e.reason; if (reason.startsWith('collision')) rev.collisionLoss += -e.delta; else if (reason === 'freightLoad') rev.freightLoad += e.delta; + // `freightUnload` is its own reason on its own event (apply.ts). It used to be counted from + // `unloadBegan` instead — unloads STARTED, not Revenue EARNED — and the two differ by every + // unload that never finished. + else if (reason === 'freightUnload') rev.freightUnload += e.delta; else if (reason === 'boarding') rev.passengerBoard += e.delta; else if (reason === 'detraining') rev.passengerDetrain += e.delta; } @@ -129,12 +133,21 @@ export function summarize( for (const i of intents) inc(intentCounts, i); - // An unload scores through the same event as a load completion in the reducer, so count the - // distinct operations directly. - rev.freightUnload = eventCounts['unloadBegan'] ?? 0; rev.net = final.players.reduce((n, p) => n + p.revenue, 0); - const grossFreight = rev.freightLoad; + /** + * FREIGHT IS BOTH HALVES. + * + * A completed load and a completed unload each earn a point, on two distinct `revenueChanged` + * reasons — and this counted only the first, so `freightShare` reported roughly 60% of the + * freight actually earned. That is the number the "freight is only 13-18% of gross" finding was + * read from, and it drove the Gap 12 industry-density change. + * + * The comment that stood here claimed "an unload scores through the same event as a load + * completion in the reducer", which is not true: `apply.ts` emits `freightUnload` separately. It + * was a comment asserting a fact about code a few lines away, and it was wrong. + */ + const grossFreight = rev.freightLoad + rev.freightUnload; const grossPassenger = rev.passengerBoard + rev.passengerDetrain; const gross = grossFreight + grossPassenger; @@ -347,7 +360,7 @@ export type StrategyBucket = { * consistently outscore passenger specialists (or vice versa), it shows up here. */ export function strategyBuckets(all: GameStats[]): StrategyBucket[] { - const scored = all.filter((g) => g.revenue.freightLoad + g.revenue.passengerBoard + g.revenue.passengerDetrain > 0); + const scored = all.filter((g) => g.revenue.freightLoad + g.revenue.freightUnload + g.revenue.passengerBoard + g.revenue.passengerDetrain > 0); const buckets: { label: string; test: (g: GameStats) => boolean }[] = [ { label: 'passenger-only (freight 0%)', test: (g) => g.freightShare === 0 }, @@ -408,6 +421,7 @@ export function formatAggregate(all: GameStats[]): string { out.push(' REVENUE (mean per game)'); out.push(` net ${num(meanOf(all, (g) => g.revenue.net))}`); out.push(` from freight loads ${num(meanOf(all, (g) => g.revenue.freightLoad))}`); + out.push(` from freight unloads ${num(meanOf(all, (g) => g.revenue.freightUnload))}`); out.push(` from passengers on ${num(meanOf(all, (g) => g.revenue.passengerBoard))}`); out.push(` from passengers off ${num(meanOf(all, (g) => g.revenue.passengerDetrain))}`); out.push(` lost to collisions ${num(-meanOf(all, (g) => g.revenue.collisionLoss))}`); diff --git a/src/sim/view.ts b/src/sim/view.ts index 17e1d89..e2ad847 100644 --- a/src/sim/view.ts +++ b/src/sim/view.ts @@ -179,6 +179,19 @@ export type Frame = { departments: string[]; /** What each face-up Department card does. */ departmentsWhat: string[]; + /** + * The two yards, by car type. + * + * Rolling stock is finite and the Classification Yard only returns to service when the Division + * Yard is BARE, so watching the Division Yard run down is now real information — and the game + * showed neither yard at all. + */ + yards: { + division: { type: string; loaded: number; empty: number }[]; + classification: { type: string; loaded: number; empty: number }[]; + divisionTotal: number; + classificationTotal: number; + }; /** 12 slots; the train number due out at each Stage, or null. */ timetable: (number | null)[]; blocked: Impediment[]; @@ -598,6 +611,12 @@ export function snapshot( deck: s.decks.homeOffice.length, departments: s.decks.departments.map((id) => (id ? cardName(s, id) : '—')), departmentsWhat: s.decks.departments.map((id) => (id ? cardDescription(s, id) : '')), + yards: { + division: countStock(s.yards.divisionYard), + classification: countStock(s.yards.classificationYard), + divisionTotal: s.yards.divisionYard.length, + classificationTotal: s.yards.classificationYard.length, + }, timetable: [...s.timetable], decision, wasted, @@ -889,12 +908,38 @@ const MODIFIER_NAMES: Record = { }; /** A train on the board, with what it is carrying — otherwise a run looks identical empty or full. */ +/** Cars in a yard, grouped by type and split loaded / empty, in a stable order. */ +function countStock( + stock: readonly { type: string; loaded: boolean }[], +): { type: string; loaded: number; empty: number }[] { + const order = ['coach', 'boxcar', 'reefer', 'hopper', 'tank', 'caboose']; + const by = new Map(); + for (const c of stock) { + const row = by.get(c.type) ?? { type: c.type, loaded: 0, empty: 0 }; + if (c.loaded) row.loaded += 1; + else row.empty += 1; + by.set(c.type, row); + } + return [...by.values()].sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)); +} + function trainChip(s: GameState, id: string): TrainChip { const t = s.trays.get(id); if (!t) return { label: id, consist: [] }; + /** + * The engine is drawn IN the consist, at the position it occupies. + * + * A Crew Tray is an engine plus its Rolling Stock, and the engine may be pulling, pushing, or in + * the middle doing both — which is a thing a player has to be able to see, since it decides which + * end cars couple onto (§A.3) and which way the train can shove. It was recorded as a boolean + * that nothing read, so every consist was drawn as an anonymous row of cars. + */ + const cars = t.consist.map(carLabel); + const at = Math.max(0, Math.min(cars.length, t.engineAt)); + cars.splice(at, 0, 'ENG'); return { label: t.trainNumber === null ? 'crew' : `T${t.trainIsExtra ? 'X' : ''}${t.trainNumber}`, - consist: t.consist.map(carLabel), + consist: cars, }; } diff --git a/src/web/main.ts b/src/web/main.ts index 87b9955..af62df8 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -261,6 +261,8 @@ function render(): void { ? f.departments.map((d, i) => cardRow(d, f.departmentsWhat[i] ?? '', null)).join('') : 'none'; + renderYards(f); + const total = f.trackSupply.reduce((n, t) => n + t.left, 0); $('supply').innerHTML = `
${total} pieces left · one may be laid per turn, during the DRAW option
` + @@ -297,6 +299,38 @@ function render(): void { save(); } +/** + * The two yards, and how close the Division Yard is to running out. + * + * §2 — used Rolling Stock is set out in the Classification Yard, and it only returns when the + * Division Yard is BARE. So the interesting number is not how much has been used but how little is + * left, and the moment the Division Yard empties a whole pile comes back at once. + */ +function renderYards(f: ReturnType): void { + const row = (rows: { type: string; loaded: number; empty: number }[]): string => + rows.length === 0 + ? 'empty' + : rows + .map( + (r) => + `` + + `${esc(r.type)} ${r.loaded}/${r.empty}`, + ) + .join(''); + + $('divyard').innerHTML = row(f.yards.division); + $('clsyard').innerHTML = row(f.yards.classification); + $('divtot').textContent = `${f.yards.divisionTotal} cars`; + $('clstot').textContent = `${f.yards.classificationTotal} cars`; + + // The one thing worth calling out: the yard about to turn over. + const bare = f.yards.divisionTotal === 0; + $('divyard').classList.toggle('bare', bare); + $('yardnote').textContent = bare + ? `The Division Yard is bare — the ${f.yards.classificationTotal} cars in Classification return to it now.` + : 'loaded / empty. Classification returns to the Division Yard only when the Division Yard is bare.'; +} + function renderDistrict(f: ReturnType): void { const open = districtMode === 'auto' ? FOCUS_PHASES.has(f.phaseKey) : districtMode === 'open'; const sec = $('district'); diff --git a/src/web/play.html b/src/web/play.html index 7a55d1b..1f51243 100644 --- a/src/web/play.html +++ b/src/web/play.html @@ -33,6 +33,15 @@ header button:hover{border-color:#4d6fa8} explicitly not a target. Previously the hand was a list of bare names and the Department slots a second list, so nothing looked like a card at all. */ .cardrow{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 2px} +/* THE YARDS. Rolling stock is finite and the Classification Yard only comes back when the Division + Yard is bare, so a player has to be able to watch the supply run down. */ +.yard{display:flex;flex-wrap:wrap;gap:5px;margin:0 0 6px} +.stock{background:#1c2129;border:1px solid #39424e;border-radius:4px;padding:2px 7px;font-size:11px} +.stock b{color:#e6e9ee} +.stock .ld{color:#8fd6a0} +.stock .mt{color:#9fb6d8} +.stock.none{opacity:.4} +.yard.bare{outline:1px dashed #e0a060;outline-offset:3px;border-radius:4px;padding:3px} .handcard{background:#242c36;border:1px solid var(--line);border-radius:5px; padding:5px 8px;font-size:11px;min-width:96px;position:relative} .handcard:focus{outline:2px solid #4d6fa8;outline-offset:1px} @@ -213,6 +222,13 @@ ol.tc-phases{display:flex;gap:6px;list-style:none;margin:0;padding:0;flex-wrap:w

Department Cards (face up)

—

Your Track Supply

+

The Yards

+

Division Yard

+
+

Classification Yard

+
+
+

Blocked — why nothing is moving

    Facilities

    diff --git a/src/web/replays.html b/src/web/replays.html new file mode 100644 index 0000000..ccc19a1 --- /dev/null +++ b/src/web/replays.html @@ -0,0 +1,124 @@ + + + + + +Station Master — replays + + + + +
    + Station Master + replays + build __BUILD__ +
    + +
    +
    +
    +

    Saved on this site

    +

    loading…

    +
    + +
    +

    Open a save file

    +

    A Station Master save is a small .json file — a seed and the list of + moves made. That is enough to rebuild the whole game, so a replay can be emailed like a text file. + Save one from inside a game with Save replay.

    + +
    +
    +
    + +
    +
    +
    + + — + +
    +
    + + + + + + + + + + 0 / 0 +
    +
    + — + phase: — + Revenue 0 + ← → to step, space to play +
    +
    + +
    +
    +

    The Division

    +

    Office Area

    +
    +
    +
    +

    What happened

    +
    +
    +
    + + + + diff --git a/src/web/replays.ts b/src/web/replays.ts index 4af4e61..62ab5e0 100644 --- a/src/web/replays.ts +++ b/src/web/replays.ts @@ -12,6 +12,8 @@ */ import { BOARD_CSS, divisionSvg, officeSvg } from '../sim/board-svg.ts'; +import { cuesFor } from '../sim/narrate.ts'; +import { playCue } from './sound.ts'; import { TOOLTIP_CSS, installTooltips } from './tooltip.ts'; import { narrate } from '../sim/narrate.ts'; import { cardName, trainName } from '../sim/view.ts'; @@ -35,7 +37,12 @@ const esc = (s: string): string => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c); /** Every position the save passes through, with the lines narrated along the way. */ -type Step = { frame: Frame; lines: { text: string; tone: string }[] }; +type Step = { + frame: Frame; + lines: { text: string; tone: string }[]; + /** What this step should sound like — from `cuesFor`, the same function the live game uses. */ + cues: string[]; +}; /** * Rebuild a game from a save, keeping a snapshot at each step. @@ -54,7 +61,7 @@ function rebuild(save: Save): { steps: Step[]; stoppedEarly: boolean } { const n = narrate(e, ctx); return { text: n.text, tone: n.tone }; }); - steps.push({ frame: snapshot(s, [], null), lines }); + steps.push({ frame: snapshot(s, [], null), lines, cues: cuesFor(events) }); }; push(pump(s)); @@ -80,13 +87,28 @@ function rebuild(save: Save): { steps: Step[]; stoppedEarly: boolean } { let steps: Step[] = []; let at = 0; let timer: ReturnType | null = null; +/** + * Sound and auto-hide, matching the playable page and the standalone replay. + * + * This viewer is a third place a game gets drawn, and it had drifted: three speeds where the + * standalone had five, and neither of these. Anything a player learns on one screen should hold on + * the others. + */ +let soundOn = false; +let districtMode: 'auto' | 'open' | 'closed' = 'auto'; +const FOCUS_PHASES = new Set(['localOps', 'loadUnload']); function show(i: number): void { + const from = at; at = Math.max(0, Math.min(steps.length - 1, i)); const step = steps[at]; if (!step) return; const f = step.frame; + // Only a single step FORWARD sounds. Scrubbing across a hundred steps would fire a hundred + // whistles, and stepping back would sound a Stage ending that is being un-done. + if (soundOn && at === from + 1) for (const c of step.cues) playCue(c); + $('vclock').textContent = `Day ${f.day} · Stage ${f.stage} — ${f.clock}`; $('vphase').textContent = f.phase; $('vrev').textContent = String(f.revenue); @@ -95,6 +117,21 @@ function show(i: number): void { $('vdivision').innerHTML = divisionSvg(f.division); $('vgrid').innerHTML = officeSvg(f.cells, f.runningRow); + // Auto-hide: the district is worth its space during the phases that change it. + const open = districtMode === 'auto' ? FOCUS_PHASES.has(f.phaseKey) : districtMode === 'open'; + const sec = $('vdistrict'); + if (open) sec.classList.remove('folded'); + else sec.classList.add('folded'); + const cars = f.cells.reduce((n, c) => n + c.cars.length, 0); + $('vdistrictsummary').textContent = + `${f.cells.length} cards · ${f.facilities.length} facilities · ${cars} cars standing`; + $('vdistricttoggle').textContent = + districtMode === 'auto' + ? (open ? 'auto-hide: on — click to keep open' : 'auto-hide: on — click to show') + : districtMode === 'open' + ? 'always showing — click for auto-hide' + : 'always hidden — click for auto-hide'; + // A window of history rather than only this step, so a jump lands in context. let log = ''; for (let k = Math.max(0, at - 18); k <= at; k++) { @@ -215,6 +252,18 @@ function wire(): void { show(at + 1); }, Number(($('vspeed') as HTMLSelectElement).value)); }; + $('vdistricttoggle').onclick = () => { + const f = steps[at]?.frame; + const open = districtMode === 'auto' ? (f ? FOCUS_PHASES.has(f.phaseKey) : true) : districtMode === 'open'; + districtMode = districtMode === 'auto' ? (open ? 'closed' : 'open') : 'auto'; + show(at); + }; + $('vsound').onclick = () => { + soundOn = !soundOn; + $('vsound').textContent = soundOn ? '\u{1F50A} sound' : '\u{1F507} muted'; + // Confirms the change, and gives the browser the gesture it needs before audio may start. + if (soundOn) playCue('stage'); + }; $('vclose').onclick = () => { if (timer) clearInterval(timer); timer = null; diff --git a/test/advance.test.ts b/test/advance.test.ts index 5ec8b33..7eb1416 100644 --- a/test/advance.test.ts +++ b/test/advance.test.ts @@ -257,7 +257,7 @@ describe('Mainline Phase (§8)', () => { id: `t${i}`, trainNumber: spec.n, trainIsExtra: spec.extra, - engineFront: true, + engineAt: 0, consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, @@ -287,7 +287,7 @@ describe('collisions are automatic (Gap 2)', () => { id, trainNumber: 2, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [{ type: 'coach', loaded: true }], direction: 'east', position: { at: 'mainline', index: 1 }, @@ -328,7 +328,7 @@ describe('collisions are automatic (Gap 2)', () => { id: 'x', trainNumber: 8, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [ { type: 'hopper', loaded: true }, { type: 'caboose', loaded: true }, @@ -359,7 +359,7 @@ describe('the Superintendent clearance interrupt (§8.1)', () => { id: 'ahead', trainNumber: 4, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [], direction: 'east', position: { at: 'mainline', index: 1 }, @@ -379,7 +379,7 @@ describe('the Superintendent clearance interrupt (§8.1)', () => { id: 'behind', trainNumber: 2, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, @@ -402,7 +402,7 @@ describe('the Superintendent clearance interrupt (§8.1)', () => { id: 'oncoming', trainNumber: 3, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [], direction: 'west', position: { at: 'mainline', index: 1 }, @@ -421,7 +421,7 @@ describe('the Superintendent clearance interrupt (§8.1)', () => { id: 'waiting', trainNumber: 2, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, diff --git a/test/apply.test.ts b/test/apply.test.ts index c688962..e6f9832 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -38,7 +38,7 @@ function placeTray(s: GameState, coord: GridCoord, consist: CrewTray['consist'] id, trainNumber: null, trainIsExtra: false, - engineFront: true, + engineAt: 0, consist, direction: 'east', position: { at: 'grid', owner: 0, coord }, diff --git a/test/enhancements.test.ts b/test/enhancements.test.ts index 2f64569..81d65f4 100644 --- a/test/enhancements.test.ts +++ b/test/enhancements.test.ts @@ -52,7 +52,7 @@ function handEnhancement(s: GameState, key: string): string { function placeTray(s: GameState, coord: GridCoord, consist: TrackCard['standing'] = []): string { const id = s.freeTrays.pop()!; s.trays.set(id, { - id, trainNumber: 9, trainIsExtra: false, engineFront: true, + id, trainNumber: 9, trainIsExtra: false, engineAt: 0, consist, direction: 'east', position: { at: 'grid', owner: 0, coord }, movesUsed: 0, }); return id; @@ -185,7 +185,7 @@ describe('Interlocking and Yard Office relieve the Office', () => { function inbound(s: GameState, consist: TrackCard['standing']) { const id = 'inbound'; s.trays.set(id, { - id, trainNumber: 9, trainIsExtra: false, engineFront: true, + id, trainNumber: 9, trainIsExtra: false, engineAt: 0, consist, direction: 'east', position: { at: 'mainline', index: 1 }, movesUsed: 0, }); const ml = s.division.nodes[1]; @@ -264,11 +264,11 @@ describe('ABS Signals amend the collision rule', () => { ml.transits.push({ tray: 'ahead', stagesRemaining: 2, stagesTotal: 2, direction: 'east' }); } s.trays.set('ahead', { - id: 'ahead', trainNumber: 4, trainIsExtra: false, engineFront: true, + id: 'ahead', trainNumber: 4, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', position: { at: 'mainline', index: 1 }, movesUsed: 0, }); s.trays.set('behind', { - id: 'behind', trainNumber: 2, trainIsExtra: false, engineFront: true, + id: 'behind', trainNumber: 2, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, movesUsed: 0, }); s.clock.phase = 'mainline'; @@ -292,7 +292,7 @@ describe('Telegraph, Telephone and Radio dispatch meets', () => { } // An oncoming senior train — §8.1 makes this an absolute bar without a device. s.trays.set('oncoming', { - id: 'oncoming', trainNumber: 3, trainIsExtra: false, engineFront: true, + id: 'oncoming', trainNumber: 3, trainIsExtra: false, engineAt: 0, consist: [], direction: 'west', position: { at: 'mainline', index: 1 }, movesUsed: 0, }); const ml = s.division.nodes[1]; @@ -304,7 +304,7 @@ describe('Telegraph, Telephone and Radio dispatch meets', () => { ml.transits.push({ tray: 'oncoming', stagesRemaining: 2, stagesTotal: 2, direction: 'west' }); } s.trays.set('mine', { - id: 'mine', trainNumber: 9, trainIsExtra: false, engineFront: true, + id: 'mine', trainNumber: 9, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, movesUsed: 0, }); s.clock.phase = 'mainline'; diff --git a/test/mainline-cards.test.ts b/test/mainline-cards.test.ts index 5455fb2..78cdc99 100644 --- a/test/mainline-cards.test.ts +++ b/test/mainline-cards.test.ts @@ -16,12 +16,15 @@ import { advance } from '../src/engine/advance.ts'; import { applyIntent, areaOf, check } from '../src/engine/apply.ts'; import { MAINLINE_MODIFIER_CARDS, + MAINLINE_PROFILES, MANEUVER_CARDS, REALIGNMENTS, crossingStages, mainlineModifierRule, + mainlineProfile, } from '../src/engine/content.ts'; import { createGame } from '../src/engine/setup.ts'; +import type { GameEvent } from '../src/engine/events.ts'; import type { GameConfig, GameState, GridCoord, TrackCard } from '../src/engine/state.ts'; import { coordKey } from '../src/engine/state.ts'; import { snapshot } from '../src/sim/view.ts'; @@ -210,11 +213,11 @@ describe('Red Flags protect a stopped train', () => { const node = pinned(s, 1, 'plains'); node.transits.push({ tray: 'ahead', stagesRemaining: 2, stagesTotal: 2, direction: 'east' }); s.trays.set('ahead', { - id: 'ahead', trainNumber: 4, trainIsExtra: false, engineFront: true, + id: 'ahead', trainNumber: 4, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', position: { at: 'mainline', index: 1 }, movesUsed: 0, }); s.trays.set('behind', { - id: 'behind', trainNumber: 2, trainIsExtra: false, engineFront: true, + id: 'behind', trainNumber: 2, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', position: { at: 'divisionPoint', side: 'west' }, movesUsed: 0, }); const dp = s.division.nodes[0]; @@ -313,7 +316,7 @@ describe('Flying Switch rolls a cut into an industry', () => { area.grid.set(coordKey(at(-2, 0)), industry); s.trays.set('crew', { - id: 'crew', trainNumber: 1, trainIsExtra: false, engineFront: true, + id: 'crew', trainNumber: 1, trainIsExtra: false, engineAt: 0, consist: [{ type: 'hopper', loaded: false }, { type: 'boxcar', loaded: false }], // Facing SOUTH, down the spur. `direction` only carries east/west, so a crew on north-south // track needs its actual port — without it the engine looks for an east exit the card has not @@ -405,12 +408,17 @@ describe('card coverage', () => { } }); - it('Poling remains unimplemented, deliberately', () => { + it('Poling stays unimplemented AND out of the deck, deliberately', () => { // Guard against someone "fixing" this by inventing an effect. The sheet says TBD; until the // source says otherwise, a silent no-op would be worse than a rejection. + // + // It is dealt zero copies rather than deleted: a card that cannot be played is worse in a + // player's hand than absent from the deck, but the entry stays so the gap remains visible and + // the card returns the moment its rule is known. const poling = MANEUVER_CARDS.find((c) => c.key === 'poling'); - assert.ok(poling, 'Poling should still be in the deck'); + assert.ok(poling, 'the Poling entry should stay, so the gap does not vanish'); assert.match(poling!.effect, /TBD/i); + assert.equal(poling!.copies, 0, 'Poling is dealt into the deck but cannot be played'); }); }); @@ -495,7 +503,7 @@ describe("a train is made up to its card's consist (§8.2)", () => { const s = game(); s.clock.phase = 'newTrain'; s.trays.set('t', { - id: 't', trainNumber: 9, trainIsExtra: false, engineFront: true, + id: 't', trainNumber: 9, trainIsExtra: false, engineAt: 0, consist: [], direction: 'west', position: { at: 'divisionPoint', side: 'east' }, movesUsed: 0, }); @@ -525,7 +533,7 @@ describe("a train is made up to its card's consist (§8.2)", () => { const s = game(); s.clock.phase = 'newTrain'; s.trays.set('t', { - id: 't', trainNumber: 9, trainIsExtra: false, engineFront: true, + id: 't', trainNumber: 9, trainIsExtra: false, engineAt: 0, consist: [{ type: 'caboose', loaded: true }], direction: 'west', position: { at: 'divisionPoint', side: 'east' }, movesUsed: 0, }); @@ -595,3 +603,87 @@ describe('regions on a Mainline card (§2.1, §8.2)', () => { } }); }); + +describe('Q13 — a train that catches the one ahead runs into it', () => { + /** + * Two trains on one card, same direction, the follower closing. `total 3 / remaining 2` puts the + * leader in region 1 after it moves; `total 2 / remaining 2` puts the follower in region 0 and + * then region 1 — so it catches up whichever order the phase happens to process them in. + */ + const twoTrains = (opts: { absSignals?: boolean } = {}): { s: GameState; events: GameEvent[] } => { + const s = createGame({ + id: 'rear', seed: 3, + config: { + mode: 'solitaire', victory: 'highestAfterDays', length: 'standard', + optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false }, + }, + playerNames: ['bot'], + }); + const index = s.division.nodes.findIndex( + (n) => n.kind === 'mainline' && !mainlineProfile(n.card).trainsMayPass, + ); + assert.ok(index >= 0, 'no single-track Mainline card in this Division'); + const node = s.division.nodes[index]!; + assert.ok(node.kind === 'mainline'); + if (node.kind !== 'mainline') throw new Error('unreachable'); + if (opts.absSignals) node.absSignals = true; + + const make = (n: number, total: number, left: number): string => { + const id = s.freeTrays.pop()!; + s.trays.set(id, { + id, trainNumber: n, trainIsExtra: false, engineAt: 0, + consist: [{ type: 'boxcar', loaded: false }], + direction: 'east', + position: { at: 'mainline', index }, + } as never); + node.transits.push({ tray: id, stagesRemaining: left, stagesTotal: total, direction: 'east' }); + return id; + }; + make(9, 3, 2); // the leader + make(11, 2, 2); // the follower, closing + + s.clock.phase = 'mainline'; + const events: GameEvent[] = []; + for (let i = 0; i < 4; i++) events.push(...advance(s).events); + return { s, events }; + }; + + it('destroys both trains, and it is the Superintendent\'s fault (§10)', () => { + // REGRESSION against a rule that was never implemented. §10 makes a Mainline collision the + // Superintendent's fault and removes both trains, and ABS Signals exists to prevent rear-enders + // — but §8.3's trigger list never named one, so granting clearance was FREE: both trains + // survived, no penalty, and ABS Signals protected against nothing. + const { events } = twoTrains(); + const smash = events.find((e) => e.type === 'trainsDestroyed'); + assert.ok(smash, 'the follower ran into the leader and nothing happened'); + if (smash.type !== 'trainsDestroyed') throw new Error('unreachable'); + assert.match(smash.reason, /ran into the train ahead/); + assert.equal(smash.trains.length, 2, 'a rear-ender removes BOTH trains'); + assert.ok( + events.some((e) => e.type === 'revenueChanged' && e.delta < 0), + 'the collision cost nothing', + ); + }); + + it('ABS Signals holds the follower short instead', () => { + // "Trains on this card will not rear-end each other; they stop short of a collision." + const { events } = twoTrains({ absSignals: true }); + assert.ok( + !events.some((e) => e.type === 'trainsDestroyed'), + 'ABS Signals did not prevent the collision', + ); + assert.ok( + events.some((e) => e.type === 'trainHeld' && /ABS Signals/.test(e.reason)), + 'nothing was held short of the train ahead', + ); + }); + + it('leaves trains alone on a card that prints "trains may pass"', () => { + // Double Track and Uncontrolled Siding hold two trains because they HAVE two roads. Catching up + // there means going past, which is what the card is for. Without this the mechanic fired 0.41 + // times a game while the bot never once granted clearance — the tell that they were all + // passing cards. + const passing = MAINLINE_PROFILES.filter((m) => m.trainsMayPass).map((m) => m.kind); + assert.deepEqual(passing, ['doubleTrack', 'uncontrolledSiding']); + }); +}); diff --git a/test/setup.test.ts b/test/setup.test.ts index 6e68049..6609bd9 100644 --- a/test/setup.test.ts +++ b/test/setup.test.ts @@ -53,7 +53,11 @@ describe('card catalogue (component 1)', () => { // Transcribed from docs/Deck cards2.xlsx, whose own total is 115 — plus 18 extra industry cards // (Gap 12, industries 9 → 27) and 7 extra office cards (Q12, offices 7 → 14). Both are // deliberate departures from the sheet and both are flagged provisional in content.ts. - assert.equal(DECK_SIZE, 140); + // + // 139, not 140: Poling is dealt ZERO copies. Its effect is "TBD in the source", so a card that + // cannot be played is worse in a hand than absent from the deck. The entry stays in the + // catalogue so the gap remains visible. + assert.equal(DECK_SIZE, 139); assert.equal(buildDeck().length, DECK_SIZE); }); @@ -69,20 +73,21 @@ describe('card catalogue (component 1)', () => { spaceUse: 12, enhancement: 18, mainlineModifier: 7, - maneuver: 7, + // 6, not 7 — Poling is dealt no copies until its rule is known. + maneuver: 6, action: 10, }); }); it('removes opponent-directed cards from a solitaire deck', () => { // Q6 — Space-use and Action cards can only be played AT another player, so in a one-player - // game they would be 22 of 140 draws (16%) that do nothing. - assert.equal(SOLITAIRE_DECK_SIZE, 118); + // game they would be 22 of 139 draws (16%) that do nothing. + assert.equal(SOLITAIRE_DECK_SIZE, 117); const solo = buildDeck('solitaire'); - assert.equal(solo.length, 118); + assert.equal(solo.length, SOLITAIRE_DECK_SIZE); assert.ok(!solo.some((c) => c.kind.kind === 'spaceUse' || c.kind.kind === 'action')); - // A competitive deck keeps them. - assert.equal(buildDeck('competitive').length, 140); + // A competitive deck keeps them. 139, not 140 — Poling is dealt no copies. + assert.equal(buildDeck('competitive').length, 139); }); it('keeps track OUT of the deck, as a per-player supply', () => { diff --git a/test/sim.test.ts b/test/sim.test.ts index d86b247..13ffd11 100644 --- a/test/sim.test.ts +++ b/test/sim.test.ts @@ -8,8 +8,11 @@ import assert from 'node:assert/strict'; import { pump } from '../src/engine/advance.ts'; import { createGame } from '../src/engine/setup.ts'; +import { refillDivisionYardIfEmpty } from '../src/engine/apply.ts'; +import { TOTAL_ROLLING_STOCK } from '../src/engine/content.ts'; import type { GameConfig, GameState, OfficeArea } from '../src/engine/state.ts'; -import { exitsFrom, hasPort, neighbour, opposite } from '../src/engine/track.ts'; +import type { Intent } from '../src/engine/intents.ts'; +import { connectionsFor, exitsFrom, hasPort, neighbour, opposite, variantsFor } from '../src/engine/track.ts'; import type { Port } from '../src/engine/track.ts'; import { developerBot, playGame, randomBot } from '../src/sim/bot.ts'; import { simulate } from '../src/sim/harness.ts'; @@ -236,8 +239,13 @@ describe('end-of-game statistics', () => { const report = simulate({ games: 30, length: 'standard', mode: 'solitaire', players: ['bot'], policy: developerBot, }); + // Both halves of freight, matching `strategyBuckets`. This filter used to omit `freightUnload` + // — the same omission the summary itself had — so a game that scored only by unloading was + // bucketed but not counted here. const scored = report.perGame.filter( - (g) => g.revenue.freightLoad + g.revenue.passengerBoard + g.revenue.passengerDetrain > 0, + (g) => + g.revenue.freightLoad + g.revenue.freightUnload + + g.revenue.passengerBoard + g.revenue.passengerDetrain > 0, ); const bucketed = strategyBuckets(report.perGame).reduce((n, b) => n + b.games, 0); assert.equal(bucketed, scored.length, 'a scoring game fell outside every bucket'); @@ -591,45 +599,49 @@ describe('Enhancements can reach the board at all (regression)', () => { }); }); -describe('rolling stock stays in circulation (regression)', () => { - it('returns the Classification Yard to service overnight', () => { - // REGRESSION, and the largest single revenue change measured on this bot: +2.32 ± 0.52 paired - // over 400 seeds (t = 8.79). +describe('rolling stock returns to service (regression)', () => { + it('empties the Classification Yard into the Division Yard when the Division Yard runs out', () => { + // §2 — used Rolling Stock is set out in the Classification Yard, used engines and cabooses go + // straight back to the Division Yard, and the Classification Yard empties ONLY when the + // Division Yard is bare. Then all of it returns at once. // - // The Classification Yard was WRITE-ONLY. Seven places pushed cars into it — retired trains, - // collisions, unjams, set-outs — and nothing in the engine ever read it. Rolling stock drained - // one way out of the game: 30 cars a game dead by the end, 37% of the 80 dealt at setup. + // REGRESSION on two counts. The Classification Yard was write-only to begin with — seven + // writers, no readers — so 37% of the stock left the game. The first fix then guessed the wrong + // trigger, returning cars at every DAY boundary, which keeps the yard topped up continuously + // and is far more generous than the rule: worth +2.42 revenue a game that the game does not + // actually grant. // - // It was not obvious this mattered. The Division Yard does not run DRY in five Days (16.6 - // loaded freight cars left, empty in 2 games of 100), which is why this was ranked third and - // predicted not to affect revenue. The aggregate was never the point: what starves freight is - // not having the RIGHT commodity when a green box needs stocking, and returning classified - // cars keeps the mix alive. - const games = 30; - let deadAtEnd = 0; - let everReturned = 0; - for (let i = 0; i < games; i++) { - const s = createGame({ - id: `cy-${i}`, - seed: 1000 + i * 7919, - config: { ...config, length: 'standard' }, - playerNames: ['bot'], - }); - const r = playGame(s, developerBot, pump); - deadAtEnd += s.yards.classificationYard.length; - // Cars must have passed THROUGH it, or the test proves nothing about the return path. - if (r.events.some((e) => e.type === 'trainCompleted')) everReturned++; - } + // So this asserts the TRIGGER, not a tidy end state. The Division Yard legitimately still holds + // cars at the end of a short game, and the Classification Yard legitimately holds a pile. + const s = createGame({ + id: 'refill', + seed: 4, + config: { ...config, length: 'standard' }, + playerNames: ['bot'], + }); - assert.ok(everReturned > 0, 'no train ever completed — nothing reached the Classification Yard'); - // A Day's worth of arrivals may still be sitting there when the game ends; a whole game's worth - // may not. Before the fix this was ~30 a game. - assert.ok( - deadAtEnd / games < 15, - `${(deadAtEnd / games).toFixed(1)} cars a game are still stranded in the Classification ` + - 'Yard at the end — rolling stock is leaving the game and never coming back', + // Drain the Division Yard and park a known car in Classification. + const parked = s.yards.divisionYard.splice(0, s.yards.divisionYard.length); + s.yards.classificationYard.push(...parked); + assert.equal(s.yards.divisionYard.length, 0); + const waiting = s.yards.classificationYard.length; + assert.ok(waiting > 0, 'nothing was parked to return'); + + const moved = refillDivisionYardIfEmpty(s); + assert.ok(moved, 'an empty Division Yard did not pull the Classification Yard back'); + assert.equal(moved!.count, waiting); + assert.equal(s.yards.divisionYard.length, waiting, 'the cars did not come back'); + assert.equal(s.yards.classificationYard.length, 0, 'the Classification Yard was not emptied'); + + // And it must NOT fire while the Division Yard still has anything at all. + s.yards.classificationYard.push({ type: 'boxcar', loaded: false }); + assert.equal( + refillDivisionYardIfEmpty(s), + null, + 'the Classification Yard returned while the Division Yard still had cars', ); }); + }); describe('measurement discipline', () => { @@ -662,3 +674,152 @@ describe('measurement discipline', () => { assert.ok(upgrades > 0, 'no office upgrade was visible in the event log across 25 games'); }); }); + +describe('the bot does not lay track that cannot work (regression)', () => { + /** Drive a game with the developer bot, reporting every track piece it lays. */ + const laysIn = (seed: number, prepare?: (s: GameState) => void): Intent[] => { + const s = createGame({ + id: `lay-${seed}`, + seed, + config: { ...config, length: 'standard' }, + playerNames: ['Solitaire'], + }); + prepare?.(s); + const laid: Intent[] = []; + const spy = { + name: 'spy', + choose(st: GameState, p: 0, opts: Intent[]) { + const pick = developerBot.choose(st, p, opts); + if (pick.type === 'track.lay') laid.push(pick); + return pick; + }, + }; + playGame(s, spy as never, pump); + return laid; + }; + + it('will not cut a turnout with no arc left to hang beneath it', () => { + // A turnout is a hole in the Running Track, worth cutting only for what hangs under it. This + // does not happen naturally — measured at 0%, because the bot lays the arc immediately after + // the turnout and never runs the supply dry first — so the situation is constructed. It guards + // against the supply changing rather than against something happening today. + const drained = laysIn(4242, (s) => { + const area = s.officeAreas.get(0)!; + for (const k of ['curved:left', 'curved:right', 'sharpCurved:left', 'sharpCurved:right']) { + area.trackSupply.set(k, 0); + } + }); + const turnouts = drained.filter((i) => i.type === 'track.lay' && i.geometry === 'turnout'); + assert.equal( + turnouts.length, + 0, + `${turnouts.length} turnouts were cut with no arc left to build beneath them`, + ); + + // And the guard must not be doing this by simply stopping the bot from building. + const normal = laysIn(4242); + assert.ok(normal.length > 5, `the bot laid only ${normal.length} pieces with a full supply`); + }); + + it('rarely lays rail that butts a card which cannot accept it', () => { + // A port facing an EMPTY square is a promise — something may be built there later. A port + // butting an OCCUPIED square whose card has no matching port is not: that square is taken, so + // the rail stops dead and always will. Reported from a replay, where an arc was dropped off a + // turnout with its far end jammed into a curve. + // + // A TIE-BREAKER rather than a veto, so this is a rate and not a zero: forbidding it outright + // measured WORSE (-0.62 revenue a game), while preferring the cleaner of two equally good + // placements measured better and cut these from 28% of pieces to 7%. + let laid = 0; + let dead = 0; + for (const seed of [1000, 8919, 16838, 24757, 32676]) { + const s = createGame({ + id: `dp-${seed}`, seed, + config: { ...config, length: 'standard' }, + playerNames: ['Solitaire'], + }); + const spy = { + name: 'spy', + choose(st: GameState, p: 0, opts: Intent[]) { + const pick = developerBot.choose(st, p, opts); + if (pick.type === 'track.lay') { + laid++; + const area = st.officeAreas.get(p)!; + const v = variantsFor(pick.geometry)[pick.variant ?? 0]; + const probe = { + geometry: { + kind: 'track', geometry: pick.geometry, + ...(v?.axis ? { axis: v.axis } : {}), + ...(v?.arc ? { arc: v.arc } : {}), + ...(v?.turnout ? { turnout: v.turnout } : {}), + }, + baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [], + } as never; + for (const port of new Set(connectionsFor(probe).flat())) { + const n = neighbour(pick.placement, port); + const nb = area.grid.get(`${n.row},${n.col}`); + if (nb && !hasPort(nb, opposite(port))) dead++; + } + } + return pick; + }, + }; + playGame(s, spy as never, pump); + } + assert.ok(laid > 40, `only ${laid} pieces laid across five games`); + const rate = dead / laid; + assert.ok( + rate < 0.15, + `${(rate * 100).toFixed(0)}% of pieces were laid butting a card that cannot accept them`, + ); + }); +}); + +describe('the freight figures count both halves (regression)', () => { + it('scores an unload as Revenue earned, not as an unload started', () => { + // REGRESSION, against the instrument rather than the game. `rev.freightUnload` was assigned + // `eventCounts['unloadBegan']` — unloads STARTED, not Revenue EARNED, which differ by every + // unload that never finished — and `grossFreight` then used `freightLoad` alone, so + // `freightShare` reported roughly 60% of the freight actually earned. That is the number the + // "freight is only 13-18% of gross" finding was read from, and it drove a density change. + // + // Asserted against the ENGINE's own events, so the summary cannot drift from what happened. + const report = simulate({ + games: 30, length: 'standard', mode: 'solitaire', players: ['bot'], policy: developerBot, + }); + + let loads = 0; + let unloads = 0; + let begun = 0; + for (const seed of [1000, 8919, 16838]) { + const s = createGame({ + id: `fu-${seed}`, seed, + config: { ...config, length: 'standard' }, + playerNames: ['bot'], + }); + const r = playGame(s, developerBot, pump); + for (const e of r.events) { + if (e.type === 'unloadBegan') begun++; + if (e.type !== 'revenueChanged') continue; + if (e.reason === 'freightLoad') loads += e.delta; + if (e.reason === 'freightUnload') unloads += e.delta; + } + } + assert.ok(unloads > 0, 'no unload ever earned Revenue'); + // `begun` is what the summary used to count. It is an upper bound on what was EARNED, because + // an unload can be started and never finished — so counting it was wrong whether or not the + // two happen to agree on a given sample. + assert.ok(begun >= unloads, 'more unloads earned Revenue than were ever begun'); + + // Every game in the report must count both halves toward its freight share. + for (const g of report.perGame) { + const gross = g.revenue.freightLoad + g.revenue.freightUnload + g.revenue.passengerBoard + g.revenue.passengerDetrain; + if (gross === 0) continue; + const expected = (g.revenue.freightLoad + g.revenue.freightUnload) / gross; + assert.ok( + Math.abs(g.freightShare - expected) < 1e-9, + `freightShare ${g.freightShare.toFixed(3)} omits the unload half (expected ${expected.toFixed(3)})`, + ); + } + }); +}); diff --git a/test/track.test.ts b/test/track.test.ts index 3f2ac5b..0a81141 100644 --- a/test/track.test.ts +++ b/test/track.test.ts @@ -545,7 +545,7 @@ describe('coupling lifts only the cars the crew ran over', () => { const area = areaFrom(grid, { row: 0, col: 0 }); const s = gameWith(area); s.trays.set('crew', { - id: 'crew', trainNumber: 1, trainIsExtra: false, engineFront: true, consist: [], + id: 'crew', trainNumber: 1, trainIsExtra: false, engineAt: 0, consist: [], direction: 'west', facing: 'w', position: { at: 'grid', owner: 0, coord: { row: 0, col: 0 } }, movesUsed: 0, }); s.clock.phase = 'localOps'; diff --git a/test/web.test.ts b/test/web.test.ts index 6c14d38..a1cfa78 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -13,6 +13,7 @@ import { dirname, join, resolve } from 'node:path'; import { cardDescription, describeIntent } from '../src/sim/view.ts'; import { divisionSvg, officeSvg } from '../src/sim/board-svg.ts'; +import { record, renderHtml } from '../src/sim/replay.ts'; import { snapshot } from '../src/sim/view.ts'; import { createGame as createEngineGame } from '../src/engine/setup.ts'; import { @@ -529,11 +530,11 @@ describe('the page explains itself', () => { const game = newGame(5); const s = game.state; s.trays.set('tray2', { - id: 'tray2', trainNumber: 4, trainIsExtra: false, engineFront: true, + id: 'tray2', trainNumber: 4, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', position: { at: 'mainline', index: 1 }, movesUsed: 0, }); s.trays.set('tray3', { - id: 'tray3', trainNumber: 7, trainIsExtra: false, engineFront: true, + id: 'tray3', trainNumber: 7, trainIsExtra: false, engineAt: 0, consist: [], direction: 'east', position: { at: 'mainline', index: 1 }, movesUsed: 0, }); s.clock.pendingDecision = { train: 'tray2', occupiedBy: 'tray3' }; @@ -1191,3 +1192,165 @@ describe('a train says what its card calls for', () => { } }); }); + +describe('the three places a game is drawn stay in step', () => { + // A game is drawn on the playable page, in the standalone replay file, and in the website's + // replay viewer. They had drifted: the viewer offered three speeds where the standalone offered + // five, and had neither the sound nor the auto-hide the other two grew. Anything a player learns + // on one screen should hold on the others. + + it('offers the same playback speeds in both replay viewers', () => { + const site = readFileSync(join(dist, 'replays.html'), 'utf8'); + const standalone = renderHtml(record(880009, 'standard', 600)); + const speeds = (html: string): string[] => + [...html.matchAll(/