The tally: a booklet tab counting the love

A new tab in the help booklet answers "how much is this getting
played?" from the ledgers themselves: games chronicled (begun and
fought to a finish), distinct wizards seated, every spell, step, and
punch recorded, time at the table estimated by the clock between
moves (gaps over ten minutes don't count — async games tell the
truth), victories split by treasure-theft versus last standing, the
longest game, the fullest table, and the date the first game was
dealt. Computed server-side over the in-memory rooms with a 60-second
cache, fetched over the socket when the tab opens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 11:31:14 -04:00
co-authored by Claude Fable 5
parent ced4dc5749
commit 85831828d7
5 changed files with 114 additions and 4 deletions
+42 -3
View File
@@ -6,13 +6,25 @@
let {
onclose,
initialTab = "play",
stats = null,
onstats,
}: {
onclose: () => void;
initialTab?: "play" | "rules" | "cards" | "about";
initialTab?: "play" | "rules" | "cards" | "about" | "tally";
stats?: Record<string, number | string | null> | null;
onstats?: () => void;
} = $props();
// svelte-ignore state_referenced_locally -- the initial tab is intentionally a one-time value
let tab = $state<"play" | "rules" | "cards" | "about">(initialTab);
let tab = $state<"play" | "rules" | "cards" | "about" | "tally">(initialTab);
function openTally() {
tab = "tally";
onstats?.();
}
function hours(mins: number): string {
return mins < 90 ? `${mins} minutes` : `${Math.round(mins / 6) / 10} hours`;
}
let search = $state("");
// The playable pool: every card actually in the 6e game (base + Exp1).
@@ -53,12 +65,33 @@
<button class:current={tab === "rules"} onclick={() => (tab = "rules")}>The rules</button>
<button class:current={tab === "cards"} onclick={() => (tab = "cards")}>Card library</button>
<button class:current={tab === "about"} onclick={() => (tab = "about")}>About</button>
<button class:current={tab === "tally"} onclick={openTally}>The tally</button>
</nav>
<button class="close" onclick={onclose} aria-label="close help">×</button>
</header>
<div class="booklet-body">
{#if tab === "about"}
{#if tab === "tally"}
<div class="tally">
<h3>How much love the maze is getting</h3>
{#if stats}
<dl class="tally-list">
<dt>{stats.gamesCreated}</dt><dd>games chronicled — {stats.gamesStarted} begun, {stats.gamesFinished} fought to a finish</dd>
<dt>{stats.wizardsSeated}</dt><dd>distinct wizards have taken a seat</dd>
<dt>{stats.commandsPlayed}</dt><dd>spells, steps, and punches recorded in the ledgers</dd>
<dt>{hours(Number(stats.minutesAtTable))}</dt><dd>spent at the table, by the clock between moves</dd>
<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>
</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>
{/if}
{:else}
<p>Counting the ledgers…</p>
{/if}
</div>
{:else if tab === "about"}
<div class="about">
<h3>What this is</h3>
<p>
@@ -222,6 +255,12 @@
.booklet-body h3:first-child { margin-top: 0; }
.booklet-body p { margin: 0.35rem 0; }
.colophon { font-style: italic; color: #6b5a41; font-size: 0.85rem; }
.tally-list { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 0.8rem; margin: 0.6rem 0 1rem; }
.tally-list dt {
font-family: "Oswald", sans-serif; font-weight: 700; font-size: 1.05rem;
color: #b3372b; text-align: right; white-space: nowrap;
}
.tally-list dd { margin: 0; align-self: center; }
.search-row { margin-bottom: 0.8rem; }
.search-row input {