The keeper can bring back a seat a browser lost: a new token on the ledger, the old one refused and any phrase it minted voided, and a link that carries it in the fragment, minted by waving-hands-reseat.sh through a /keeper door Caddy does not proxy; the join page keeps a linked seat only once the server takes it and never over a seat that still opens, a watcher at a duel in play is told both ways back, the hall asks before forgetting a duel in play, and the server's rooms have tests of their own

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbtUPJ24P6vc6tE1x6RS3m
This commit is contained in:
Eric WagonerandClaude Opus 5.5 committed 2026-10-13 18:24:20 -04:00
1 parent fbe110a7bb
commit b51934ecd6
13 files changed
+198 -20

No files matched your search

+29 -14
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { goto, replaceState } from '$app/navigation';
import { page } from '$app/state';
import Board from '$lib/components/Board.svelte';
import ReportSlip from '$lib/components/ReportSlip.svelte';
import TableTalk from '$lib/components/TableTalk.svelte';
import { seatFor } from '$lib/game/client';
import { rememberSeat, seatFor, seatInLink, trustSeat, type HeldSeat } from '$lib/game/client';
import { COMPACT_QUERY, Duel, NAME_MAX, playerName } from '$lib/game/duel.svelte';
const roomId = $derived((page.params.code ?? '').toUpperCase());
@@ -71,23 +71,34 @@
/** A watcher may still sit while the table is laid and a seat is free. */
const seatFree = $derived(!!room?.remote && !room.remote.started && room.remote.seats.length < room.remote.size);
// Return to a held seat, or take a place in the gallery and offer one.
// Return to a held seat, or take a place in the gallery and offer one. A link from the keeper carries a seat:
// it leaves the address bar at once, is kept only once the server takes it, and never replaces a seat that
// still opens, so a link someone else sends cannot take a working seat away.
$effect(() => {
const id = roomId;
room = null;
error = '';
const sent = seatInLink(location.hash);
if (sent) replaceState(location.pathname + location.search, {});
const held = seatFor(id);
if (!held) {
name = playerName();
joining = true;
Duel.watchRoom(id)
.then((remote) => (room = new Duel(remote)))
.catch((e: Error) => (error = e.message));
return;
}
joining = false;
Duel.resumeRoom(id, held)
.then((remote) => (room = new Duel(remote)))
const take = (s: HeldSeat) =>
Duel.resumeRoom(id, s).then((remote) => {
rememberSeat(id, s);
trustSeat(id);
return remote;
});
const arrive = held
? Duel.resumeRoom(id, held).catch((e: Error) => (sent ? take(sent) : Promise.reject(e)))
: sent
? take(sent).catch(() => Duel.watchRoom(id))
: Duel.watchRoom(id);
joining = !held && !sent;
if (!held) name = playerName();
arrive
.then((remote) => {
room = new Duel(remote);
if (remote.spectating) joining = true;
})
.catch((e: Error) => (error = e.message));
});
@@ -191,6 +202,10 @@
<p class="notice muted">On the other device, open the hall and claim this phrase within ten minutes: <b class="phrase">{phrase.phrase}</b></p>
{/if}
{#if watching && room?.remote?.started && !room.state.over}
<p class="notice muted">Had a seat at this table on another device? Transfer it from there and claim the phrase in the hall, or <a href="mailto:eric@ericwagoner.com?subject={encodeURIComponent(`My seat in room ${roomId}`)}">write to the keeper</a> for a link that brings it back.</p>
{/if}
{#if room?.remote?.started && room.state.over && !watching}
{@const r = room.remote}
<p class="notice muted">