v0.4.9d — three playtest bugs: a train that looked gone, an unload that ignored the pick, a hang that wasn't one
Patched directly onto 0.4.9a rather than the in-progress 0.5.0 line. A switching train vanished from the board the moment it left the Office. `selectedTrain` in `officeSvg` (board-svg.ts), which gates whether a card draws a train's crew badge, was only ever assigned inside the `cell.adTracks !== null` branch — true for the Office card alone. A train standing anywhere else, which is everywhere it stands while actually being switched, drew no badge at all. Game state was never affected; confirmed against the reported save with the engine directly. Fix hoists the assignment out of the guard so it runs for any card with a train on it. Unloading always took the westmost car, whichever one was picked. `laborer.beginUnload` carried the player's chosen `carIndex`, and `check()` validated that specific car, but the `unloadBegan` event it produced carried only the car's type — the reducer that performs the swap re-derived the target with `industryTrack.cars.findIndex(c => c.loaded)`, which always answers the first loaded car in track order regardless of what was requested. Fix adds `carIndex` to the event and uses it directly. A legal decision could render with zero buttons, which looked exactly like a hang. The "where does this Extra start" decision is titled by `trainCardTitle`, beginning "Making up Extra X22…" — the same prefix `renderActions()` stripped from the action list on the assumption it only ever belonged to the separate yard-chip car-placement panel. With no tray yet being filled, that panel is null, so nothing rendered the Extra's decision either: a legal, correctly-computed option with no button anywhere on the page. Replaying the reported save found no engine deadlock at any step — the engine always had a move. Fix matches the exact title of the one group the yard-chip panel covers instead of a title-prefix regex. New tests for all three: a train parked on an ordinary facility card must draw its crew badge; three differently-typed loaded cars, unloading index 2, must leave indices 0 and 1 alone; the existing softlock regression test now mirrors the real render filter instead of only the raw menu, plus a deterministic test for the exact reported scenario. 590 tests, 0 failures.
This commit is contained in:
+18
-2
@@ -1663,7 +1663,13 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] {
|
||||
case 'laborer.beginUnload': {
|
||||
const f = facilityAt(s, player, i.at)!;
|
||||
return [
|
||||
{ type: 'unloadBegan', player, at: i.at, carType: f.industryTrack.cars[i.carIndex]!.type },
|
||||
{
|
||||
type: 'unloadBegan',
|
||||
player,
|
||||
at: i.at,
|
||||
carType: f.industryTrack.cars[i.carIndex]!.type,
|
||||
carIndex: i.carIndex,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2206,7 +2212,17 @@ export function reduce(s: GameState, e: GameEvent): void {
|
||||
|
||||
case 'unloadBegan': {
|
||||
const f = facilityAt(s, e.player, e.at)!;
|
||||
const ci = f.industryTrack.cars.findIndex((c) => c.loaded);
|
||||
/**
|
||||
* THE CAR THE PLAYER PICKED, not merely "a loaded one".
|
||||
*
|
||||
* REPORTED: with several loaded cars spotted on one industry track, unloading always took the
|
||||
* WESTMOST car regardless of which one was chosen. This used to re-derive the target with
|
||||
* `industryTrack.cars.findIndex(c => c.loaded)`, which returns the same answer — the first
|
||||
* loaded car in track order — no matter which `carIndex` the intent actually named; `check`
|
||||
* had already validated that specific car, and the choice was thrown away between there and
|
||||
* here. `e.carIndex` is exactly what `laborer.beginUnload`'s reducer read off the intent.
|
||||
*/
|
||||
const ci = e.carIndex;
|
||||
if (ci >= 0) {
|
||||
/**
|
||||
* §9.3 — the replacement empty comes out of the Division Yard. Conjuring it here is what
|
||||
|
||||
@@ -167,7 +167,11 @@ export type GameEvent =
|
||||
| { type: 'loadAdvanced'; player: PlayerIndex; at: GridCoord; fromBox: number; toBox: number }
|
||||
| { type: 'unloadCompleted'; player: PlayerIndex; at: GridCoord; carType: CarType }
|
||||
| { type: 'loadCompleted'; player: PlayerIndex; at: GridCoord; carType: CarType }
|
||||
| { type: 'unloadBegan'; player: PlayerIndex; at: GridCoord; carType: CarType }
|
||||
/**
|
||||
* `carIndex` carries the SPECIFIC car the player picked off the industry track — see the comment
|
||||
* on `laborer.beginUnload` in `apply.ts` for why the reducer must not re-derive it.
|
||||
*/
|
||||
| { type: 'unloadBegan'; player: PlayerIndex; at: GridCoord; carType: CarType; carIndex: number }
|
||||
// -- consequences
|
||||
| { type: 'revenueChanged'; player: PlayerIndex; delta: number; total: number; reason: string }
|
||||
| { type: 'phaseEnded'; player: PlayerIndex; phase: string };
|
||||
|
||||
+10
-2
@@ -568,13 +568,21 @@ export function officeSvg(
|
||||
* Selected train's chip is lit the same amber the crew strip and action buttons already use,
|
||||
* and it is the one whose consist is drawn at the rail below. Every occupied chip carries
|
||||
* `data-crew`, so the play page can wire a click to switch which train that is.
|
||||
*
|
||||
* COMPUTED FOR EVERY CARD, NOT JUST THE OFFICE. This used to sit inside the `adTracks !== null`
|
||||
* branch below, so it was only ever assigned on the Office card — a train standing anywhere else
|
||||
* in the district (which is everywhere it stands while actually being switched) got no chip at
|
||||
* all: no label, no engine arrow, and no cars drawn, though the game state never lost it. REPORTED:
|
||||
* dropping a car at the Freight House made "the train vanish off the face of the earth" — it had
|
||||
* simply left the one square this code drew a crew on. The Office is the only square that can hold
|
||||
* more than one train, so `cell.trains[0]` is exactly right everywhere else too.
|
||||
*/
|
||||
let selectedTrain: CellView['trains'][number] | null = null;
|
||||
const selectedTrain: CellView['trains'][number] | null =
|
||||
cell.trains.find((t) => t.trayId === selectedTrayId) ?? cell.trains[0] ?? null;
|
||||
if (cell.adTracks !== null) {
|
||||
const adTracks = cell.adTracks;
|
||||
const gaps = Math.max(0, adTracks - 1);
|
||||
const chipW = Math.min(52, (W - 12 - 4 * gaps) / adTracks);
|
||||
selectedTrain = cell.trains.find((t) => t.trayId === selectedTrayId) ?? cell.trains[0] ?? null;
|
||||
for (let i = 0; i < adTracks; i++) {
|
||||
const train = cell.trains[i] ?? null;
|
||||
const cx = 6 + i * (chipW + 4);
|
||||
|
||||
+11
-1
@@ -832,9 +832,19 @@ function renderActions(
|
||||
* Everything about a card in hand now lives on the card, and everything about making up a train
|
||||
* lives on the yard chip. What remains is the rest of the turn: the Local Operations choice,
|
||||
* drawing, switching moves, the Freight Agent, and finishing.
|
||||
*
|
||||
* EXCLUDE BY TITLE, NOT BY PREFIX. This used to be `!/^Making up /.test(g.title)`, on the
|
||||
* assumption only the yard-chip panel's own group is ever titled that way. It is not: "choose
|
||||
* where Extra X22 starts" is titled by `trainCardTitle`, which also begins "Making up Extra
|
||||
* X22…", so the regex swallowed it too — and with no tray yet being filled, `menu.makeUp` is
|
||||
* null, so nothing rendered it anywhere else. REPORTED as the game hanging with the Freight
|
||||
* House mid-cycle: an Extra came due, the action panel had a legal decision and zero buttons,
|
||||
* and nothing short of restarting looked like it would ever move again. Matching the exact
|
||||
* title of the ONE group `menu.makeUp` actually covers leaves every other "Making up …" group,
|
||||
* however it is titled, on screen where a player can act on it.
|
||||
*/
|
||||
html += menu.direct
|
||||
.filter((g) => !/^(Play|Discard) a card from my hand$/.test(g.title) && !/^Making up /.test(g.title))
|
||||
.filter((g) => !/^(Play|Discard) a card from my hand$/.test(g.title) && g.title !== menu.makeUp?.title)
|
||||
.map(
|
||||
(g) =>
|
||||
`<div class="grp"><h3>${esc(g.title)}</h3>` +
|
||||
|
||||
Reference in New Issue
Block a user