clone

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

main
mosaic / site / tests / games_test.ts
games_test.ts 3.5K
1// Async game start: start returns a generating entry at once, observe and2// list report it, join is refused until ready, and games nobody joined3// expire (their manifest-only log is deleted). Rejected commits echo the4// value they rejected.5import { assert, assertEquals, assertRejects } from "@std/assert";6import {7 EXPIRE_MS,8 getEntry,9 getGame,10 listGames,11 resetGames,12 startGame,13 sweep,14 whenReady,15} from "../lib/games.ts";16import { logPath } from "../lib/log.ts";17import { Referee } from "../lib/referee.ts";18import { gridLayout, stubJudge, synthesise } from "./fixture.ts";1920const tmp = await Deno.makeTempDir();21Deno.env.set("MOSAIC_GAMES", tmp + "/");2223const fixture = () =>24 Promise.resolve(structuredClone(synthesise(gridLayout(2, 8))));2526Deno.test("start is asynchronous: generating, then playing; expiry sweeps the unjoined", async () => {27 resetGames();28 let release: () => void = () => {};29 const gate = new Promise<void>((r) => release = r);30 const slowGen = async () => {31 await gate;32 return structuredClone(synthesise(gridLayout(2, 8)));33 };34 const t0 = 1_000_000;35 const pending = startGame({ G: 15, kind: "grid", n: 8 }, slowGen, () => t0);36 assertEquals(pending.status, "generating");37 assertEquals(getGame(pending.game_id), undefined);38 const e = getEntry(pending.game_id);39 assert(e && "status" in e && e.status === "generating");40 assertEquals(listGames()[0].status, "generating");41 release();42 const ref = await whenReady(pending.game_id);43 assertEquals(ref.gameId, pending.game_id);44 assertEquals(listGames()[0].status, "playing");45 assert(await Deno.stat(logPath(pending.game_id)));46 // nobody joined: after EXPIRE_MS the game and its manifest-only log go47 assertEquals(await sweep(t0 + EXPIRE_MS - 1), []);48 assertEquals(await sweep(t0 + EXPIRE_MS), [pending.game_id]);49 assertEquals(getEntry(pending.game_id), undefined);50 await assertRejects(() => Deno.stat(logPath(pending.game_id)));51 // a joined game is kept52 const p2 = startGame({ G: 15, kind: "grid", n: 8 }, fixture, () => t0);53 const ref2 = await whenReady(p2.game_id);54 await ref2.join({55 name: "x",56 harness: "t",57 machine_id: "m-x",58 verified: true,59 });60 assertEquals(await sweep(t0 + 2 * EXPIRE_MS), []);61 assert(getGame(p2.game_id));62 // a failed generation is reported and swept63 const p3 = startGame(64 { G: 15, kind: "grid", n: 8 },65 () => Promise.reject(new Error("boom")),66 () => t0,67 );68 await assertRejects(() => whenReady(p3.game_id), Error, "boom");69 assertEquals(70 listGames().find((g) => g.game_id === p3.game_id)?.status,71 "failed",72 );73 assertEquals(await sweep(t0 + EXPIRE_MS), [p3.game_id]);74 resetGames();75});7677Deno.test("a wrong commit echoes the value it rejected", async () => {78 const out = await fixture();79 const ref = await Referee.create(80 crypto.randomUUID(),81 out,82 { guess: "accept", depth: 0 },83 stubJudge,84 { persist: false },85 );86 const j = await ref.join({87 harness: "t",88 machine_id: "m-anon",89 verified: true,90 });91 const r = await ref.reveal(ref.revision, j.seed_tile, "", j.agent);92 const G = out.manifest.G;93 const unknown = ref.state.geo!.cellsOf[j.seed_tile].filter((c) =>94 !ref.state.cells[c]95 );96 const id = unknown[0];97 const truth = out.solution[Math.floor(id / G)][id % G] === "1" ? 1 : 0;98 const c = await ref.commit(99 r.revision,100 [[id, 1 - truth]],101 "wrong on purpose",102 j.agent,103 );104 assertEquals(c.rejected, [{ id, reason: "wrong", v: (1 - truth) as 0 | 1 }]);105});