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:
+87
-9
@@ -36,7 +36,7 @@ export { cardName, describeIntent, snapshot } from './view.ts';
|
||||
import type { Frame } from './view.ts';
|
||||
import type { Decision } from './view.ts';
|
||||
import { cardName, describeDecision, snapshot, trainName } from './view.ts';
|
||||
import { BOARD_CSS, divisionSvg, ghostSvg, officeSvg } from './board-svg.ts';
|
||||
import { BOARD_CSS, divisionSvg, officeSvg } from './board-svg.ts';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frame shape — only what the viewer draws
|
||||
@@ -147,19 +147,94 @@ export function record(seed: number, length: GameLength, maxSteps = 100_000): Re
|
||||
*
|
||||
* Only the wire format changes: `record()` still returns complete frames, so tests and any other
|
||||
* consumer are unaffected. The page resolves a null by walking back to the last frame that carried
|
||||
* the field.
|
||||
* the field, and rebuilds each cell from the dictionaries below.
|
||||
*
|
||||
* MEASURED, on a 735-frame game, before any of this: `cells` was 62% of 7.1 MB of JSON, and within
|
||||
* it the cost was not where the TODO said. Carrying `links` forward — the item this started as —
|
||||
* would have saved 5% of cells. What actually costs:
|
||||
*
|
||||
* what 32% of cells long prose, and the SAME prose on every turnout in the district
|
||||
* facility 24% of cells the identical object is already in the frame's `facilities`
|
||||
* identity 26% of cells row/col/kind/label/running/links, fixed once the card is laid
|
||||
*
|
||||
* So each of the three is interned instead. A card's identity and its description are written once
|
||||
* for the whole recording and referenced by integer, and a cell points at its facility rather than
|
||||
* carrying a second copy of it. What stays per-frame is only what genuinely changes: the cars
|
||||
* standing there, the crew, and any Enhancement laid on it.
|
||||
*/
|
||||
function compress(frames: Frame[]): unknown[] {
|
||||
type Packed = {
|
||||
/** Static per square, written once: row, col, kind, label, running, links. */
|
||||
cards: unknown[];
|
||||
/** Distinct `what` descriptions, written once. */
|
||||
whats: string[];
|
||||
frames: unknown[];
|
||||
};
|
||||
|
||||
export function compress(frames: Frame[]): Packed {
|
||||
const cards: unknown[] = [];
|
||||
const cardAt = new Map<string, number>();
|
||||
const whats: string[] = [];
|
||||
const whatAt = new Map<string, number>();
|
||||
|
||||
const packed = frames.map((f) => {
|
||||
const cells = f.cells.map((c) => {
|
||||
const idKey = JSON.stringify([c.row, c.col, c.kind, c.label, c.running, c.links]);
|
||||
let ci = cardAt.get(idKey);
|
||||
if (ci === undefined) {
|
||||
ci = cards.length;
|
||||
cards.push([c.row, c.col, c.kind, c.label, c.running, c.links]);
|
||||
cardAt.set(idKey, ci);
|
||||
}
|
||||
let wi = whatAt.get(c.what);
|
||||
if (wi === undefined) {
|
||||
wi = whats.length;
|
||||
whats.push(c.what);
|
||||
whatAt.set(c.what, wi);
|
||||
}
|
||||
// Identity, not equality: `snapshot` pushes the very same FacilityView object into both the
|
||||
// cell and the frame's `facilities`, so this always resolves.
|
||||
const fi = c.facility ? f.facilities.indexOf(c.facility) : -1;
|
||||
return [ci, wi, c.enhancements, c.tray, c.cars, fi];
|
||||
});
|
||||
return { ...f, cells } as unknown as Frame;
|
||||
});
|
||||
|
||||
const keys = ['cells', 'facilities', 'division'] as const;
|
||||
let prev: Record<string, string> = {};
|
||||
return frames.map((f, i) => {
|
||||
const out: Record<string, unknown> = { ...f };
|
||||
const out = packed.map((f, i) => {
|
||||
const o: Record<string, unknown> = { ...f };
|
||||
for (const k of keys) {
|
||||
const json = JSON.stringify(f[k]);
|
||||
if (i > 0 && prev[k] === json) out[k] = null;
|
||||
if (i > 0 && prev[k] === json) o[k] = null;
|
||||
prev[k] = json;
|
||||
}
|
||||
return out;
|
||||
return o;
|
||||
});
|
||||
|
||||
return { cards, whats, frames: out };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild whole CellViews from the compact wire rows.
|
||||
*
|
||||
* Exported and then emitted into the page by `toString()`, exactly as the two board renderers are:
|
||||
* a second copy of this in the page's inline script could drift from the packing above, and the
|
||||
* failure would be a board that draws the wrong cards rather than an error.
|
||||
*/
|
||||
export function rehydrateCells(
|
||||
packed: unknown[],
|
||||
cards: unknown[],
|
||||
whats: string[],
|
||||
facs: unknown[],
|
||||
): unknown[] {
|
||||
return packed.map((row) => {
|
||||
const p = row as [number, number, string[], string | null, string[], number];
|
||||
const c = cards[p[0]] as [number, number, string, string, boolean, string[]];
|
||||
return {
|
||||
row: c[0], col: c[1], kind: c[2], label: c[3], running: c[4], links: c[5],
|
||||
what: whats[p[1]], enhancements: p[2], tray: p[3], cars: p[4],
|
||||
facility: p[5] < 0 ? null : facs[p[5]],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -322,7 +397,8 @@ kbd{background:#2a3038;border:1px solid var(--line);border-radius:3px;padding:0
|
||||
const divisionSvg = ${divisionSvg.toString()};
|
||||
const officeSvg = ${officeSvg.toString()};
|
||||
|
||||
const FRAMES = ${JSON.stringify(compress(rec.frames))};
|
||||
const PACKED = ${JSON.stringify(compress(rec.frames))};
|
||||
const FRAMES = PACKED.frames, CARDS = PACKED.cards, WHATS = PACKED.whats;
|
||||
/* The board changes rarely, so cells/facilities/division are emitted as null when identical to the
|
||||
previous frame and resolved by walking back. Re-serialising the full grid every frame was 63% of
|
||||
a 5.2 MB page. The scrubber jumps anywhere, hence the walk rather than a running pointer. */
|
||||
@@ -330,6 +406,8 @@ function carry(i, key) {
|
||||
for (let k = i; k >= 0; k--) if (FRAMES[k][key] !== null) return FRAMES[k][key];
|
||||
return [];
|
||||
}
|
||||
const rehydrateCells = ${rehydrateCells.toString()};
|
||||
function cellsAt(i) { return rehydrateCells(carry(i, 'cells'), CARDS, WHATS, carry(i, 'facilities')); }
|
||||
let i = 0, timer = null;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c]));
|
||||
@@ -376,7 +454,7 @@ function render() {
|
||||
$('fno').textContent = i;
|
||||
$('scrub').value = i;
|
||||
|
||||
const CELLS = carry(i, 'cells'), FACS = carry(i, 'facilities'), DIV = carry(i, 'division');
|
||||
const CELLS = cellsAt(i), FACS = carry(i, 'facilities'), DIV = carry(i, 'division');
|
||||
|
||||
$('division').innerHTML = divisionSvg(DIV);
|
||||
// The same office renderer the playable app uses, so replay and game draw one board.
|
||||
|
||||
Reference in New Issue
Block a user