The host has a hand on the other seats: once the duel begins, Seats makes a transfer phrase for another wizard's seat, which gives it a new token when claimed, and gives the seat of a wizard who cannot go on to a bot, which duels the turn it owes at once and keeps gestures already sealed; the wizard's token opens nothing while it stands in, their page goes to the gallery with the reason and a claim box, the roster says a bot duels for them, and a rematch holds their seat without the stand-in; the server pings every socket and closes one a dead browser left open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
1 parent
a673ea9766
commit
f98bfba107
14 files changed
+438
-31
No files matched your search
@@ -169,6 +169,9 @@ export const api = {
|
||||
rematch: (roomId: string, token: string) =>
|
||||
call<{ roomId: string; seat: WizardId | null; token: string | null; created: boolean }>('POST', `/api/rooms/${roomId}/rematch`, { token }),
|
||||
transfer: (roomId: string, token: string) => call<{ phrase: string; expiresAt: number }>('POST', `/api/rooms/${roomId}/transfer`, { token }),
|
||||
/** The host's phrase for another wizard's seat; claimed, it re-seats them. */
|
||||
hostPhrase: (roomId: string, token: string, seat: WizardId) => call<{ phrase: string; expiresAt: number }>('POST', `/api/rooms/${roomId}/phrase`, { token, seat }),
|
||||
standIn: (roomId: string, token: string, seat: WizardId) => call<RoomView>('POST', `/api/rooms/${roomId}/standin`, { token, seat }),
|
||||
claim: (phrase: string) => call<{ roomId: string; seat: WizardId; token: string }>('POST', '/api/transfer', { phrase }),
|
||||
report: (roomId: string, token: string, happened: string, expected: string) =>
|
||||
call<{ id: string }>('POST', `/api/rooms/${roomId}/report`, { token, happened, expected }),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { tick } from 'svelte';
|
||||
import { chooseBotTurn } from './bot';
|
||||
import { api, rememberSeat, watchRoom, type HeldSeat } from './client';
|
||||
import { api, rememberSeat, ServerError, watchRoom, type HeldSeat } from './client';
|
||||
import { markTalkSeen } from './talk';
|
||||
import {
|
||||
cellsOverlap,
|
||||
@@ -219,6 +219,8 @@ export interface Remote extends HeldSeat {
|
||||
started: boolean;
|
||||
connected: boolean;
|
||||
error: string;
|
||||
/** The server stopped taking this seat's token while the page was open, and why; the page goes to the gallery. */
|
||||
lost: string;
|
||||
chat: ChatLine[];
|
||||
/** How many watch from the Peanut Gallery. */
|
||||
audience: number;
|
||||
@@ -867,6 +869,26 @@ export class Duel {
|
||||
return api.transfer(r.roomId, r.token);
|
||||
}
|
||||
|
||||
/** Host only: a phrase for another wizard's seat, which gives it a new token when claimed. */
|
||||
hostPhrase(seat: WizardId): Promise<{ phrase: string; expiresAt: number }> {
|
||||
const r = this.remote!;
|
||||
return api.hostPhrase(r.roomId, r.token, seat);
|
||||
}
|
||||
|
||||
/** Host only: a bot duels in this wizard's seat for the rest of the duel, until a phrase gives it back. */
|
||||
async standIn(seat: WizardId): Promise<boolean> {
|
||||
const r = this.remote;
|
||||
if (!r) return false;
|
||||
r.error = '';
|
||||
try {
|
||||
this.apply(await api.standIn(r.roomId, r.token, seat));
|
||||
return true;
|
||||
} catch (e) {
|
||||
r.error = e instanceof Error ? e.message : 'The seat could not be given to a bot.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Say something to the whole table. Resolves false when the words did not arrive. */
|
||||
async say(text: string): Promise<boolean> {
|
||||
const r = this.remote;
|
||||
@@ -913,7 +935,10 @@ export class Duel {
|
||||
const refresh = () => {
|
||||
fetchView()
|
||||
.then((v) => this.apply(v))
|
||||
.catch((e: Error) => (r.error = e.message));
|
||||
.catch((e: Error) => {
|
||||
if (e instanceof ServerError && e.status === 403 && !r.spectating) r.lost = e.message;
|
||||
else r.error = e.message;
|
||||
});
|
||||
};
|
||||
return watchRoom(r.roomId, r.token, refresh, (open) => (r.connected = open));
|
||||
}
|
||||
@@ -1045,6 +1070,7 @@ function remoteFrom(view: RoomView, token: string): Remote {
|
||||
started: view.state !== null,
|
||||
connected: false,
|
||||
error: '',
|
||||
lost: '',
|
||||
chat: view.chat,
|
||||
audience: view.audience,
|
||||
spectating: view.me === SPECTATOR,
|
||||
|
||||
@@ -33,7 +33,8 @@ export interface RoomView {
|
||||
me: WizardId;
|
||||
/** The wizard who opened the table and may begin the duel. */
|
||||
host: WizardId;
|
||||
seats: { id: WizardId; name: string; bot: boolean }[];
|
||||
/** Who sits where; `standIn` marks a wizard's seat the host gave to a bot, which duels in it until they take it back. */
|
||||
seats: { id: WizardId; name: string; bot: boolean; standIn?: true }[];
|
||||
/** Seats that still have to move before the turn resolves. */
|
||||
waitingOn: WizardId[];
|
||||
/** Turns played so far, time stops included; gestures are sent with the count they were made against. */
|
||||
|
||||
Reference in new issue
Block a user