clone

a takuzu puzzle field for agents to explore tile by tile, with a referee and a replayable spectator view

site/viewer: checkpointed fold for large replays

dami committed 7d51830
A state every 32 revisions, folded forward on demand; live appends
extend the last checkpoint. Replaces foldAll in the viewer, which kept
one full state per revision and cannot hold a daily-sized log.
commit
7d518304bdacb0ac1075a0ffb3e57cffb9278bda
parent
d6328fd2223862028f90a78469fcdb0054aecc11
author
dami <gazey55@gmail.com>
3 files changed, +103 -6
Msite/islands/Viewer.tsx18+++++---
Asite/lib/viewer/checkpoints.ts70++++++++++++++++++++++++++++++
Msite/tests/viewer_test.ts21+++++++++
M site/islands/Viewer.tsx +12 -6
A site/lib/viewer/checkpoints.ts +70 -0
@@ -0,0 +1,70 @@
1+// Checkpointed folding for the viewer. Folding every revision into its own2+// state (foldAll) is fine for a hundred events on a small board and3+// impossible for ten thousand on a daily-sized one, where each state4+// carries a map of every known cell. The viewer keeps a checkpoint every5+// K revisions and folds forward from the nearest one on demand; live6+// appends extend the last checkpoint. Scrubbing costs at most K applies.7+8+import { applyEvent, emptyState, type GameEvent, type State } from "../fold.ts";9+10+export class Checkpoints {11+ private cps: State[] = []; // cps[i] = state at revision i * K12+ private events: GameEvent[] = [];13+ private last: { rev: number; state: State } | null = null;14+ readonly K: number;15+16+ constructor(K = 32) {17+ this.K = K;18+ }19+20+ /** Replace or extend the event list (append-only in live mode). */21+ feed(events: GameEvent[]): void {22+ const shared = this.events.length <= events.length &&23+ this.events.every((e, i) => e === events[i]);24+ if (!shared) {25+ this.cps = [];26+ this.last = null;27+ }28+ this.events = events;29+ }30+31+ get length(): number {32+ return this.events.length;33+ }34+35+ /** State at a revision (index into the event list), folded on demand. */36+ at(rev: number): State {37+ const n = this.events.length;38+ if (n === 0) return emptyState();39+ rev = Math.max(0, Math.min(n - 1, rev));40+ // extend checkpoints as far as needed41+ const need = Math.floor(rev / this.K);42+ if (this.cps.length === 0) {43+ this.cps.push(applyEvent(emptyState(), this.events[0]));44+ }45+ while (this.cps.length <= need) {46+ const i = this.cps.length;47+ let s = this.cps[i - 1];48+ for (let r = (i - 1) * this.K + 1; r <= i * this.K; r++) {49+ s = applyEvent(s, this.events[r]);50+ }51+ this.cps.push(s);52+ }53+ // fold forward from the last answer when it is behind and closer54+ let start: number, s: State;55+ if (this.last && this.last.rev <= rev && this.last.rev >= need * this.K) {56+ start = this.last.rev;57+ s = this.last.state;58+ } else {59+ start = need * this.K;60+ s = this.cps[need];61+ }62+ for (let r = start + 1; r <= rev; r++) s = applyEvent(s, this.events[r]);63+ this.last = { rev, state: s };64+ return s;65+ }66+67+ checkpointCount(): number {68+ return this.cps.length;69+ }70+}
M site/tests/viewer_test.ts +21 -0