Show the whole Division mainline. district now has option for auto hiding on phases where not needed. scaffold for multi-player geometry showing division. regions on mainline cards implemented. bug fix on laying rail cards. optimized replay (carried links forward). Add sounds, first pass

This commit is contained in:
Jesse
2026-08-04 11:26:18 -04:00
parent ec792dfc15
commit c3c5c59a45
16 changed files with 1249 additions and 129 deletions
+53 -3
View File
@@ -14,7 +14,7 @@ import { createGame } from '../src/engine/setup.ts';
import type { GameConfig } from '../src/engine/state.ts';
import { developerBot, playGame } from '../src/sim/bot.ts';
import { impediments, isVisible, narrate, phaseLabel } from '../src/sim/narrate.ts';
import { record, renderHtml } from '../src/sim/replay.ts';
import { compress, rehydrateCells, record, renderHtml } from '../src/sim/replay.ts';
import { summarize } from '../src/sim/stats.ts';
const config: GameConfig = {
@@ -189,8 +189,16 @@ describe('replay HTML', () => {
assert.deepEqual(externals, [], `external references: ${externals.join(', ')}`);
});
it('embeds the frame data', () => {
assert.match(html, /const FRAMES = \[/);
it('embeds the frame data and the dictionaries it is packed against', () => {
// The frames are packed now: a card's identity and its description are written once into
// CARDS/WHATS and referenced by integer. A page with FRAMES but no dictionaries would draw an
// empty board rather than fail, so all three are checked.
assert.match(html, /const PACKED = \{/);
assert.match(html, /FRAMES = PACKED\.frames/);
assert.match(html, /CARDS = PACKED\.cards/);
assert.match(html, /WHATS = PACKED\.whats/);
// And the unpacking function must be emitted, not just referenced.
assert.match(html, /const rehydrateCells = function/);
});
it('offers the controls that make it usable', () => {
@@ -300,3 +308,45 @@ describe('replay HTML', () => {
assert.ok(mb < 5, `replay is ${mb.toFixed(1)} MB`);
});
});
describe('the replay wire format is lossless (regression)', () => {
it('rebuilds every cell exactly, through the same function the page runs', () => {
// The frames are packed before they reach the page: a card's identity and its description are
// written once and referenced by integer, and a cell points at its facility instead of carrying
// a second copy of it. That is 45% of the file — and it is only safe if unpacking is exact.
//
// `rehydrateCells` is the function the page itself runs, emitted by toString(), so this checks
// the real thing rather than a second implementation of it.
const rec = record(880009, 'standard', 4000);
const packed = compress(rec.frames);
let checked = 0;
let carried: unknown[] = [];
let carriedFacs: unknown[] = [];
for (let i = 0; i < rec.frames.length; i++) {
const wire = packed.frames[i] as Record<string, unknown>;
// null means "unchanged since the last frame that carried it", exactly as the page resolves it
if (wire['cells'] !== null) carried = wire['cells'] as unknown[];
if (wire['facilities'] !== null) carriedFacs = wire['facilities'] as unknown[];
const back = rehydrateCells(carried, packed.cards, packed.whats, carriedFacs);
assert.deepEqual(back, rec.frames[i]!.cells, `frame ${i} did not survive the round trip`);
checked++;
}
assert.ok(checked > 100, `only ${checked} frames checked`);
});
it('writes each distinct card and description once, not once per frame', () => {
const rec = record(880009, 'standard', 4000);
const packed = compress(rec.frames);
const cellsSeen = rec.frames.reduce((n, f) => n + f.cells.length, 0);
assert.ok(
packed.cards.length < cellsSeen / 20,
`${packed.cards.length} card entries for ${cellsSeen} cell-frames — identity is not interned`,
);
assert.ok(
packed.whats.length < cellsSeen / 20,
`${packed.whats.length} descriptions for ${cellsSeen} cell-frames — prose is not interned`,
);
});
});