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
@@ -38,7 +38,7 @@ import { extname, join, normalize, sep } from "node:path";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import { cardDef } from "@wizwar/engine";
|
||||
import type { Command, PlayerId } from "@wizwar/engine";
|
||||
import { callRematch,
|
||||
import { callKeeper, callRematch,
|
||||
catchUpSteps,
|
||||
momentSteps,
|
||||
claimTransferCode,
|
||||
@@ -86,6 +86,11 @@ const MAX_ROOMS_PER_CONN = 10; // rooms one connection may create
|
||||
// Per-address limits, held across reconnects: a table of friends never
|
||||
// nears them; a script filling the vault or the reports desk does.
|
||||
const roomsPerAddress = new SlidingLimit(12, 60 * 60 * 1000);
|
||||
/** Calls to the keeper: a real person's phone rings for each. */
|
||||
const challengesPerAddress = new SlidingLimit(3, 60 * 60 * 1000);
|
||||
/** The keeper of this table: the wizard a lobby may challenge. */
|
||||
const KEEPER = process.env.WIZWAR_KEEPER ?? "Kestrel";
|
||||
const PUBLIC_URL = (process.env.WIZWAR_PUBLIC_URL ?? "https://wizwar.kestrelsnest.social").replace(/\/$/, "");
|
||||
const reportsPerAddress = new SlidingLimit(6, 60 * 60 * 1000);
|
||||
const MAX_COMMAND_BYTES = 16384; // serialized game command
|
||||
const MAX_MYGAMES_SEATS = 50; // seats checked per myGames request
|
||||
@@ -524,6 +529,8 @@ function roomInfo(room: Room) {
|
||||
rematch: room.rematch ?? null,
|
||||
expected: room.expected ?? [],
|
||||
expansion: room.expansion,
|
||||
challenge: room.challenge ?? null,
|
||||
keeper: KEEPER,
|
||||
colors: Object.fromEntries(room.colorChoices),
|
||||
bots: Object.fromEntries(
|
||||
// A mystery machine keeps its mood only while the game lives: once it
|
||||
@@ -721,11 +728,38 @@ wss.on("connection", (socket, req) => {
|
||||
// from firing again on every return to the room.
|
||||
if (room.state) {
|
||||
send(socket, { type: "events", events: redactFor(room.events, name), replayed: true });
|
||||
} else if (room.chat.length > 0) {
|
||||
// The lobby's talk so far, for whoever just sat down.
|
||||
send(socket, { type: "events", events: room.chat.map((c) => ({ type: "tableTalk", player: c.player, text: c.text })), replayed: true });
|
||||
}
|
||||
broadcastRoomState(room);
|
||||
runBots(room);
|
||||
break;
|
||||
}
|
||||
case "challengeKeeper": {
|
||||
const room = session.roomId ? getRoom(session.roomId) : undefined;
|
||||
if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" });
|
||||
if (!challengesPerAddress.allow(session.address)) {
|
||||
return send(socket, { type: "error", message: "the keeper has been called enough from here for one hour" });
|
||||
}
|
||||
const called = callKeeper(room, session.playerId, KEEPER);
|
||||
if ("error" in called) return send(socket, { type: "error", message: called.error });
|
||||
// The alarm the keeper listens for: one issue per room, so each
|
||||
// call rings once, with the door in the message.
|
||||
const link = `${PUBLIC_URL}/join/${room.id}`;
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.captureMessage(`${session.playerId} challenges ${KEEPER} to a game — ${link}`, {
|
||||
level: "warning",
|
||||
fingerprint: ["challenge", room.id],
|
||||
tags: { room: room.id, challenger: session.playerId },
|
||||
extra: { link, players: room.players.join(", ") },
|
||||
});
|
||||
}
|
||||
const said = addChat(room, session.playerId, `calls ${KEEPER} to the table`);
|
||||
if (!("error" in said)) broadcast(room, () => ({ type: "chat", player: session.playerId, text: said.text, at: said.at }));
|
||||
broadcastRoomState(room);
|
||||
break;
|
||||
}
|
||||
case "watch": {
|
||||
// The Peanut Gallery: no name, no seat, no ledger line — a pure
|
||||
// reader of the public broadcast, counted but never identified.
|
||||
|
||||
Reference in New Issue
Block a user