1// The public view of a live game under fog (lib/redact.ts): the broadcast2// lists geometry only for reached tiles, fills tiles in as they come into3// reach, strips rejected values, and still folds to the same play.4import { assert, assertEquals } from "@std/assert";5import {6 applyEvent,7 emptyState,8 fold,9 frontierOf,10 type State,11} from "../lib/fold.ts";12import type { GenOutput } from "../lib/engine.ts";13import { Referee } from "../lib/referee.ts";14import { redactedViewOf, redactLog } from "../lib/redact.ts";15import { scatterLayout, stubJudge, synthesise } from "./fixture.ts";1617const tmp = await Deno.makeTempDir();18Deno.env.set("MOSAIC_GAMES", tmp + "/");1920function solutionOf(out: GenOutput) {21 const G = out.manifest.G;22 return (id: number) =>23 (out.solution[Math.floor(id / G)][id % G] === "1" ? 1 : 0) as 0 | 1;24}2526async function play() {27 const out = structuredClone(synthesise(scatterLayout("redact", 100)));28 const ref = await Referee.create(29 crypto.randomUUID(),30 out,31 { guess: "accept", depth: 0, fog: true },32 stubJudge,33 { seed: "redact", persist: false },34 );35 const sol = solutionOf(out);36 const agents: string[] = [];37 for (const session of ["a", "b"]) {38 const j = await ref.join({39 name: "dami",40 harness: "test",41 machine_id: "m-0123456789abcdef",42 verified: true,43 session,44 });45 agents.push(j.agent);46 }47 for (let i = 0; i < 24 && ref.state.status === "playing"; i++) {48 const agent = agents[i % 2];49 const mine = frontierOf(ref.state, agent).filter((t) =>50 ref.state.tiles[t] === "hidden" || ref.state.tileRevealer[t] === agent51 );52 if (mine.length === 0) continue;53 const id = mine[0];54 if (ref.state.tiles[id] === "hidden") {55 await ref.reveal(ref.revision, id, `reveal #${id}`, agent);56 }57 const unknown = ref.state.geo!.cellsOf[id].filter((c) =>58 !ref.state.cells[c]59 );60 const batch = unknown.slice(0, 40).map((c) =>61 [c, sol(c)] as [number, number]62 );63 if (unknown.length > 40 && i < 4) {64 batch.push([unknown[40], (1 - sol(unknown[40])) as 0 | 1]);65 }66 if (batch.length) await ref.commit(ref.revision, batch, `#${id}`, agent);67 }68 return ref;69}7071Deno.test("the redacted view shows geometry only within reach and folds to the same play", async () => {72 const ref = await play();73 const full = ref.log.events;74 const view = redactLog(full);75 assertEquals(view.length, full.length);76 // the cached live view is the same view77 assertEquals(78 JSON.stringify(redactedViewOf(ref).events),79 JSON.stringify(view),80 );81 let f: State = emptyState();82 let r: State = emptyState();83 let hiddenSomewhere = false;84 for (let i = 0; i < full.length; i++) {85 f = applyEvent(f, full[i]);86 r = applyEvent(r, view[i]);87 assertEquals(r.revision, f.revision);88 // a tile has a shape in the view iff someone reached it89 for (const t of r.manifest!.tiles) {90 const shown = t.rects.length > 0;91 assertEquals(shown, !!f.reached[t.id], `rev ${i} tile ${t.id}`);92 if (!shown) hiddenSomewhere = true;93 }94 // and reach is the same on both sides95 assertEquals([...r.reached], [...f.reached], `reached at rev ${i}`);96 assertEquals(r.tiles, f.tiles);97 assertEquals(r.solvedCells, f.solvedCells);98 assertEquals(r.agents, f.agents);99 const ev = view[i];100 if (ev.kind === "batch" && ev.op === "commit") {101 for (const c of ev.rejected) assert(!("v" in c), "no rejected value");102 }103 }104 assert(hiddenSomewhere, "some tile was out of reach at some point");105 // the final analysis matches106 assertEquals(107 JSON.stringify(fold(view).analysis),108 JSON.stringify(fold(full).analysis),109 );110 // and something was actually rejected along the way111 assert(112 full.some((e) =>113 e.kind === "batch" && e.op === "commit" && e.rejected.length > 0114 ),115 );116});