The host begins at every table size and may send a bot away

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-23 11:21:42 -04:00
co-authored by Claude Fable 5.1
parent 457a536f58
commit ddb18cb08a
11 changed files with 55 additions and 9 deletions
+17 -2
View File
@@ -31,14 +31,15 @@
{:else}
<p class="eyebrow">room {v.roomId}</p>
<h2>The table is laid</h2>
<p>Send this link, or the code. Whoever opens it takes a seat. Anyone who arrives once the game has begun watches from the Peanut Gallery.</p>
<p>Send this link, or the code. Whoever opens it takes a seat, and the game begins when you say. Anyone who arrives once it has begun watches from the Peanut Gallery.</p>
<p class="link-row"><code>{link}</code><button type="button" class="quiet" onclick={copyLink}>{copied ? 'Copied' : 'Copy link'}</button></p>
{/if}
<ul class="roster">
{#each v.seats as s (s.id)}
<li>
<span class="seat-name">{s.name}</span>
<span class="muted">{s.id === v.host ? 'opened the table' : s.bot ? 'the bot' : 'seated'}{s.id === v.me ? ', you' : ''}</span>
<span class="muted">{s.id === v.host ? 'opened the table' : s.bot ? 'the bot' : 'seated'}{s.id === v.me ? ', you' : ''}{#if s.bot && room.isHost}
<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>
@@ -101,6 +102,20 @@
font-weight: 500;
}
.unseat {
background: none;
border: 0;
color: var(--bone-faint);
font-size: 1.1rem;
padding: 0 0.3rem;
margin-left: 0.3rem;
line-height: 1;
}
.unseat:hover {
color: var(--blood);
}
.ways {
display: flex;
flex-wrap: wrap;
+1
View File
@@ -88,6 +88,7 @@ export function makeApi<State, Input>() {
join: (roomId: string, name: string) => call<Seated>('POST', `/api/rooms/${roomId}/join`, { name }),
addBot: (roomId: string, token: string) => call<View>('POST', `/api/rooms/${roomId}/bot`, { token }),
begin: (roomId: string, token: string) => call<View>('POST', `/api/rooms/${roomId}/begin`, { token }),
unseat: (roomId: string, token: string, seat: SeatId) => call<View>('POST', `/api/rooms/${roomId}/unseat`, { token, seat }),
view: (roomId: string, token: string) => call<View>('GET', `/api/rooms/${roomId}?token=${encodeURIComponent(token)}`),
/** The gallery's view: no token, no seat, nothing any player could not see. */
watch: (roomId: string) => call<View>('GET', `/api/rooms/${roomId}`),
+8 -2
View File
@@ -104,6 +104,11 @@ export class Room {
return this.act(() => api.addBot(this.roomId, this.token), 'The bot could not be seated.');
}
/** Send a bot away before the game begins; only the host may. */
unseat(seat: SeatId): Promise<boolean> {
return this.act(() => api.unseat(this.roomId, this.token, seat), 'The bot could not be sent away.');
}
say(text: string): Promise<boolean> {
if (this.spectating) return Promise.resolve(false);
return this.act(() => api.say(this.roomId, this.token, text), 'The table did not hear you.');
@@ -125,11 +130,12 @@ export class Room {
return new Room(seated.view, seated.token);
}
/** A room with a bot already seated, so a solo game is a ledger like any other. */
/** A room with a bot seated and the game begun, so a solo game is a ledger like any other. */
static async createWithBot(name: string): Promise<Room> {
const seated = await api.create(name, 2);
rememberSeat(seated.view.roomId, { seat: seated.seat, token: seated.token });
return new Room(await api.addBot(seated.view.roomId, seated.token), seated.token);
await api.addBot(seated.view.roomId, seated.token);
return new Room(await api.begin(seated.view.roomId, seated.token), seated.token);
}
static async join(roomId: string, name: string): Promise<Room> {
+1 -1
View File
@@ -30,7 +30,7 @@
<section id="table">
<h2>The table</h2>
<p>A table fills as players open the link. Whoever laid it is the host: they may seat a bot in any empty chair, and they begin the game when the company suits them. Once begun, the seats are closed.</p>
<p>A table fills as players open the link. Whoever laid it is the host: they may seat a bot in any empty chair or send one away, and they begin the game when the company suits them, however full the table. Once begun, the seats are closed.</p>
</section>
<section id="board">