Rematches, seats that travel by phrase, the keeper's bell with the hour where they live, the host's leave for the gallery to talk, and seats doubted before they are dropped

Ported from Wiz-War, where each earned its keep. All on the ledger:
galleryTalk, challenge and rematch lines, gallery chat signed with a
name, held seats on a rematch table's room line. Twenty-two protocol
checks against a scratch server and a replay after restart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric WagonerandClaude Fable 5.1 committed 2026-09-23 12:19:03 -04:00
1 parent 65d62f2ab3
commit 6dce3b82f0
14 files changed
+521 -28

No files matched your search

+63 -1
View File
@@ -7,8 +7,29 @@
const v = $derived(room.view);
const hostName = $derived(v.seats.find((s) => s.id === v.host)?.name ?? 'the host');
const seatFree = $derived(v.seats.length < v.size);
/** A seat is free for a newcomer once the held ones are counted. */
const seatOpen = $derived(v.seats.length + v.expected.length < v.size);
const keeperSeated = $derived(v.seats.some((s) => s.name.toLowerCase() === v.keeper.toLowerCase()));
let copied = $state(false);
/** The hour where the keeper lives, in words, so a caller knows whether to wait. */
let minute = $state(0);
$effect(() => {
const id = setInterval(() => (minute += 1), 60_000);
return () => clearInterval(id);
});
const keeperHour = $derived.by(() => {
void minute;
try {
const hour = Number(new Intl.DateTimeFormat('en-US', { hour: 'numeric', hour12: false, timeZone: v.keeperZone }).format(new Date())) % 24;
const word = ['twelve', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'][hour % 12];
const part = hour < 5 ? 'in the small hours' : hour < 12 ? 'in the morning' : hour < 18 ? 'in the afternoon' : 'in the evening';
return `${word} ${part}`;
} catch {
return '';
}
});
async function copyLink() {
try {
await navigator.clipboard.writeText(link);
@@ -42,8 +63,17 @@
<button type="button" class="unseat" title="Send this bot away" aria-label="Send {s.name} away" onclick={() => room.unseat(s.id)}>×</button>{/if}</span>
</li>
{/each}
<li class="empty muted">{v.size - v.seats.length} of {v.size} seats empty{#if v.audience > 0}; {v.audience} in the gallery{/if}</li>
{#each v.expected as name (name)}
<li>
<span class="seat-name">{name}</span>
<span class="muted">{v.challenge && name.toLowerCase() === v.keeper.toLowerCase() ? 'called to the table; a seat is held' : 'from the last table; a seat is held'}</span>
</li>
{/each}
<li class="empty muted">{v.size - v.seats.length - v.expected.length} of {v.size} seats empty{#if v.audience > 0}; {v.audience} in the gallery{v.galleryTalk ? ', allowed to talk' : ''}{/if}</li>
</ul>
{#if v.rematchOf}
<p class="muted">A rematch of table {v.rematchOf}: the same company, seats held for those not yet back.</p>
{/if}
{#if !room.spectating}
<div class="ways">
{#if seatFree && room.isHost}
@@ -56,6 +86,20 @@
<span class="muted">{hostName} will begin when the table is ready.</span>
{/if}
</div>
{#if room.isHost}
<label class="switch muted">
<input type="checkbox" checked={v.galleryTalk} onchange={(e) => room.setGalleryTalk(e.currentTarget.checked)} />
Let the Peanut Gallery talk at the table
</label>
{/if}
{#if v.keeper && !v.challenge && !keeperSeated && seatOpen}
<div class="keeper">
<button type="button" class="quiet" title="A seat is held for {v.keeper}, who keeps this site, and their phone rings" onclick={() => room.callKeeper()}>Challenge {v.keeper}, the keeper</button>
<p class="muted small">{v.keeper} keeps this site and answers every call, sometimes within the minute, sometimes after a night's sleep.{#if keeperHour} It is {keeperHour} where {v.keeper} lives.{/if} The seat is held either way.</p>
</div>
{:else if v.challenge}
<p class="muted small">{v.keeper} has been called to the table by {room.nameOf(v.challenge.by)}.{#if keeperHour} It is {keeperHour} there.{/if} A seat is held: if they can come now they will take it; if they are asleep or away they will leave a word below when they can. Meanwhile the table is yours: seat a bot, invite a friend, or open another game in a new tab and play on. This table keeps its place in your hall until you come back.</p>
{/if}
{/if}
{#if room.error}<p class="warning">{room.error}</p>{/if}
<div class="lobby-talk"><TableTalk {room} /></div>
@@ -143,4 +187,22 @@
max-width: 28em;
margin-top: 1rem;
}
.switch {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.6rem;
font-size: 0.9rem;
}
.keeper {
margin-top: 0.8rem;
max-width: 38em;
}
.small {
font-size: 0.85rem;
margin-top: 0.4rem;
}
</style>