Play now against the bot opens a room the server keeps

Every duel then leaves a ledger and shows in the visitors digest; the
offline duel in the browser remains as a fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric Wagoner
2026-09-22 21:56:30 -04:00
co-authored by Claude Fable 5.1
parent 098dadf482
commit 885dcf56e6
2 changed files with 32 additions and 9 deletions
+25 -9
View File
@@ -32,6 +32,20 @@
}); });
async function playBot() { async function playBot() {
busy = true;
error = '';
try {
rememberName(name);
const remote = await Duel.createBotRoom(name.trim());
await goto(`/join/${remote.roomId}`);
} catch (e) {
error = e instanceof Error ? e.message : 'The duel could not be opened.';
} finally {
busy = false;
}
}
async function playOffline() {
rememberName(name); rememberName(name);
await goto('/play'); await goto('/play');
} }
@@ -66,7 +80,7 @@
const g = games[held.roomId]; const g = games[held.roomId];
if (g === undefined) return 'looking'; if (g === undefined) return 'looking';
if (g === null) return 'unreachable'; if (g === null) return 'unreachable';
const others = g.seats.filter((s) => s.id !== held.seat).map((s) => s.name); const others = g.seats.filter((s) => s.id !== held.seat).map((s) => (s.bot ? `${s.name} (the bot)` : s.name));
if (!g.state) return `waiting to start${g.seats.length ? ` · ${g.seats.map((s) => s.name).join(', ')}` : ''}`; if (!g.state) return `waiting to start${g.seats.length ? ` · ${g.seats.map((s) => s.name).join(', ')}` : ''}`;
if (g.state.over) { if (g.state.over) {
if (g.state.over.winner === held.seat) return 'you won'; if (g.state.over.winner === held.seat) return 'you won';
@@ -105,12 +119,14 @@
<span>Your wizard's name</span> <span>Your wizard's name</span>
<input type="text" bind:value={name} maxlength={NAME_MAX} placeholder="e.g. Morwenna" autocomplete="nickname" /> <input type="text" bind:value={name} maxlength={NAME_MAX} placeholder="e.g. Morwenna" autocomplete="nickname" />
</label> </label>
<button type="button" class="reveal primary" disabled={!ready} onclick={playBot}> <button type="button" class="reveal primary" disabled={!ready || busy} onclick={playBot}>Play now against the bot</button>
{local ? `Continue against ${local.foe.name} (turn ${local.turnNumber})` : 'Play now against the bot'} <p class="muted small">
</button> {#if local}
{#if local} <button type="button" class="link" onclick={playOffline}>Continue your offline duel against {local.foe.name} (turn {local.turnNumber})</button>
<p class="muted small">Or start afresh from the board's "New duel".</p> {:else}
{/if} <button type="button" class="link" onclick={playOffline}>Or play offline, in this browser only</button>
{/if}
</p>
<div class="ways"> <div class="ways">
<button type="button" class="quiet" disabled={!ready || busy} onclick={create}>Create a duel</button> <button type="button" class="quiet" disabled={!ready || busy} onclick={create}>Create a duel</button>
<form class="joinform" onsubmit={join}> <form class="joinform" onsubmit={join}>
@@ -149,8 +165,8 @@
{#if local} {#if local}
<div class="ledger-row your-move"> <div class="ledger-row your-move">
<a class="ledger-resume" href="/play"> <a class="ledger-resume" href="/play">
<span class="ledger-code">bot</span> <span class="ledger-code">here</span>
<span class="ledger-info">your move · turn {local.turnNumber} · against {local.foe.name}</span> <span class="ledger-info">offline · your move · turn {local.turnNumber} · against {local.foe.name}</span>
</a> </a>
</div> </div>
{/if} {/if}
+7
View File
@@ -794,6 +794,13 @@ export class Duel {
return remoteFrom(seated.view, seated.token); return remoteFrom(seated.view, seated.token);
} }
/** A room with the bot already seated: a duel the server keeps, so it can be replayed and seen. */
static async createBotRoom(name: string): Promise<Remote> {
const seated = await api.create(name, 2);
rememberSeat(seated.view.roomId, { seat: seated.seat, token: seated.token });
return remoteFrom(await api.addBot(seated.view.roomId, seated.token), seated.token);
}
static async joinRoom(roomId: string, name: string): Promise<Remote> { static async joinRoom(roomId: string, name: string): Promise<Remote> {
const seated = await api.join(roomId, name); const seated = await api.join(roomId, name);
rememberSeat(roomId, { seat: seated.seat, token: seated.token }); rememberSeat(roomId, { seat: seated.seat, token: seated.token });