The lobby tells a caller what hour it is where the keeper lives, and that other games can go on in another tab

WIZWAR_KEEPER_TZ names the zone (Eastern by default); the room carries it
and the client words the hour by the minute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-23 08:49:37 -04:00
co-authored by Claude Fable 5.1
parent 31bfcf7bc0
commit 35d805fda3
4 changed files with 29 additions and 3 deletions
+3
View File
@@ -147,6 +147,8 @@ function receiveFeedbackImage(req: IncomingMessage, res: ServerResponse, reportI
/** Calls to the keeper: a real person's phone rings for each. */
const challengesPerAddress = new SlidingLimit(3, 60 * 60 * 1000);
/** Where the keeper sleeps, so a caller can tell four in the morning from four in the afternoon. */
const KEEPER_ZONE = process.env.WIZWAR_KEEPER_TZ ?? "America/New_York";
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
@@ -594,6 +596,7 @@ function roomInfo(room: Room) {
expansion: room.expansion,
challenge: room.challenge ?? null,
keeper: KEEPER,
keeperZone: KEEPER_ZONE,
colors: Object.fromEntries(room.colorChoices),
bots: Object.fromEntries(
// A mystery machine keeps its mood only while the game lives: once it
+22 -3
View File
@@ -1723,6 +1723,23 @@
/** The attack on the stack draws its sight line; a cast that resolved
* at once (a creation, a curse) draws the line it was accepted on for a
* few seconds, so the table can see how the aim was legal. */
/** The hour where the keeper lives, in words, kept current by the minute. */
let minuteTick = $state(0);
$effect(() => {
const id = setInterval(() => { minuteTick++; }, 60_000);
return () => clearInterval(id);
});
const keeperHour = $derived.by(() => {
void minuteTick;
if (!net.keeperZone) return null;
try {
const hour = Number(new Intl.DateTimeFormat("en-US", { hour: "numeric", hour12: false, timeZone: net.keeperZone }).format(new Date())) % 24;
const word = ["twelve", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven"][hour % 12];
const part = hour < 5 ? "in the small hours" : hour < 12 ? "in the morning" : hour < 18 ? "in the afternoon" : "in the evening";
return `${word} ${part}`;
} catch { return null; }
});
let castTrace = $state.raw<SightLine | null>(null);
let castTraceTimer: ReturnType<typeof setTimeout> | null = null;
const sightTrace = $derived((view ? stackSightTrace(view) : null) ?? castTrace);
@@ -2739,13 +2756,15 @@
title="a raven flies to {net.keeper}, who keeps this table">
⚔ Challenge {net.keeper}, the keeper</button>
<div class="challenge-note">{net.keeper} keeps this table and answers every raven — sometimes within the minute,
sometimes after a night's sleep. The seat is held either way.</div>
sometimes after a night's sleep. {keeperHour ? `It is ${keeperHour} where ${net.keeper} lives.` : ""} The seat is held either way.</div>
</div>
{:else if net.challenge}
<div class="challenge-note">The raven has flown to {net.keeper}, called by {net.challenge.by}. A seat is held for them:
<div class="challenge-note">The raven has flown to {net.keeper}, called by {net.challenge.by}.
{keeperHour ? `It is ${keeperHour} there.` : ""} A seat is held for them:
if they can come now, they will take it; if they are at supper, or asleep, or off in the world, they will
leave a word below when they can. Meanwhile the table is yours — seat a clockwork wizard, invite a friend,
or leave the room on the ledger and come back to it later.</div>
or open another game in a new tab and play on; this room keeps its place on your ledger until you come
back, as long as nobody abandons it.</div>
{/if}
<!-- Talk before the boards flip: to say when, or to leave a word. -->
{@const lobbyTalk = net.log.filter((l) => l.text.startsWith("\u{1F4AC}")).slice(-6)}
+3
View File
@@ -383,6 +383,8 @@ class Net {
challenge = $state<{ by: string; at: string } | null>(null);
/** The keeper's name, once the room has said it. */
keeper = $state<string | null>(null);
/** The keeper's time zone (IANA), for telling a caller what hour it is there. */
keeperZone = $state<string | null>(null);
view = $state<GameView | null>(null);
log = $state<LogLine[]>([]);
error = $state<string | null>(null);
@@ -541,6 +543,7 @@ class Net {
this.expected = msg.expected ?? [];
this.challenge = msg.challenge ?? null;
if (typeof msg.keeper === "string") this.keeper = msg.keeper;
if (typeof msg.keeperZone === "string") this.keeperZone = msg.keeperZone;
if (this.you && this.token) {
const seat: Seat = { name: this.you, roomId: msg.roomId, token: this.token };
localStorage.setItem(SEAT_KEY, JSON.stringify(seat));