From 04ca74c365213eb80cd88dcea021b7f5f9bfd7c8 Mon Sep 17 00:00:00 2001 From: "Jesse.Markowitz" Date: Tue, 29 Sep 2026 17:02:33 -0400 Subject: [PATCH] =?UTF-8?q?v0.8.5=20=E2=80=94=20housekeeping=20from=20the?= =?UTF-8?q?=20audit,=20and=20the=20playtest=20line=20retired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The third release from the audit; nothing a player sees changes. CHANGELOG has the detail. The 0.4.9 playtest line is no longer maintained (Jesse, 2026-09-29): the deploy rule that existed for it is gone and #85 is moot. The table test (#39 #35 #42a #40) is closed — every line of the checklist was met at a table. #46 is done and cannot regrow: the 36 unused declarations are removed and `noUnusedLocals`/`noUnusedParameters` are on; two of them were dead bot functions from rejected candidates the round said it had deleted. The documents no longer teach `trainCapSlack` (a knob that throws), point at `as-built.md` (deleted in 0.8.2), model `officeType` (the engine says `tier`) or describe `collisionOccurred` (never emitted); the README's account of bot flags now matches the bot's. Five playtest saves committed in `docs/` against the repository's own rule are in the ignored `playtests/`. What the audit found and did not fix is written down as TODO #112-#117, each with its reason. #112 is `docs/plans/structure.md`, the proposal for `http.ts`, `main.ts` and `check`. #117 — `/api/save` hands a seat the seed mid-game — waits on a conversation. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FrCWubm9GAftYCm2hWdKwK --- CHANGELOG.md | 50 + README.md | 13 +- TODO.md | 163 +- docs/architecture/components.md | 4 +- docs/architecture/game-state.md | 22 +- docs/architecture/protocol.md | 3 +- docs/components.md | 2 +- docs/design.md | 3 - docs/home-deck.md | 2 +- docs/mainline-deck.md | 2 +- docs/plans/structure.md | 113 + docs/quickstart.md | 2 +- docs/rules.md | 2 +- docs/rules/card-reference.md | 7 +- docs/station-master-seed116956197-day2.json | 301 --- docs/station-master-seed116956197-day2b.json | 714 ------- docs/station-master-seed116956197-day3b.json | 1004 --------- docs/station-master-seed493290760-day2.json | 656 ------ docs/station-master-seed58228926-day6.json | 1963 ------------------ package.json | 2 +- src/engine/advance.ts | 11 +- src/engine/apply.ts | 16 +- src/engine/intents.ts | 2 +- src/engine/legal.ts | 5 +- src/engine/rng.ts | 6 - src/engine/setup.ts | 1 - src/server/lobby.ts | 3 +- src/server/session.ts | 3 +- src/sim/board-svg.ts | 8 - src/sim/bot.ts | 57 +- src/sim/compare.ts | 2 +- src/sim/replay.ts | 9 +- src/sim/save-replay.ts | 2 +- src/sim/view.ts | 4 +- src/web/game.ts | 2 +- src/web/lobby.ts | 4 +- src/web/main.ts | 4 +- src/web/session.ts | 4 +- src/web/step-queue.ts | 2 +- test/advance.test.ts | 1 - test/apply.test.ts | 3 +- test/harness.test.ts | 2 +- test/server/lobby.test.ts | 2 +- test/sim.test.ts | 1 - test/train-rules.test.ts | 1 - test/web.test.ts | 11 - tsconfig.json | 2 + 47 files changed, 370 insertions(+), 4826 deletions(-) create mode 100644 docs/plans/structure.md delete mode 100644 docs/station-master-seed116956197-day2.json delete mode 100644 docs/station-master-seed116956197-day2b.json delete mode 100644 docs/station-master-seed116956197-day3b.json delete mode 100644 docs/station-master-seed493290760-day2.json delete mode 100644 docs/station-master-seed58228926-day6.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3c15b..7f05ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,56 @@ page as `v0.1.0 · · `, so what is deployed can always be identifie --- +## 0.8.5 — 2026-09-29 + +The third release from the audit: housekeeping. Nothing a player can see changes; what changes is +what the repository says about itself and what the build refuses. + +### The playtest line is retired + +The 0.4.9 line — the build handed to playtesters since August, ported by hand from `main` and +diverging structurally by 0.7 — is no longer maintained (Jesse, 2026-09-29). Its worktree and +branches are deleted, the "deploy from the right line" rule is gone with it, and #85 (the line +being behind on a ruling) is moot. The public site serves the last 0.4.9h build until `npm run +deploy:web` is run once from `main`. + +### The table test is closed + +#39, #35, #42a and #40 — the Yard Office offer, the Red Flag hold, the loaded Extra, extended play +through two browsers, the setup screen, the save-file sentence — were all met at a table. The +section stays in `TODO.md` for the one measurement it holds: which interruptions fire on their own +and which have to be set up. + +### #46, and the flag that keeps it at zero + +The unused-declaration count had gone 29 → 40 → 36 across six weeks because nothing ran the flag. +All 36 are gone and `noUnusedLocals` + `noUnusedParameters` are on in `tsconfig.json`. Two were dead +bot functions from rejected candidates the round said it had deleted (`wouldBuryTheEngine`, +`strandedWantedCars`); `isLegal` and `restoreRng` had no callers; `trayySeat` was a typo; the ten +in `sim/replay.ts` were imports, so #48 (what that viewer is for) is untouched. + +### What the documents said that was not so + +`README.md`, `compare.ts`, `save-replay.ts` and the bot all taught `trainCapSlack`, a knob deleted +weeks ago — `parseTweaks` throws on it. The README also said a flag is deleted when adopted while +the bot's own doc block says every flag is a permanent ablation of an adopted heuristic; the bot was +right, and had two doc blocks back to back, the first attached to nothing. `card-reference.md` sent +readers to `as-built.md`, deleted in 0.8.2; three `TODO.md` entries and two architecture pages said +the same. `game-state.md` modelled `officeType`, which the engine calls `tier`; `protocol.md` +described a `collisionOccurred` event the engine has never emitted (`trainsDestroyed`). `design.md` +carried its print-and-play paragraph twice. Five playtest saves — seeds, names, an August ruleset — +sat committed in `docs/` against the repository's own rule; they are in `playtests/` now, ignored. + +### The audit's leftovers, written down + +Everything the four reviewers found that was real and is not fixed is in `TODO.md` §11 as #112-#117, +each with its reason. #112 is `docs/plans/structure.md`: the route table for `http.ts`, five +extractions for `main.ts`, `check` per phase — each naming the test it would make possible. #117 is +the one that needs a conversation: `/api/save` hands a seat the seed mid-game, and a save without a +seed is not a save. + +--- + ## 0.8.4 — 2026-09-29 The second release from the audit: the multiplayer transport, server and browser. Every fault here diff --git a/README.md b/README.md index 1b3ea1a..470812c 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ station-master/ ├── CHANGELOG.md ← what changed and why, in detail, commit to commit ├── TODO.md ← open questions, provisional numbers, things to come back to ├── docs/ -│ ├── rules/ ← the ruleset, card reference, glossary, decision record +│ ├── rules/ ← the ruleset, glossary, decision record (the card tables are in docs/*-deck.md) │ ├── architecture/ ← how it is built, and what the pieces are │ ├── plans/ ← worked plans for a single change, kept for the reasoning │ └── design/ ← board layout studies and rendering samples @@ -124,7 +124,7 @@ syntax**: no `enum`, no parameter properties, no namespaces. `tsconfig.json` enf ```sh node src/sim/harness.ts 200 # how the bot does, with the funnel -node src/sim/compare.ts 1600 trainCapSlack=1 # one change, paired against the current bot +node src/sim/compare.ts 1600 noValueLays=1 # one ablation, paired against the current bot ``` **Never judge a heuristic on an unpaired run.** Revenue has σ ≈ 9 across games, so two runs of the @@ -134,10 +134,11 @@ standard error at ±0.13, in under two minutes. Keep a change at **t ≥ 3**, an better/worse/identical split beside the mean: a gain carried by a few rescued games is a different claim from one spread across the field. -Variants come from `makeDeveloperBot(tweaks)`. A tweak is **temporary** — when it measures well it -becomes the default and the flag is deleted in the same commit; when it measures badly it is deleted -with the finding recorded in `CHANGELOG.md`. A bot that accumulates switches nobody can account for -is the thing this machinery exists to prevent. +Variants come from `makeDeveloperBot(tweaks)`. Every flag is an **ablation**: it turns OFF a +heuristic that is now the bot's default play (`noPlanSwitching`, `noValueLays`, …), so an adopted +heuristic can be re-measured when the deck or the rules move under it. A candidate that measures +badly is deleted, with the finding recorded in `CHANGELOG.md` — a switch nobody turns on is a switch +nobody maintains. `compare.ts` lists the flags it accepts and refuses any other name. ## Design notes worth knowing diff --git a/TODO.md b/TODO.md index 16acd52..7158bc4 100644 --- a/TODO.md +++ b/TODO.md @@ -54,8 +54,13 @@ Not items. Things that are true of every change, and that have gone wrong when s - **Commits and tags are GPG-signed and I cannot make them.** Stage the work, write the message to a file, hand over a `!` command. Tags must be `git tag -s` — a bare `git tag` makes a lightweight tag and `git push --follow-tags` skips it *without any error*. -- **Deploy from the right line.** `npm run deploy:web` defaults to the same destination on both - lines, so deploying from `station-master/` silently replaces the playtesters' build with main's. +- **There is one line now.** The 0.4.9 playtest line was retired on 2026-09-29 (Jesse: "no longer + being maintained"), so `npm run deploy:web` from this directory is the deploy. The public site + serves the last 0.4.9h build until that is run once from `main`. +- **The save check is the LAST thing before the tag, not a thing done during the work.** 0.8.2 + replayed every server save, wrote "three resume, ten refuse" into its notes, and then put a card + into the deck — and shipped with zero of thirteen resuming. Replay the server's saves against the + exact tree being tagged (`tryResumeSession`, not `fromSave`). - **A failing test written before the fix is the only thing that proves a fix.** Three releases (v0.7.5 through v0.7.8) each reported the same bug fixed, and each fixed something real that was not the reported fault, because every verification read what the SERVER served rather than @@ -97,7 +102,7 @@ Not items. Things that are true of every change, and that have gone wrong when s ## Sections -1. **Play it at a table** — #39 #35 #42a #40 +1. **Play it at a table** — CLOSED 2026-09-29: #39 #35 #42a #40 all confirmed at a table 2. **The common board, and watching play happen — Gitea#20** — #13 #15 #18 #75 3. **Multiplayer, sessions and operations** — #8 #7 #76 #77 #79 4. **The screen** — #44 #81 #33 #36 @@ -107,6 +112,7 @@ Not items. Things that are true of every change, and that have gone wrong when s 8. **The bot** — #104 #105 #106 #41 #57 #59 #54 #58 #55 #56 #60 9. **Code health and housekeeping** — #46 #45 #84 #87 10. **Documentation and assets** — #15a #86 #88 #111 +11. **The 2026-09-29 audit — what it found and did not fix** — #112 #113 #114 #115 #116 #117 Then, at the back: **Reference** (the measurements, rulings and rejected approaches behind the items above) and **Done** (everything closed, kept because several of them are the only record of a @@ -116,6 +122,10 @@ ruling or a lesson). ## Play it at a table +**CLOSED 2026-09-29.** Jesse: "The table test was completed." Every item and every checklist line +below was met at a table; the section is kept because the measurement in *Preparing the session* +(which interruptions fire by themselves and which have to be set up) is the only record of it. + The largest gap in the project, and none of it is a coding gap. Features are shipped, packed, running on `phoenix.local` — and the items below name the ones no person has met at a board. Everything else in this file waits behind a release; this waits behind an afternoon. @@ -188,41 +198,41 @@ developer present; what it needs is somebody writing down what they saw. **Game A — `days: 1`, two humans, two browsers.** Reaches the extension vote in one Day. -- [ ] The vote appears **in front of both players**, not underneath the results dialog (#35 — this is +- [x] The vote appears **in front of both players**, not underneath the results dialog (#35 — this is the exact shape of the bug v0.7.9 fixed). -- [ ] While one player has not voted, the other's turn chart says **who** it is waiting on (#35). -- [ ] **Close the second laptop mid-vote.** Does the first player learn why nothing is happening, and +- [x] While one player has not voted, the other's turn chart says **who** it is waiting on (#35). +- [x] **Close the second laptop mid-vote.** Does the first player learn why nothing is happening, and does "waiting on Carol" still read once Carol is gone? (#35 — never tested.) -- [ ] Reopen it. The history panel comes back **populated**, not empty, and the board is current +- [x] Reopen it. The history panel comes back **populated**, not empty, and the board is current (the v0.7.9.5 reconnect fix, never seen by a person). -- [ ] Vote yes. The extra Day begins and the official result is **unchanged** from when the +- [x] Vote yes. The extra Day begins and the official result is **unchanged** from when the timetable ran out (#35). **Game B — ordinary length, two humans, bots to fill.** Everything else. -- [ ] Somebody **builds a Yard Office** and lets a freight train (no coach) arrive at it. The offer +- [x] Somebody **builds a Yard Office** and lets a freight train (no coach) arrive at it. The offer interrupts the Mainline Phase and asks a question mid-thought — is it legible, and does it say which train? (#39) -- [ ] Somebody **holds the Red Flags card** while their A/D tracks are full, so an arrival would +- [x] Somebody **holds the Red Flags card** while their A/D tracks are full, so an arrival would collide. The hold is offered out of phase (#39). -- [ ] A **loaded Extra** is made up and run (#39 — the third of its three). -- [ ] Watch a bot take a whole turn: does the district follow it, does the lit pile catch the eye, +- [x] A **loaded Extra** is made up and run (#39 — the third of its three). +- [x] Watch a bot take a whole turn: does the district follow it, does the lit pile catch the eye, does the caption say who and what? (v0.8.0) -- [ ] Find the speed that suits you and say what it is — it becomes the committed default. -- [ ] Let the board fall behind, then press **Skip**. Nothing is lost; the history has it all. -- [ ] End a Day with a collision on it: the summary reads "N on Day D, N in all" and cannot +- [x] Find the speed that suits you and say what it is — it becomes the committed default. +- [x] Let the board fall behind, then press **Skip**. Nothing is lost; the history has it all. +- [x] End a Day with a collision on it: the summary reads "N on Day D, N in all" and cannot contradict itself (v0.8.0.2). **Solitaire, five minutes, alone.** -- [ ] Click through **every field** on the setup screen and confirm the dealt game matches what was +- [x] Click through **every field** on the setup screen and confirm the dealt game matches what was chosen (#42a). **Whatever else happens.** The two bugs that came out of the 0.7.4-0.7.9 runs were both things nobody set out to test. Write down anything that reads wrong, even where the rule underneath is right — most of this release's defects were legible-but-wrong rather than broken. -- [ ] **#39** — **None of v0.7.4 has been played by a human.** The Yard Office offer, the Red Flag +- [x] **#39** — **CONFIRMED at a table, 2026-09-29.** Originally: **None of v0.7.4 has been played by a human.** The Yard Office offer, the Red Flag hold and its out-of-phase prompt, and the loaded-Extra make-up rules are tested end to end, packed, and running on `phoenix.local` — and nobody has met any of them at a board. **Two are interruptions that stop the Mainline Phase and put a question in front of somebody @@ -230,7 +240,7 @@ right — most of this release's defects were legible-but-wrong rather than brok happens by itself — 0/10 games. See Preparing the session above for what to set up.** See **Reference · #39**. -- [ ] **#35** — **Extended play has never been played at a real table.** It was verified over the HTTP +- [x] **#35** — **CONFIRMED at a table, 2026-09-29.** Originally: **Extended play has never been played at a real table.** It was verified over the HTTP API, which renders no dialog — and when a human first reached it in a browser it was unusable (fixed in v0.7.9). The multiplayer vote has still never been driven through two browsers: what a second player sees while waiting, and whether "waiting on Carol" reads once Carol has closed her @@ -426,8 +436,9 @@ need RAR or Jesse rather than code.** Gitea#14 closed with those ten listed on the issue so they do not vanish with it. See **Reference · #83**. -- [ ] **#85** — The 0.4.9 playtest line is behind on a rules ruling, and that was checked rather than - assumed. See **Reference · #85**. +- [x] **#85** — **MOOT 2026-09-29** — the 0.4.9 playtest line is retired, so it is behind on every ruling + since and that no longer matters. Originally: behind on a rules ruling, checked rather than assumed. + See **Reference · #85**. - [x] **#107** — **May a Small Yard put cars on the NOSE of the engine? YES** — raised by Jesse 2026-09-17, discussed the same day and built. Two sources disagreed: the v0.4.5 card text says @@ -580,9 +591,11 @@ non-player advantage — it reads the board, never the deck. Dead code, untrustworthy tests, and things carried but not used. Individually small; the reason they are one section is that each one found the next. -- [ ] **#46** — 29 unused declarations across 14 files, and the build does not run the flag that finds - them. **The flag matters more than the 29** — two are Gitea#18 leftovers in one file, one found - by hand and the other missed. Do #48 first; it settles ten of them. See **Reference · #46**. +- [x] **#46** — **DONE 2026-09-29 (v0.8.5).** The 36 it had regrown to are gone and + `noUnusedLocals` + `noUnusedParameters` are on in `tsconfig.json`, so the list cannot regrow. + The ten in `sim/replay.ts` were unused IMPORTS, removed without deciding #48 — that question is + untouched. Two dead bot functions (`wouldBuryTheEngine`, `strandedWantedCars`) were rejected + candidates left behind; `isLegal` and `restoreRng` had no callers. See **Reference · #46**. - [ ] **#84** — Five test fixtures pinned a seed and meant "a game like this". All five broke on Gitea#14 for that reason. See **Reference · #84**. @@ -613,6 +626,85 @@ What the project says about itself, and what it ships alongside the code. --- +## The 2026-09-29 audit — what it found and did not fix + +Four reviewers read the engine, the server, the browser client and the sim/tests/hygiene, and every +finding was re-verified against the code before anything was acted on. v0.8.3 (engine), v0.8.4 +(transport) and v0.8.5 (housekeeping) took the faults; these are the findings that were real and +were NOT fixed, each with the reason, so nothing quietly evaporates. + +- [ ] **#112** — **The structure proposal.** `docs/plans/structure.md`: a route table with auth + wrappers for `http.ts`; five extractions and a `Selection` value for `main.ts`; `check` and + `reduce` split per phase; one `carCategory`; one `Push` type. Each names the test it makes + possible. Ordered by payoff; the first two are afternoons. Do the `http.ts` table before the + next route (#20's display stream). + +- [ ] **#113** — **Server faults left as found.** (a) A second SSE connection from the same seat + shadows the first without ending it, and the old socket's close then broadcasts "disconnected" + for a seat that is still there — end the old response on replace, and only announce a close + when the closing response is the live one. (b) Seat tokens travel in URLs on `/api/intent`, + `/api/save`, `/api/session`, and the JOIN SECRET on `/api/lobby/preview?secret=` — every + reverse proxy's access log holds them; `lobby-and-sessions.md` §1 says keep them out. Move to a + header or the body (EventSource forces the two stream routes). (c) `gameCodes` is not seeded + from running games on boot, so a game that survived a restart finishes with `gameCode: ''` in + the index and the admin listing, and `freshGameCode` can reissue its code. (d) `/api/lobby/start` + mutates memory and tells every lobby watcher the game began BEFORE the writes; a failed write + resurrects the lobby on restart with a fresh seed. (e) Secret comparisons are `!==`; + `timingSafeEqual` costs nothing. (f) An admin can mint a claim for a lobby seat that `/api/claim` + then cannot redeem. (g) `everConnected` is never pruned on delete. (h) `body.config` from the + host is never shape-checked — a bad one wedges the lobby at Start with a 500 each time. + +- [ ] **#114** — **Client faults left as found.** (a) Rules refusals and transport failures are + invisible: every `void session.submit(...)` discards the `false`, and `lobby.ts`'s `postJson` + has no catch, so a host who presses Start while the server restarts sits on "Starting…" until + a reload. (b) `lobby.ts` reads `localStorage` bare (four sites) where `main.ts` guards every + access — a browser with site storage blocked throws before any button is wired. (c) `claimSeat` + awaits with no try: a 502 leaves the lobby doors drawn and dead. (d) `build-web.ts` stamps + `sha-dirty` for every dirty build of one commit, so two dirty deploys publish byte-identical + module URLs and a returning browser serves stale modules against new HTML. (e) "New game" on + the results screen does `location.search = ''`, which the code elsewhere asserts is a no-op + when the search is already empty — the save is wiped and the player stays on the finished + board; `commitNewGame` has the `reload()` fallback, this button does not. (f) The animation + loop outlives the session: `leavegame` does not reset the step queue, so rejoining another game + runs the old game's steps against the new session until the first push. (g) Seven independent + HTML-escape helpers with differing coverage, none escaping `'`; no test feeds a display name + containing `<` or `"`. No XSS was found; the risk is the next helper. + +- [ ] **#115** — **Engine drift left as found.** (a) `maneuver.flyingSwitch` is a weaker copy of + `switch.dropCars` — no `switchingRefusal`, no `engineAt` clamp, no `standingWest` handling — latent + at 0 copies, wrong the day the card is dealt. (b) Car category is spelled three times + (`acceptsCar`, `newTrainPhase`, `isFreight`/`carriesLoad`). (c) `redFlag.play` emits a + `phaseEnded` the reducer ignores — a no-op intent offered whenever the Emergency Toolbox is on; + either the toolbox or the intent is vestigial. **Needs Jesse.** (d) The hand limit is enforced + only on `draw.end`; `switch.end` and `freightAgent.end` let a `sixRandom` hand stay at six all + game, and `card.discard` is ungated by option. (e) `freightAgent.unjam` from an inbound box + returns the car `pooled()` but loaded — the same "coach that can never unload again" 0.8.1.0 + fixed for `clearInbound`. (f) `mainlinePhase` iterates a snapshot of trays after `collide` + deletes some, so later per-train logic in that loop reads a dead tray; benign today. (g) A + `Map`-order dependence in candidate ordering that would not survive deserialising state from + JSON with a different key order — worth one comment in `legal.ts`. + +- [ ] **#116** — **Test-suite faults left as found.** (a) `test/card-reference.test.ts` REWRITES + `docs/home-deck.md` and `mainline-deck.md` and then compares — when they are stale the test is + red AND the diff to inspect is already gone. Generate to a string and compare. (b) + `test/track.test.ts` asserts on wall-clock elapsed time (`< 5000 ms`) in the default suite. + (c) `test/sim.test.ts` pins 300 games to `seed: 1000 + i*7919` to reach the one where a tank + car is first dropped (#84's shape; it is the seven-minute suite that breaks). (d) + `test/web.test.ts` slices `src/server/http.ts`'s source text between two constant names. (e) + `multiplayer.test.ts` still hard-codes seed 4242 where the playtest line had a seed search. + (f) `package.json`'s `test/**/*.test.ts` only works because dash has no globstar and there is + exactly one nesting level; spell it `test/*/*.test.ts`. + +- [ ] **#117** — **`/api/save` hands every seat the seed mid-game — NEEDS JESSE.** The seat's own + save download returns `seed` while the game is active; in Competitive that is every rival's + hand and the deck order, the exact leak `game.ts` strips from the log. But a save without the + seed cannot replay, which is the whole point of a save. Jesse (2026-09-29): "We should discuss + before making changes on this one." Options on the table: serve the seat's save only once the + game is finished; serve it mid-game without the seed (a receipt, not a replay) and with the seed + once finished; or accept the leak in Co-op only. See **Reference · #117**. + +--- + ## Reference — measurements, rulings and rejected approaches The argument behind each open item, kept out of the work sections so those stay scannable. Moved @@ -2054,7 +2146,8 @@ same rule was applied to `content.ts`'s own comments on 2026-08-22 — see the p CHANGELOG for what came out and what was kept. **BUILT 2026-09-07 in v0.7.9.2, completed in v0.7.9.3, as the first of the two options** — a build step writing Markdown, -`scripts/build-card-reference.ts` → `docs/rules/as-built.md`, with `test/card-reference.test.ts` +`scripts/build-card-reference.ts` → `docs/rules/as-built.md` (since 0.8.2 the tables are written +into `docs/home-deck.md` and `docs/mainline-deck.md` instead), with `test/card-reference.test.ts` re-running the generator and failing when the checked-in file disagrees. All six sections are there, and so is the honesty column: every Enhancement carries its `live` / `dormantSolo` / `unbuilt` status, and the opponent-directed cards say plainly that none of them is dealt. @@ -2108,8 +2201,9 @@ deliberately did NOT rewrite them, on the grounds that changing a Laborer count decision rather than a documentation one. **That reasoning still stands and nothing was changed in the engine.** -What changed is that `card-reference.md` is no longer where anyone looks. `docs/rules/as-built.md` -is generated from `content.ts` and carries the industry table the game actually runs; the old file +What changed is that `card-reference.md` is no longer where anyone looks. The card tables (in +`docs/home-deck.md` and `docs/mainline-deck.md` since 0.8.2; `docs/rules/as-built.md` before) are +generated from `content.ts` and carries the industry table the game actually runs; the old file keeps its SUPERSEDED banner, now pointing forward, and its numbers are read as what the v0.4.5 placeholder said. **The balance question the entry was really guarding is #70** (the rolling stock supply and the uniform 1/1/1 industry model, both marked provisional in `content.ts`) — that is @@ -2191,6 +2285,20 @@ distinction that matters: is the editorial half that generation was never going to do. +### The 2026-09-29 audit + +#### #117 — `/api/save` and the seed + +Found by the server reviewer, verified: `http.ts`'s `/api/save` returns `session.exportSave()` — +seed, config, names, full history — to any valid seat token while `status === 'active'`. The comment +above the route says "every one of those moves is already on this player's screen", which is true +of the moves and false of the seed. `game.ts` strips the seed from the shared log for exactly this +reason ("handed each of them the whole future of the deal"), `test/redaction.test.ts` pins that a +Frame never carries it, and `/api/lobby/preview` refuses to send it. Jesse's question, which is the +right one: a save without the seed is not a save. The finished-game-only answer keeps the download +button honest (it works; it just waits for the end); the receipt answer keeps a mid-game download +possible but replays nothing; the Co-op answer accepts that a co-operative table has nothing to hide. + ## Done Closed items, kept because several are the only record of a ruling or a lesson. Newest first within @@ -2501,7 +2609,8 @@ the numbers stay so cross-references above and below still resolve. it describes the v0.4.5 deck, where 3/4 is a Mail-Express with three coaches against a `content.ts` whose train 3 is the Express with two freight cars. **The fix is not a rewritten table** — every file in `docs/rules/` is a deliberate historical record and worth more intact than patched. A new - `as-built.md` is GENERATED from the same catalogues the engine instantiates from, by + `as-built.md` (folded into `docs/home-deck.md` and `docs/mainline-deck.md` in 0.8.2) is + GENERATED from the same catalogues the engine instantiates from, by `scripts/build-card-reference.ts` (`npm run build:cards`), and `test/card-reference.test.ts` re-runs the generator and fails if the checked-in file disagrees. **Worth knowing:** a hand-written replacement would have drifted the same way and for the same reason — nothing fails diff --git a/docs/architecture/components.md b/docs/architecture/components.md index 9982be0..27a3c93 100644 --- a/docs/architecture/components.md +++ b/docs/architecture/components.md @@ -60,7 +60,9 @@ is what makes the whole system testable without a server. - **Where:** Engine · **When:** loaded once at startup, immutable thereafter - **Does:** the 52-card deck composition, freight and passenger facility profiles, Modifier effects, - train consist specs, track geometries. Source: [`../rules/card-reference.md`](../rules/card-reference.md) + train consist specs, track geometries. Source: `docs/Deck cards5.xlsx` and the PDFs, transcribed + into `content.ts`; the generated tables in [`../home-deck.md`](../home-deck.md) and + [`../mainline-deck.md`](../mainline-deck.md) are what it deals today - **MVP: S** · **Final: S** · ~200 → ~350 LOC - **Size driver:** it is data, not logic. Grows only if card variants are added. - **Note:** every number here is provisional and will be retuned repeatedly. Keep it as data files, diff --git a/docs/architecture/game-state.md b/docs/architecture/game-state.md index d0eb151..2ed1c0e 100644 --- a/docs/architecture/game-state.md +++ b/docs/architecture/game-state.md @@ -131,24 +131,24 @@ Office; everything else is Secondary Track. ``` OfficeArea owner : PlayerIndex - officeType : whistlePost | depot | station | terminal + tier : whistlePost | depot | station | terminal -- `OfficeTier`; `officeType` in early drafts grid : Map -- sparse; cards are placed during play officeCoord : GridCoord -- where the Office card sits runningRow : integer -- the grid row that is the Running Track limitsWest : GridCoord -- moves outward as the Running Track grows limitsEast : GridCoord - adOccupancy : CrewTrayId[] -- length ≤ adTrackCount(officeType) + adOccupancy : CrewTrayId[] -- length ≤ adTrackCount(tier) ``` ``` adTrackCount: whistlePost 1 | depot 2 | station 3 | terminal 4 -- §11.1 -isControlPoint = officeType != whistlePost -isPassengerFacility = officeType != whistlePost +isControlPoint = tier != whistlePost +isPassengerFacility = tier != whistlePost ``` **Office cards are geometrically interchangeable** (§11.3). All four tiers carry the same track footprint — a through track plus a plain junction stub above and below — and differ *only* in the -three properties above. An upgrade therefore changes `officeType` and nothing else; it must never +three properties above. An upgrade therefore changes `tier` and nothing else; it must never touch `grid`, `connections`, or anything attached to the Office card. The junction stubs carry no directional restriction: §A.1's turnout rule governs drawn turnout cards only. @@ -187,7 +187,7 @@ These are the ones that will be got wrong if they are not written down explicitl 9. **An Office upgrade must preserve every connection** (§11.3). All four tiers share identical geometry precisely so the upgrade is a property change, not a card swap. An implementation that models the upgrade as "remove old card, place new card" will silently orphan any Secondary Track - hanging off the Office — mutate `officeType` in place instead. + hanging off the Office — mutate `tier` in place instead. 10. **The Office card's stubs are not turnouts.** Do not route them through the §A.1 directional logic; a train may pass between the Running Track and either Secondary row freely. @@ -258,8 +258,9 @@ Facility modifiers : ModifierRef[] -- adjacent cards raising capacity, track length or workers ``` -All of these are populated from [`../rules/card-reference.md`](../rules/card-reference.md), which is -the authoritative per-card catalogue. +All of these are populated from `content.ts`, transcribed from the catalogue spreadsheets; the +generated tables in [`../home-deck.md`](../home-deck.md) and [`../mainline-deck.md`](../mainline-deck.md) +are the per-card reference (`card-reference.md` is the superseded placeholder). **`industryTrack` is where cars are spotted for loading and unloading**, and it is the field that stops being Operational Rail while `menAtWork` holds any load (constraint 4 below). Its `length` is @@ -275,8 +276,9 @@ the car (§9.3). A Porter earns a point in one action. This asymmetry is deliber per Stage, and stocking a green box or clearing a red one consumes the whole of it (§6.3). That one-action-per-Stage budget is what actually limits Revenue — roughly one point per action, ceiling 12 per Day. Worker counts mostly determine how often a facility idles. See -[`../rules/card-reference.md`](../rules/card-reference.md#7-economy-summary) for the full model; it is -what §3's targets are calibrated against. +[`../rules/card-reference.md`](../rules/card-reference.md#7-economy-summary) for the model as it was +first worked out (that document is otherwise superseded); the figures §3's targets were calibrated +against are the measurements in `TODO.md`'s Play balance section. --- diff --git a/docs/architecture/protocol.md b/docs/architecture/protocol.md index 5368999..3302996 100644 --- a/docs/architecture/protocol.md +++ b/docs/architecture/protocol.md @@ -96,7 +96,8 @@ or a replay viewer has to reconstruct the whole board to draw one frame. Two consequences worth knowing before adding an event: -- **`collisionOccurred` carries `faultPlayer` explicitly** rather than leaving clients to derive it. +- **`trainsDestroyed` carries the player at fault (`player`) explicitly** rather than leaving clients to + derive it (the event was called `collisionOccurred` in this design; the engine emits `trainsDestroyed`). Fault depends on *where* the wreck happened — Superintendent for a Mainline card, the local player between their Limits (§10) — and getting it wrong misattributes a −5 and, in Competitive, feeds a floor that ends the game. diff --git a/docs/components.md b/docs/components.md index 45e0bf5..ebfb314 100644 --- a/docs/components.md +++ b/docs/components.md @@ -1,6 +1,6 @@ # Station Master — Components and Markers -**Version 0.8.4** · 2026-09-29 +**Version 0.8.5** · 2026-09-29 **Scope:** non-card physical components and supplies. Card-created facilities, workers, deck piles, hand state, timetable state and other markers are documented with their cards or in the diff --git a/docs/design.md b/docs/design.md index edb68f0..93aa9be 100644 --- a/docs/design.md +++ b/docs/design.md @@ -120,6 +120,3 @@ writes a self-contained HTML file — open it in any browser and step through th Division, the Office Area grid, every facility's boxes and `MEN|AT|WORK` track, plain-English narration of each event, and a **Blocked** panel explaining why nothing is moving. -Running alongside, and independent of all of it: **print-and-play components.** `card-reference.md` -specifies every card face, so layout and art are the only remaining work before a table playtest — -which answers the one question simulation cannot, whether it is fun. diff --git a/docs/home-deck.md b/docs/home-deck.md index 1f81cdd..8456acd 100644 --- a/docs/home-deck.md +++ b/docs/home-deck.md @@ -1,6 +1,6 @@ # Station Master — Home Deck -**Version 0.8.4** · 2026-09-29 +**Version 0.8.5** · 2026-09-29 **Scope:** the Home Office deck — how it is dealt, drawn, discarded and reshuffled, and what the rules are for playing each kind of card out of it. diff --git a/docs/mainline-deck.md b/docs/mainline-deck.md index 0db7d25..8bdd139 100644 --- a/docs/mainline-deck.md +++ b/docs/mainline-deck.md @@ -1,6 +1,6 @@ # Station Master — Mainline Deck -**Version 0.8.4** · 2026-09-29 +**Version 0.8.5** · 2026-09-29 **Scope:** the tarot-sized Mainline cards placed between Offices — how the deck is dealt, what a card does to a train crossing it, and the Home Deck cards played onto one. diff --git a/docs/plans/structure.md b/docs/plans/structure.md new file mode 100644 index 0000000..af6348e --- /dev/null +++ b/docs/plans/structure.md @@ -0,0 +1,113 @@ +# Structure — a proposal (2026-09-29) + +What the 2026-09-29 audit found about the SHAPE of the code, as distinct from its faults, and what +to do about it. Nothing here changes behaviour; each item is a seam that would let the next fault be +found by a test instead of by a reviewer reading three thousand lines. Ordered by payoff. + +The faults the audit fixed in 0.8.3 and 0.8.4 were nearly all in the three largest functions or in +the one file no test stood up. That is the argument: the structure is where the bugs were. + +## 1. `http.ts` — a route table, and one place for authentication + +**Today.** `startServer` is one 600-line async closure of `if (pathname === … && method === …)` +chains. The token → session → lobby-or-game resolution is copy-pasted eight times; the admin-secret +check, the join-secret check and the host check are each inline where they are needed. The catch at +the bottom is the only error path, and until 0.8.4 it was itself a crash. + +**Proposed.** A table of routes, each `{ method, path, auth, handler }`, with three auth wrappers: + +``` +withToken(handler) // resolves { ps, lobby?, game? } from the token, 404s if none +withHost(handler) // withToken, then refuses anyone but lobby.hostToken +withAdmin(handler) // the x-admin-secret gate; the whole prefix is absent when unset +``` + +Handlers become ten to thirty lines each and take a typed context. The one dispatcher owns +`readJson`, `sendJson`, the `HttpError` catch and the `headersSent` guard — one place, so 0.8.4's +crash fix cannot be forgotten by the next route. The per-game `inTurn` queue becomes a property of +the game context rather than something a handler has to remember to call. + +**Cost.** A day. Every route is exercised by `test/server/http.test.ts` now, so the move is +mechanical and verifiable. Do it before adding the next route (the seatless display stream, #20). + +## 2. `main.ts` — five extractions, and a `Selection` value + +**Today.** 3200 lines, ~21 module-level `let`s (`session`, `selected`, `mode`, `pendingAt`, +`selectedCrew`, `peekPlayer`, `zoom`, `soundOn`, `gameCode`, `remoteToken`, …), and `start()` runs at +import. `render()` is ~470 lines; `renderActions` ~360. `test/web.test.ts` has to stub the DOM before +importing, can never build two pages, and cannot call `render()` with a `Frame` of its choosing — +which is why every screen fault in the audit was found by reading, not by a test. + +**Proposed, in the order they pay:** + +1. **`web/remote-store.ts`** — `RemoteRecord`, `readStore`/`writeStore`, `loadRemote`/`saveRemote`/ + `forgetRemote`/`knownRemote`, and `lobbyHandlers`. Pure functions over `localStorage`; testable + with a Map. Today they are untestable except through the page. +2. **`web/url-options.ts`** — `RULE_PARAMS`/`VICTORY_PARAMS`/`OPTIONAL_PARAMS`, `gameOptionsFromUrl`, + `solitaireDefaults`, `rulesToUrl`. A pure round trip; the audit found the `location.search = ''` + no-op assumption (#114) by reading this code, and a test on the round trip would have found it. +3. **`web/setup-screen.ts`** — `wireGameTypeBlock`, `commitNewGame`, `runSolitaireSetup`. +4. **A `Selection` object** replacing the five `let`s (`selected`, `mode`, `pendingAt`, + `selectedCrew`, `peekPlayer`) that every click handler mutates. One value, one `reset()`, passed + to `render` rather than read from module scope — which is what lets a test call `render(frame, + selection)` and assert on the HTML. +5. **`web/game-shell.ts`** — session routing: `beginRemote`, `abandonRemote`, `claimSeat`, `start`, + `applyCapabilities`, the handoff. This is where the animation loop outliving the session (#114) + lives, and it is easier to see once it is not surrounded by rendering. + +Keep `render()` in `main.ts` but split the board-and-hand wiring (~240 lines of `addEventListener`) +into `wireBoard(selection)` so what is DRAWN and what is CLICKABLE are separate functions. + +**Cost.** Two to three days across the five, each its own commit; the first two are an afternoon +each and carry no risk. + +## 3. The engine — `check` per phase, and one car category + +**Today.** `check` (~600 lines) and `reduce` (~700) are single switches mixing every phase; +`moveTrain` nests four position kinds by three decision kinds. Car category (coach / caboose / +freight) is computed in three places with three spellings. The seat guard 0.8.3 added to the four +switching intents is the same four lines four times. + +**Proposed.** + +- Split `check` by phase — `checkLocalOps`, `checkNewTrain`, `checkLoadUnload`, `checkMainline` — + each a switch over its own intents, dispatched by `inPhase`. The shared guards (`ownTray`, the + option-chosen test, the Moves test) become the first lines of `checkLocalOps` rather than four + repeats. `reduce` splits the same way. +- One exported `carCategory(type)` in `content.ts`, used by `acceptsCar`, `newTrainPhase`, + `consistNeeds` and `isFreight`/`carriesLoad`. +- `maneuver.flyingSwitch` is a weaker copy of `switch.dropCars` (no `switchingRefusal`, no + `engineAt` clamp, no `standingWest` handling). It should CALL the dropCars path with a flag, + or be deleted until the card is dealt (it is at 0 copies). Deciding is #115's job; the structure + point is that there must be one cut-dropping reducer. + +**Cost.** The `check`/`reduce` split is a day and is pure motion — every test in `apply.test.ts` +and `advance.test.ts` runs unchanged. `carCategory` is an hour. + +## 4. One `Push` type + +`web/session.ts` mirrors `server/session.ts`'s `Push` by hand so the browser bundle never imports +from `src/server/`. Every envelope change (0.8.4 added `lastSeq`) is two edits. Move the wire types +— `Push`, `LobbyPush`, `LobbyPreview` — into `src/sim/wire.ts`, which both sides already import +from, and delete the mirror. + +**Cost.** An hour. + +## 5. Tests that would then exist + +Each extraction above names the test it makes possible: + +| after | test | +| --- | --- | +| route table | one test per route for the 404/403/400 answers, from a table | +| `remote-store.ts` | the `RemoteRecord` round trip, the "seat tied to this browser" notice (#33) | +| `url-options.ts` | `rulesToUrl(gameOptionsFromUrl(x)) === x`, and the `search = ''` case | +| `Selection` | `render(frame, selection)` snapshot tests for each screen state | +| `check` per phase | the seat guard once, as a property over every switching intent | + +## What not to do + +- Do not refactor `render()` and fix a screen bug in the same commit. The audit's value was that + each finding could be verified against unchanged code. +- Do not introduce a framework or a runtime dependency to do any of this. The zero-dependency rule + is what makes the package a 63 MB `.s9pk` and the site a static upload. diff --git a/docs/quickstart.md b/docs/quickstart.md index 94ff7d3..6d224e7 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,6 +1,6 @@ # Station Master — Quickstart -**Version 0.8.4** · 2026-09-29 +**Version 0.8.5** · 2026-09-29 For a player who has never played. diff --git a/docs/rules.md b/docs/rules.md index 9495ad0..1b38212 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1,6 +1,6 @@ # Station Master — Rules -**Version 0.8.4** · 2026-09-29 +**Version 0.8.5** · 2026-09-29 **Authority:** observed code paths and tests. Where a card face, a prototype document and executable behaviour differ, this document reports **executable behaviour** and marks unimplemented material. diff --git a/docs/rules/card-reference.md b/docs/rules/card-reference.md index 149cdde..35806e8 100644 --- a/docs/rules/card-reference.md +++ b/docs/rules/card-reference.md @@ -4,9 +4,10 @@ > 2026-07-30 in `docs/Deck cards2.xlsx`, `Trains3.pdf` and `Mainline Cards.pdf`, and is transcribed > in `src/engine/content.ts`. See [`implications.md`](implications.md) for the full comparison. > -> **For what the cards say today, read [`as-built.md`](as-built.md)** — generated from -> `content.ts` and checked against it by the test suite, so it cannot fall behind the way this file -> did. For several releases `content.ts` named *this* page as the current reference while the banner +> **For what the cards say today, read the generated tables in [`home-deck.md`](../home-deck.md) +> and [`mainline-deck.md`](../mainline-deck.md)** — written by `npm run build:cards` from +> `content.ts` and checked against it by the test suite, so they cannot fall behind the way this +> file did. For several releases `content.ts` named *this* page as the current reference while the banner > here said otherwise, and a reader following the code landed on the v0.4.5 deck. > > Kept for the reasoning it records — the economy analysis in §7 was how we knew what questions to diff --git a/docs/station-master-seed116956197-day2.json b/docs/station-master-seed116956197-day2.json deleted file mode 100644 index ba914f0..0000000 --- a/docs/station-master-seed116956197-day2.json +++ /dev/null @@ -1,301 +0,0 @@ -{ - "seed": 116956197, - "history": [ - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c27" - }, - { - "type": "card.play", - "cardId": "c30" - }, - { - "type": "card.play", - "cardId": "c4" - }, - { - "type": "card.play", - "cardId": "c2" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c197", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c163", - "placement": { - "row": 1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c41", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c127", - "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": "c183", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c150", - "placement": { - "row": 1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c136", - "placement": { - "row": 1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 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": "c77", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c171", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c153", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "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": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - } - ], - "rules": { - "startingHand": "sixRandom", - "revenue": { - "passengerPerCoach": 1, - "freightPerLoad": 1, - "trainPerTransit": 0 - } - } -} \ No newline at end of file diff --git a/docs/station-master-seed116956197-day2b.json b/docs/station-master-seed116956197-day2b.json deleted file mode 100644 index dcbfd51..0000000 --- a/docs/station-master-seed116956197-day2b.json +++ /dev/null @@ -1,714 +0,0 @@ -{ - "seed": 116956197, - "history": [ - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c27" - }, - { - "type": "card.play", - "cardId": "c30" - }, - { - "type": "card.play", - "cardId": "c4" - }, - { - "type": "card.play", - "cardId": "c2" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c197", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c163", - "placement": { - "row": 1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c41", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c127", - "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": "c183", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c150", - "placement": { - "row": 1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c136", - "placement": { - "row": 1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 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": "c77", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c171", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c153", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "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": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": 1, - "col": 0 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c162", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "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": 0 - }, - "box": 1 - }, - { - "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": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 1, - "col": 0 - }, - "carType": "boxcar" - }, - { - "type": "laborer.startLoad", - "at": { - "row": 1, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 1, - "col": 0 - }, - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c50", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -2 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false, - "via": { - "row": 0, - "col": 0 - } - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 1 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "tank", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 3 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -2 - }, - "reverse": true, - "via": { - "row": 0, - "col": 1 - } - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.end" - } - ], - "rules": { - "startingHand": "sixRandom", - "revenue": { - "passengerPerCoach": 1, - "freightPerLoad": 1, - "trainPerTransit": 0 - } - } -} \ No newline at end of file diff --git a/docs/station-master-seed116956197-day3b.json b/docs/station-master-seed116956197-day3b.json deleted file mode 100644 index 14c2aaf..0000000 --- a/docs/station-master-seed116956197-day3b.json +++ /dev/null @@ -1,1004 +0,0 @@ -{ - "seed": 116956197, - "history": [ - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c27" - }, - { - "type": "card.play", - "cardId": "c30" - }, - { - "type": "card.play", - "cardId": "c4" - }, - { - "type": "card.play", - "cardId": "c2" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c197", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 2 - }, - { - "type": "card.play", - "cardId": "c163", - "placement": { - "row": 1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c41", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c127", - "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": "c183", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c150", - "placement": { - "row": 1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c136", - "placement": { - "row": 1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 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": "c77", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c171", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c153", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "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": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": 1, - "col": 0 - }, - "carIndex": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c162", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "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": 0 - }, - "box": 1 - }, - { - "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": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 1, - "col": 0 - }, - "carType": "boxcar" - }, - { - "type": "laborer.startLoad", - "at": { - "row": 1, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 1, - "col": 0 - }, - "index": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c50", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -2 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false, - "via": { - "row": 0, - "col": 0 - } - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 1 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "tank", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 3 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -2 - }, - "reverse": true, - "via": { - "row": 0, - "col": 1 - } - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": 1, - "col": 0 - }, - "carIndex": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c83", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c140", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c61", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 1, - "col": 0 - }, - "index": 0 - }, - { - "type": "laborer.beginUnload", - "at": { - "row": 1, - "col": 0 - }, - "carIndex": 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": 0 - }, - "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": 0 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c100", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "tank", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "tank", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "tank", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 1, - "col": 0 - }, - "index": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 1, - "col": 0 - }, - "carType": "boxcar" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": 1, - "col": 0 - }, - "carIndex": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c21" - }, - { - "type": "draw.end" - } - ], - "rules": { - "startingHand": "sixRandom", - "revenue": { - "passengerPerCoach": 1, - "freightPerLoad": 1, - "trainPerTransit": 0 - } - } -} \ No newline at end of file diff --git a/docs/station-master-seed493290760-day2.json b/docs/station-master-seed493290760-day2.json deleted file mode 100644 index 183b8be..0000000 --- a/docs/station-master-seed493290760-day2.json +++ /dev/null @@ -1,656 +0,0 @@ -{ - "seed": 493290760, - "history": [ - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c138", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c144", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c11" - }, - { - "type": "card.play", - "cardId": "c24" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c31" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c182", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c159", - "placement": { - "row": 1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c49", - "placement": { - "row": 1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c5" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false - }, - { - "type": "mainline.clearance", - "allow": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.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": "tray3", - "to": { - "row": 0, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 1, - "col": 0 - }, - "carType": "tank" - }, - { - "type": "laborer.startLoad", - "at": { - "row": 1, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c194", - "placement": { - "row": 0, - "col": -2 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 0 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c154", - "placement": { - "row": 1, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c102", - "node": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.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": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": -2 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 2 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": -2 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 3 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.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": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 1, - "col": -2 - }, - "reverse": true - } - ] -} \ No newline at end of file diff --git a/docs/station-master-seed58228926-day6.json b/docs/station-master-seed58228926-day6.json deleted file mode 100644 index 02a6ff5..0000000 --- a/docs/station-master-seed58228926-day6.json +++ /dev/null @@ -1,1963 +0,0 @@ -{ - "seed": 58228926, - "history": [ - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "mainline.modify", - "cardId": "c108", - "node": 3 - }, - { - "type": "card.play", - "cardId": "c136", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c19" - }, - { - "type": "card.discard", - "cardId": "c98", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "boxcar", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c66", - "placement": { - "row": 1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c131", - "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": "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": "c109", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c21" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c116", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c23" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c162", - "placement": { - "row": -1, - "col": -2 - }, - "variant": 1 - }, - { - "type": "card.play", - "cardId": "c60", - "placement": { - "row": -1, - "col": -1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c138", - "placement": { - "row": -1, - "col": 0 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c30" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c0" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c191", - "placement": { - "row": 0, - "col": 2 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c182", - "placement": { - "row": 1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c48", - "placement": { - "row": 1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c49", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c57", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c38", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c115", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c133", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c10" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c32", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c25", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "coach", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c124", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c100", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c69", - "placement": { - "row": -2, - "col": -2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c1" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c43", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c179", - "toSlot": 2 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c46", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c11" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "reefer", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "reefer", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c5" - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c121", - "toSlot": 1 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -3 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": -1, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": -3 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.end" - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -1 - }, - "carIndex": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c192", - "placement": { - "row": 0, - "col": 1 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c187", - "placement": { - "row": -1, - "col": 1 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": -1, - "col": -1 - }, - "index": 0 - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c121", - "placement": { - "row": -1, - "col": 2 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 0 - }, - { - "type": "card.play", - "cardId": "c46", - "placement": { - "row": -1, - "col": 3 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c124", - "placement": { - "row": 1, - "col": 1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.end" - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c76", - "placement": { - "row": -1, - "col": 4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "hopper", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray3", - "carType": "caboose", - "loaded": true - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "coach", - "loaded": true - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.play", - "cardId": "c96", - "placement": { - "row": 0, - "col": -1 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray2", - "carType": "caboose", - "loaded": true - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 0, - "col": 0 - }, - "carType": "coach" - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -3 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 2 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 2 - }, - "reverse": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 1, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray2", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 3 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray2", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.detrain", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "porter.board", - "at": { - "row": 0, - "col": 0 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": 1, - "col": 3 - }, - "carType": "tank" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray0", - "carType": "coach", - "loaded": false - }, - { - "type": "laborer.startLoad", - "at": { - "row": 1, - "col": 3 - } - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -1 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -3 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1, - "fromNose": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1, - "fromNose": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 3 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 2, - "fromNose": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 2 - }, - "reverse": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray3", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": -1, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": -3 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray3", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "switch.end" - }, - { - "type": "switch.end" - }, - { - "type": "switch.end" - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 3 - }, - "box": 1 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "hopper" - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": 1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "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": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.stockOutbound", - "at": { - "row": -1, - "col": 3 - }, - "carType": "hopper" - }, - { - "type": "laborer.startLoad", - "at": { - "row": -1, - "col": 3 - } - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "freightAgent" - }, - { - "type": "freightAgent.clearInbound", - "at": { - "row": 0, - "col": 0 - }, - "index": 0 - }, - { - "type": "newTrain.placeCar", - "trayId": "tray0", - "carType": "reefer", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray0", - "carType": "boxcar", - "loaded": true - }, - { - "type": "newTrain.placeCar", - "trayId": "tray0", - "carType": "caboose", - "loaded": true - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": 3 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c73", - "toSlot": 0 - }, - { - "type": "draw.end" - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromHomeOffice" - }, - { - "type": "card.discard", - "cardId": "c113", - "toSlot": 0 - }, - { - "type": "card.play", - "cardId": "c155", - "placement": { - "row": 1, - "col": 4 - }, - "variant": 0 - }, - { - "type": "draw.end" - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "hopper", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "tank", - "loaded": false - }, - { - "type": "newTrain.placeCar", - "trayId": "tray1", - "carType": "caboose", - "loaded": true - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "draw" - }, - { - "type": "draw.fromDepartment", - "slot": 1 - }, - { - "type": "card.play", - "cardId": "c133", - "placement": { - "row": 0, - "col": 3 - }, - "variant": 0 - }, - { - "type": "card.play", - "cardId": "c183", - "placement": { - "row": 0, - "col": 4 - }, - "variant": 1 - }, - { - "type": "draw.end" - }, - { - "type": "mainline.clearance", - "allow": false - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray0", - "to": { - "row": 0, - "col": -3 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray0", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray0", - "to": { - "row": -1, - "col": -1 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray0", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray0", - "to": { - "row": -1, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.dropCars", - "trayId": "tray0", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray0", - "to": { - "row": -1, - "col": 2 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray0", - "to": { - "row": 0, - "col": 0 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray0", - "to": { - "row": 0, - "col": -3 - }, - "reverse": true - }, - { - "type": "laborer.beginUnload", - "at": { - "row": -1, - "col": -1 - }, - "carIndex": 0 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 2 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray0", - "to": { - "row": 0, - "col": 0 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 0, - "col": 5 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 1, - "col": 4 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 1 - }, - { - "type": "laborer.advanceLoad", - "at": { - "row": -1, - "col": -1 - }, - "box": 0 - }, - { - "type": "loadUnload.end" - }, - { - "type": "localOps.choose", - "option": "switch" - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 0, - "col": 5 - }, - "reverse": true - }, - { - "type": "switch.dropCars", - "trayId": "tray1", - "count": 1 - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 0, - "col": 3 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 1, - "col": 1 - }, - "reverse": false - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 0, - "col": 5 - }, - "reverse": true - }, - { - "type": "switch.move", - "trayId": "tray1", - "to": { - "row": 1, - "col": 4 - }, - "reverse": false - }, - { - "type": "switch.end" - }, - { - "type": "loadUnload.end" - } - ], - "rules": { - "startingHand": "threeTrackThreeOther", - "revenue": { - "passengerPerCoach": 1, - "freightPerLoad": 1, - "trainPerTransit": 1 - } - } -} \ No newline at end of file diff --git a/package.json b/package.json index 330bd45..af7fcb9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "station-master", - "version": "0.8.4", + "version": "0.8.5", "private": true, "type": "module", "description": "Station Master — a railroad operations game", diff --git a/src/engine/advance.ts b/src/engine/advance.ts index c9f4b0e..abb5f79 100644 --- a/src/engine/advance.ts +++ b/src/engine/advance.ts @@ -730,7 +730,6 @@ function entryConflict( /** Puts a train onto a Mainline card with its crossing time already computed. */ function enterMainline( - s: GameState, node: Extract, id: TrayId, tray: CrewTray, @@ -862,7 +861,7 @@ function moveTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent[] if (conflict === 'held') return 'held'; if (conflict === 'collided') return 'moved'; - enterMainline(s, node, id, tray, target); + enterMainline(node, id, tray, target); const dp = s.division.nodes[dpIndex]; if (dp?.kind === 'divisionPoint') dp.holding = dp.holding.filter((t) => t !== id); events.push({ @@ -926,7 +925,7 @@ function moveTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent[] // The wreck's A/D track is released by `collide` itself, which is why it has to be. if (conflict === 'collided') return 'moved'; - enterMainline(s, node, id, tray, target); + enterMainline(node, id, tray, target); area.adOccupancy = area.adOccupancy.filter((t) => t !== id); events.push({ type: 'trainHighballed', @@ -998,7 +997,7 @@ function moveTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent[] if (conflict === 'collided') return 'moved'; node.holding = node.holding.filter((t) => t !== id); - enterMainline(s, node, id, tray, index, true); + enterMainline(node, id, tray, index, true); events.push({ type: 'trainHighballed', trainNumber: tray.trainNumber ?? 0, @@ -1400,7 +1399,7 @@ function redFlagStop( return c?.kind.kind === 'maneuver' && c.kind.key === 'redFlags'; }); if (!holdsFlag) return 'proceed'; - if (!arrivalWouldCollide(s, id, tray, dest.seat)) return 'proceed'; + if (!arrivalWouldCollide(s, dest.seat)) return 'proceed'; s.clock.pendingDecision = { kind: 'redFlag', train: id, seat: dest.seat, from }; return 'ask'; @@ -1413,7 +1412,7 @@ function redFlagStop( * if these two ever diverge, the prompt offers a flag against a collision that will not happen, or * stays silent before one that will. */ -function arrivalWouldCollide(s: GameState, id: TrayId, tray: CrewTray, seat: SeatIndex): boolean { +function arrivalWouldCollide(s: GameState, seat: SeatIndex): boolean { const area = areaAtSeat(s, seat); const hasInterlocking = [...area.grid.values()].some((c) => c.enhancements.includes('interlocking')); const full = area.adOccupancy.length >= officeProfile(area.tier).adTracks; diff --git a/src/engine/apply.ts b/src/engine/apply.ts index 1a23441..540aae0 100644 --- a/src/engine/apply.ts +++ b/src/engine/apply.ts @@ -16,7 +16,6 @@ import { FREIGHT_PROFILES, - LABORER_ACTIONS_PER_LOAD, MAX_CONSIST, REALIGNMENTS, consistSize, @@ -65,7 +64,6 @@ import { pooled, railFacingOf, seatOf, - spaceOn, standingSides, trackOrder, turnOf, @@ -647,7 +645,7 @@ function passengerWork( * crew is unaffected by all of them. */ /** Which district a tray is standing in; 0 when it is out on the Division. */ -function trayySeat(tray: CrewTray): SeatIndex { +function traySeat(tray: CrewTray): SeatIndex { return tray.position.at === 'grid' ? tray.position.seat : 0; } @@ -1578,7 +1576,7 @@ function checkPlay( return target && target.kind === 'mainline' ? null : 'NOT_CONNECTED'; } if (!placement) return 'NO_PLACEMENT'; - return checkEnhancementPlacement(s, area, card.kind.key, placement); + return checkEnhancementPlacement(area, card.kind.key, placement); } case 'mainlineModifier': @@ -1587,7 +1585,7 @@ function checkPlay( // copy uses the enhancement's grid placement rather than going onto a Mainline card. if (card.kind.key === 'facingPointLocksMainline') { if (!placement) return 'NO_PLACEMENT'; - return checkEnhancementPlacement(s, area, 'facingPointLocks', placement); + return checkEnhancementPlacement(area, 'facingPointLocks', placement); } // The rest are laid on a Mainline card, which is not a grid coordinate — see // `mainline.modify`. @@ -1830,7 +1828,6 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] { const from = trayCoord(s, i.trayId)!; const dests = destinationsFor(s, player, i.trayId, from, i.reverse); const dest = selectDestination(dests, i.to, i.via)!; - const tray = s.trays.get(i.trayId)!; // The port the crew pulls out THROUGH — the same one `destinationsFor` explored from, so the // cut it recouples on the way out is the cut the walk counted. const facingNow = facingPort(s, i.trayId); @@ -2454,7 +2451,7 @@ export function reduce(s: GameState, e: GameEvent): void { * front of `stock` is a drop being undone, so it is refunded on the square it was left on * instead of being charged again at the far end. */ - const owner = playerAtSeat(s, trayySeat(tray)); + const owner = playerAtSeat(s, traySeat(tray)); const taken = e.recoupled ? e.stock.slice(e.recoupled.stock.length) : e.stock; spendFreightBudget(s, owner, tray, e.at, taken); if (e.recoupled) refundFreightBudget(s, owner, tray, e.recoupled.at, e.recoupled.stock); @@ -2477,7 +2474,7 @@ export function reduce(s: GameState, e: GameEvent): void { */ tray.engineAt = e.engineAt; // "Spends one move in the yard" — the sort costs a Move. - const sorter = turnOf(s, playerAtSeat(s, trayySeat(tray))); + const sorter = turnOf(s, playerAtSeat(s, traySeat(tray))); sorter.movesRemaining = Math.max(0, sorter.movesRemaining - 1); break; } @@ -2524,7 +2521,7 @@ export function reduce(s: GameState, e: GameEvent): void { // cars set out to the east leave it where it was. card.standingWest = onWestSide ? k + cut.length : k; } - spendFreightBudget(s, playerAtSeat(s, trayySeat(tray)), tray, e.at, e.stock); + spendFreightBudget(s, playerAtSeat(s, traySeat(tray)), tray, e.at, e.stock); break; } @@ -3184,7 +3181,6 @@ export function watertowersRemovable(area: OfficeArea): GridCoord[] { * - ABS Signals → a Mainline card, not the Office Area */ export function checkEnhancementPlacement( - s: GameState, area: OfficeArea, key: string, placement: GridCoord, diff --git a/src/engine/intents.ts b/src/engine/intents.ts index 0ca7f60..aaf4ba3 100644 --- a/src/engine/intents.ts +++ b/src/engine/intents.ts @@ -4,7 +4,7 @@ * An intent is a PROPOSAL. It may be rejected. Contrast with an event (events.ts), which is a fact. */ -import type { CarType, Direction, Hand, OfficeTier, TrackGeometry } from './content.ts'; +import type { CarType, Direction, OfficeTier } from './content.ts'; import type { CardId, GridCoord, PlayerIndex, SeatIndex, TrayId } from './state.ts'; // --------------------------------------------------------------------------- diff --git a/src/engine/legal.ts b/src/engine/legal.ts index bbe01a1..14280ef 100644 --- a/src/engine/legal.ts +++ b/src/engine/legal.ts @@ -12,7 +12,7 @@ * If you find yourself writing a rule here, it belongs in apply.ts. */ -import type { CarType, Hand, TrackGeometry } from './content.ts'; +import type { CarType } from './content.ts'; import { enhancementRule, mainlineProfile } from './content.ts'; import { check, areaOf, destinationsFor, withRouteCache } from './apply.ts'; import type { Intent } from './intents.ts'; @@ -191,9 +191,6 @@ export function legalSwitchingActions(s: GameState, player: PlayerIndex): Intent return withRouteCache(s, () => switchCandidates(s, player).filter((i) => check(s, player, i) === null)); } -export function isLegal(s: GameState, player: PlayerIndex, i: Intent): boolean { - return check(s, player, i) === null; -} /** * Candidate generation. Over-generates freely — `check` is the authority, so a candidate that diff --git a/src/engine/rng.ts b/src/engine/rng.ts index c08ae54..2ae590f 100644 --- a/src/engine/rng.ts +++ b/src/engine/rng.ts @@ -67,9 +67,3 @@ export function createRng(seed: number): Rng { }; } -/** Restore an RNG mid-stream, for rebuilding a game from a snapshot. */ -export function restoreRng(state: number): Rng { - // mulberry32 advances from its state before drawing, and createRng seeds state directly, - // so restoring is just seeding with the saved state. - return createRng(state); -} diff --git a/src/engine/setup.ts b/src/engine/setup.ts index 7a77bb7..3c02f1a 100644 --- a/src/engine/setup.ts +++ b/src/engine/setup.ts @@ -19,7 +19,6 @@ import { OPENING_DEALS, MAINLINE_DECK, houseRules, - mainlineProfile, TRACK_CARDS, ROLLING_STOCK_SUPPLY, STAGES_PER_DAY, diff --git a/src/server/lobby.ts b/src/server/lobby.ts index 9a17dfc..b3c9290 100644 --- a/src/server/lobby.ts +++ b/src/server/lobby.ts @@ -91,8 +91,9 @@ export function playerCountAllowed(mode: GameConfig['mode'], count: number): boo return mode === 'solitaire' ? count === 1 : count >= 2 && count <= 4; } -/** The creating player is the host and takes seat 0 (`lobby-and-sessions.md` §2). */ /** + * The creating player is the host and takes seat 0 (`lobby-and-sessions.md` §2). + * * THE TABLE SIZE IS FIXED WHEN THE GAME IS CREATED, and `seats.length` is it. * * The host says how many are playing, so the seats array is built at full length with the host in diff --git a/src/server/session.ts b/src/server/session.ts index 382d104..9f1688f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -326,8 +326,7 @@ function buildSession( * `lobby-and-sessions.md` §5's turn clock exists to learn how long HUMANS take, and a bot decides * in zero wall-clock time by definition. Called once at construction (a resume could land exactly * on a bot's turn) and once after every accepted human intent. - */ -/** + * * §3.3, EXTENDED PLAY (Gitea#11) — the bots' half of a unanimous vote. * * "Bots will not disagree with the human. Humans get to vote first. If all humans vote yes, then diff --git a/src/sim/board-svg.ts b/src/sim/board-svg.ts index f0ea58f..9257504 100644 --- a/src/sim/board-svg.ts +++ b/src/sim/board-svg.ts @@ -113,14 +113,6 @@ export function divisionSvg(nodes: DivisionView[], roster?: DivisionRoster | nul const CHIP_Y = RAIL_Y - 10; const BELOW_Y = RAIL_Y + 13; const GAP = 6; - /** - * ONE FIXED SLOT PER A/D TRACK, so the Office Running Track cell is drawn wide enough to hold - * them without spilling onto its neighbours (docs/plans/switching-paths.md — "The Roster Pass"). - * Sized by CAPACITY, not by how many are occupied right now: a cell drawn for the trains it HAS - * holds still as they come and go, where sizing by occupancy moved the East Division Point (and - * everything past it) sideways every time an A/D track filled or cleared. - */ - const CHIP_W = 54; /** * Room for the buffer stops. THE LABELS NO LONGER LIVE OUT HERE. * diff --git a/src/sim/bot.ts b/src/sim/bot.ts index d84f533..d07bc76 100644 --- a/src/sim/bot.ts +++ b/src/sim/bot.ts @@ -91,19 +91,6 @@ function because(reason: string, intent: Intent): Intent { return intent; } -/** - * KNOBS FOR A/B MEASUREMENT, and nothing else. - * - * A heuristic change has to be measured against the bot it replaces, over the SAME deals — and - * editing the bot between runs makes that impossible to do honestly, because the two sides of the - * comparison never exist at once. Every flag here is off by default, so `makeDeveloperBot({})` is - * byte-identical to the bot that came before this existed. - * - * TEMPORARY BY CONSTRUCTION. When a flag measures well it becomes the default and the flag is - * deleted in the same commit; when it measures badly it is deleted with its finding recorded in the - * changelog. What must not happen is a bot that accumulates switches nobody can account for — a - * heuristic with no measurement attached is exactly what this machinery exists to prevent. - */ /** * ABLATIONS, for re-measuring the heuristics that are now the bot's default play. * @@ -356,40 +343,13 @@ function committedTrains(s: GameState): number { * * §7 lets you play as many train cards as you draw, and a train that arrives with nowhere to stand * is an automatic collision (Gap 2d) — so the two rules together make a train card actively harmful - * once the A/D tracks are spoken for. Off unless `trainCapSlack` is set. + * once the A/D tracks are spoken for. `noTrainCap` switches the cap off for re-measurement. */ function trainWouldOverfillTheOffice(s: GameState, player: PlayerIndex, tweaks: BotTweaks): boolean { if (tweaks.noTrainCap) return false; return committedTrains(s) >= officeProfile(areaOf(s, player).tier).adTracks; } -/** - * Would running here leave the engine buried among its own cars? - * - * Cars met on a FORWARD move couple onto the nose (§A.3), which pushes the engine back through its - * own train — `carsCoupled` moves `engineAt` by the number taken. So the engine ends up buried - * whenever it had cars behind it already and picks up more in front, and §8.2 then refuses to let - * the train leave the Office. Only trains care: a local crew has nowhere it must depart from. - * - * Asked of the engine's own destination list, so the count is the count that will really couple. - */ -function wouldBuryTheEngine( - s: GameState, - player: PlayerIndex, - move: Extract, -): boolean { - const tray = s.trays.get(move.trayId); - if (!tray || tray.trainNumber === null) return false; - if (tray.position.at !== 'grid') return false; - if (move.reverse) return false; // cars taken while backing up couple BEHIND the engine - const len = tray.consist.length; - if (len === 0 || tray.engineAt >= len) return false; // nothing behind the engine to bury it against - - const dest = destinationsFor(s, player, move.trayId, tray.position.coord, false).find( - (d) => d.coord.row === move.to.row && d.coord.col === move.to.col, - ); - return (dest?.couples.length ?? 0) > 0; -} function chooseLocalOption( s: GameState, @@ -1427,8 +1387,6 @@ function followThrough( (i): i is Extract => i.type === 'draw.fromDepartment' && isWorthTaking(s, player, i.slot, tweaks), ); - // Best-ranked pile rather than the first that qualifies: an Office card and a train card - // both "qualify", and only one of them stops the collisions. // Best-ranked pile, not the first that qualifies. Measured as a near no-op — an Office card // and a train card are face up together 1.6 decisions a game — but ranking them is what the // ranking function is for, and a coin flip on the card that decides whether the district @@ -1980,19 +1938,6 @@ function sidingsWorthCollecting( return out; } -/** Cars standing on ordinary track that some facility would actually take. */ -function strandedWantedCars(s: GameState, player: PlayerIndex): { row: number; col: number }[] { - const out: { row: number; col: number }[] = []; - const tray = trayOf(s, player); - if (!tray || tray.consist.length >= MAX_CONSIST) return out; - for (const [key, card] of areaOf(s, player).grid) { - if (card.facility || card.standing.length === 0) continue; - if (!card.standing.some((c) => facilitiesWanting(s, player, c).length > 0)) continue; - const [row, col] = key.split(',').map(Number); - out.push({ row: row!, col: col! }); - } - return out; -} function trayOf(s: GameState, player: PlayerIndex) { for (const tray of s.trays.values()) { diff --git a/src/sim/compare.ts b/src/sim/compare.ts index 291e97a..d7dcb2b 100644 --- a/src/sim/compare.ts +++ b/src/sim/compare.ts @@ -251,7 +251,7 @@ if (isMain) { const tweaks = parseTweaks(args); if (Object.keys(tweaks).length === 0) { - console.error('nothing to compare — pass at least one tweak, e.g. trainCapSlack=1'); + console.error(`nothing to compare — pass at least one ablation, e.g. noValueLays=1 (one of: ${[...BOOLEAN_TWEAKS].join(', ')})`); process.exitCode = 1; } else { console.log(formatPaired(compare(tweaks, games, length))); diff --git a/src/sim/replay.ts b/src/sim/replay.ts index 018ae78..3dfeac3 100644 --- a/src/sim/replay.ts +++ b/src/sim/replay.ts @@ -19,25 +19,22 @@ import { writeFileSync } from 'node:fs'; import { advance } from '../engine/advance.ts'; -import { applyIntent, areaOf, facilityCarType, laborersLeft, portersLeft } from '../engine/apply.ts'; +import { applyIntent } from '../engine/apply.ts'; import type { GameLength } from '../engine/content.ts'; import { DEFAULT_MAX_COLLISIONS_PER_DAY, DEFAULT_MAX_COLLISIONS_TOTAL, - MAINLINE_PROFILES, collectiveRevenueFloor, lengthProfile, - officeProfile, } from '../engine/content.ts'; import type { GameEvent } from '../engine/events.ts'; -import type { Intent } from '../engine/intents.ts'; import { legalActions } from '../engine/legal.ts'; import { createGame } from '../engine/setup.ts'; -import type { Facility, GameConfig, GameState } from '../engine/state.ts'; +import type { GameConfig } from '../engine/state.ts'; import { actingPlayer } from '../engine/state.ts'; import { reasonSentence } from '../web/panels.ts'; import { developerBot, lastChoiceReason } from './bot.ts'; -import { carLabel, cuesFor, idleNote, isVisible, narrate } from './narrate.ts'; +import { cuesFor, idleNote, isVisible, narrate } from './narrate.ts'; // The view-model lives in its own module so the browser build can import it without dragging in // this file's Node dependencies. Re-exported because tests and the web app import it from here. export type { CellView, DivisionView, FacilityView, Frame, Decision, TrainChip } from './view.ts'; diff --git a/src/sim/save-replay.ts b/src/sim/save-replay.ts index 2d3bd16..204f436 100644 --- a/src/sim/save-replay.ts +++ b/src/sim/save-replay.ts @@ -18,7 +18,7 @@ * * Run with: * node src/sim/save-replay.ts 400 --top 3 - * node src/sim/save-replay.ts 400 --top 3 trainCapSlack=0 + * node src/sim/save-replay.ts 400 --top 3 noValueLays=1 */ import { readdirSync, unlinkSync, writeFileSync } from 'node:fs'; diff --git a/src/sim/view.ts b/src/sim/view.ts index dea9af0..5f96ca6 100644 --- a/src/sim/view.ts +++ b/src/sim/view.ts @@ -889,14 +889,14 @@ export function describeDecision( groups.set(o.type, list); } const rejected = [...groups.entries()] - .map(([kind, list]) => ({ kind, count: list.length, detail: sampleDetail(s, kind, list) })) + .map(([kind, list]) => ({ kind, count: list.length, detail: sampleDetail(s, list) })) .sort((a, b) => b.count - a.count); return { actor, chose: describeIntent(s, chosen), why, rejected, totalOptions: options.length }; } /** A short, concrete example of what a group of rejected options would have done. */ -function sampleDetail(s: GameState, kind: string, list: Intent[]): string { +function sampleDetail(s: GameState, list: Intent[]): string { // Deduplicate by DESCRIPTION. Orientation variants and repeated copies of a card describe // identically, so the raw list reads "play Overpass at (0,0)" three times over and hides the // actual range of choices — the opposite of what this panel is for. diff --git a/src/web/game.ts b/src/web/game.ts index dc24c60..7e06c58 100644 --- a/src/web/game.ts +++ b/src/web/game.ts @@ -37,7 +37,7 @@ import { collectStep, newCollector } from '../sim/display-step.ts'; import type { DisplayCollector } from '../sim/display-step.ts'; // Import from the view module, NOT replay.ts — replay.ts writes files and reads process.argv, // which would pull node:fs into a browser bundle. -import { cardDescription, cardName, currentActorOfState, describeIntent, geometryLabel, simpleCardName, snapshot, trainName, variantLabel } from '../sim/view.ts'; +import { cardDescription, cardName, currentActorOfState, describeIntent, simpleCardName, snapshot, trainName, variantLabel } from '../sim/view.ts'; import { DEFAULT_DAYS, DEFAULT_HOUSE_RULES, diff --git a/src/web/lobby.ts b/src/web/lobby.ts index c1e1bdd..a2266cc 100644 --- a/src/web/lobby.ts +++ b/src/web/lobby.ts @@ -421,7 +421,7 @@ export function runLobby(handlers: LobbyHandlers, resume?: { token: string; game // -- seating -------------------------------------------------------------------------------- - function renderSeating(lobby: Lobby, you: PlayerIndex, token: string): void { + function renderSeating(lobby: Lobby, token: string): void { $('lb-gamecode').textContent = lobby.gameCode; const isHost = lobby.hostToken === token; @@ -566,7 +566,7 @@ export function runLobby(handlers: LobbyHandlers, resume?: { token: string; game handlers.onReady({ token, gameId, seat: push.you, gameCode: push.lobby.gameCode }); return; } - renderSeating(push.lobby, push.you, token); + renderSeating(push.lobby, token); }; /** diff --git a/src/web/main.ts b/src/web/main.ts index af45c0d..3fc2f3a 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -149,8 +149,8 @@ function saveSettings(patch: Partial): void { } /** - * The game, behind the Session boundary — `LocalSession` (solitaire, `?seat=` absent from the URL) - * or `RemoteSession` (`?seat=` present, Phase 2). Typed as the common `Session` surface; every + * The game, behind the Session boundary — `LocalSession` (solitaire) or `RemoteSession` (a seat + * this browser holds in `localStorage`, or a claim link; `?seat=` is no longer a route). Typed as the common `Session` surface; every * LocalSession-only touch (undo, local saves, dealing a new game) goes through `isLocal` below rather * than assuming, since `session` may now be either. */ diff --git a/src/web/session.ts b/src/web/session.ts index c88c868..5b99071 100644 --- a/src/web/session.ts +++ b/src/web/session.ts @@ -6,8 +6,8 @@ * * - `LocalSession` runs the engine in this browser. Solitaire, exactly as it has always worked, * with no server involved at any point. - * - `RemoteSession` (not built yet — see `docs/architecture/multiplayer.md` Phase 2) will hold no - * authoritative state at all. It cannot: it has neither the deck order nor the other players' + * - `RemoteSession` (`docs/architecture/multiplayer.md` Phase 2, built) holds no authoritative + * state at all. It cannot: it has neither the deck order nor the other players' * hands, and if it did the game would be cheatable. * * So this interface is deliberately the SMALLER of the two — everything a remote client could diff --git a/src/web/step-queue.ts b/src/web/step-queue.ts index adb0a8e..d7b0c8b 100644 --- a/src/web/step-queue.ts +++ b/src/web/step-queue.ts @@ -242,6 +242,7 @@ export function createStepQueue( behind: () => pending.filter((s) => dwell(s) > 0).length, showing: () => last, lit: () => litPiles, + pendingLines: () => pending.reduce((n, s) => n + s.lines.length, 0), /** * STILL SHOWING SOMETHING, not just still holding something back. * @@ -253,7 +254,6 @@ export function createStepQueue( * `dueAt` is non-null exactly while the step on screen has time left, so the two together mean * "there is more to come, or what is up has not had its moment yet". */ - pendingLines: () => pending.reduce((n, s) => n + s.lines.length, 0), flashing: () => flashedCards, busy: () => pending.length > 0 || dueAt !== null, }; diff --git a/test/advance.test.ts b/test/advance.test.ts index 930f1f4..963dd3e 100644 --- a/test/advance.test.ts +++ b/test/advance.test.ts @@ -12,7 +12,6 @@ import { applyIntent, areaOf, check, isBeingMadeUp } from '../src/engine/apply.t import { EXPEDITE_FAULT_PENALTY, HAND_LIMIT, MAX_CONSIST, STAGES_PER_DAY, TOTAL_ROLLING_STOCK } from '../src/engine/content.ts'; import { legalActions } from '../src/engine/legal.ts'; import { createGame } from '../src/engine/setup.ts'; -import { developerBot } from '../src/sim/bot.ts'; import type { CrewTray, DivisionNode, GameConfig, GameState } from '../src/engine/state.ts'; import { coordKey, railFacingOf } from '../src/engine/state.ts'; diff --git a/test/apply.test.ts b/test/apply.test.ts index 22a9ec7..b8478b6 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -14,7 +14,7 @@ import { legalActions } from '../src/engine/legal.ts'; import { createGame } from '../src/engine/setup.ts'; import type { CrewTray, GameConfig, GameState, GridCoord, TrackCard } from '../src/engine/state.ts'; import { carsOn, coordKey, railFacingOf, spaceOn, turnOf } from '../src/engine/state.ts'; -import { cardDescription, snapshot } from '../src/sim/view.ts'; +import { snapshot } from '../src/sim/view.ts'; const config: GameConfig = { mode: 'solitaire', @@ -1243,7 +1243,6 @@ describe('a Modifier only goes beside a host that can use it (regression)', () = // REPORTED from playtesting: two Ice Houses in one district. Industries have been barred from // doubling up since Q4, but a Modifier is a different card kind and had no such check at all. const s = game(); - const area = areaOf(s, 0); applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' }); const copies = [...s.cards.entries()] diff --git a/test/harness.test.ts b/test/harness.test.ts index 2e0cad4..0563a8e 100644 --- a/test/harness.test.ts +++ b/test/harness.test.ts @@ -231,7 +231,7 @@ describe('the game conserves Rolling Stock', () => { for (let t = 0; t < 50_000; t++) { const before = census(s); - const pumped = pump(s); + pump(s); /** * A COLLISION DESTROYS NO CAR, and this used to assume it destroyed all of them. * diff --git a/test/server/lobby.test.ts b/test/server/lobby.test.ts index 888f402..b691da1 100644 --- a/test/server/lobby.test.ts +++ b/test/server/lobby.test.ts @@ -242,7 +242,7 @@ describe('playerCountAllowed', () => { describe('game codes', () => { it('skips codes the caller marks taken', () => { let calls = 0; - const code = freshGameCode((c) => { + const code = freshGameCode(() => { calls++; return calls < 3; // taken twice, free on the third }); diff --git a/test/sim.test.ts b/test/sim.test.ts index 3a59abd..ca4a70c 100644 --- a/test/sim.test.ts +++ b/test/sim.test.ts @@ -9,7 +9,6 @@ import assert from 'node:assert/strict'; import { pump } from '../src/engine/advance.ts'; import { createGame } from '../src/engine/setup.ts'; import { applyIntent, check, refillDivisionYardIfEmpty } from '../src/engine/apply.ts'; -import { TOTAL_ROLLING_STOCK } from '../src/engine/content.ts'; import type { GameConfig, GameState, OfficeArea } from '../src/engine/state.ts'; import type { Intent } from '../src/engine/intents.ts'; import { connectionsFor, exitsFrom, hasPort, joins, neighbour, opposite, variantsFor } from '../src/engine/track.ts'; diff --git a/test/train-rules.test.ts b/test/train-rules.test.ts index dcc1f99..03510a5 100644 --- a/test/train-rules.test.ts +++ b/test/train-rules.test.ts @@ -31,7 +31,6 @@ const config: GameConfig = { optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false }, }; const game = (seed = 5): GameState => createGame({ id: 'g', seed, config, playerNames: ['p'] }); -const at = (row: number, col: number): GridCoord => ({ row, col }); const straight = (standing: RollingStock[] = []): TrackCard => ({ geometry: { kind: 'track', geometry: 'straight' }, diff --git a/test/web.test.ts b/test/web.test.ts index ac6abd5..146f1cb 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -1794,17 +1794,6 @@ describe('the static build', () => { }; assert.ok(clickFirst(actions, 'button.act'), 'no action button rendered'); - // Specifically the PLAY verb: a hand usually holds cards that can only be discarded, and - // discarding highlights the Department piles rather than the board. - const clickVerb = (el: Record, verb: string): boolean => { - const fn = el['querySelectorAll'] as (s: string) => Record[]; - const target = fn - .call(el, 'button.cardact') - .find((n) => (n['dataset'] as Record)['verb'] === verb); - if (!target) return false; - (target['onclick'] as (() => void) | null)?.(); - return true; - }; /** * TRY EVERY PLAY BUTTON, NOT JUST THE FIRST — a card offering "play" does not necessarily play * ONTO THE BOARD. diff --git a/tsconfig.json b/tsconfig.json index fe80123..4de40ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,8 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "noEmit": true, "allowImportingTsExtensions": true,