The tally in the hall's about panel, with the board played and how each game was won

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-23 18:16:32 -04:00
co-authored by Claude Fable 5.1
parent c553902d8a
commit bac7182915
6 changed files with 177 additions and 2 deletions
+39 -1
View File
@@ -4,7 +4,7 @@
// about panel with the rules, the guide and a way to reach the keeper.
import { goto } from '$app/navigation';
import { otherGames, type FamilyGame } from '$lib/family';
import { allSeats, doubtSeat, forgetSeat, rememberSeat, ServerError, trustSeat, type Report } from '$lib/net/client';
import { allSeats, doubtSeat, forgetSeat, rememberSeat, ServerError, trustSeat, type Report, type Tally } from '$lib/net/client';
import { api, NAME_MAX, playerName, rememberName, Room } from '$lib/net/room.svelte';
import { unreadTalk } from '$lib/net/talk';
import type { RoomView } from '$lib/net/view';
@@ -20,6 +20,12 @@
let seats = $state(allSeats());
let games = $state<Record<string, RoomView<State> | null>>({});
let aboutOpen = $state(false);
/** The ledgers' tally, fetched when the about panel opens. */
let tally = $state<Tally | null>(null);
$effect(() => {
if (aboutOpen && !tally) api.tally().then((t) => (tally = t)).catch(() => (tally = null));
});
const hours = (minutes: number) => (minutes < 90 ? `${minutes} minutes` : `${Math.round(minutes / 60)} hours`);
/** The other games of the Kestrel's Nest, for the about panel. */
let family = $state<FamilyGame[]>([]);
$effect(() => {
@@ -261,6 +267,23 @@
<p>It is free and keeps no accounts. A game between people lives on a small server as an append-only ledger of moves, so it can be replayed from its first move, and each player is shown only what the rules let them see.</p>
<h3>Send word</h3>
<p>A rule read wrong, a bug, a game you would like to tell of: write to <a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>, <a href="https://bsky.app/profile/kestrelsnest.social" target="_blank" rel="noreferrer">@kestrelsnest.social</a> on Bluesky, or <a href="https://toots.kestrelsnest.social/@eric" target="_blank" rel="noreferrer">@eric@toots.kestrelsnest.social</a> on Mastodon. The keeper of this hall roosts at <a href="https://kestrelsnest.social" target="_blank" rel="noreferrer">kestrelsnest.social</a>.</p>
{#if tally}
<h3>The tally</h3>
<dl class="tally">
<dt>{tally.gamesFinished}</dt><dd>{tally.gamesFinished === 1 ? 'game' : 'games'} played to a finish, of {tally.gamesBegun} begun at {tally.tablesOpened} {tally.tablesOpened === 1 ? 'table' : 'tables'}</dd>
<dt>{tally.namesSeated}</dt><dd>{tally.namesSeated === 1 ? 'name has' : 'names have'} taken a seat</dd>
<dt>{tally.turnsPlayed}</dt><dd>turns recorded; {tally.longestGameTurns} in the longest game</dd>
<dt>{hours(tally.minutesAtTable)}</dt><dd>at the table, by the clock between moves</dd>
<dt>{tally.gamesWithBot}</dt><dd>{tally.gamesWithBot === 1 ? 'game' : 'games'} with a housecarl seated; {tally.talkLines} {tally.talkLines === 1 ? 'line' : 'lines'} of table talk</dd>
{#each Object.entries(tally.byGame) as [label, n] (label)}
<dt>{n}</dt><dd>{label}</dd>
{/each}
</dl>
<p class="muted small">
{#if tally.firstGameAt}The first game began {new Date(tally.firstGameAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}.{/if}
Names are counted, not people: one player under two names is two here. Table time counts only the minutes between moves, so a game left open overnight adds nothing for the night.
</p>
{/if}
{#if family.length}
<h3>More games at the Kestrel's Nest</h3>
<ul class="family">
@@ -593,4 +616,19 @@
margin: 0 auto;
border-radius: 4px;
}
.tally {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.25rem 0.9rem;
margin: 0.4rem 0 0.6rem;
}
.tally dt {
text-align: right;
font-weight: 500;
}
.tally dd {
margin: 0;
}
</style>