From 79947e31babaad4f7e1b98e2a1dc708da18eca8e Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 23 Sep 2026 08:47:15 -0400 Subject: [PATCH] Ledgers close with an over line, so the digest can say who won Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up --- deploy/visitors.sh | 11 ++++++++--- server/src/rooms.ts | 3 +++ server/src/store.ts | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/deploy/visitors.sh b/deploy/visitors.sh index 507f61e..3bbcd22 100755 --- a/deploy/visitors.sh +++ b/deploy/visitors.sh @@ -81,19 +81,23 @@ for f in glob.glob("/var/lib/waving-hands/rooms/*.jsonl"): except Exception: continue created = datetime.datetime.fromtimestamp(meta["createdAt"] / 1000, datetime.timezone.utc) - humans, bots_seated, started, turns, last = [], [], False, 0, created + humans, bots_seated, started, turns, last, over = [], [], False, 0, created, None + names = {} for x in lines[1:]: if x["t"] == "seat": (bots_seated if x["bot"] else humans).append(x["name"]) + names[x["id"]] = x["name"] elif x["t"] == "start": started = True elif x["t"] == "turn": turns += 1 last = datetime.datetime.fromtimestamp(x["at"] / 1000, datetime.timezone.utc) + elif x["t"] == "over": + over = "a draw" if x["winner"] is None else "%s won" % names.get(x["winner"], x["winner"]) for h in humans: if h not in first_seen or created < first_seen[h]: first_seen[h] = created - rooms.append(dict(id=meta["id"], size=meta["seats"], created=created, humans=humans, bots=bots_seated, started=started, turns=turns, last=last)) + rooms.append(dict(id=meta["id"], size=meta["seats"], created=created, humans=humans, bots=bots_seated, started=started, turns=turns, last=last, over=over)) today = sorted([r for r in rooms if r["created"].strftime("%F") == day], key=lambda r: r["created"]) names = sorted({h for r in today for h in r["humans"]}) @@ -101,7 +105,8 @@ new = [n for n in names if first_seen[n].strftime("%F") == day] print("players today: %s | NEW today: %s" % (", ".join(names) or "none", ", ".join(new) or "none")) print("rooms today: %d (of %d ever)" % (len(today), len(rooms))) for r in today: - state = "lobby only" if not r["started"] else ("started, no turns" if not r["turns"] else "in play") + idle_h = (now - r["last"]).total_seconds() / 3600 + state = "lobby only" if not r["started"] else ("started, no turns" if not r["turns"] else (r["over"] or ("abandoned" if idle_h > 12 else "in play"))) span = ("%d min" % round((r["last"] - r["created"]).total_seconds() / 60)) if r["turns"] else "" seated = "+".join(r["humans"]) + ((" vs %d bot%s" % (len(r["bots"]), "" if len(r["bots"]) == 1 else "s")) if r["bots"] else "") empty = r["size"] - len(r["humans"]) - len(r["bots"]) diff --git a/server/src/rooms.ts b/server/src/rooms.ts index 427414c..1882ba3 100644 --- a/server/src/rooms.ts +++ b/server/src/rooms.ts @@ -166,6 +166,7 @@ export class Rooms { const actor = room.state.timeStops[0]; this.commit(room, { t: 'turn', at: Date.now(), inputs: { [actor]: chooseBotTurn(room.state, actor) } }); } + if (room.state?.over) this.commit(room, { t: 'over', at: Date.now(), winner: room.state.over.winner, reason: room.state.over.reason }); } view(room: Room, seatId: WizardId): RoomView { @@ -228,6 +229,8 @@ function apply(room: Room, line: LedgerLine): void { room.state = resolveTurn(room.state, line.inputs); room.pending = {}; break; + case 'over': + break; } } diff --git a/server/src/store.ts b/server/src/store.ts index 3685693..382e5fd 100644 --- a/server/src/store.ts +++ b/server/src/store.ts @@ -10,7 +10,9 @@ export type LedgerLine = | { t: 'room'; id: string; seats: number; createdAt: number } | { t: 'seat'; id: WizardId; name: string; token: string; bot: boolean } | { t: 'start'; seed: number } - | { t: 'turn'; at: number; inputs: Record }; + | { t: 'turn'; at: number; inputs: Record } + /** Written after the turn that ends the duel, for anything that reads ledgers without the engine. */ + | { t: 'over'; at: number; winner: WizardId | null; reason: string }; export class Store { constructor(private dir: string) {