1// The public view of a live game under fog. Geometry is not public2// during play: the broadcast manifest lists only the tiles someone has3// reached; every other tile is a placeholder (its id, no shape), and an4// event that brings tiles into reach carries their full specs in a `reach`5// field, which the fold fills in before the event body applies. Rejected6// cells carry no value (the committing agent alone saw them). The full7// log is released in the day's archive. The view is derived from the8// referee's own fold, event by event, and cached per game.9import {10 applyEvent,11 emptyState,12 type GameEvent,13 type GrowEvent,14 type Manifest,15 type ManifestEvent,16 type State,17 type TileSpec,18} from "./fold.ts";19import type { Referee } from "./referee.ts";2021/** A tile the viewer knows only by id. */22export function placeholderOf(t: TileSpec): TileSpec {23 return {24 id: t.id,25 x0: 0,26 y0: 0,27 w: 0,28 h: 0,29 rects: [],30 cells: 0,31 m: 0,32 overlaps: [],33 };34}3536/** Folds the full log and emits the redacted one alongside it. */37export class RedactedView {38 state: State = emptyState();39 readonly events: GameEvent[] = [];40 private shown = new Uint8Array(0);4142 push(ev: GameEvent): GameEvent {43 const next = applyEvent(this.state, ev);44 const reached = next.reached;45 let out: GameEvent;46 if (ev.kind === "manifest") {47 const m = (ev as ManifestEvent).manifest as { version?: number };48 if (m.version === 3) {49 const m3 = m as Manifest;50 out = {51 ...ev,52 manifest: {53 ...m3,54 tiles: m3.tiles.map((t) => reached[t.id] ? t : placeholderOf(t)),55 },56 } as ManifestEvent;57 } else out = ev;58 this.shown = new Uint8Array(reached);59 } else {60 const was = this.shown.length;61 const reach: TileSpec[] = [];62 for (let t = 0; t < was; t++) {63 if (reached[t] && !this.shown[t]) reach.push(next.manifest!.tiles[t]);64 }65 let body: GameEvent = ev;66 if (ev.kind === "grow") {67 // new tiles are not in the manifest before the body applies, so68 // they arrive in the body itself, placeholders unless reached69 const g = ev as GrowEvent;70 body = {71 ...g,72 tiles: g.tiles.map((t) => reached[t.id] ? t : placeholderOf(t)),73 } as GrowEvent;74 } else if (ev.kind === "batch" && ev.op === "commit") {75 body = {76 ...ev,77 rejected: ev.rejected.map(({ id, reason }) => ({ id, reason })),78 };79 }80 out = reach.length ? { ...body, reach } as unknown as GameEvent : body;81 this.shown = new Uint8Array(reached);82 }83 this.state = next;84 this.events.push(out);85 return out;86 }87}8889/** The whole log, redacted. */90export function redactLog(events: GameEvent[]): GameEvent[] {91 const v = new RedactedView();92 for (const ev of events) v.push(ev);93 return v.events;94}9596const views = new WeakMap<Referee, RedactedView>();9798/** The cached view of a live game, caught up to its log. */99export function redactedViewOf(ref: Referee): RedactedView {100 let v = views.get(ref);101 if (!v) {102 v = new RedactedView();103 views.set(ref, v);104 }105 const log = ref.log.events;106 while (v.events.length < log.length) v.push(log[v.events.length]);107 return v;108}