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
@@ -2653,7 +2653,7 @@
|
||||
<span class="seat-portrait empty"></span>
|
||||
<span class="seat-text">
|
||||
<span class="seat-name">{n}</span>
|
||||
<span class="seat-role">from the last table — not yet seated</span>
|
||||
<span class="seat-role">{net.challenge && n === net.keeper ? `called to the table by ${net.challenge.by} — awaiting an answer` : "from the last table — not yet seated"}</span>
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -2680,6 +2680,32 @@
|
||||
<summary>the link itself, for pasting by hand</summary>
|
||||
<code class="invite-link">{inviteLink}</code>
|
||||
</details>
|
||||
{#if !net.challenge && !net.players.includes(net.keeper) && net.players.length + net.expected.length < 6}
|
||||
<div class="challenge-row">
|
||||
<button class="stamp quiet-lid" onclick={() => net.challengeKeeper()}
|
||||
title="{net.keeper} keeps this table and gets a call; if they can come they take the seat, and either way they may leave a word here">
|
||||
⚔ Challenge {net.keeper}, the keeper</button>
|
||||
</div>
|
||||
{:else if net.challenge}
|
||||
<div class="challenge-note">{net.keeper} has been called by {net.challenge.by}. If they can come, they take their seat; if not, they may leave a word below.</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)}
|
||||
<div class="lobby-talk">
|
||||
{#each lobbyTalk as line, i (i)}
|
||||
<div class="lobby-line">{line.text}</div>
|
||||
{/each}
|
||||
<form class="say-box lobby-say" onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const t = chatDraft.trim();
|
||||
if (!t) return;
|
||||
if (net.sendChat(t)) chatDraft = "";
|
||||
else net.flash("Not sent — the table is out of reach; your words wait in the box");
|
||||
}}>
|
||||
<input class="say-input" bind:value={chatDraft} maxlength="300" placeholder="say something to the table…" aria-label="table talk" />
|
||||
<button class="stamp tiny" type="submit" disabled={!chatDraft.trim()}>say</button>
|
||||
</form>
|
||||
</div>
|
||||
{#if net.you === net.hostId && addingBot && net.players.length < 6}
|
||||
<div class="bot-panel">
|
||||
<div class="bot-field">
|
||||
@@ -4014,6 +4040,12 @@
|
||||
padding: 0.5rem 0 0.7rem;
|
||||
}
|
||||
.fill-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; }
|
||||
.challenge-row { margin-top: 0.6rem; }
|
||||
.stamp.quiet-lid { background: #f4eede; box-shadow: none; }
|
||||
.challenge-note { margin-top: 0.6rem; font-size: 0.88rem; color: #6b5a41; font-style: italic; }
|
||||
.lobby-talk { margin-top: 0.7rem; text-align: left; }
|
||||
.lobby-line { font-family: "Courier Prime", monospace; font-size: 0.82rem; color: #43331f; padding: 0.1rem 0; }
|
||||
.lobby-say .say-input { background: #f4eede; color: #43331f; border: 1px solid #b3a687; }
|
||||
.fill-row .stamp.current { background: #e4dbc2; }
|
||||
.invite-fallback { margin: 0.4rem 0 0; font-size: 0.8rem; }
|
||||
.invite-fallback summary { cursor: pointer; color: #8a6d3f; font-style: italic; }
|
||||
|
||||
@@ -369,6 +369,10 @@ class Net {
|
||||
rematchCall = $state<{ roomId: string; by: string } | null>(null);
|
||||
/** A rematch lobby: the last table's wizards not yet seated. */
|
||||
expected = $state<string[]>([]);
|
||||
/** The table called the keeper of this site, and by whom. */
|
||||
challenge = $state<{ by: string; at: string } | null>(null);
|
||||
/** The keeper's name, as the server knows it. */
|
||||
keeper = $state("Kestrel");
|
||||
view = $state<GameView | null>(null);
|
||||
log = $state<LogLine[]>([]);
|
||||
error = $state<string | null>(null);
|
||||
@@ -523,6 +527,8 @@ class Net {
|
||||
this.audience = msg.audience ?? 0;
|
||||
this.rematchCall = msg.rematch ?? null;
|
||||
this.expected = msg.expected ?? [];
|
||||
this.challenge = msg.challenge ?? null;
|
||||
if (typeof msg.keeper === "string") this.keeper = msg.keeper;
|
||||
if (this.you && this.token) {
|
||||
const seat: Seat = { name: this.you, roomId: msg.roomId, token: this.token };
|
||||
localStorage.setItem(SEAT_KEY, JSON.stringify(seat));
|
||||
@@ -796,6 +802,11 @@ class Net {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Call the keeper of this site to the table: a seat is held, and their phone rings. */
|
||||
challengeKeeper(): void {
|
||||
this.send({ type: "challengeKeeper" });
|
||||
}
|
||||
|
||||
/** Call for a rematch from a finished table, or join the one already called. */
|
||||
callRematch(): void {
|
||||
this.send({ type: "rematch" });
|
||||
@@ -886,6 +897,7 @@ class Net {
|
||||
this.chatPending = null;
|
||||
this.rematchCall = null;
|
||||
this.expected = [];
|
||||
this.challenge = null;
|
||||
this.roomId = null;
|
||||
this.roomIdPending = null;
|
||||
this.view = null;
|
||||
|
||||
Reference in New Issue
Block a user