v0.4.9e — five of six playtest bugs: one button per train, and a load that has to go somewhere
Gameplay testing on 0.4.9d returned six reports. Five are fixed; the sixth could not be reproduced and is written up in TODO.md with the two questions that would pin it down. TWO TRAINS AT ONE PLATFORM ANSWERED TO ONE BUTTON. `porter.board` and `porter.detrain` carried no tray, so there was one button per platform however many trains stood at it and the reducer filled the first empty coach on the A/D tracks. `check` and the reducer were not even asking the same question: `check` skipped a train whose card refuses passenger work and the reducer did not. Both intents now carry an optional `trayId`, one function resolves the train and the coach for check/execute/reduce alike, `legal.ts` offers one candidate per train, and the label names it. A LOAD COULD BE MADE AND BROKEN WITHOUT GOING ANYWHERE. A Freight House could unload the boxcar it had just loaded; a platform could detrain the passengers it had just boarded. Full Revenue at both ends for a movement that never happened. Jesse's rule: a load made anywhere in an Office Area may not be broken anywhere in that Office Area, ever — it has to be carried to another district. The load carries the seat that made it (`RollingStock.origin`), stripped by `pooled` at every yard push. Measured at -0.60 +/- 0.10 Revenue a game (t = -6.1) over 400 paired deals: 78 worse, 3 better, 319 unchanged — free Revenue coming off the board, not a nerf. THE GROCER'S WAREHOUSE SHIPPED AND THE REFINERY RECEIVED. Both were `flow: 'both'` on the reading that "Freight House" was a collective term for exactly those two, and therefore what §9.3 described. The engine has dealt a Freight House CARD since before v0.4.9, so §9.3 names it and the argument goes. The card set agrees: all three Refinery modifiers grant +1 outbound. Refinery outbound-only, Grocer's inbound-only, Freight House the one two-way industry — which leaves exactly the one same-district pairing the rule above refuses. NOT REPRODUCED: cars left behind when backing up over them. Five layouts tried, including cars spotted at an industry; every one couples the lot. Three are pinned in `apply.test.ts`. One way to create such cars was closed anyway — `flyingSwitch` wrote its cut past `carsOn`. Both published replays that had gone dead were re-recorded; a rules change retires a save, and `harness.test.ts` is what catches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011nbvwWMef8CuEP6t5cgkTv
This commit is contained in:
co-authored by
Claude Opus 5
parent
f9c4d9fa92
commit
7c35e002af
+137
@@ -19,6 +19,143 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.4.9e — 2026-08-22
|
||||
|
||||
Six bugs came back from a gameplay-testing session on 0.4.9d. Five are fixed here; the sixth could not
|
||||
be reproduced and is written up in `TODO.md` with the two questions that would pin it down. Patched
|
||||
onto the 0.4.9d line, and applied to the 0.5/0.6 line as the same change.
|
||||
|
||||
### Two trains at one platform answered to one button
|
||||
|
||||
**REPORTED:** "Operating two trains in a station: the select button does not work. Regardless of which
|
||||
you pick, it is always one train, not the other."
|
||||
|
||||
It did not work because there was nothing for it to do. `porter.board` and `porter.detrain` carried
|
||||
**no tray at all** — `{ type, at }` and nothing else — so there was one "board passengers at (0,0)"
|
||||
button however many trains were standing at the platform, and the reducer walked `adOccupancy` and
|
||||
filled the first empty coach it met. The roster chip the player clicked chose which crew the board
|
||||
DREW and nothing else. Two independent things were wrong at once:
|
||||
|
||||
- `check` asked whether SOME train at the Office had an empty coach, skipping any whose card refuses
|
||||
passenger work (`refusesPassengers`, `refusesThisOffice`). The reducer did not skip those. So with a
|
||||
Military train and an ordinary one at the same platform, `check` said yes on behalf of the ordinary
|
||||
one and the reducer boarded the Military.
|
||||
- The action list collapses identical labels, and "board passengers at (0,0)" describes both trains —
|
||||
the same trap that once ate a turnout's second rotation and a Department discard.
|
||||
|
||||
Both intents now carry an optional `trayId`, one function (`passengerWork`) resolves which train and
|
||||
which coach for `check`, `execute` and the reducer alike, `legal.ts` enumerates one candidate per
|
||||
train standing at the Office, and the label names it: *"board passengers at (0,0) onto Train 9"*.
|
||||
`trayId` is OPTIONAL for the reason `switch.move`'s `via` is — intents are the canonical record every
|
||||
save and undo replay against, and absent still means "the first eligible train".
|
||||
|
||||
The events carry `trayId` and `coachIndex` rather than leaving the reducer to find them again, which
|
||||
is the lesson `unloadBegan`'s `carIndex` already taught: a reducer that re-derives the target is a
|
||||
second implementation of the rule, and it disagreed with the first.
|
||||
|
||||
### A load could be made and broken without going anywhere
|
||||
|
||||
**REPORTED, twice over:** "Freight House: boxcars loaded cannot be immediately unloaded. In the game
|
||||
we'll put the chip upside down in the tray to indicate." And: "Passenger stations: passengers just
|
||||
boarded cannot be immediately unloaded."
|
||||
|
||||
They could. A Freight House permits both directions, so the boxcar its own Laborers had just loaded
|
||||
was standing on its own industry track, loaded, with an empty of that type in the Division Yard and a
|
||||
free red box — every gate said yes. Passengers were worse: `porter.board` filled a coach and
|
||||
`porter.detrain` looked for "a loaded coach on a train at the Office", which is the coach that had
|
||||
just been filled. Full Revenue at both ends of a movement that never happened, for one Porter action.
|
||||
|
||||
**Jesse's rule, and it is wider than the report:** freight or passengers loaded anywhere in an Office
|
||||
Area may not be unloaded anywhere in that same Office Area — not at another facility, not in a later
|
||||
Stage. A train has to carry them to a different district. So a load carries a stamp naming the SEAT
|
||||
that made it (`RollingStock.origin`), and the stamp never expires; `laborer.beginUnload` and
|
||||
`porter.detrain` refuse a car stamped with the district they are standing in, with a rejection code of
|
||||
its own (`LOADED_IN_THIS_DISTRICT`) because "the car is loaded, the Laborer is free, the boxes are
|
||||
clear, and the only thing wrong is where it came from" deserves better than "wrong car".
|
||||
|
||||
**A seat, not a player**, because Employee Rotation moves players between chairs and the district
|
||||
stays with the chair. **Undefined, not −1**, for "no origin": the Division Yard opens with loaded cars
|
||||
and loaded coaches out of the common supply, and those are exactly the inbound traffic a solitaire
|
||||
district lives on — a sentinel inside `SeatIndex`'s own range is not a sentinel. And `pooled` strips
|
||||
the stamp at every yard push, because the stamp belongs to the LOAD: a train can retire at a Division
|
||||
Point with freight still aboard, and that car must not carry a district it left three Days ago into
|
||||
whatever train is made up from it next.
|
||||
|
||||
**Measured: −0.60 ± 0.10 Revenue a game** (t = −6.1) over 400 paired deals — 78 deals worse, 3 better,
|
||||
319 unchanged. That shape is the point. This is not a nerf spread across the game; it is a narrow
|
||||
piece of free Revenue coming off the board, and on four deals in five the bot never took it.
|
||||
|
||||
The screen's version of the upside-down chip: a car or coach loaded by this district reads *"loaded
|
||||
boxcar (loaded here)"* on the card, in the tray and in the facility panel.
|
||||
|
||||
### The Grocer's Warehouse shipped, and the Refinery received
|
||||
|
||||
**REPORTED:** "Grocer's warehouse should be receive only, does not ship anything out." And:
|
||||
"Refinery: only ships out tanks, does not receive anything."
|
||||
|
||||
Both were `flow: 'both'` in `content.ts`, put there deliberately and for a reason that has since
|
||||
collapsed. `card-reference.md` read: *"'Freight House' is not a card. It is the collective term for a
|
||||
freight facility that loads and unloads — the Grocer's Warehouse and the Oil Refinery."* If that were
|
||||
true, §9.3's "Passenger Facilities and Freight Houses permit cars to move each direction" named
|
||||
exactly those two, and they had to be two-way. But the engine has dealt a `freightHouse` card since
|
||||
before v0.4.9 — 6 copies, one slot each direction — so §9.3 names it, and the argument evaporates.
|
||||
|
||||
The card set says the same thing without needing the rules text. All three Refinery modifiers —
|
||||
Pipelines, Oil Depot, Viscosity Breakers — grant **+1 outbound**; a two-way Refinery would be the only
|
||||
industry in the game with no card able to raise one of its two directions.
|
||||
`StationMaster-Home-Deck-v0.4.5.md` prints "Refinery · Outbound · 1 out / 0 in" and "Grocer's
|
||||
Warehouse · Inbound · 0 out / 1 in".
|
||||
|
||||
So the Refinery ships and the Grocer's receives, and the **Freight House is the one two-way industry**
|
||||
— which also means the only same-district load-and-unload the district rule above has to refuse is a
|
||||
Freight House unloading its own work. The two fixes meet exactly where the report said they would.
|
||||
|
||||
A consequence worth naming rather than discovering: an **Ice House beside a Grocer's Warehouse is now
|
||||
a dead card**, its +1 outbound dropped on a direction the host does not have. That is the design, not
|
||||
an oversight — the Home Deck sheet says so outright, and names the Truck Dock's inbound grant beside
|
||||
the outbound-only Packing Sheds as the other example. `suppressedGrants` already reports it on the
|
||||
page. It does mean the v0.4.7 note in `TODO.md` that opened these two facilities up was half wrong,
|
||||
and it is annotated there rather than deleted: the *machinery* it built (an industry's printed flow is
|
||||
absolute; drop the grant, never open the direction) is exactly what makes this correction land.
|
||||
|
||||
### Not reproduced: cars left behind when backing up over them
|
||||
|
||||
**REPORTED:** "When I back up to collect standing cars and, further down the tracks, the caboose, I
|
||||
get the caboose but the cars remain. I can later drive right through them."
|
||||
|
||||
Not found, and not for want of looking. Cars on plain track on the way; cars spotted at an INDUSTRY on
|
||||
the way (Jesse's own guess at the shape); the train's own cut on the square it is pulling out of; a
|
||||
stale `standingWest`; an industry locked by MEN AT WORK. Every one couples the lot, and the last
|
||||
correctly blocks the whole route rather than letting the crew past. Three of them are now pinned in
|
||||
`apply.test.ts` so the case, when it is found, is somewhere none of them cover.
|
||||
|
||||
The reason it is hard to make happen is structural: coupling is mandatory (§A.4) and `exploreMoves`
|
||||
accumulates what it meets card by card, so a route that reaches the caboose has already met everything
|
||||
between. `carsOn` is the single answer to "what is standing here", and the movement walk, the sweep in
|
||||
`carsCoupled` and every renderer all ask it — so cars a train can drive through would have to be cars
|
||||
that are on screen and not in `carsOn`, and there is no such place.
|
||||
|
||||
There was one way to MAKE such a place, and it is closed: `flyingSwitch`'s reducer wrote the cut
|
||||
straight into `industryTrack`, which is not where `carsOn` looks on a Passenger Facility. `check`
|
||||
refuses a non-freight target so it never fired, but a trap that needs another rule to stay unsprung is
|
||||
still a trap; it goes through `carsOn` now.
|
||||
|
||||
`TODO.md` carries the two questions that would settle it: was there a second route to the caboose, and
|
||||
what did the move button say it would couple. The label names every car, so "couples caboose" and
|
||||
"couples 2 boxcars, caboose" are different bugs — the first is route selection, the second the sweep.
|
||||
|
||||
### Also
|
||||
|
||||
- **An unload never checks the facility's commodity** — found reading `laborer.beginUnload` for the
|
||||
district rule, not from play. It gates on `allows.inbound`, a loaded car, a matching empty in the
|
||||
yard and room in the red box, but never on `facilityCarTypes`, which `freightAgent.stockOutbound`
|
||||
does check. So a Freight House will unload a hopper. Left alone and logged: the district rule now
|
||||
refuses the one same-Office pairing that made it easy to reach, and the fix is a rule question about
|
||||
what an industry will accept, not a one-line guard.
|
||||
- **Both published replays that had gone dead were re-recorded** (`save-replay.ts 400 --top 3`). A save
|
||||
is a save from a particular ruleset, so a rules change retires the files that no longer replay —
|
||||
`harness.test.ts` catches it, which is what that test is for.
|
||||
|
||||
## 0.4.9d — 2026-08-21
|
||||
|
||||
Three bugs from the same playtest session, patched directly onto 0.4.9a rather than the in-progress
|
||||
|
||||
Reference in New Issue
Block a user