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

+27 -4
View File
@@ -1,11 +1,13 @@
<script lang="ts">
import { tick } from 'svelte';
import type { Room } from '$lib/net/room.svelte';
import { NAME_MAX, playerName, rememberName, type Room } from '$lib/net/room.svelte';
let { room }: { room: Room } = $props();
const lines = $derived(room.view.chat);
let draft = $state('');
/** The gallery signs its lines; the name is the one the hall remembers. */
let galleryName = $state(playerName());
let sending = $state(false);
let list = $state<HTMLElement | null>(null);
@@ -25,6 +27,17 @@
if (await room.say(text)) draft = '';
sending = false;
}
async function sayFromGallery(e: SubmitEvent) {
e.preventDefault();
const text = draft.trim();
const name = galleryName.trim();
if (!text || !name || sending) return;
sending = true;
rememberName(name);
if (await room.sayFromGallery(name, text)) draft = '';
sending = false;
}
</script>
<section class="talk" aria-label="Table talk">
@@ -34,11 +47,17 @@
<li class="muted">Nobody has said a word.</li>
{/if}
{#each lines as line, i (line.at + ':' + i)}
<li class:you={line.id === room.me}><em>{line.id === room.me ? 'You' : room.nameOf(line.id)}</em> {line.text}</li>
<li class:you={!line.name && line.id === room.me}><em>{line.name ? `${line.name} (gallery)` : line.id === room.me ? 'You' : room.nameOf(line.id)}</em> {line.text}</li>
{/each}
</ul>
{#if room.spectating}
<p class="muted small">The gallery listens; only seated players speak.</p>
{#if room.spectating && room.view.galleryTalk}
<form onsubmit={sayFromGallery}>
<input type="text" class="name" bind:value={galleryName} maxlength={NAME_MAX} placeholder="Your name" aria-label="Your name in the gallery" autocomplete="nickname" />
<input type="text" bind:value={draft} maxlength="300" placeholder="Say something from the gallery" aria-label="Say something from the gallery" autocomplete="off" />
<button type="submit" class="quiet" disabled={!draft.trim() || !galleryName.trim() || sending}>Say</button>
</form>
{:else if room.spectating}
<p class="muted small">The gallery listens; the host has not let it talk.</p>
{:else}
<form onsubmit={say}>
<input type="text" bind:value={draft} maxlength="300" placeholder="Say something to the table" aria-label="Say something to the table" autocomplete="off" />
@@ -102,6 +121,10 @@
min-width: 0;
}
input.name {
flex: 0 1 8rem;
}
.small {
font-size: 0.85rem;
margin-top: 0.4rem;