clone

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

daily: reset a day — set its log aside, wipe its rows, restart from the same field

dami committed f72a9e5
deno task reset-day <date> [--host] posts to /admin/reset/<date> with
the push token; the running game ends under the write lock, its log
moves to data/games/<game>.reset-<n>.jsonl (kept, not archived), the
day's index row and archive go, and the day restarts at revision 0
from the same pushed field, seed, rings and honours; /status reports
reset: n.
commit
f72a9e5cbc1b6184faac4dec7273e40bd2b1253b
parent
074a42a750d19534b2d99642f254c5db93c9e49c
author
dami <gazey55@gmail.com>
6 files changed, +174 -2
Msite/deno.json3+-
Msite/lib/daily.ts56+++++++++++++++++++++++++++++-
Asite/routes/admin/reset/[date].ts32+++++++++++++++++
Msite/routes/status.ts1+
Asite/scripts/reset-day.ts31+++++++++++++++++
Msite/tests/daily_test.ts53++++++++++++++++++++++++++++
M site/deno.json +2 -1
M site/lib/daily.ts +55 -1
@@ -24,7 +24,7 @@ import {
2424 unwrapField,2525 } from "./engine.ts";2626 import type { Judge } from "./referee.ts";27-import { dataDir, readLog } from "./log.ts";27+import { dataDir, gamesDir, logPath, readLog } from "./log.ts";2828 import { fold, type Honour, type ManifestEvent, parseLog } from "./fold.ts";2929 import {3030 type Boards,
@@ -222,6 +222,9 @@ export class DaysIndex {
222222 row.winner_model,223223 );224224 }225+ delete(date: string): void {226+ this.db.prepare(`DELETE FROM days WHERE date = ?`).run(date);227+ }225228 get(date: string): DayRow | null {226229 const r = this.db.prepare(`SELECT * FROM days WHERE date = ?`).get(date) as227230 | Record<string, string>
@@ -265,6 +268,8 @@ export type DailyStatus = {
265268 latest_archived: { date: string; game_id: string } | null;266269 /** tomorrow's field, pushed to this box by the nightly */267270 tomorrow: { date: string; pushed: boolean };271+ /** times today was reset (play wiped, same field restarted) */272+ reset: number;268273 };269274 270275 export type DailyState = {
@@ -273,6 +278,8 @@ export type DailyState = {
273278 seed: string;274279 /** where the day's field came from; resume reads the same source */275280 source: "pushed" | "emergency";281+ /** times this day was reset (its earlier logs are kept as .reset-<n>) */282+ reset?: number;276283 };277284 278285 type Opts = {
@@ -615,6 +622,52 @@ export class Daily {
615622 return row;616623 }617624 625+ /**626+ * Wipe a day's play and start it again from the same field: the627+ * running game ends without being archived (its log moves to628+ * `<game>.reset-<n>.jsonl`, kept but outside the calendar and the629+ * boards), the day's index row and archive go, and the day restarts at630+ * revision 0 from the same pushed field, seed, rings and honours, so631+ * agents can join again with clean health.632+ */633+ async reset(date: string): Promise<{ game_id: string; reset: number }> {634+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error("bad date");635+ const gameId = gameIdFor(date, this.cfg);636+ const cur = this.current?.date === date ? this.current : null;637+ const n = (cur?.reset ?? 0) + 1;638+ if (getGame(gameId)) {639+ // end it under the write lock (any growth in flight lands first),640+ // then forget it; the log is set aside as it stands, replayable641+ await endGame(gameId);642+ dropGame(gameId);643+ }644+ // set the log aside (nothing is lost, nothing is archived)645+ try {646+ await Deno.rename(647+ logPath(gameId),648+ `${gamesDir()}${gameId}.reset-${n}.jsonl`,649+ );650+ } catch (e) {651+ this.log(`reset ${date}: no log to set aside (${e})`);652+ }653+ this.index.delete(date);654+ try {655+ await Deno.remove(`${this.dir}days/${date}/`, { recursive: true });656+ } catch {657+ // not archived658+ }659+ this.current = null;660+ if (date === this.today()) {661+ await this.startDay(date);662+ if (this.current) {663+ (this.current as DailyState).reset = n;664+ await this.saveState();665+ }666+ }667+ this.log(`reset ${date} (${n}): ${gameId} restarted from its field`);668+ return { game_id: gameId, reset: n };669+ }670+618671 /** What `/` should show right now. */619672 status(): DailyStatus {620673 const cur = this.current;
@@ -643,6 +696,7 @@ export class Daily {
643696 ? { date: latest.date, game_id: latest.game_id }644697 : null,645698 tomorrow: { date: tomorrow, pushed },699+ reset: cur?.reset ?? 0,646700 };647701 }648702
A site/routes/admin/reset/[date].ts +32 -0
M site/routes/status.ts +1 -0
A site/scripts/reset-day.ts +31 -0
M site/tests/daily_test.ts +53 -0
@@ -260,3 +260,56 @@ Deno.test("rollover with nothing pushed makes an emergency field and says so", a
260260 d.stop();261261 resetGames();262262 });263+264+Deno.test("reset wipes a day's play and restarts it from the same field", async () => {265+ resetGames();266+ let now = Date.parse("2026-10-01T10:00:00Z");267+ const clock = () => now;268+ const resetLogs: string[] = [];269+ const d = new Daily({270+ cfg,271+ dir: tmp + "/reset/",272+ now: clock,273+ generate,274+ judge: stubJudge,275+ log: (m) => resetLogs.push(m),276+ });277+ await d.boot();278+ const day = dateFor(now, cfg);279+ const g = getGame(d.current!.game_id)!;280+ const id = d.current!.game_id;281+ const manifestId = g.state.manifest!.id;282+ const a = await g.join({ name: "alice", model: "model-a" });283+ await play(g, a.agent, d.current!.seed, 2);284+ const rev = g.revision;285+ assert(rev > 3, `played to ${rev}`);286+ assertEquals(d.status().reset, 0);287+288+ const r = await d.reset(day);289+ assertEquals(r, { game_id: id, reset: 1 });290+ const g2 = getGame(id)!;291+ assert(g2 !== g, "a fresh referee");292+ assert(g2, "the day restarted");293+ assertEquals(g2.revision, 0);294+ assertEquals(g2.state.agents.length, 0);295+ assertEquals(g2.state.manifest!.id, manifestId);296+ assertEquals(d.status().reset, 1);297+ assertEquals(d.index.get(day), null);298+ // the old log is kept aside, the live one starts over299+ const moved = await Deno.readTextFile(300+ `${Deno.env.get("MOSAIC_GAMES")}${id}.reset-1.jsonl`,301+ ).catch(() => null);302+ assert(moved, `the old log was set aside: ${resetLogs.join("; ")}`);303+ const movedEvents = moved.split("\n").filter(Boolean);304+ assert(movedEvents.length >= rev + 1, "every event kept");305+ assertEquals(JSON.parse(movedEvents.at(-1)!).kind, "end");306+ const fresh = await readLog(id);307+ assert(fresh, "a fresh log");308+ assertEquals(fresh.split("\n").filter(Boolean).length, 1);309+ // play again from scratch with clean health310+ const b = await g2.join({ name: "alice", model: "model-a" });311+ assertEquals(g2.state.agents[0].health.state, "healthy");312+ await play(g2, b.agent, d.current!.seed, 1);313+ assert(g2.revision > 1);314+ now += 1;315+});