Draw freight on the card, and teach the bot to build a railroad

This commit is contained in:
Jesse
2026-08-04 04:59:23 -04:00
parent e23d81eade
commit 982952fd6c
11 changed files with 1288 additions and 89 deletions
+95
View File
@@ -12,6 +12,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { cardDescription, describeIntent } from '../src/sim/view.ts';
import { officeSvg } from '../src/sim/board-svg.ts';
import {
actionGroups,
actionMenu,
@@ -188,6 +189,100 @@ describe('the action menu presents choices the way they are made', () => {
});
});
describe('the board shows freight work happening', () => {
it('moves the load across MEN | AT | WORK on the card itself', () => {
// Advancing a load changed only the side panel, so the card being worked showed nothing. The
// load crossing green -> MEN|AT|WORK -> a spotted car IS freight; the printed cards put these
// squares under the track for exactly this reason.
const game = newGame(5);
const area = game.state.officeAreas.get(0)!;
area.grid.set('-1,0', {
geometry: { kind: 'facility', facility: 'mineTipple', axis: 'ns' },
baseOperationalRail: true, standing: [], modifiers: [], enhancements: [],
facility: {
kind: 'freight', subtype: 'mineTipple',
allows: { outbound: true, inbound: false },
outboundBox: [{ type: 'hopper', loaded: true }], inboundBox: [],
capacity: { outbound: 1, inbound: 0 },
menAtWork: [null, null, null],
industryTrack: { length: 2, cars: [{ type: 'hopper', loaded: false }] },
laborers: 3, porters: 0, usedThisStage: { laborers: 0, porters: 0 },
},
} as never);
const filled = (): string[] => {
const cell = view(game).cells.find((c) => c.row === -1 && c.col === 0)!;
const svg = officeSvg([cell], 0);
return [...svg.matchAll(/class="bs-wb (bs-\w+)( bs-occ)?"/g)].map(
(m) => m[1]!.replace('bs-', '') + (m[2] ? '=FULL' : ''),
);
};
assert.deepEqual(filled(), ['green=FULL', 'maw', 'maw', 'maw', 'red'], '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.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.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');
});
it('names where the work has got to, in the tooltip', () => {
const game = newGame(5);
const area = game.state.officeAreas.get(0)!;
area.grid.set('-1,0', {
geometry: { kind: 'facility', facility: 'mineTipple', axis: 'ns' },
baseOperationalRail: true, standing: [], modifiers: [], enhancements: [],
facility: {
kind: 'freight', subtype: 'mineTipple',
allows: { outbound: true, inbound: false },
outboundBox: [], inboundBox: [],
capacity: { outbound: 1, inbound: 0 },
menAtWork: [null, { type: 'hopper', dir: 'out' }, null],
industryTrack: { length: 2, cars: [] },
laborers: 2, porters: 0, usedThisStage: { laborers: 0, porters: 0 },
},
} as never);
const cell = view(game).cells.find((c) => c.row === -1 && c.col === 0)!;
assert.match(cell.what, /a load is on AT/, 'the card does not say where the work has got to');
});
it('keeps the enhancement label clear of the pipeline squares', () => {
// `overpass` and `facingPointLocks` are placed `onCard`, so they can land on a facility. At its
// old baseline the label printed straight through the green/MEN|AT|WORK/red row.
const game = newGame(5);
const area = game.state.officeAreas.get(0)!;
area.grid.set('-1,0', {
geometry: { kind: 'facility', facility: 'mineTipple', axis: 'ns' },
baseOperationalRail: true, standing: [], modifiers: [], enhancements: ['overpass'],
facility: {
kind: 'freight', subtype: 'mineTipple',
allows: { outbound: true, inbound: false },
outboundBox: [], inboundBox: [],
capacity: { outbound: 1, inbound: 0 },
menAtWork: [null, null, null],
industryTrack: { length: 2, cars: [] },
laborers: 2, porters: 0, usedThisStage: { laborers: 0, porters: 0 },
},
} as never);
const cell = view(game).cells.find((c) => c.row === -1 && c.col === 0)!;
const svg = officeSvg([cell], 0);
const enhY = Number(/class="bs-enh" x="\d+" y="(\d+)"/.exec(svg)?.[1]);
const boxTop = Math.min(
...[...svg.matchAll(/class="bs-wb [^"]*" x="\d+" y="(\d+)"/g)].map((m) => Number(m[1])),
);
assert.ok(Number.isFinite(enhY) && Number.isFinite(boxTop), 'label or squares are not drawn');
// 9px text sits roughly 9px above its baseline, so the baseline itself must clear the squares.
assert.ok(enhY <= boxTop, `enhancement label at y=${enhY} prints over the squares at y=${boxTop}`);
});
});
describe('board highlighting', () => {
it('gives every spot a coordinate to highlight', () => {
const game = newGame(555);