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:
Jesse.Markowitz
2026-08-21 23:55:08 -04:00
co-authored by Claude Opus 5
parent f9c4d9fa92
commit 7c35e002af
25 changed files with 2753 additions and 1812 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+89 -24
View File
@@ -1,7 +1,7 @@
{
"seed": 927523,
"title": "12 Revenue · seed 927523",
"note": "12 Revenue over 5 Days · 2 train(s) on the timetable · 20 cards down · no collisions · played by developer",
"title": "11 Revenue · seed 927523",
"note": "11 Revenue over 5 Days · 2 train(s) on the timetable · 21 cards down · no collisions · played by developer",
"rules": {
"startingHand": "threeRandom",
"revenue": {
@@ -320,21 +320,24 @@
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray3"
},
{
"type": "porter.board",
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray3"
},
{
"type": "porter.detrain",
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray3"
},
{
"type": "loadUnload.end"
@@ -589,21 +592,24 @@
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray2"
},
{
"type": "porter.board",
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray2"
},
{
"type": "porter.detrain",
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray2"
},
{
"type": "loadUnload.end"
@@ -951,36 +957,38 @@
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray3"
},
{
"type": "porter.board",
"at": {
"row": 0,
"col": 0
}
},
{
"type": "porter.detrain",
"at": {
"row": 0,
"col": 0
}
},
"trayId": "tray3"
},
{
"type": "loadUnload.end"
},
{
"type": "localOps.choose",
"option": "freightAgent"
"option": "draw"
},
{
"type": "freightAgent.clearInbound",
"at": {
"row": 0,
"col": 0
"type": "draw.fromHomeOffice"
},
{
"type": "card.play",
"cardId": "c162",
"placement": {
"row": -2,
"col": 2
},
"index": 0
"variant": 1
},
{
"type": "draw.end"
},
{
"type": "loadUnload.end"
@@ -1179,6 +1187,12 @@
"carType": "coach",
"loaded": false
},
{
"type": "newTrain.placeCar",
"trayId": "tray3",
"carType": "coach",
"loaded": false
},
{
"type": "laborer.advanceLoad",
"at": {
@@ -1236,7 +1250,58 @@
"option": "switch"
},
{
"type": "switch.end"
"type": "switch.move",
"trayId": "tray3",
"to": {
"row": 0,
"col": 1
},
"reverse": true
},
{
"type": "switch.move",
"trayId": "tray3",
"to": {
"row": 0,
"col": 0
},
"reverse": false
},
{
"type": "switch.move",
"trayId": "tray3",
"to": {
"row": 0,
"col": 1
},
"reverse": true
},
{
"type": "switch.move",
"trayId": "tray3",
"to": {
"row": 0,
"col": 0
},
"reverse": false
},
{
"type": "switch.move",
"trayId": "tray3",
"to": {
"row": 0,
"col": 1
},
"reverse": true
},
{
"type": "switch.move",
"trayId": "tray3",
"to": {
"row": 0,
"col": 0
},
"reverse": false
},
{
"type": "laborer.advanceLoad",