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.78import { applyEvent, emptyState, type GameEvent, type State } from "../fold.ts";910export 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 private fixed: number | null;15 K = 32;1617 /** K fixed, or adaptive: at most ~24 checkpoints for a long log, since a18 * checkpoint of a daily-sized field carries every known cell. */19 constructor(K?: number) {20 this.fixed = K ?? null;21 }2223 /** Replace or extend the event list (append-only in live mode). */24 feed(events: GameEvent[]): void {25 const shared = this.events.length <= events.length &&26 this.events.every((e, i) => e === events[i]);27 if (!shared || this.cps.length === 0) {28 this.cps = [];29 this.last = null;30 this.K = this.fixed ?? Math.max(32, Math.ceil(events.length / 24));31 }32 this.events = events;33 }3435 get length(): number {36 return this.events.length;37 }3839 /** State at a revision (index into the event list), folded on demand. */40 at(rev: number): State {41 const n = this.events.length;42 if (n === 0) return emptyState();43 rev = Math.max(0, Math.min(n - 1, rev));44 // extend checkpoints as far as needed45 const need = Math.floor(rev / this.K);46 if (this.cps.length === 0) {47 this.cps.push(applyEvent(emptyState(), this.events[0]));48 }49 while (this.cps.length <= need) {50 const i = this.cps.length;51 let s = this.cps[i - 1];52 for (let r = (i - 1) * this.K + 1; r <= i * this.K; r++) {53 s = applyEvent(s, this.events[r]);54 }55 this.cps.push(s);56 }57 // fold forward from the last answer when it is behind and closer58 let start: number, s: State;59 if (this.last && this.last.rev <= rev && this.last.rev >= need * this.K) {60 start = this.last.rev;61 s = this.last.state;62 } else {63 start = need * this.K;64 s = this.cps[need];65 }66 for (let r = start + 1; r <= rev; r++) s = applyEvent(s, this.events[r]);67 this.last = { rev, state: s };68 return s;69 }7071 checkpointCount(): number {72 return this.cps.length;73 }74}