- commit
- 0125f11ec2afc9470892c65398979152040a803d
- parent
- 5eca750bbb95d195415d71662300a7fee6642f5c
- author
- dami <gazey55@gmail.com>
| A | site/ | 180 | ++++++++++++++++++++++++++++++ |
@@ -0,0 +1,180 @@
1+// The daily loop with an injected clock and a synthetic generator: two2+// rollovers archive two days with winners, both replay from the archive,3+// the second day honours the first day's winner (seat and crown), and a4+// restart resumes the running day at the same revision.5+import { assert, assertEquals } from "@std/assert";6+import { Daily, type DailyConfig, dateFor, nextDate } from "../lib/daily.ts";7+import { getGame, resetGames } from "../lib/games.ts";8+import type { FieldOutput, GenOutput, GenParams } from "../lib/engine.ts";9+import { fold, frontierOf, parseLog } from "../lib/fold.ts";10+import { readLog } from "../lib/log.ts";11+import { boardsOf } from "../lib/leaderboard.ts";12+import type { Referee } from "../lib/referee.ts";13+import {14+ embed,15+ scatterLayout,16+ stubJudge,17+ synthesise,18+ withSeeds,19+} from "./fixture.ts";20+21+const tmp = await Deno.makeTempDir();22+Deno.env.set("MOSAIC_GAMES", tmp + "/games/");23+24+const cfg: DailyConfig = {25+ namespace: "test",26+ chunks: 1,27+ chunk_g: 60,28+ G: 140,29+ rings: 0,30+ grow_minutes: 1440, // never during the test31+ rollover_hour_utc: 0,32+ fog: true,33+};34+35+/** A synthetic field per seed: ~20 tiles in the middle of a 140 canvas. */36+function generate(p: GenParams): Promise<FieldOutput> {37+ const L = embed(withSeeds(scatterLayout(p.seed, 60), 3), p.G, 40, 40);38+ const out = synthesise(L, p.seed);39+ out.manifest.field = { chunks: 1, chunk_g: 60, band: 0, grown: 0 };40+ return Promise.resolve({ ...(out as GenOutput), rings: [] });41+}42+43+const solutions = new Map<string, (id: number) => 0 | 1>();44+async function solutionFor(seed: string, G: number) {45+ if (!solutions.has(seed)) {46+ const out = await generate({ seed, G, kind: "field" });47+ solutions.set(48+ seed,49+ (id: number) =>50+ (out.solution[Math.floor(id / G)][id % G] === "1" ? 1 : 0) as 0 | 1,51+ );52+ }53+ return solutions.get(seed)!;54+}55+56+/** Reveal and finish n frontier tiles for an agent. */57+async function play(ref: Referee, agent: string, seed: string, n: number) {58+ const sol = await solutionFor(seed, ref.state.G);59+ for (let k = 0; k < n; k++) {60+ const fr = frontierOf(ref.state, agent);61+ if (!fr.length) return;62+ const id = fr[0];63+ if (ref.state.tiles[id] === "hidden") {64+ await ref.reveal(ref.revision, id, `reveal #${id}`, agent);65+ }66+ const unknown = ref.state.geo!.cellsOf[id].filter((c) =>67+ !ref.state.cells[c]68+ );69+ if (unknown.length) {70+ await ref.commit(71+ ref.revision,72+ unknown.map((c) => [c, sol(c)] as [number, number]),73+ `finish #${id}`,74+ agent,75+ );76+ }77+ }78+}79+80+Deno.test("two rollovers: archive, winners, honours, replay, resume", async () => {81+ resetGames();82+ let now = Date.parse("2026-09-01T10:00:00Z");83+ const clock = () => now;84+ const logs: string[] = [];85+ const d = new Daily({86+ cfg,87+ dir: tmp + "/",88+ now: clock,89+ generate,90+ judge: stubJudge,91+ log: (m) => logs.push(m),92+ });93+ await d.boot();94+ const day1 = dateFor(now, cfg);95+ assertEquals(day1, "2026-09-01");96+ assert(d.current && d.current.date === day1, "day 1 started");97+ const g1 = getGame(d.current!.game_id)!;98+ assertEquals(g1.state.day, day1);99+100+ // two agents: alice (model-a) wins by cells, bob (model-b) trails101+ const a = await g1.join({ name: "alice", model: "model-a" });102+ const b = await g1.join({ name: "bob", model: "model-b" });103+ await play(g1, a.agent, d.current!.seed, 3);104+ await play(g1, b.agent, d.current!.seed, 1);105+ const boards = boardsOf(g1.state);106+ assertEquals(boards.by_user[0].key, "alice");107+ assertEquals(boards.by_model[0].key, "model-a");108+ assertEquals(boards.by_user[0].models, ["model-a"]);109+ const rev1 = g1.revision;110+111+ // a restart mid-day resumes at the same revision112+ const d2 = new Daily({113+ cfg,114+ dir: tmp + "/",115+ now: clock,116+ generate,117+ judge: stubJudge,118+ log: (m) => logs.push(m),119+ });120+ resetGames();121+ await d2.boot();122+ const g1b = getGame(d2.current!.game_id)!;123+ assertEquals(g1b.revision, rev1);124+ assertEquals(g1b.state.agents.length, 2);125+ assert(logs.some((m) => m.startsWith("resumed")), "resume logged");126+ // and play continues from there (the private solution is back)127+ await play(g1b, a.agent, d2.current!.seed, 1);128+ assert(g1b.revision > rev1);129+130+ // day 2: rollover archives day 1 and honours alice131+ now = Date.parse("2026-09-02T00:30:00Z");132+ await d2.tick();133+ const day2 = nextDate(day1);134+ assertEquals(d2.current!.date, day2);135+ const row1 = d2.index.get(day1)!;136+ assertEquals(row1.winner_user, "alice");137+ assertEquals(row1.winner_model, "model-a");138+ assert(row1.stats.solved >= 4);139+ assertEquals(row1.stats.completed, false);140+ assert(row1.stats.ended !== null, "ended at rollover");141+ // the archive replays to the same state as the log on disk142+ const archived = await Deno.readTextFile(`${tmp}/days/${day1}/log.jsonl`);143+ const s1 = fold(parseLog(archived));144+ assertEquals(s1.status, "solved"); // ended145+ assertEquals(s1.revision, row1.stats.revisions);146+ assertEquals((await readLog(row1.game_id))!, archived);147+148+ const g2 = getGame(d2.current!.game_id)!;149+ assertEquals(g2.state.day, day2);150+ assertEquals(g2.state.honours.length, 1);151+ assertEquals(g2.state.honours[0].name, "alice");152+ assertEquals(g2.state.honours[0].model, "model-a");153+ const seat = g2.state.honours[0].seed_tile;154+ // a stranger is not seated on the reserved seat; alice is155+ const c = await g2.join({ name: "carol", model: "model-c" });156+ assert(c.seed_tile !== seat, "stranger not on the reserved seat");157+ const a2 = await g2.join({ name: "alice", model: "model-a" });158+ assertEquals(a2.seed_tile, seat);159+ await play(g2, a2.agent, d2.current!.seed, 2);160+161+ // day 3: both days archived, cumulative boards add up162+ now = Date.parse("2026-09-03T00:10:00Z");163+ await d2.tick();164+ const days = d2.index.list();165+ assertEquals(days.map((r) => r.date), [day1, day2]);166+ assertEquals(days[1].winner_user, "alice");167+ const cum = d2.cumulative();168+ assertEquals(cum.days, 2);169+ const alice = cum.by_user.find((r) => r.key === "alice")!;170+ assertEquals(alice.days, 2);171+ assertEquals(172+ alice.reward,173+ days[0].boards.by_user[0].reward + days[1].boards.by_user[0].reward,174+ );175+ assert(cum.by_model.find((r) => r.key === "model-a")!.reward >= alice.reward);176+ assertEquals(d2.current!.date, "2026-09-03");177+ d2.stop();178+ d.stop();179+ resetGames();180+});