Credibility pass: one teleport step, one bell, one step type, and the comments that argued are gone

Since 6e49fc9. Engine: the teleport BFS in the view and the engine's
wallIgnoringDistance share teleportStep; the counter trail is weighed
between steps instead of on six arms; the bush attack takes its
adjacency from SIDES like the wall does; a dead redirected clause, a
one-member set, an identity ternary, and a stray try/catch are gone;
two tests now assert what their names promise. Server: KEEPER and
MAX_SEATS live in rooms.ts, the three Sentry bells ring through one
helper, the unread reply field is dropped, actingSeat comes from the
engine, and the protocol header names every message. Web: ReplayStep
is one type, cardName one helper, the say box and the reference section
one snippet each, the keeper's name arrives from the server rather than
the source, and the lifesaver ruling that ruled nothing is out.

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-22 09:56:49 -04:00
co-authored by Claude Fable 5.1
parent c4a555f8df
commit 66b5c06f17
21 changed files with 245 additions and 239 deletions
+17 -14
View File
@@ -1,5 +1,6 @@
// Websocket client + reactive session state (Svelte 5 runes).
import type { ReplayStep } from "./catchup";
import type { Command, GameEvent, GameView, Side } from "@wizwar/engine";
import { cardDef, isPermanentDuration } from "@wizwar/engine";
import { receiptFor } from "./receipt";
@@ -301,7 +302,6 @@ export interface FeedbackReportView {
happened: string;
expected: string;
player?: string;
reply?: { at: string; text: string; status: string };
/** The exchange in order: the desk's replies and the player's answers. */
thread?: { at: string; text: string; from: "desk" | "player"; status?: string }[];
image?: string;
@@ -381,8 +381,8 @@ class Net {
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");
/** The keeper's name, once the room has said it. */
keeper = $state<string | null>(null);
view = $state<GameView | null>(null);
log = $state<LogLine[]>([]);
error = $state<string | null>(null);
@@ -410,15 +410,15 @@ class Net {
/** Board flourishes: the app hooks in to animate live event batches. */
onFx: ((events: GameEvent[]) => void) | null = null;
/** A catch-up reel delivered by the server. */
catchUp = $state<{ seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[] | null>(null);
catchUp = $state<ReplayStep[] | null>(null);
/** The missed steps, fetched for the summary slip before any reel is opened. */
missedSteps = $state<{ seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[] | null>(null);
missedSteps = $state<ReplayStep[] | null>(null);
/** Where the catch-up reel opens: the step a summary line pointed at. */
catchUpStart = $state(0);
private catchUpWanted: "summary" | "reel" = "reel";
/** One turn's reel, summoned from a chronicle line's instant-replay
* eye — steps plus the wizard whose eyes the camera wears. */
moment = $state<{ steps: { seq: number; actor: string; events: GameEvent[]; view: GameView; chat?: { player: string; text: string }[] }[]; owner: string } | null>(null);
moment = $state<{ steps: ReplayStep[]; owner: string } | null>(null);
/** Turns witnessed so far: counts the same boundary events the server
* counts, so a chronicle line can name the turn it belongs to. */
private turnCounter = -1;
@@ -648,9 +648,7 @@ class Net {
break;
case "rematched":
// The caller's own move: the seat and room for the new table follow.
this.resetChronicle();
this.view = null;
this.started = false;
this.leaveTable();
this.roomId = null;
this.roomIdPending = msg.roomId;
break;
@@ -693,8 +691,8 @@ class Net {
// stays — a restarting server, a stale restore, or the wrong
// backend all answer with ignorance, and ignorance is not
// deletion. The wallet is capped by age instead of by trust.
// And a seat is dropped only on the SECOND refusal in a row: one
// refusal around a restart or a stale answer must not cost a seat.
// A seat goes only on the SECOND refusal in a row: one refusal
// around a restart or a stale answer must not cost a seat.
const voided = new Set((msg.voided as string[] | undefined) ?? []);
const doubted = new Set<string>();
for (const key of voided) { if (this.voidedOnce.has(key)) doubted.add(key); }
@@ -852,9 +850,7 @@ class Net {
/** Take the seat kept for you at the rematch table. */
acceptRematch(roomId: string): void {
if (!this.you) return;
this.resetChronicle();
this.view = null;
this.started = false;
this.leaveTable();
this.join(roomId, this.you);
}
@@ -996,6 +992,13 @@ class Net {
}
/** The chronicle and its turn count reset together, always. */
/** Rise from a finished table: the chronicle, the view, and the game go. */
private leaveTable(): void {
this.resetChronicle();
this.view = null;
this.started = false;
}
private resetChronicle(): void {
this.log = [];
this.turnCounter = -1;