The tally becomes a ledger of its own — and hotseat tables count
Instead of re-scanning every room's log on demand, stats.json now accumulates beside the room files: each room is remembered at its highest counted stage (created → started → finished), so boots and replays reconcile without double-counting, and the tally will survive any future pruning of old rooms. Finished games contribute their moves, table-time, and manner of victory exactly once, at the moment of victory. And with an accumulator to receive them, hotseat games finally count: each local game mints an anonymous id, pings "started" with its player count, tracks its own between-moves clock, and on the final move reports counts only — commands, minutes, players, win reason. No names, no moves leave the device. The server dedupes by id, clamps everything to sane ranges, and the booklet's tally now shows how many of the games were hotseat tables. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
85831828d7
commit
4114386ed8
@@ -83,9 +83,10 @@
|
||||
<dt>{stats.winsByTreasure} / {stats.winsByLastStanding}</dt><dd>victories by treasure-theft / by last wizard standing</dd>
|
||||
<dt>{stats.longestGameCommands}</dt><dd>moves in the longest game yet played</dd>
|
||||
<dt>{stats.fullestTable}</dt><dd>wizards at the fullest table</dd>
|
||||
<dt>{stats.hotseatGames}</dt><dd>of the games were hotseat tables, reporting in anonymously</dd>
|
||||
</dl>
|
||||
{#if stats.firstGameAt}
|
||||
<p class="colophon">The first game was dealt {new Date(String(stats.firstGameAt)).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}. Hotseat games are played off the ledger and go uncounted.</p>
|
||||
<p class="colophon">The first game was dealt {new Date(String(stats.firstGameAt)).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}. Hotseat tables send only counts — names and moves stay on the device.</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Counting the ledgers…</p>
|
||||
|
||||
@@ -14,13 +14,17 @@ import {
|
||||
type GameView,
|
||||
type PlayerId,
|
||||
} from "@wizwar/engine";
|
||||
import { humanize } from "./net.svelte";
|
||||
import { humanize, net } from "./net.svelte";
|
||||
|
||||
const SAVE_KEY = "wizwar-hotseat";
|
||||
|
||||
interface SavedHotseat {
|
||||
config: GameConfig;
|
||||
commands: { playerId: PlayerId; command: Command }[];
|
||||
/** Anonymous tally identity + table-time bookkeeping (added later; optional). */
|
||||
tallyId?: string;
|
||||
activeMs?: number;
|
||||
lastMoveAt?: number;
|
||||
}
|
||||
|
||||
/** Whose input does the game need right now? */
|
||||
@@ -48,6 +52,9 @@ class LocalGame {
|
||||
|
||||
private config: GameConfig | null = null;
|
||||
private commands: { playerId: PlayerId; command: Command }[] = [];
|
||||
private tallyId: string | null = null;
|
||||
private activeMs = 0;
|
||||
private lastMoveAt = 0;
|
||||
|
||||
hasSave(): boolean {
|
||||
return localStorage.getItem(SAVE_KEY) !== null;
|
||||
@@ -107,6 +114,10 @@ class LocalGame {
|
||||
this.active = true;
|
||||
this.viewerId = null;
|
||||
this.handoffTo = actorId(state);
|
||||
this.tallyId = crypto.randomUUID();
|
||||
this.activeMs = 0;
|
||||
this.lastMoveAt = Date.now();
|
||||
net.reportHotseat({ id: this.tallyId, stage: "started", players: cleaned.length });
|
||||
this.persist();
|
||||
return null;
|
||||
}
|
||||
@@ -134,6 +145,9 @@ class LocalGame {
|
||||
}
|
||||
this.config = saved.config;
|
||||
this.commands = saved.commands;
|
||||
this.tallyId = saved.tallyId ?? crypto.randomUUID();
|
||||
this.activeMs = saved.activeMs ?? 0;
|
||||
this.lastMoveAt = saved.lastMoveAt ?? Date.now();
|
||||
this.gameState = current;
|
||||
this.active = true;
|
||||
this.viewerId = null;
|
||||
@@ -165,6 +179,18 @@ class LocalGame {
|
||||
}
|
||||
this.gameState = result.state;
|
||||
this.commands = [...this.commands, { playerId: this.viewerId, command }];
|
||||
const now = Date.now();
|
||||
if (this.lastMoveAt && now - this.lastMoveAt < 10 * 60 * 1000) this.activeMs += now - this.lastMoveAt;
|
||||
this.lastMoveAt = now;
|
||||
if (this.gameState.phase === "finished" && this.tallyId) {
|
||||
net.reportHotseat({
|
||||
id: this.tallyId, stage: "finished",
|
||||
players: this.gameState.players.length,
|
||||
commands: this.commands.length,
|
||||
minutes: Math.round(this.activeMs / 60_000),
|
||||
winReason: this.gameState.winReason ?? undefined,
|
||||
});
|
||||
}
|
||||
for (const e of result.events) {
|
||||
const line = humanize(e);
|
||||
if (line) this.log = [...this.log, line];
|
||||
@@ -195,7 +221,10 @@ class LocalGame {
|
||||
if (!this.config) return;
|
||||
localStorage.setItem(
|
||||
SAVE_KEY,
|
||||
JSON.stringify({ config: this.config, commands: this.commands } satisfies SavedHotseat),
|
||||
JSON.stringify({
|
||||
config: this.config, commands: this.commands,
|
||||
tallyId: this.tallyId ?? undefined, activeMs: this.activeMs, lastMoveAt: this.lastMoveAt,
|
||||
} satisfies SavedHotseat),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +376,14 @@ class Net {
|
||||
this.send({ type: "stats" });
|
||||
}
|
||||
|
||||
/** Anonymous count-only ping so hotseat tables show in the tally. */
|
||||
reportHotseat(report: {
|
||||
id: string; stage: "started" | "finished";
|
||||
players?: number; commands?: number; minutes?: number; winReason?: string;
|
||||
}): void {
|
||||
this.send({ type: "hotseatReport", ...report });
|
||||
}
|
||||
|
||||
refreshGames(): void {
|
||||
if (this.seats.length > 0) this.send({ type: "myGames", seats: $state.snapshot(this.seats) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user