More bug fixes. Show the timetable, and stop the arrival message contradicting itself. Fixed division point in labels. And highlighted discard as targets.

This commit is contained in:
Jesse
2026-08-05 23:08:03 -04:00
parent 543584a2ca
commit f4c0f49604
13 changed files with 381 additions and 50 deletions
+71 -7
View File
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from 'node:path';
import { cardDescription, cardName, describeIntent, variantLabel } from '../src/sim/view.ts';
import { variantsFor } from '../src/engine/track.ts';
import { divisionSvg, officeSvg } from '../src/sim/board-svg.ts';
import { timetableHtml } from '../src/web/panels.ts';
import { record, renderHtml } from '../src/sim/replay.ts';
import { snapshot } from '../src/sim/view.ts';
import { createGame as createEngineGame } from '../src/engine/setup.ts';
@@ -343,18 +344,54 @@ describe('the board shows freight work happening', () => {
);
};
assert.deepEqual(filled(), ['green=FULL', 'maw', 'maw', 'maw', 'red'], 'the waiting load is not drawn');
// A Mine Tipple only ships OUT, so it has no red Unloading box at all — only the boxes an
// industry actually uses are drawn now.
assert.deepEqual(filled(), ['green=FULL', 'maw', 'maw', 'maw'], 'the waiting load is not drawn');
game.state.clock.phase = 'loadUnload';
game.state.clock.currentActor = 0;
assert.ok(submit(game, { type: 'laborer.startLoad', at: { row: -1, col: 0 } }));
assert.deepEqual(filled(), ['green', 'maw=FULL', 'maw', 'maw', 'red'], 'starting a load is invisible');
assert.deepEqual(filled(), ['green', 'maw=FULL', 'maw', 'maw'], 'starting a load is invisible');
assert.ok(submit(game, { type: 'laborer.advanceLoad', at: { row: -1, col: 0 }, box: 0 }));
assert.deepEqual(filled(), ['green', 'maw', 'maw=FULL', 'maw', 'red'], 'advancing a load is invisible');
assert.deepEqual(filled(), ['green', 'maw', 'maw=FULL', 'maw'], 'advancing a load is invisible');
assert.ok(submit(game, { type: 'laborer.advanceLoad', at: { row: -1, col: 0 }, box: 1 }));
assert.deepEqual(filled(), ['green', 'maw', 'maw', 'maw=FULL', 'red'], 'the load did not reach WORK');
assert.deepEqual(filled(), ['green', 'maw', 'maw', 'maw=FULL'], 'the load did not reach WORK');
});
it('draws the pipeline the way the freight actually flows', () => {
// REPORTED after unloading at a warehouse: "it went W, A, M, and then to the red box at the far
// right." §9.3 runs loading Green → MEN → AT → WORK → car, and unloading the other way entirely:
// car → WORK → AT → MEN → red. So green and red BOTH sit beside MEN and the car sits beside
// WORK — but red was drawn at the far right, which is exactly where the car is, so an unload
// looked like it ran backwards across the row and landed on the end it came from.
const game = newGame(5);
const area = game.state.officeAreas.get(0)!;
const fac = (kind: string, out: boolean, into: boolean) => ({
geometry: { kind: 'facility', facility: kind },
baseOperationalRail: true, standing: [], modifiers: [], enhancements: [],
facility: {
kind: 'freight', subtype: kind,
allows: { outbound: out, inbound: into },
outboundBox: [], inboundBox: [], capacity: { outbound: out ? 1 : 0, inbound: into ? 1 : 0 },
menAtWork: [null, null, null], industryTrack: { length: 2, cars: [] },
laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 },
},
});
area.grid.set('-1,0', fac('grocersWarehouse', false, true) as never);
area.grid.set('-1,1', fac('mineTipple', true, false) as never);
const cells = view(game).cells;
const order = (row: number, col: number): string[] => {
const cell = cells.find((c) => c.row === row && c.col === col)!;
return [...officeSvg([cell], 0).matchAll(/class="bs-wb (bs-\w+)/g)].map((m) => m[1]!.replace('bs-', ''));
};
// Receives only: the red box sits BEFORE the sign, which is the end an unload arrives at.
assert.deepEqual(order(-1, 0), ['red', 'maw', 'maw', 'maw'], 'the red box is not beside MEN');
// Ships out only: green before the sign, and no red box at all.
assert.deepEqual(order(-1, 1), ['green', 'maw', 'maw', 'maw'], 'the green box is not beside MEN');
});
it('names where the work has got to, in the tooltip', () => {
@@ -1465,6 +1502,32 @@ describe('the static build', () => {
assert.notEqual(front, back, 'the two ends still read identically, so one is dropped from the list');
});
it('shows where a played train card landed on the timetable', () => {
// REPORTED: playing a train card rolls 1D12 for its departure Stage and the card simply left the
// hand — the answer arrived only as one line in the history panel. The twelve slots have been in
// the Frame all along and only the standalone replay ever drew them.
const game = newGame(430);
let landed: number | null = null;
for (let i = 0; i < 900 && currentActor(game) !== null; i++) {
const { options } = actionGroups(game);
if (options.length === 0 || !submit(game, options[0]!)) break;
if (game.scheduled !== null) {
landed = game.scheduled;
break;
}
}
assert.ok(landed !== null, 'no train was ever scheduled');
const f = view(game);
assert.ok(f.timetable[landed!] !== null, 'the slot the roll reported is empty');
// The roll gets a sound, because it is the one die a player rolls and it landed silently.
assert.ok(game.cues.includes('schedule'), 'scheduling a train made no sound');
// And the panel flashes the slot that changed, marking the moment rather than the state.
const html = timetableHtml(f, landed);
assert.match(html, /tt-slot[^"]*fresh/, 'the new slot is not flagged');
assert.equal((timetableHtml(f, null).match(/fresh/g) ?? []).length, 0, 'the flash must not persist');
assert.match(html, new RegExp(`T${f.timetable[landed!]}`), 'the train is not named in its slot');
});
it('draws the side panels from one place, on both screens', () => {
// A replay is the game being WATCHED rather than played, so it should look like the game. The
// site's viewer had three panels against the play page's eight — no facilities, no blockers, no
@@ -1632,12 +1695,13 @@ describe('the Division map shows the whole route', () => {
it('draws the Division as a line with two ends, never as a loop', () => {
// Seating players around a table invites exactly one misreading: that the route joins up. It
// does not — trains enter at one Division Point and leave at the other.
// does not. Both ends are labelled as ends, not as an entrance and an exit: odd trains run west
// and even run east, so each Division Point is a way on AND a way off.
for (const players of [1, 2, 3, 4]) {
const svg = divisionFor(players);
assert.equal((svg.match(/class="bs-stop"/g) ?? []).length, 2, `${players}p has no pair of buffer stops`);
assert.match(svg, /trains enter/, `${players}p does not label where trains enter`);
assert.match(svg, /trains leave/, `${players}p does not label where trains leave`);
assert.match(svg, /west end · in and out/, `${players}p does not label the west end`);
assert.match(svg, /east end · in and out/, `${players}p does not label the east end`);
}
});
});