v0.4.8 — the Limits bound the whole district, the nine spots really are nine, and an action points at its square

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQAJ4dND7enLj54eLyiF2C
This commit is contained in:
Jesse
2026-08-19 13:38:06 -04:00
co-authored by Claude Opus 5
parent 9f3b92d08e
commit 461c4d9bac
25 changed files with 3264 additions and 4524 deletions
+86 -2
View File
@@ -598,6 +598,54 @@ describe('the board shows freight work happening', () => {
assert.ok(y > 48, `the caption at y=${y} prints over the rail`);
});
it('draws the Limits as the edge of the buildable district, at every row', () => {
/**
* Track may not be laid outside the Limits at ANY row now (§2.1), and two signs sitting on the
* Running Track could not say that: a player looking at open ground beyond a sign had no way to
* know nothing of his would ever go there until the square failed to light up.
*
* Just OUTSIDE the sign's own column, because the sign stands ON the boundary — a siding may run
* under it — so a line drawn inside the sign would teach the opposite of the rule.
*/
const game = newGame(555);
const area = game.state.officeAreas.get(0)!;
const f = view(game);
assert.deepEqual(
f.limits,
{ west: area.limitsWest.col, east: area.limitsEast.col },
'the Frame does not carry the district edges a remote client cannot look up',
);
const svg = officeSvg(f.cells, f.runningRow, [], [], f.limits);
const xs = [...svg.matchAll(/class="bs-limitline" x1="(-?[\d.]+)"/g)].map((m) => Number(m[1]));
assert.equal(xs.length, 2, 'the district was drawn without both of its edges');
// The sign cards themselves must fall INSIDE the two lines.
const signX = [...svg.matchAll(/data-cell="(-?\d+),(-?\d+)"[^>]*transform="translate\((-?[\d.]+)/g)]
.filter((m) => Number(m[2]) === area.limitsWest.col || Number(m[2]) === area.limitsEast.col)
.map((m) => Number(m[3]));
assert.ok(signX.length > 0, 'no Limits sign was drawn to check the boundary against');
for (const x of signX) {
assert.ok(
x >= Math.min(...xs) && x <= Math.max(...xs),
`a Limits sign at x=${x} sits outside its own boundary (${xs.join(', ')})`,
);
}
assert.match(svg, /bs-limitlab/, 'the boundary is drawn but never named');
// AND ON THE CANVAS. The west sign is the leftmost card, so a line drawn outside its column sits
// at a negative x — off a viewBox that starts at 0, which is a boundary nobody can see.
const vb = /viewBox="(-?[\d.]+) (-?[\d.]+) ([\d.]+) ([\d.]+)"/.exec(svg);
assert.ok(vb, 'the board produced no viewBox');
const x0 = Number(vb![1]);
for (const x of xs) {
assert.ok(
x >= x0 && x <= x0 + Number(vb![3]),
`the boundary at x=${x} is drawn outside the canvas (${x0} to ${x0 + Number(vb![3])})`,
);
}
});
it('says on the card which way an industry runs, and how many workers it has', () => {
/**
* TWO REPORTS, ONE CARD.
@@ -1314,7 +1362,7 @@ describe('the static build', () => {
}
});
it('renders clickable highlights on the board when a card is picked', async () => {
it('renders clickable highlights on the board, and points at the square an action names', async () => {
// The real check: load the EMITTED bundle with a DOM stub, click a card, and confirm the board
// came back with highlighted squares wired to handlers. Asserting the data has coordinates says
// nothing about whether the page draws them.
@@ -1366,7 +1414,12 @@ describe('the static build', () => {
const classes = new Set<string>();
found.set(key, {
onclick: null,
classList: { add: (c: string) => void classes.add(c), has: (c: string) => classes.has(c) },
classList: {
add: (c: string) => void classes.add(c),
// Pointing at a square is a HOVER: it has to come off again when the cursor leaves.
remove: (c: string) => void classes.delete(c),
has: (c: string) => classes.has(c),
},
classes,
});
}
@@ -1493,6 +1546,37 @@ describe('the static build', () => {
const lit = (grid['highlighted'] as () => unknown[])();
const ghosts = /data-ghost="/.test(html);
assert.ok(lit.length > 0 || ghosts, 'picking a card highlighted nothing on the board');
/**
* POINTING AT THE SQUARE A BUTTON MEANS — wired on the emitted bundle, not asserted off the menu.
*
* The menu carrying a coordinate proves nothing about whether the page draws it, which is the
* same reason the highlight check above loads the real bundle. Reported by Jesse: the action list
* is a column of near-identical sentences separated by "(1,3)" against "(-1,3)", and picking the
* wrong one is a click to undo in solitaire and unrecoverable in a multiplayer game.
*/
const withSquare = (
(actions['querySelectorAll'] as (s: string) => Record<string, unknown>[])
.call(actions, 'button.act')
.filter((n) => (n['dataset'] as Record<string, string>)['square'])
);
assert.ok(withSquare.length > 0, 'no action button carries the square it acts on');
const btn = withSquare[0]!;
const key = (btn['dataset'] as Record<string, string>)['square']!;
assert.match(key, /^-?\d+,-?\d+$/, `"${key}" is not a grid coordinate`);
(btn['onmouseenter'] as (() => void) | null)?.();
const pointed = (grid['highlighted'] as () => Record<string, unknown>[])().filter((g) =>
(g['classes'] as Set<string>).has('bs-point'),
);
assert.equal(pointed.length, 1, `hovering the action for (${key}) marked ${pointed.length} squares`);
(btn['onmouseleave'] as (() => void) | null)?.();
assert.equal(
(grid['highlighted'] as () => Record<string, unknown>[])().filter((g) =>
(g['classes'] as Set<string>).has('bs-point'),
).length,
0,
'the square stayed lit after the cursor left the button',
);
});
it('busts the cache on every module, so a deploy cannot half-load', () => {