The keeper can say a word at a table through the keeper's door, signed as the keeper and heard whether or not the gallery may talk, so a call answered too late, after the duel began, can still be answered; waving-hands-say.sh sends it
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
745b8e99b9
commit
f21b2c3b16
9 files changed
+48
-4
No files matched your search
@@ -31,6 +31,7 @@
|
||||
//
|
||||
// GET /keeper/rooms/:id who sits at the table, and when each wizard last moved or spoke
|
||||
// POST /keeper/rooms/:id/reseat {seat} a new token for a player locked out, as a link to send them
|
||||
// POST /keeper/rooms/:id/say {text} a word from the keeper in the table's talk
|
||||
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
@@ -234,6 +235,11 @@ async function handleKeeper(req: IncomingMessage, res: ServerResponse, parts: st
|
||||
// The token rides the fragment, which the browser keeps to itself: it reaches neither Caddy's log nor this server.
|
||||
return send(res, 200, { name: seat.name, link: `${PUBLIC_URL}/join/${room.id}#seat=${seat.id}.${token}` });
|
||||
}
|
||||
if (parts[3] === 'say' && parts.length === 4 && req.method === 'POST') {
|
||||
const body = await readBody(req);
|
||||
rooms.keeperSays(room, KEEPER, body.text);
|
||||
return send(res, 204, {});
|
||||
}
|
||||
throw new RoomError('Not here.', 404);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { Rooms } from './rooms';
|
||||
import { chooseBotTurn } from '../../src/lib/game/bot';
|
||||
import { Store } from './store';
|
||||
import { SPECTATOR } from '../../src/lib/game/state';
|
||||
|
||||
let dir: string;
|
||||
beforeEach(() => {
|
||||
@@ -38,6 +39,15 @@ describe('rooms', () => {
|
||||
expect(() => rooms.seatOf(reopened, bo)).toThrow(/opens no seat/);
|
||||
});
|
||||
|
||||
it("puts the keeper's word in the table's talk though the gallery may not talk, and a restart keeps it", () => {
|
||||
const { rooms, room } = table();
|
||||
expect(room.galleryTalk).toBe(false);
|
||||
rooms.keeperSays(room, 'Kestrel', 'I came too late for the seat; call me again.');
|
||||
const said = { id: SPECTATOR, name: 'Kestrel', keeper: true, text: 'I came too late for the seat; call me again.' };
|
||||
expect(room.chat.at(-1)).toMatchObject(said);
|
||||
expect(new Rooms(new Store(dir)).get(room.id).chat.at(-1)).toMatchObject(said);
|
||||
});
|
||||
|
||||
it('re-seats only a wizard, and says when each last acted', () => {
|
||||
const { rooms, room, ann } = table();
|
||||
expect(() => rooms.reseat(room, 'Z')).toThrow(/No wizard sits/);
|
||||
|
||||
+6
-1
@@ -203,6 +203,11 @@ export class Rooms {
|
||||
this.commit(room, { t: 'chat', at: Date.now(), id: SPECTATOR, name, text: cleanTalk(raw) });
|
||||
}
|
||||
|
||||
/** The keeper of the site says a word at a table from the keeper's door, whether or not the gallery may talk: the answer to a call they could not take up. */
|
||||
keeperSays(room: Room, keeper: string, raw: unknown): void {
|
||||
this.commit(room, { t: 'chat', at: Date.now(), id: SPECTATOR, name: keeper, text: cleanTalk(raw), keeper: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* A seated wizard calls the keeper of the site to the table: a seat is
|
||||
* held under the keeper's name and the call goes on the ledger. Once per
|
||||
@@ -552,7 +557,7 @@ function apply(room: Room, line: LedgerLine): void {
|
||||
case 'over':
|
||||
break;
|
||||
case 'chat':
|
||||
room.chat.push({ id: line.id, text: line.text, at: line.at, ...(line.name ? { name: line.name } : {}) });
|
||||
room.chat.push({ id: line.id, text: line.text, at: line.at, ...(line.name ? { name: line.name } : {}), ...(line.keeper ? { keeper: true as const } : {}) });
|
||||
if (room.chat.length > CHAT_KEEP) room.chat.splice(0, room.chat.length - CHAT_KEEP);
|
||||
break;
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export type LedgerLine =
|
||||
/** Written after the turn that ends the duel, for anything that reads ledgers without the engine. */
|
||||
| { t: 'over'; at: number; winner: WizardId | null; reason: string }
|
||||
/** A line of table talk: from a seat, or, with `name`, from the Peanut Gallery (id SPECTATOR). */
|
||||
| { t: 'chat'; at: number; id: WizardId; text: string; name?: string }
|
||||
| { t: 'chat'; at: number; id: WizardId; text: string; name?: string; keeper?: true }
|
||||
/** The host let the gallery talk, or hushed it. */
|
||||
| { t: 'galleryTalk'; at: number; on: boolean; by: WizardId }
|
||||
/** A seat is held for the keeper of the site, called to the table. */
|
||||
|
||||
Reference in new issue
Block a user