From 461c4d9bacacbed327eeb28518d1db11b1be4203 Mon Sep 17 00:00:00 2001 From: Jesse Date: Wed, 19 Aug 2026 13:38:06 -0400 Subject: [PATCH] =?UTF-8?q?v0.4.8=20=E2=80=94=20the=20Limits=20bound=20the?= =?UTF-8?q?=20whole=20district,=20the=20nine=20spots=20really=20are=20nine?= =?UTF-8?q?,=20and=20an=20action=20points=20at=20its=20square?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YQAJ4dND7enLj54eLyiF2C --- .gitignore | 7 +- CHANGELOG.md | 108 + README.md | 2 +- TODO.md | 15 + docs/rules/card-reference.md | 10 +- docs/rules/rules-v0.2.md | 8 +- node_modules | 1 - package-lock.json | 4 +- package.json | 2 +- public/replays/seed-404869.json | 2758 ----------------- public/replays/seed-737467.json | 1265 ++++++++ .../{seed-2717217.json => seed-927523.json} | 2277 ++++++-------- .../{seed-2701379.json => seed-967118.json} | 795 +++-- src/engine/apply.ts | 20 + src/engine/intents.ts | 5 + src/engine/legal.ts | 30 + src/engine/track.ts | 39 +- src/sim/board-svg.ts | 45 +- src/sim/replay.ts | 2 +- src/sim/view.ts | 10 + src/web/game.ts | 53 +- src/web/main.ts | 62 +- src/web/replays.ts | 2 +- test/apply.test.ts | 180 +- test/web.test.ts | 88 +- 25 files changed, 3264 insertions(+), 4524 deletions(-) delete mode 120000 node_modules delete mode 100644 public/replays/seed-404869.json create mode 100644 public/replays/seed-737467.json rename public/replays/{seed-2717217.json => seed-927523.json} (69%) rename public/replays/{seed-2701379.json => seed-967118.json} (84%) diff --git a/.gitignore b/.gitignore index 159674e..89ae834 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,12 @@ Thumbs.db *.swp # Dependencies / build output -node_modules/ +# +# NO TRAILING SLASH on node_modules, deliberately. `node_modules/` matches a directory and not a +# SYMLINK of that name — so one pointing at a shared install was committed in v0.4.7, and because it +# pointed at a path that does not exist, a fresh checkout had no TypeScript: `npm run typecheck` said +# "tsc: not found" and 20 tests failed for a reason that had nothing to do with the code. +node_modules dist/ build/ target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a05aab..7414ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,114 @@ page as `v0.1.0 · · `, so what is deployed can always be identifie --- +## 0.4.8 — 2026-08-19 + +Two reports from the same game, both about squares: which ones a card may go on, and which one a +button in the action list means. + +### The Limits bound the district, not just the Running Track + +**Reported:** + +> "Sidings should not be allowed to be built outside the limits." + +The Limits sign "denotes the limit of your control area" (§2.1) and §3 defines Secondary Track as +"all tracks **in your limits** that are not the Running Track" — but the bound was only ever enforced +on the Running Track row itself, with a comment in `canPlaceAt` stating outright that "the district +below the Running Track is unbounded". So a siding could run east past a player's own sign, take +industries with it, and put cars on track that §8.1 and §10 do not consider his territory at all: the +rules reason about "the track between the train and the Limits", and running past another player's +Limits is what makes a collision his fault. + +`withinLimits` is now the district's rule at **every row**, and a Facility is bounded by it too — +§11.2 is explicit that "Facility cards carry their own rails: placing a Facility places track". The +refusal is its own code, `OUTSIDE_LIMITS`, rather than `NOT_CONNECTED`, for the same reason +`BREAKS_RUNNING_TRACK` is: the card would join perfectly well, and is refused for a different reason. + +**Inclusive of the sign's own column**, which is the half that keeps the game playable. Jesse's call. +The sign stands *on* the boundary rather than beyond it, and a district opens with signs at ±1 around +the Office — so the strict reading would leave exactly one buildable column and break §11.3's promise +that both Secondary rows, and the nine-spot Modifier neighbourhood, are usable from the first Stage. +A siding may run under the sign; nothing may go past it. + +**What it costs the bot: nothing measurable.** Out-of-limits building was rare for it to begin with — +5 cards across 100 games, in 4 of them, never more than two columns over — and paired over 200 seeds +the bound moved 4 games and −0.015 Revenue (t = −0.26). It is a human building deliberately who hits +this, which is exactly how it was found. + +### The nine spots really are nine — the four diagonals were legal and unofferable + +**Reported:** + +> "Modifiers should be able to go on any diagonal — we were unable to place it to the south-east." + +§9 places a Modifier "adjacent to a Facility, on any of the nine nearby spots", and `check` has +always accepted all eight neighbours. The fault was upstream in `placementCandidates`, which walks +north, south, east and west from every occupied square: a diagonal square with no *orthogonal* +occupied neighbour was never generated, so it was never offered. Measured on a facility below the +Office: `check` says legal at all four diagonals, the menu offered the three orthogonal ones. At a +stub industry — the case in the report — every diagonal is in that position. + +Generated in a **second pass, only around a Facility**. Track has to *join* and a diagonal shares no +edge, so a Modifier is the only card those squares can ever take; generating diagonals around every +card instead costs 54% more simulation time producing candidates `check` then rejects. Appending +rather than interleaving leaves the existing candidate order untouched, which matters because the bot +breaks ties by first-best — measured over 200 seeded games, **0 played differently**, so every figure +in `TODO.md` still stands. + +### A Modifier may hang outside the Limits — but not in the Running Track's row + +Jesse's call, both halves. A Modifier is not track (§9), so unlike a siding it keeps all nine of its +spots when its host stands at the limit: refusing the outer three would make the card unplayable +exactly where the district ends. What it may not do is stand in the row the Running Track grows +along. Inside the Limits that row is always full, so the rule bites only beyond the sign — and that +is the ground the main extends onto, where a parked Modifier would block the player's own sign from +moving outward (§2.1) with nothing on screen to warn him. Industries have been barred from the +Running Track since the "Placed" column was read properly; this is the same rule for the same reason. + +### The board draws where the district ends + +Two signs on one row cannot say that nothing may be built outside their columns at *any* row: a +player looking at open ground beyond a sign had no way to know it was unbuildable until the square +failed to light up. `officeSvg` now draws a quiet dashed edge down the whole canvas at each limit, +labelled, and just **outside** the sign's own column so the picture agrees with the rule — a siding +may run under the sign. It moves outward on its own as the Running Track is extended, which is the +reward for extending made visible. The Frame carries `limits` for it, resolved server-side like +everything else a remote client cannot look up (`multiplayer.md` §5). + +The margin on the viewBox is not cosmetic: the west sign is usually the leftmost card on the canvas, +so the line outside its column lands at x = −1.5 and simply is not there on a canvas starting at 0. +Caught by drawing one, not by reading the code. + +### Hovering an action points at the square it means + +**Reported:** + +> "When I have my mouse over the action for a particular square, could the corresponding square in +> the office area be highlighted to minimize my mistakes of selecting the wrong square — 1,3 versus +> −1,3? In a solitaire game with Undo it is not fatal. In a multiplayer game that could be a major +> disaster." + +The action list is a column of near-identical sentences separated by a coordinate, and the board +beside it said nothing about which was which. Every action that names a square now carries it as +`data-square`, and hovering — or focusing, so the keyboard route is not a lesser one — lights that +square on the board, ghost targets included, which is precisely the moment a player is choosing +between coordinates. + +The coordinate rides on the **menu**, resolved from the intent by `coordOf`, rather than being parsed +back out of the label: a remote client holds no `GameState` and cannot look up where a tray is +standing, which is the same reason the menu already carries card descriptions. Checked over 12 bot +games: 870 direct actions name a square, and all 870 carry it. + +### Also + +- The three published replays were re-recorded. A save is a seed and its intents, so tightening a + placement rule kills every replay containing one — `harness.test.ts` catches it rather than letting + them go quietly dead, which has happened twice before. Old saves are not preserved across rule + changes and are not meant to be yet. +- `node_modules` was a **tracked symlink pointing at a path that does not exist**, so `tsc` was + missing and 20 tests failed before any of this was written. Replaced with the real dependencies. + ## 0.4.7 — 2026-08-19 Eight play reports and one design that had been written up and not built. The through-line is the diff --git a/README.md b/README.md index 0f53c97..1d6af3c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ train into an occupied Subdivision. Get that wrong and two trains meet at speed. ## Status -**v0.4.3 — solitaire is playable in a browser.** The whole game runs client-side: the engine is pure, +**v0.4.8 — solitaire is playable in a browser.** The whole game runs client-side: the engine is pure, imports nothing outside itself, and never touches `Math.random`, so a static host is all it needs. - **Rules** — specified, with **three open questions** left. Thirteen gaps in the original prototype diff --git a/TODO.md b/TODO.md index 636eecf..ffcda71 100644 --- a/TODO.md +++ b/TODO.md @@ -52,6 +52,14 @@ Ordered within each section by how much it is currently costing us. this needs re-measuring); or move the Express's freight budget into the Cargo phase, where an expedited train does still get a turn. **Nothing is broken** — this is a contradiction between two lines on one card, and the timing rule itself is behaving exactly as recorded. +- [ ] **A DISTRICT CAN NOW ONLY WIDEN AS FAR AS ITS MAIN REACHES (v0.4.8) — worth watching in the + rebalance rather than acting on now.** Track stays inside the Limits at every row, so extending + the Running Track is the only way to buy room for sidings, and a straight laid on the sign is + worth more than it was. The bot barely notices — it built outside its own Limits 5 times in 100 + games — but the bot also builds close to its Office; a human building deliberately hits this on + the first wide district, which is how it was reported. If territory turns out to be the real + constraint on freight, this is one of the two places to look (the other is the track supply, + below). - [ ] **REBALANCE, once the rules are right — deliberately deferred.** Card counts, industry counts and the track mix all need a pass together, and none of them should move until the rules stop moving. Standing distortions to account for when it happens: offices are doubled (Q12) and @@ -623,6 +631,13 @@ target is settled and freight carries its intended share. - [ ] **The 5 MB replay size limit is arbitrary.** Invented, not a browser constraint. It has earned its place — it caught a 5.2 MB payload that turned out to be the whole grid re-serialised every frame — but the number itself deserves a reason. +- [ ] **The test suite fails at random under `npm test`, and it is the runner rather than the code.** + `node --test test/**/*.test.ts` runs the files in parallel and three suites write and read the + same `dist/` — the static build, the published-replay check and "the three places a game is + drawn stay in step". Back-to-back full runs measured **9 failures then 0**; run one file at a + time and every suite passes. That is worse than a slow suite: it trains us to shrug at a red + run, which is exactly how a real regression gets waved through. Give the build test its own + output directory, or mark the trio to run serially. - [ ] **Curves are drawn as two straight segments meeting**, not true arcs. Fine at this size, angular close up. - [ ] **Wide boards scroll.** A 40-card district and a 13-section Division both need horizontal diff --git a/docs/rules/card-reference.md b/docs/rules/card-reference.md index 2ec00c8..a0f4ab8 100644 --- a/docs/rules/card-reference.md +++ b/docs/rules/card-reference.md @@ -138,8 +138,14 @@ above and below. Upgrades are drop-in and must never disturb a connection. ## 5. Modifier cards Five cards, one each. A Modifier is **not track**. It is placed adjacent to a Facility, on any of the -nine nearby spots, and if adjacent to two Facilities its effect may be used on only one per Stage -(§9). +nine nearby spots — **diagonals included** — and if adjacent to two Facilities its effect may be used +on only one per Stage (§9). + +Two placement rules follow from it not being track. It is **not bounded by the Limits**: a host +standing at the edge of the district keeps all nine of its spots, or the card would be unplayable +exactly where the district ends. And it may **never stand in the Running Track's row**, which inside +the Limits is always full anyway — beyond the sign that row is the ground the main grows onto, and a +Modifier parked there would block the Limits sign from moving outward (§2.1). | Card | Effect | Applies to | | --- | --- | --- | diff --git a/docs/rules/rules-v0.2.md b/docs/rules/rules-v0.2.md index 20744ba..7d4c66f 100644 --- a/docs/rules/rules-v0.2.md +++ b/docs/rules/rules-v0.2.md @@ -593,10 +593,16 @@ horizontal row through the Office, Limit-to-Limit; everything else is Secondary - Plain track, turnouts and Facility cards are all played from hand into an empty adjacent grid spot, and **must connect to existing track**. Facility cards carry their own rails — placing a Facility places track. +- Track stays **inside the Limits at every row**, not only on the Running Track. §2.1 makes the sign + "the limit of your control area" and §3 defines Secondary Track as "all tracks in your limits", so + a siding may run *under* a sign — the sign stands on the boundary — and never past it. Widening a + district therefore means extending the main first. - Extending the **Running Track** horizontally moves that player's Limits sign outward with it (§2.1). Growth is uncapped in the prototype. - Modifier Facility cards are not track. They are placed adjacent to a Facility, on any of the nine - nearby spots (§9). + nearby spots (§9) — diagonals included, and not bounded by the Limits, since a host at the edge of + the district would otherwise have nowhere to put one. The one square they may not take is in the + Running Track's own row, which is where the main grows. A longer Running Track is more track to keep clear of arriving trains, so territory carries its own collision risk. diff --git a/node_modules b/node_modules deleted file mode 120000 index 6c57164..0000000 --- a/node_modules +++ /dev/null @@ -1 +0,0 @@ -../../../node_modules \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 05b9c11..b86beeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "station-master", - "version": "0.0.1", + "version": "0.4.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "station-master", - "version": "0.0.1", + "version": "0.4.8", "devDependencies": { "@types/node": "^26.1.2", "typescript": "^7.0.2" diff --git a/package.json b/package.json index 8bcbb0b..7dbb2de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "station-master", - "version": "0.4.7", + "version": "0.4.8", "private": true, "type": "module", "description": "Station Master — a railroad operations game", diff --git a/public/replays/seed-404869.json b/public/replays/seed-404869.json deleted file mode 100644 index 71a256c..0000000 --- a/public/replays/seed-404869.json +++ /dev/null @@ -1,2758 +0,0 @@ -{ - "seed": 404869, - "title": "16 Revenue · seed 404869", - "note": "16 Revenue over 5 Days · 1 train(s) on the timetable · 9 cards down · no collisions · played by developer", - "rules": { - "startingHand": "threeRandom", - "revenue": { - "passengerPerCoach": 1, - "freightPerLoad": 1, - "trainPerTransit": 0 - } - }, - "history": [ - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c22" - }, - { - "type": "card.play", - "cardId": "c6" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c14" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.startExtra", - "trainNumber": 15, - "atSeat": null - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c15" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.startExtra", - "trainNumber": 16, - "atSeat": null - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c181", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "boxcar", - "loaded": true - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c163", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c74", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c32" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c151", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c53", - "placement": { - "row": -1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c37", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "hopper", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c166", - "placement": { - "row": -1, - "col": 4 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c202", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": 3 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": 3 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 2 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": -1, - "col": 1 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - } - ] -} \ No newline at end of file diff --git a/public/replays/seed-737467.json b/public/replays/seed-737467.json new file mode 100644 index 0000000..fe6ce1d --- /dev/null +++ b/public/replays/seed-737467.json @@ -0,0 +1,1265 @@ +{ + "seed": 737467, + "title": "12 Revenue · seed 737467", + "note": "12 Revenue over 5 Days · 1 train(s) on the timetable · 10 cards down · no collisions · played by developer", + "rules": { + "startingHand": "threeRandom", + "revenue": { + "passengerPerCoach": 1, + "freightPerLoad": 1, + "trainPerTransit": 0 + } + }, + "history": [ + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.play", + "cardId": "c23" + }, + { + "type": "card.play", + "cardId": "c32" + }, + { + "type": "card.play", + "cardId": "c120", + "placement": { + "row": 0, + "col": 1 + }, + "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": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c171", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c87", + "placement": { + "row": 0, + "col": 1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c139", + "placement": { + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c168", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c6" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c137", + "placement": { + "row": 0, + "col": -2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c67", + "placement": { + "row": 1, + "col": 0 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c162", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c25", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c193", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c89", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 2 + }, + { + "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": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c22", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c110", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 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": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c97", + "node": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c157", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c21" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.startExtra", + "trainNumber": 22, + "atSeat": null + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 1 + }, + { + "type": "card.discard", + "cardId": "c69", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c136", + "placement": { + "row": 0, + "col": -3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c36", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c62", + "toSlot": 2 + }, + { + "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": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c36", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c62", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c36", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 2 + }, + { + "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": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c62", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c36", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c62", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c36", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c57", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c62", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "boxcar", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c36", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c40", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "loadUnload.end" + } + ] +} \ No newline at end of file diff --git a/public/replays/seed-2717217.json b/public/replays/seed-927523.json similarity index 69% rename from public/replays/seed-2717217.json rename to public/replays/seed-927523.json index 9fa5364..ebb45b9 100644 --- a/public/replays/seed-2717217.json +++ b/public/replays/seed-927523.json @@ -1,7 +1,7 @@ { - "seed": 2717217, - "title": "18 Revenue · seed 2717217", - "note": "18 Revenue over 5 Days · 2 train(s) on the timetable · 25 cards down · 2 collision(s) · played by developer", + "seed": 927523, + "title": "12 Revenue · seed 927523", + "note": "12 Revenue over 5 Days · 2 train(s) on the timetable · 20 cards down · no collisions · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -16,16 +16,11 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 1 + "type": "draw.fromHomeOffice" }, { "type": "card.play", - "cardId": "c7" - }, - { - "type": "card.play", - "cardId": "c197", + "cardId": "c125", "placement": { "row": 0, "col": 1 @@ -34,90 +29,7 @@ }, { "type": "card.play", - "cardId": "c166", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c165", - "placement": { - "row": -1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c117", + "cardId": "c187", "placement": { "row": 0, "col": -1 @@ -139,112 +51,20 @@ }, { "type": "card.play", - "cardId": "c24" - }, - { - "type": "card.play", - "cardId": "c5" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c151", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c90", + "cardId": "c182", "placement": { - "row": 0, + "row": -1, "col": -1 }, + "variant": 1 + }, + { + "type": "card.play", + "cardId": "c57", + "placement": { + "row": -1, + "col": 0 + }, "variant": 0 }, { @@ -262,12 +82,12 @@ }, { "type": "card.play", - "cardId": "c163", + "cardId": "c152", "placement": { - "row": -2, - "col": 0 + "row": -1, + "col": 1 }, - "variant": 1 + "variant": 0 }, { "type": "draw.end" @@ -282,57 +102,9 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.discard", - "cardId": "c72", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c105", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, { "type": "card.play", - "cardId": "c200", + "cardId": "c174", "placement": { "row": 0, "col": -2 @@ -352,118 +124,9 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.play", - "cardId": "c66", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, { "type": "draw.end" }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, { "type": "loadUnload.end" }, @@ -476,7 +139,7 @@ }, { "type": "card.play", - "cardId": "c173", + "cardId": "c154", "placement": { "row": -1, "col": -2 @@ -498,11 +161,7 @@ }, { "type": "card.play", - "cardId": "c33" - }, - { - "type": "card.play", - "cardId": "c34" + "cardId": "c4" }, { "type": "draw.end" @@ -519,37 +178,127 @@ }, { "type": "card.play", - "cardId": "c17" + "cardId": "c23" + }, + { + "type": "card.play", + "cardId": "c31" + }, + { + "type": "card.play", + "cardId": "c35" }, { "type": "draw.end" }, { - "type": "newTrain.startExtra", - "trainNumber": 18, - "atSeat": null + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" }, { "type": "newTrain.placeCar", "trayId": "tray3", "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c9" + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "reefer", "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true + "trayId": "tray2", + "carType": "reefer", + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true + "trayId": "tray2", + "carType": "reefer", + "loaded": false }, { "type": "newTrain.placeCar", - "trayId": "tray3", + "trayId": "tray2", "carType": "caboose", "loaded": true }, @@ -563,39 +312,15 @@ { "type": "draw.fromHomeOffice" }, - { - "type": "card.play", - "cardId": "c145", - "placement": { - "row": -2, - "col": -1 - }, - "variant": 1 - }, { "type": "draw.end" }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c177", - "placement": { - "row": -1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } }, { "type": "porter.board", @@ -614,6 +339,61 @@ { "type": "loadUnload.end" }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": 0, + "col": 0 + }, + "carType": "coach" + }, + { + "type": "loadUnload.end" + }, { "type": "localOps.choose", "option": "draw" @@ -634,15 +414,106 @@ "type": "draw.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c114", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c54", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c50", + "toSlot": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 0 + }, + { + "type": "card.discard", + "cardId": "c33", + "toSlot": 0 + }, + { + "type": "draw.end" }, { "type": "newTrain.placeCar", "trayId": "tray2", - "carType": "boxcar", + "carType": "coach", + "loaded": true + }, + { + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "coach", "loaded": true }, { @@ -657,155 +528,18 @@ }, { "type": "card.play", - "cardId": "c43", + "cardId": "c147", "placement": { "row": -1, - "col": -3 + "col": 2 }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" + "variant": 1 }, { "type": "card.play", - "cardId": "c37", + "cardId": "c54", "placement": { "row": -1, - "col": -4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": -4 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -4 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -4 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c119", - "placement": { - "row": 0, - "col": 3 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c86", - "placement": { - "row": 0, "col": 3 }, "variant": 0 @@ -814,120 +548,22 @@ "type": "draw.end" }, { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": -4 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -4 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -3 - }, - "carType": "hopper" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -4 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c20" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.startExtra", - "trainNumber": 21, - "atSeat": null + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "reefer", + "loaded": false }, { "type": "newTrain.placeCar", "trayId": "tray3", - "carType": "hopper", + "carType": "reefer", "loaded": false }, { "type": "newTrain.placeCar", "trayId": "tray3", "carType": "hopper", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", - "loaded": false + "loaded": true }, { "type": "newTrain.placeCar", @@ -936,12 +572,137 @@ "loaded": true }, { - "type": "laborer.advanceLoad", + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "porter.detrain", "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.board", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "porter.detrain", + "at": { + "row": 0, + "col": 0 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c88", + "toSlot": 1 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { "row": -1, - "col": -4 + "col": 2 }, - "box": 2 + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 3 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 2 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": -1, + "col": 3 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray3", + "count": 1 + }, + { + "type": "switch.move", + "trayId": "tray3", + "to": { + "row": 0, + "col": 0 + }, + "reverse": true }, { "type": "loadUnload.end" @@ -955,10 +716,10 @@ }, { "type": "card.play", - "cardId": "c169", + "cardId": "c128", "placement": { - "row": -1, - "col": -5 + "row": 0, + "col": -3 }, "variant": 0 }, @@ -966,12 +727,26 @@ "type": "draw.end" }, { - "type": "laborer.beginUnload", - "at": { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c39", + "placement": { "row": -1, - "col": -4 + "col": 4 }, - "carIndex": 0 + "variant": 0 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -985,10 +760,32 @@ }, { "type": "card.play", - "cardId": "c186", + "cardId": "c133", "placement": { - "row": -1, - "col": 2 + "row": 0, + "col": -4 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c144", + "placement": { + "row": -2, + "col": 1 }, "variant": 1 }, @@ -996,18 +793,60 @@ "type": "draw.end" }, { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "hopper", - "loaded": false + "type": "loadUnload.end" }, { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 2 + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.discard", + "cardId": "c50", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c60", + "toSlot": 2 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromDepartment", + "slot": 2 + }, + { + "type": "card.discard", + "cardId": "c34", + "toSlot": 2 + }, + { + "type": "draw.end" }, { "type": "loadUnload.end" @@ -1021,575 +860,7 @@ }, { "type": "card.play", - "cardId": "c181", - "placement": { - "row": 0, - "col": -3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -2 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -3 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -2 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -3 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": -3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -3 - } - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -4 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": -4 - }, - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -3 - }, - "box": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -3 - }, - "carType": "hopper" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -3 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -4 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -3 - }, - "box": 2 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -4 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c75", - "placement": { - "row": -2, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c153", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": -4 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -4 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -3 - }, - "carType": "hopper" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "hopper", - "loaded": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -4 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c118", - "placement": { - "row": 0, - "col": -4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "maneuver.flyingSwitch", - "cardId": "c109", - "trayId": "tray1", - "count": 1, - "to": { - "row": -1, - "col": -3 - } - }, - { - "type": "switch.end" - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -3 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -3 - }, - "box": 0 - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -4 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -3 - }, - "carType": "hopper" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -3 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -3 - }, - "box": 2 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c190", + "cardId": "c117", "placement": { "row": 0, "col": 4 @@ -1600,97 +871,58 @@ "type": "draw.end" }, { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false }, { - "type": "loadUnload.end" - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false }, { "type": "loadUnload.end" }, { "type": "localOps.choose", - "option": "switch" + "option": "draw" }, { - "type": "switch.end" + "type": "draw.fromHomeOffice" }, { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -4 - } + "type": "card.discard", + "cardId": "c85", + "toSlot": 0 }, { - "type": "loadUnload.end" + "type": "draw.end" }, { - "type": "localOps.choose", - "option": "freightAgent" + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": true }, { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": -4 - }, - "index": 0 + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": true }, { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 0 + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "hopper", + "loaded": true }, { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -4 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -4 - }, - "box": 1 + "type": "newTrain.placeCar", + "trayId": "tray2", + "carType": "caboose", + "loaded": true }, { "type": "loadUnload.end" @@ -1704,24 +936,427 @@ }, { "type": "card.play", - "cardId": "c148", + "cardId": "c111", "placement": { - "row": -2, - "col": 3 + "row": 0, + "col": 5 }, - "variant": 1 + "variant": 0 }, { "type": "draw.end" }, + { + "type": "porter.board", + "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": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "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": -1, + "col": 3 + }, + "reverse": false + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.dropCars", + "trayId": "tray2", + "count": 1 + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, { "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -4 + "col": 3 }, "box": 2 }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": 0, + "col": 0 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "coach", + "loaded": false + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 3 + }, + "carIndex": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 3 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.clearInbound", + "at": { + "row": -1, + "col": 3 + }, + "index": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, { "type": "loadUnload.end" } diff --git a/public/replays/seed-2701379.json b/public/replays/seed-967118.json similarity index 84% rename from public/replays/seed-2701379.json rename to public/replays/seed-967118.json index 88c64bf..3a31458 100644 --- a/public/replays/seed-2701379.json +++ b/public/replays/seed-967118.json @@ -1,7 +1,7 @@ { - "seed": 2701379, - "title": "14 Revenue · seed 2701379", - "note": "14 Revenue over 5 Days · 0 train(s) on the timetable · 8 cards down · no collisions · played by developer", + "seed": 967118, + "title": "12 Revenue · seed 967118", + "note": "12 Revenue over 5 Days · 1 train(s) on the timetable · 12 cards down · no collisions · played by developer", "rules": { "startingHand": "threeRandom", "revenue": { @@ -16,16 +16,11 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 2 + "type": "draw.fromHomeOffice" }, { "type": "card.play", - "cardId": "c20" - }, - { - "type": "card.play", - "cardId": "c115", + "cardId": "c187", "placement": { "row": 0, "col": 1 @@ -34,54 +29,25 @@ }, { "type": "card.play", - "cardId": "c197", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c169", + "cardId": "c152", "placement": { "row": -1, - "col": -1 + "col": 1 }, "variant": 1 }, + { + "type": "card.play", + "cardId": "c36", + "placement": { + "row": -1, + "col": 2 + }, + "variant": 0 + }, { "type": "draw.end" }, - { - "type": "newTrain.startExtra", - "trainNumber": 21, - "atSeat": null - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, { "type": "loadUnload.end" }, @@ -90,14 +56,65 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 2 + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" }, { "type": "card.play", - "cardId": "c60", + "cardId": "c124", "placement": { - "row": -1, + "row": 0, + "col": -1 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c11" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c182", + "placement": { + "row": 0, "col": -2 }, "variant": 0 @@ -113,14 +130,97 @@ "option": "draw" }, { - "type": "draw.fromDepartment", - "slot": 2 + "type": "draw.fromHomeOffice" + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" }, { "type": "card.play", - "cardId": "c44", + "cardId": "c115", + "placement": { + "row": 0, + "col": 2 + }, + "variant": 0 + }, + { + "type": "card.play", + "cardId": "c164", "placement": { "row": -1, + "col": 3 + }, + "variant": 1 + }, + { + "type": "draw.end" + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "boxcar", + "loaded": false + }, + { + "type": "newTrain.placeCar", + "trayId": "tray3", + "carType": "caboose", + "loaded": true + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c185", + "placement": { + "row": 0, + "col": 3 + }, + "variant": 0 + }, + { + "type": "draw.end" + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "draw" + }, + { + "type": "draw.fromHomeOffice" + }, + { + "type": "card.play", + "cardId": "c141", + "placement": { + "row": 0, "col": -3 }, "variant": 0 @@ -140,7 +240,7 @@ "trayId": "tray3", "to": { "row": -1, - "col": -1 + "col": 1 }, "reverse": false }, @@ -154,7 +254,7 @@ "trayId": "tray3", "to": { "row": -1, - "col": -2 + "col": 2 }, "reverse": false }, @@ -168,22 +268,9 @@ "trayId": "tray3", "count": 1 }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, { "type": "switch.end" }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, { "type": "loadUnload.end" }, @@ -195,7 +282,29 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": -2 + "col": 2 + }, + "carType": "boxcar" + }, + { + "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" }, @@ -203,9 +312,9 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, - "box": 2 + "box": 0 }, { "type": "loadUnload.end" @@ -221,7 +330,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -239,7 +348,155 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.startLoad", + "at": { + "row": -1, + "col": 2 + } + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "freightAgent" + }, + { + "type": "freightAgent.stockOutbound", + "at": { + "row": -1, + "col": 2 + }, + "carType": "boxcar" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.beginUnload", + "at": { + "row": -1, + "col": 2 + }, + "carIndex": 0 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 2 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 + }, + "box": 1 + }, + { + "type": "loadUnload.end" + }, + { + "type": "localOps.choose", + "option": "switch" + }, + { + "type": "switch.end" + }, + { + "type": "laborer.advanceLoad", + "at": { + "row": -1, + "col": 2 }, "box": 0 }, @@ -254,7 +511,7 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "index": 0 }, @@ -262,7 +519,7 @@ "type": "laborer.startLoad", "at": { "row": -1, - "col": -2 + "col": 2 } }, { @@ -276,7 +533,7 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "carType": "boxcar" }, @@ -284,7 +541,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -302,7 +559,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -320,7 +577,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -338,7 +595,7 @@ "type": "laborer.beginUnload", "at": { "row": -1, - "col": -2 + "col": 2 }, "carIndex": 0 }, @@ -356,7 +613,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -374,7 +631,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -392,7 +649,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -407,7 +664,7 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "index": 0 }, @@ -415,7 +672,7 @@ "type": "laborer.startLoad", "at": { "row": -1, - "col": -2 + "col": 2 } }, { @@ -429,7 +686,7 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "carType": "boxcar" }, @@ -437,7 +694,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -455,7 +712,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -473,7 +730,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -491,7 +748,7 @@ "type": "laborer.beginUnload", "at": { "row": -1, - "col": -2 + "col": 2 }, "carIndex": 0 }, @@ -509,7 +766,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -527,7 +784,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -545,7 +802,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -560,7 +817,7 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "index": 0 }, @@ -568,7 +825,7 @@ "type": "laborer.startLoad", "at": { "row": -1, - "col": -2 + "col": 2 } }, { @@ -582,7 +839,7 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "carType": "boxcar" }, @@ -590,7 +847,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -608,7 +865,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -626,7 +883,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -644,7 +901,7 @@ "type": "laborer.beginUnload", "at": { "row": -1, - "col": -2 + "col": 2 }, "carIndex": 0 }, @@ -662,7 +919,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -680,7 +937,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -698,7 +955,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -713,7 +970,7 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "index": 0 }, @@ -721,7 +978,7 @@ "type": "laborer.startLoad", "at": { "row": -1, - "col": -2 + "col": 2 } }, { @@ -735,7 +992,7 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "carType": "boxcar" }, @@ -743,7 +1000,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -761,7 +1018,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -779,7 +1036,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -797,7 +1054,7 @@ "type": "laborer.beginUnload", "at": { "row": -1, - "col": -2 + "col": 2 }, "carIndex": 0 }, @@ -815,7 +1072,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -833,7 +1090,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -851,7 +1108,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -866,7 +1123,7 @@ "type": "freightAgent.clearInbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "index": 0 }, @@ -874,7 +1131,7 @@ "type": "laborer.startLoad", "at": { "row": -1, - "col": -2 + "col": 2 } }, { @@ -888,7 +1145,7 @@ "type": "freightAgent.stockOutbound", "at": { "row": -1, - "col": -2 + "col": 2 }, "carType": "boxcar" }, @@ -896,7 +1153,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 0 }, @@ -914,7 +1171,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 1 }, @@ -932,7 +1189,7 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, @@ -950,7 +1207,7 @@ "type": "laborer.beginUnload", "at": { "row": -1, - "col": -2 + "col": 2 }, "carIndex": 0 }, @@ -968,298 +1225,10 @@ "type": "laborer.advanceLoad", "at": { "row": -1, - "col": -2 + "col": 2 }, "box": 2 }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": -2 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -2 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -2 - }, - "carType": "boxcar" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": -2 - }, - "index": 0 - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": -2 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": -2 - }, - "carType": "reefer" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -2 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -2 - }, - "carIndex": 0 - }, { "type": "loadUnload.end" } diff --git a/src/engine/apply.ts b/src/engine/apply.ts index 3f1c294..8fb5dee 100644 --- a/src/engine/apply.ts +++ b/src/engine/apply.ts @@ -77,6 +77,7 @@ import { opposite, reachableDestinations, variantsFor, + withinLimits, } from './track.ts'; export type ApplyResult = @@ -1105,6 +1106,9 @@ function checkPlay( if (placement.row === area.runningRow && !carriesThroughTrack(proto)) { return 'BREAKS_RUNNING_TRACK'; } + // Same reasoning: a siding reaching past your own sign JOINS perfectly well, and is refused + // because it leaves your territory (§2.1). `canPlaceAt` enforces it too — this only names it. + if (!withinLimits(area, placement)) return 'OUTSIDE_LIMITS'; return canPlaceAt(area, placement, proto) ? null : 'NOT_CONNECTED'; } @@ -1123,6 +1127,10 @@ function checkPlay( * main line. */ if (placement.row === area.runningRow) return 'ON_RUNNING_TRACK'; + // A Facility CARRIES TRACK — "placing a Facility places track" (§11.2) — so it is bounded by + // the Limits exactly as a siding is. An industry outside them is how a district used to leave + // its own territory. + if (!withinLimits(area, placement)) return 'OUTSIDE_LIMITS'; // Q4 — a lockout prevents BUILDING both in one district: no duplicate, and never a producer // alongside the consumer of the same commodity. if (isLockedOut(area, card.kind.facility)) return 'FACILITY_LOCKED'; @@ -1133,6 +1141,18 @@ function checkPlay( case 'modifier': { if (!placement) return 'NO_PLACEMENT'; if (area.grid.has(coordKey(placement))) return 'NOT_CONNECTED'; + /** + * NOT ON THE RUNNING TRACK ROW — AND NOT BOUNDED BY THE LIMITS EITHER. Jesse's call, both + * halves. + * + * A Modifier is not track (§9), so unlike a siding it may hang outside the Limits: a Facility + * standing at the limit has three of its nine spots out there, and refusing them would make + * the card unplayable exactly where the district ends. What it may NOT do is stand in the row + * the Running Track grows along. Inside the Limits that row is always full, so this bites only + * beyond the sign — which is the ground the main extends onto, and a Modifier parked there + * would block your own sign from moving outward (§2.1) with nothing on screen to warn you. + */ + if (placement.row === area.runningRow) return 'ON_RUNNING_TRACK'; // One of a kind per Office Area, as with industries (Q4) — no district gets two Ice Houses. if (hasModifierInArea(area, card.kind.modifier)) return 'FACILITY_LOCKED'; // §9 — a Modifier is not track. It must sit adjacent to a Facility THAT CAN HOST IT (one of diff --git a/src/engine/intents.ts b/src/engine/intents.ts index 7063bb4..d298ded 100644 --- a/src/engine/intents.ts +++ b/src/engine/intents.ts @@ -143,6 +143,11 @@ export type RejectionCode = | 'ON_RUNNING_TRACK' /** A card with no east-west road would dead-end the Running Track it was laid in. */ | 'BREAKS_RUNNING_TRACK' + /** + * §2.1 — track, and the Facilities that carry it, stay inside your own Limits at every row. A + * Modifier is not track and is exempt (§9's nine spots), which is why this is not shared with it. + */ + | 'OUTSIDE_LIMITS' /** * A turnout may be laid on top of a straight, or of a curve on the same arc as its diverging leg * — but only those, and only while the square is idle. See `checkTurnoutUpgrade`. diff --git a/src/engine/legal.ts b/src/engine/legal.ts index ee69802..cd6879f 100644 --- a/src/engine/legal.ts +++ b/src/engine/legal.ts @@ -313,5 +313,35 @@ function placementCandidates(s: GameState, player: PlayerIndex): GridCoord[] { out.push(c); } } + + /** + * THE NINE-SPOT MODIFIER NEIGHBOURHOOD (§9), which the four-square walk above cannot reach. + * + * A Modifier goes "adjacent to a Facility, on any of the nine nearby spots" — diagonals included — + * and `check` has always accepted all eight. It was the CANDIDATES that were orthogonal-only, so a + * diagonal square with no orthogonal neighbour was legal and never offered: reported by Jesse as + * being unable to place a Modifier to the south-east of his industry. + * + * Only around a Facility, and only in a second pass. Track has to JOIN, and a diagonal shares no + * edge, so a Modifier is the only card these squares can ever take — generating diagonals around + * every card instead costs 54% more simulation time for candidates `check` then rejects. Appending + * rather than interleaving leaves the existing order untouched, which matters because the bot + * breaks ties by first-best: measured over 200 seeded games, 0 played differently. + */ + for (const [key, card] of area.grid) { + if (!card.facility) continue; + const [row, col] = key.split(',').map(Number); + for (const c of [ + { row: row! + 1, col: col! + 1 }, + { row: row! + 1, col: col! - 1 }, + { row: row! - 1, col: col! + 1 }, + { row: row! - 1, col: col! - 1 }, + ]) { + const k = `${c.row},${c.col}`; + if (seen.has(k) || area.grid.has(k)) continue; + seen.add(k); + out.push(c); + } + } return out; } diff --git a/src/engine/track.ts b/src/engine/track.ts index 83664b5..ef489d8 100644 --- a/src/engine/track.ts +++ b/src/engine/track.ts @@ -559,6 +559,31 @@ export function carriesThroughTrack(card: TrackCard): boolean { * a TURNOUT laid on the Running Track — the Office no longer carries a stub of its own, because no * office or industry card on the printed sheet does. */ +/** + * §2.1 — IS THIS SQUARE INSIDE THE DISTRICT AT ALL? + * + * The Limits sign denotes "the limit of your control area", and §3 defines Secondary Track as "all + * tracks IN YOUR LIMITS that are not the Running Track" — so the bound belongs to the district, not + * to the one row the sign happens to stand in. This used to guard the Running Track alone, on the + * reasoning that the rows above and below were unbounded; the consequence was a siding snaking east + * past your own sign, taking industries with it, and a player switching cars on track outside the + * territory §8.1 and §10 reason about. Reported by Jesse: sidings must not be built outside the + * Limits. + * + * INCLUSIVE OF THE SIGN'S OWN COLUMN. The sign stands ON the boundary rather than beyond it, and the + * opening district is signs at ±1 around the Office — so the strict reading would leave exactly one + * buildable column and break §11.3's promise that both Secondary rows, and the nine-spot Modifier + * neighbourhood, are usable from the first Stage. + * + * MODIFIERS ARE NOT SUBJECT TO THIS, and are not track: §9 places one on any of the nine spots + * around a Facility, and a Facility standing at the limit has three of its nine outside them. + * Jesse's call. `check` bars them from the Running Track ROW instead, which is the ground the main + * grows onto. + */ +export function withinLimits(area: OfficeArea, coord: GridCoord): boolean { + return coord.col >= area.limitsWest.col && coord.col <= area.limitsEast.col; +} + export function canPlaceAt(area: OfficeArea, coord: GridCoord, card: TrackCard): boolean { const existing = cardAt(area, coord); // A Limits sign on the Running Track is the GROWTH POINT, not an obstacle. Extending means laying @@ -575,14 +600,12 @@ export function canPlaceAt(area: OfficeArea, coord: GridCoord, card: TrackCard): // the sign itself is the only growth point. Allowing a placement beyond it built track on the far // side of the sign and then planted a SECOND sign further out, leaving the board reading // limits · Whistle Post · limits · straight · limits - // with a Limits card stranded mid-track. The district below the Running Track is unbounded. - if (coord.row === area.runningRow) { - if (coord.col < area.limitsWest.col || coord.col > area.limitsEast.col) return false; - // And it must not BREAK the Running Track. A curve laid here has no east-west road, so the main - // stops dead at it — the bot did exactly this, turning the west end of its own Running Track - // into a stub and cutting the Office off from the Limits. - if (!carriesThroughTrack(card)) return false; - } + // with a Limits card stranded mid-track. + if (!withinLimits(area, coord)) return false; + // And it must not BREAK the Running Track. A curve laid here has no east-west road, so the main + // stops dead at it — the bot did exactly this, turning the west end of its own Running Track + // into a stub and cutting the Office off from the Limits. + if (coord.row === area.runningRow && !carriesThroughTrack(card)) return false; const ports: Port[] = ['n', 's', 'e', 'w']; for (const p of ports) { diff --git a/src/sim/board-svg.ts b/src/sim/board-svg.ts index 501d376..d7e8b75 100644 --- a/src/sim/board-svg.ts +++ b/src/sim/board-svg.ts @@ -383,6 +383,16 @@ export function officeSvg( * offered without a word. Reported as the Depot lighting up for no visible reason. */ legal: { row: number; col: number; label: string }[] = [], + /** + * The columns the Limits signs stand in, drawn as the district's east and west edges. + * + * Omitted by the previews and by the replay viewers' single-card renders, which have no district + * to bound. Where it is given, the boundary is drawn down the WHOLE canvas: track may not be laid + * outside these columns at any row (§2.1), and two signs sitting on one row could not say that — + * a player looking at open ground past a sign had no way to know nothing of his could ever go + * there until the square failed to light up. + */ + limits: { west: number; east: number } | null = null, ): string { const W = 166; const H = 96; @@ -447,7 +457,15 @@ export function officeSvg( return { x: W / 2, y: H }; }; - let out = ``; + /** + * ROOM FOR THE BOUNDARY. The Limits edge is drawn just outside the sign's own column, and the west + * sign is usually the leftmost card on the canvas — so at x = −1.5 the line lands outside a viewBox + * starting at 0 and is simply not there. The margin is added only when there is a boundary to draw. + */ + const margin = limits ? 3 : 0; + let out = + ``; for (const cell of cells) { const x = px(cell.col); const y = py(cell.row); @@ -804,6 +822,22 @@ export function officeSvg( `${esc(l.label)}`; } + /** + * THE LIMITS, AS THE EDGE OF THE BUILDABLE DISTRICT. + * + * Drawn just OUTSIDE the sign's own column, because the sign stands on the boundary rather than + * beyond it: a siding may run under the sign, and the line has to agree with the rule or it + * teaches the wrong one. It moves outward on its own as the Running Track is extended, which is + * the reward for extending made visible. + */ + if (limits) { + const edge = (x: number, side: string): string => + `` + + `LIMITS`; + out += edge(px(limits.west) - PAD / 2, 'w') + edge(px(limits.east) + W + PAD / 2, 'e'); + } + // Mark the Running Track so the spine of the district is unmistakable. out += `RUNNING TRACK`; out += ''; @@ -914,6 +948,10 @@ export const BOARD_CSS = ` text.bs-mod{fill:#c8a04a} .bs-enh{fill:#7fb0e6;font:9px ui-monospace,monospace} .bs-rowlab{fill:#5f6b7a;font:600 9px ui-monospace,monospace;letter-spacing:.1em} +/* THE EDGE OF THE DISTRICT. Deliberately quiet — it is a boundary, not an action — and dashed, so it + reads as a line on the table rather than as rail. Nothing is laid outside it (§2.1). */ +.bs-limitline{stroke:#5f6b7a;stroke-width:1.5;stroke-dasharray:3 5;opacity:.7} +.bs-limitlab{fill:#5f6b7a;font:600 8px ui-monospace,monospace;letter-spacing:.12em} .bs-ghost rect{fill:rgba(90,169,230,.07);stroke:#5aa9e6;stroke-width:2;stroke-dasharray:5 4} .bs-ghost text{fill:#5aa9e6;font:11px ui-monospace,monospace} /* The caption on a legal square that already carries a card. Same blue as the ghost squares and the @@ -924,6 +962,11 @@ text.bs-mod{fill:#c8a04a} .bs-ghost,g[data-cell].bs-legal{cursor:pointer} g[data-cell].bs-legal .bs-card{stroke:#5aa9e6;stroke-width:2.5} g[data-cell].bs-legal:hover .bs-card,.bs-ghost:hover rect{fill:#233246} +/* THE SQUARE THE ACTION UNDER THE CURSOR MEANS. White, and brighter than anything else the board + marks, because it answers a question being asked RIGHT NOW rather than describing a standing + state — and it has to win when it lands on a square that is already legal, focused or blocked. */ +g[data-cell].bs-point .bs-card,g[data-ghost].bs-point rect{stroke:#f2f5f8;stroke-width:3.5;stroke-dasharray:none} +g[data-cell].bs-point .bs-card{fill:#2b3b52} /* WHERE THE CREW CAN GO. Amber, the colour this game spends on "you can do this", so a reachable card reads the same way an action button does. bs-from is where it is standing now. */ g[data-cell].bs-focus .bs-card{stroke:#e0c060;stroke-width:2.5} diff --git a/src/sim/replay.ts b/src/sim/replay.ts index fe0a3fd..ba81d5b 100644 --- a/src/sim/replay.ts +++ b/src/sim/replay.ts @@ -499,7 +499,7 @@ function render() { $('division').innerHTML = divisionSvg(DIV); // The same office renderer the playable app uses, so replay and game draw one board. - $('grid').innerHTML = officeSvg(CELLS, f.runningRow); + $('grid').innerHTML = officeSvg(CELLS, f.runningRow, [], [], f.limits); /* Auto-hide: the district is worth its space during the phases that change it, and folded otherwise. A summary stays, because a panel that vanishes reads as broken. */ diff --git a/src/sim/view.ts b/src/sim/view.ts index 3ba65aa..92e14da 100644 --- a/src/sim/view.ts +++ b/src/sim/view.ts @@ -356,6 +356,15 @@ export type Frame = { cells: CellView[]; /** Which grid row is the Running Track — the spine the district hangs beneath. */ runningRow: number; + /** + * The columns the Limits signs stand in — the district's east and west edges, at EVERY row. + * + * Track may not be laid outside them (§2.1), so a player looking at open ground beyond a sign is + * looking at ground no card of his will ever go on. Two signs on one row could not say that; the + * board draws the boundary down the whole district instead. Carried on the Frame rather than read + * off the area for the usual reason: a remote client holds no `GameState`. + */ + limits: { west: number; east: number }; /** * Moves left in this Local Operations turn, or null outside a switching turn. * @@ -1201,6 +1210,7 @@ export function snapshot( handCount: (s.decks.hands.get(viewer) ?? []).length, objective: objectiveOf(s, viewer), runningRow: area.runningRow, + limits: { west: area.limitsWest.col, east: area.limitsEast.col }, movesLeft: s.clock.phase === 'localOps' && turnOf(s, viewer).option === 'switch' ? turnOf(s, viewer).movesRemaining : null, moves: switchingMoves(s, viewer), blocked: impediments(s, viewer), diff --git a/src/web/game.ts b/src/web/game.ts index 8d89172..3a498d6 100644 --- a/src/web/game.ts +++ b/src/web/game.ts @@ -87,9 +87,36 @@ export type ActionGroup = { * from the browser, which needs the whole `GameState`. A remote client has no state, so the menu * carries it. See `docs/architecture/multiplayer.md` §5. */ - actions: { index: number; label: string; tip?: string }[]; + /** + * `coord` is the square the action HAPPENS ON, when it happens on one. + * + * Carried as data rather than left inside the label. Half these buttons are near-identical + * sentences distinguished only by a coordinate — "(1,3)" against "(-1,3)" — and picking the wrong + * one is recoverable in solitaire, where Undo is a click, and a disaster in a multiplayer game + * where it is not. The page hovers the matching square on the board instead of asking the player + * to read the row and column off the button. Reported by Jesse. + * + * Resolved HERE for the same reason `tip` is: a remote client holds no `GameState` and cannot look + * up where a tray is standing. See `docs/architecture/multiplayer.md` §5. + */ + actions: { index: number; label: string; tip?: string; coord?: { row: number; col: number } }[]; }; +/** + * The square an intent acts on, or null when it acts on none. + * + * Only squares the intent NAMES. A card played out on the Mainline (`node`) is not a district + * coordinate and must not be treated as one — that confusion is exactly why `placement` and `node` + * are separate fields on the intent (see `intents.ts`) — and an action like ending a turn has no + * square at all. + */ +export function coordOf(i: Intent): { row: number; col: number } | null { + if ('at' in i) return i.at; + if ('to' in i) return i.to; + if (i.type === 'card.play' && i.placement) return i.placement; + return null; +} + export type Game = { state: GameState; seed: number; @@ -212,7 +239,13 @@ export function actionGroups(game: Game): { options: Intent[]; groups: ActionGro * to survive every edit to a line nobody would think to check. One stray byte and every crew's * moves silently collapsed back into a single group. The tray is data; it travels as data. */ - type Entry = { index: number; label: string; tip?: string; trayId?: string }; + type Entry = { + index: number; + label: string; + tip?: string; + trayId?: string; + coord?: { row: number; col: number }; + }; const byKind = new Map(); options.forEach((intent, index) => { const label = describeIntent(game.state, intent); @@ -227,14 +260,26 @@ export function actionGroups(game: Game): { options: Intent[]; groups: ActionGro * on the crew as well makes the de-duplication per crew, which is what it always meant. */ if (!list.some((a) => a.label === label && a.trayId === trayId)) { - list.push({ index, label, ...(tip ? { tip } : {}), ...(trayId ? { trayId } : {}) }); + const coord = coordOf(intent); + list.push({ + index, + label, + ...(tip ? { tip } : {}), + ...(trayId ? { trayId } : {}), + ...(coord ? { coord } : {}), + }); } byKind.set(intent.type, list); }); /** Groups take the plain shape; the crew was only ever needed to split them. */ const plain = (entries: Entry[]): ActionGroup['actions'] => - entries.map(({ index, label, tip }) => ({ index, label, ...(tip ? { tip } : {}) })); + entries.map(({ index, label, tip, coord }) => ({ + index, + label, + ...(tip ? { tip } : {}), + ...(coord ? { coord } : {}), + })); const groups: ActionGroup[] = []; const used = new Set(); diff --git a/src/web/main.ts b/src/web/main.ts index cd50c98..9007615 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -301,7 +301,7 @@ function render(): void { : 'ATTACH TO THIS CARD'; return [{ row: cell.row, col: cell.col, label }]; }); - grid.innerHTML = officeSvg(f.cells, f.runningRow, ghostCoords, legalCaps); + grid.innerHTML = officeSvg(f.cells, f.runningRow, ghostCoords, legalCaps, f.limits); // Highlighting rides on top of the drawing: outline the legal squares and make them clickable. for (const [key, list] of spotsAt) { @@ -650,6 +650,51 @@ function renderDistrict(f: Frame): void { }; } +/** + * The square an action button acts on, as an attribute the board can be matched against. + * + * `null` for the actions that act on no square at all — ending a turn, drawing a card, a train card + * that goes to the timetable — so those buttons carry nothing and highlight nothing. + */ +function cellRef(coord: { row: number; col: number } | null | undefined): string { + return coord ? ` data-square="${coord.row},${coord.col}"` : ''; +} + +/** + * POINT AT THE SQUARE THE BUTTON MEANS. + * + * The action list is a column of sentences that differ by a coordinate — "(1,3)" against "(-1,3)" — + * and the board is right beside it saying nothing about which one is which. Hovering a button now + * lights its square up, so the check happens with the eye rather than by reading two numbers off a + * button and finding them on a map. Reported by Jesse: recoverable in solitaire, where Undo is a + * click; a disaster in multiplayer, where it is not. + * + * ON FOCUS AS WELL AS HOVER, so tabbing through the list works the same way as pointing at it — the + * keyboard route is not a lesser one. + * + * BOTH KINDS OF SQUARE. A played card is a `data-cell`; a square being placed ONTO is an empty + * `data-ghost` target, which is precisely the case where the player is choosing between coordinates. + * Matching only one of the two would leave the most mistake-prone moment unhelped. + */ +function wirePointing(node: HTMLElement, key: string): void { + const grid = $('grid'); + const marks = (): Element[] => + [ + grid.querySelector(`g[data-cell="${key}"]`), + grid.querySelector(`g[data-ghost="${key}"]`), + ].filter((g): g is Element => g !== null); + const on = (): void => { + for (const g of marks()) g.classList.add('bs-point'); + }; + const off = (): void => { + for (const g of marks()) g.classList.remove('bs-point'); + }; + node.onmouseenter = on; + node.onmouseleave = off; + node.onfocus = on; + node.onblur = off; +} + function renderActions( menu: Menu, f: Frame, @@ -694,14 +739,19 @@ function renderActions( * the same card in hand explained itself perfectly — reported on "Realignment on Mainline card 3", * which had neither a name for the card it meant nor a word about what it would do. */ - const actionButton = (a: { index: number; label: string; tip?: string }): string => { + const actionButton = (a: { + index: number; + label: string; + tip?: string; + coord?: { row: number; col: number }; + }): string => { const { label, index } = a; const cut = label.indexOf(' — '); const head = cut > 0 ? label.slice(0, cut) : label; // The menu resolved the card's description server-side, so the page never needs the state. const rest = cut > 0 ? label.slice(cut + 3) : (a.tip ?? ''); return ( - `` + `` ); }; @@ -748,7 +798,7 @@ function renderActions( f.moves .map( (m) => - `(${m.from.row}, ${m.from.col})`, ) @@ -847,7 +897,7 @@ function renderActions( ? ` data-tip-html="${esc(piecePreview(sp.links, item.subject))}"` : ''; return ( - `` ); }) @@ -882,6 +932,8 @@ function renderActions( render(); } : () => apply(Number(node.dataset['i'])); + const square = node.dataset['square']; + if (square) wirePointing(node, square); } } diff --git a/src/web/replays.ts b/src/web/replays.ts index 00f6ae5..7437d82 100644 --- a/src/web/replays.ts +++ b/src/web/replays.ts @@ -122,7 +122,7 @@ function show(i: number): void { $('vpos').textContent = `${at} / ${steps.length - 1}`; ($('vscrub') as HTMLInputElement).value = String(at); $('vdivision').innerHTML = divisionSvg(f.division); - $('vgrid').innerHTML = officeSvg(f.cells, f.runningRow); + $('vgrid').innerHTML = officeSvg(f.cells, f.runningRow, [], [], f.limits); // 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'; diff --git a/test/apply.test.ts b/test/apply.test.ts index e2ff8a4..6173052 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -930,6 +930,177 @@ describe('a Modifier only goes beside a host that can use it (regression)', () = // --------------------------------------------------------------------------- +describe('the Limits bound the district, and the nine spots reach round a Facility', () => { + /** + * A district whose Running Track has been extended one square east, so the sign stands at col 2 + * and there is a siding hanging below the main to build on: + * + * row 0: [lim] [office] [turnout, leg south] [lim] <- Running Track, Limits at -1 and 2 + * row -1: [curve ne] + * + * Returns the industry-ready square under the sign, at (-1, 2). + */ + function district(s: GameState): GridCoord { + const area = areaOf(s, 0); + const plain = (geometry: object): TrackCard => ({ + geometry: geometry as TrackCard['geometry'], + baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [], + }); + const col = area.limitsEast.col; + area.grid.set(coordKey(at(area.runningRow, col)), plain({ + kind: 'track', geometry: 'turnout', turnout: { stem: 'w', through: 'e', diverge: 's' }, hand: 'left', + })); + area.grid.set(coordKey(at(area.runningRow - 1, col)), plain({ + kind: 'track', geometry: 'curved', arc: 'ne', hand: 'left', + })); + area.limitsEast = at(area.runningRow, col + 1); + area.grid.set(coordKey(area.limitsEast), plain({ kind: 'limits' })); + s.clock.phase = 'localOps'; + s.clock.currentActor = 0; + turnOf(s, 0).option = 'draw'; + return at(area.runningRow - 1, col + 1); + } + + const trackInHand = (s: GameState, geometry: string): string => { + for (const [id, card] of s.cards) { + if (card.kind.kind === 'track' && card.kind.geometry === geometry) { + s.decks.hands.set(0, [id]); + return id; + } + } + throw new Error(`no ${geometry} card`); + }; + + const industryOf = (s: GameState, facility: string): string => { + for (const [id, card] of s.cards) { + if (card.kind.kind === 'freightFacility' && card.kind.facility === facility) { + s.decks.hands.set(0, [id]); + return id; + } + } + throw new Error(`no industry card: ${facility}`); + }; + + const modifierOf = (s: GameState, modifier: string): string => { + for (const [id, card] of s.cards) { + if (card.kind.kind === 'modifier' && card.kind.modifier === modifier) { + s.decks.hands.set(0, [id]); + return id; + } + } + throw new Error(`no modifier card: ${modifier}`); + }; + + /** An industry standing on the board, so a Modifier has a host to hang off. */ + const buildFacility = (s: GameState, kind: string, coord: GridCoord): void => { + areaOf(s, 0).grid.set(coordKey(coord), { + geometry: { kind: 'facility', facility: kind, axis: 'ew' }, + baseOperationalRail: true, standing: [], modifiers: [], enhancements: [], + facility: { + kind: 'freight', subtype: kind, + allows: { outbound: true, inbound: false }, + outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 }, + menAtWork: [null, null, null], + industryTrack: { cars: [] }, + laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 }, + }, + } as never); + }; + + it('accepts a siding in the sign\'s own column and refuses one past it', () => { + /** + * REPORTED by Jesse: "sidings should not be allowed to be built outside the limits". §3 defines + * Secondary Track as "all tracks IN YOUR LIMITS that are not the Running Track", so the bound + * belongs to the district rather than to the one row the sign stands in — it used to guard the + * Running Track alone, and a siding could run east past a player's own sign, taking industries + * with it, onto track that §8.1 and §10 do not consider his territory at all. + * + * INCLUSIVE of the sign's column, which is the half that keeps the game playable: the opening + * district is signs at ±1 around the Office, so the strict reading would leave one buildable + * column and break §11.3's promise that both Secondary rows are usable from the first Stage. + */ + const s = game(); + const under = district(s); + const cardId = trackInHand(s, 'straight'); + assert.equal( + check(s, 0, { type: 'card.play', cardId, placement: under }), + null, + 'a siding may run under the Limits sign — the sign stands ON the boundary, not beyond it', + ); + assert.equal( + check(s, 0, { type: 'card.play', cardId, placement: at(under.row, under.col + 1) }), + 'OUTSIDE_LIMITS', + 'a siding was built outside the district it belongs to', + ); + }); + + it('refuses an industry outside the Limits — a Facility carries track', () => { + // §11.2: "Facility cards carry their own rails — placing a Facility places track." So the bound + // is the same one, and said with the same code rather than left to read as NOT_CONNECTED. + const s = game(); + const under = district(s); + const cardId = industryOf(s, 'mineTipple'); + assert.equal(check(s, 0, { type: 'card.play', cardId, placement: under }), null); + assert.equal( + check(s, 0, { type: 'card.play', cardId, placement: at(under.row, under.col + 1) }), + 'OUTSIDE_LIMITS', + ); + }); + + it('offers a Modifier the DIAGONAL spots around its host, not just the four orthogonal ones', () => { + /** + * REPORTED by Jesse: a Modifier could not be placed to the south-east of his industry. §9 places + * one "adjacent to a Facility, on any of the nine nearby spots" and `check` has always accepted + * all eight neighbours — it was `placementCandidates` that walked north, south, east and west + * only, so a diagonal square with no orthogonal neighbour was legal and never offered. + */ + const s = game(); + const under = district(s); + buildFacility(s, 'packingSheds', under); + const cardId = modifierOf(s, 'iceHouse'); + + const offered = legalActions(s, 0) + .filter((i) => i.type === 'card.play' && i.cardId === cardId && i.placement !== undefined) + .map((i) => coordKey((i as { placement: GridCoord }).placement)); + + // South-east of the host, and orthogonally adjacent to nothing at all. + const southEast = at(under.row - 1, under.col + 1); + assert.ok( + offered.includes(coordKey(southEast)), + `the south-east spot (${southEast.row}, ${southEast.col}) is legal but was never offered — offered: ${offered.join(' ')}`, + ); + assert.equal(check(s, 0, { type: 'card.play', cardId, placement: southEast }), null); + }); + + it('lets a Modifier hang outside the Limits, but never in the Running Track row', () => { + /** + * Jesse's call, both halves. A Modifier is not track (§9), so a host standing at the limit keeps + * all nine of its spots — refusing the outer three would make the card unplayable exactly where + * the district ends. The Running Track ROW is the exception: inside the Limits that row is + * always full, so this bites only beyond the sign, and that is the ground the main grows onto — + * a Modifier parked there would block the player's own sign from moving outward (§2.1). + */ + const s = game(); + const under = district(s); + buildFacility(s, 'packingSheds', under); + const cardId = modifierOf(s, 'iceHouse'); + const area = areaOf(s, 0); + + assert.equal( + check(s, 0, { type: 'card.play', cardId, placement: at(under.row, under.col + 1) }), + null, + 'a Modifier beside a host at the limit was refused the spot outside it', + ); + assert.equal( + check(s, 0, { type: 'card.play', cardId, placement: at(area.runningRow, under.col + 1) }), + 'ON_RUNNING_TRACK', + 'a Modifier was allowed to stand in the row the Running Track grows along', + ); + }); +}); + +// --------------------------------------------------------------------------- + describe("a Modifier grants only what its host's flow can use", () => { const withFacility = (kind: string, out: boolean, into: boolean) => ({ geometry: { kind: 'facility', facility: kind, axis: 'ew' }, @@ -1111,8 +1282,13 @@ describe('Industry cards go on a stub, and lock each other out', () => { * A district with a siding hanging off the Running Track, which is the only place an industry may * go. Returns the siding square east of the curve. * - * row 0: [lim] [office] [turnout, leg south] … <- Running Track + * row 0: [lim] [office] [turnout, leg south] [lim] <- Running Track * row -1: [curve ne] [siding square] + * + * The turnout is laid ON the east Limits sign, which is how the Running Track grows — so the sign + * MOVES OUT with it (§2.1, Gap 4a), exactly as `extendLimitsIfNeeded` does when the card is played + * rather than written straight into the grid. Without that the siding square would be outside the + * district's own Limits, which is no longer a place track may go. */ function withSiding(s: GameState): GridCoord { const area = areaOf(s, 0); @@ -1127,6 +1303,8 @@ describe('Industry cards go on a stub, and lock each other out', () => { area.grid.set(coordKey(at(area.runningRow - 1, col)), plain({ kind: 'track', geometry: 'curved', arc: 'ne', hand: 'left', })); + area.limitsEast = at(area.runningRow, col + 1); + area.grid.set(coordKey(area.limitsEast), plain({ kind: 'limits' })); s.clock.phase = 'localOps'; s.clock.currentActor = 0; turnOf(s, 0).option = 'draw'; diff --git a/test/web.test.ts b/test/web.test.ts index 4aabe1c..4e6be5b 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -598,6 +598,54 @@ describe('the board shows freight work happening', () => { assert.ok(y > 48, `the caption at y=${y} prints over the rail`); }); + it('draws the Limits as the edge of the buildable district, at every row', () => { + /** + * Track may not be laid outside the Limits at ANY row now (§2.1), and two signs sitting on the + * Running Track could not say that: a player looking at open ground beyond a sign had no way to + * know nothing of his would ever go there until the square failed to light up. + * + * Just OUTSIDE the sign's own column, because the sign stands ON the boundary — a siding may run + * under it — so a line drawn inside the sign would teach the opposite of the rule. + */ + const game = newGame(555); + const area = game.state.officeAreas.get(0)!; + const f = view(game); + assert.deepEqual( + f.limits, + { west: area.limitsWest.col, east: area.limitsEast.col }, + 'the Frame does not carry the district edges a remote client cannot look up', + ); + + const svg = officeSvg(f.cells, f.runningRow, [], [], f.limits); + const xs = [...svg.matchAll(/class="bs-limitline" x1="(-?[\d.]+)"/g)].map((m) => Number(m[1])); + assert.equal(xs.length, 2, 'the district was drawn without both of its edges'); + + // The sign cards themselves must fall INSIDE the two lines. + const signX = [...svg.matchAll(/data-cell="(-?\d+),(-?\d+)"[^>]*transform="translate\((-?[\d.]+)/g)] + .filter((m) => Number(m[2]) === area.limitsWest.col || Number(m[2]) === area.limitsEast.col) + .map((m) => Number(m[3])); + assert.ok(signX.length > 0, 'no Limits sign was drawn to check the boundary against'); + for (const x of signX) { + assert.ok( + x >= Math.min(...xs) && x <= Math.max(...xs), + `a Limits sign at x=${x} sits outside its own boundary (${xs.join(', ')})`, + ); + } + assert.match(svg, /bs-limitlab/, 'the boundary is drawn but never named'); + + // AND ON THE CANVAS. The west sign is the leftmost card, so a line drawn outside its column sits + // at a negative x — off a viewBox that starts at 0, which is a boundary nobody can see. + const vb = /viewBox="(-?[\d.]+) (-?[\d.]+) ([\d.]+) ([\d.]+)"/.exec(svg); + assert.ok(vb, 'the board produced no viewBox'); + const x0 = Number(vb![1]); + for (const x of xs) { + assert.ok( + x >= x0 && x <= x0 + Number(vb![3]), + `the boundary at x=${x} is drawn outside the canvas (${x0} to ${x0 + Number(vb![3])})`, + ); + } + }); + it('says on the card which way an industry runs, and how many workers it has', () => { /** * TWO REPORTS, ONE CARD. @@ -1314,7 +1362,7 @@ describe('the static build', () => { } }); - it('renders clickable highlights on the board when a card is picked', async () => { + it('renders clickable highlights on the board, and points at the square an action names', async () => { // The real check: load the EMITTED bundle with a DOM stub, click a card, and confirm the board // came back with highlighted squares wired to handlers. Asserting the data has coordinates says // nothing about whether the page draws them. @@ -1366,7 +1414,12 @@ describe('the static build', () => { const classes = new Set(); found.set(key, { onclick: null, - classList: { add: (c: string) => void classes.add(c), has: (c: string) => classes.has(c) }, + classList: { + add: (c: string) => void classes.add(c), + // Pointing at a square is a HOVER: it has to come off again when the cursor leaves. + remove: (c: string) => void classes.delete(c), + has: (c: string) => classes.has(c), + }, classes, }); } @@ -1493,6 +1546,37 @@ describe('the static build', () => { const lit = (grid['highlighted'] as () => unknown[])(); const ghosts = /data-ghost="/.test(html); assert.ok(lit.length > 0 || ghosts, 'picking a card highlighted nothing on the board'); + + /** + * POINTING AT THE SQUARE A BUTTON MEANS — wired on the emitted bundle, not asserted off the menu. + * + * The menu carrying a coordinate proves nothing about whether the page draws it, which is the + * same reason the highlight check above loads the real bundle. Reported by Jesse: the action list + * is a column of near-identical sentences separated by "(1,3)" against "(-1,3)", and picking the + * wrong one is a click to undo in solitaire and unrecoverable in a multiplayer game. + */ + const withSquare = ( + (actions['querySelectorAll'] as (s: string) => Record[]) + .call(actions, 'button.act') + .filter((n) => (n['dataset'] as Record)['square']) + ); + assert.ok(withSquare.length > 0, 'no action button carries the square it acts on'); + const btn = withSquare[0]!; + const key = (btn['dataset'] as Record)['square']!; + assert.match(key, /^-?\d+,-?\d+$/, `"${key}" is not a grid coordinate`); + (btn['onmouseenter'] as (() => void) | null)?.(); + const pointed = (grid['highlighted'] as () => Record[])().filter((g) => + (g['classes'] as Set).has('bs-point'), + ); + assert.equal(pointed.length, 1, `hovering the action for (${key}) marked ${pointed.length} squares`); + (btn['onmouseleave'] as (() => void) | null)?.(); + assert.equal( + (grid['highlighted'] as () => Record[])().filter((g) => + (g['classes'] as Set).has('bs-point'), + ).length, + 0, + 'the square stayed lit after the cursor left the button', + ); }); it('busts the cache on every module, so a deploy cannot half-load', () => {