A lobby may challenge the keeper, and the table can talk before the boards flip
"Challenge Kestrel, the keeper" holds a seat under the keeper's name, writes the call to the ledger, says so at the table, and raises a Sentry issue fingerprinted to the room — one ring per call, with the room's link in the message — which is the alarm the keeper's phone listens for. The keeper answers by the link: taking the seat, and leaving a word if the game must wait. For that word, and for any table to settle when to start, the lobby now shows its talk and carries a composer; a joiner sees what was said before they sat. Three calls per address per hour, one per room. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
de279364ce
commit
1be1cf7d1b
@@ -56,6 +56,8 @@ export interface Room {
|
||||
rematch?: { roomId: string; by: PlayerId };
|
||||
/** A rematch lobby: the wizards of the last table who have not yet sat. */
|
||||
expected?: PlayerId[];
|
||||
/** The table called the keeper of this site to a seat, and by whom. */
|
||||
challenge?: { by: PlayerId; at: string };
|
||||
}
|
||||
|
||||
const rooms = new Map<string, Room>();
|
||||
@@ -131,6 +133,22 @@ export function createRoom(
|
||||
return { room, token };
|
||||
}
|
||||
|
||||
/** A lobby calls the keeper to the table: a seat is held under their
|
||||
* name, the call is written to the ledger, and the caller (index.ts)
|
||||
* raises the alarm the keeper listens for. Once per room. */
|
||||
export function callKeeper(room: Room, byId: PlayerId, keeper: PlayerId): { at: string } | { error: string } {
|
||||
if (room.state) return { error: "the game has started" };
|
||||
if (!room.players.includes(byId) || room.bots.has(byId)) return { error: "take a seat first" };
|
||||
if (room.challenge) return { error: `${keeper} has already been called to this table` };
|
||||
if (room.players.includes(keeper)) return { error: `${keeper} is already here` };
|
||||
if (room.players.length + (room.expected?.length ?? 0) >= 6) return { error: "the table is full" };
|
||||
const at = new Date().toISOString();
|
||||
room.challenge = { by: byId, at };
|
||||
room.expected = [...(room.expected ?? []).filter((n) => n !== keeper), keeper];
|
||||
appendLine(room.id, { kind: "challenge", by: byId, at });
|
||||
return { at };
|
||||
}
|
||||
|
||||
/** A finished table calls for a rematch: a new room with the same
|
||||
* clockwork at the same tiers and the same expansion setting, waiting for
|
||||
* the same humans. The first caller hosts it; anyone at the old table may
|
||||
@@ -804,6 +822,10 @@ function rebuildRoom(id: string, lines: RoomLine[]): Room | null {
|
||||
if ("error" in r) throw new Error(`replay start failed: ${r.error}`);
|
||||
} else if (line.kind === "rematch") {
|
||||
room.rematch = { roomId: line.to, by: line.by };
|
||||
} else if (line.kind === "challenge") {
|
||||
room.challenge = { by: line.by, at: line.at };
|
||||
const keeper = process.env.WIZWAR_KEEPER ?? "Kestrel";
|
||||
if (!room.players.includes(keeper)) room.expected = [...(room.expected ?? []).filter((n) => n !== keeper), keeper];
|
||||
} else if (line.kind === "chat") {
|
||||
// File order preserves the interleaving with commands.
|
||||
room.chat.push({ player: line.player, text: line.text, at: line.at });
|
||||
|
||||
Reference in New Issue
Block a user