Credibility pass: the duels leave the code, staying in the ledgers

Third pass, scoped from 7a32370. Three blind reviewers (engine, web,
server/tools) each concluded the work is coherent engineering with
seam-level tells; every finding was verified before touching a line.

Session biography left the comments: the bot brain's heuristics no
longer cite the opponent who taught them, the RNRX coma parenthetical
and the thief-chase citation are gone, the seat-wallet comments state
their invariants without the war stories, and process-named test
groups now name the behaviors they pin. The incident record lives
where history belongs — commit messages and the ledgers.

Structural dedup: one VISIONSTONE one-edge-sight loop serves both
LOS paths; one creature-arrival touch handler serves walking and
warp-stepping (error text aligned); one facingWedge helper draws both
keymap ribbons; one spriteVisibleInCol rule serves the draw pass and
the hover test (which also stops re-sorting per pointermove); and
deepestFacing joins the director, replacing four copied scans.

Test hardening exposed real rot the tells were hiding: the tight
CreatureState cast caught two literals with a bogus field masking
three missing ones; the number-hoarding rig had NEVER run (its
column didn't exist on seed 42 — it now carves its own geometry);
the bank-guard rig now drives the whole table to an arrival
assertion; the bent-trace test walls off straight sight so the bend
must answer. Silent `return`-on-rig-failure became loud throws, and
can-never-fail assertions were removed.

Sweep-up: the eyeTurn ghost comment, the stacked leave() doc
comments (leave now delegates to leaveLocal), the dead ternary in
the seat client, the kick handler's name-coercion drift, kick ledger
lines gain timestamps, archiveRoomFile reuses fileFor, the RULES_REV
alias retires in favor of the engine constant, hitTest un-exports,
the NUL-sentinel hover shape becomes an honest "none" variant, and
the steering holds get named constants. The bezel's stride cluster
also centers per the table's note.

288 tests, 24 ledgers verified, all workspaces typecheck.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-27 15:43:24 -04:00
co-authored by Claude Fable 5
parent 7bd8eff11c
commit 3bd7e3c81d
17 changed files with 254 additions and 234 deletions
+14 -23
View File
@@ -120,6 +120,18 @@
}
// BIG MAN towers; SHRINK dwindles. The token says so at a glance.
/** The facing ribbon: a shallow golden triangle hugging a token's
* leading edge — base the token's width, rising a quarter of it. */
function facingWedge(side: "N" | "E" | "S" | "W", half: number): string {
const a = side === "E" ? 0 : side === "S" ? Math.PI / 2 : side === "W" ? Math.PI : -Math.PI / 2;
const fx = Math.cos(a), fy = Math.sin(a);
const px = -fy, py = fx;
const rise = half * 0.5;
return `${fx * half + px * half},${fy * half + py * half} ` +
`${fx * half - px * half},${fy * half - py * half} ` +
`${fx * (half + rise)},${fy * (half + rise)}`;
}
function wizardScale(id: string): number {
if (view.sustained.some((e) => e.cardId === "big-man" && e.targetId === id)) return 1.5;
if (view.sustained.some((e) => e.cardId === "shrink" && e.targetId === id)) return 0.62;
@@ -596,19 +608,7 @@
{/if}
</g>
{#if povFacing && !povCreatureId && p.id === view.you}
<!-- A short wide ribbon hugging the token's leading edge: base as
wide as the token, rising a fifth of that outward. -->
{@const fa = povFacing === "E" ? 0 : povFacing === "S" ? Math.PI / 2 : povFacing === "W" ? Math.PI : -Math.PI / 2}
{@const fh = CELL * 0.3 * wizardScale(p.id)}
{@const fxv = Math.cos(fa)}
{@const fyv = Math.sin(fa)}
{@const px = -fyv}
{@const py = fxv}
{@const rise = fh * 0.5}
<polygon
points={`${fxv * fh + px * fh},${fyv * fh + py * fh} ${fxv * fh - px * fh},${fyv * fh - py * fh} ${fxv * (fh + rise)},${fyv * (fh + rise)}`}
class="pov-wedge"
/>
<polygon points={facingWedge(povFacing, CELL * 0.3 * wizardScale(p.id))} class="pov-wedge" />
{/if}
</g>
{/each}
@@ -658,16 +658,7 @@
</g>
{#if povFacing && povCreatureId === c.id}
<!-- The rider's facing ribbon, worn by the mount. -->
{@const fa2 = povFacing === "E" ? 0 : povFacing === "S" ? Math.PI / 2 : povFacing === "W" ? Math.PI : -Math.PI / 2}
{@const ch = CELL * 0.26}
{@const cfx = Math.cos(fa2)}
{@const cfy = Math.sin(fa2)}
{@const cpx = -cfy}
{@const cpy = cfx}
<polygon
points={`${cfx * ch + cpx * ch},${cfy * ch + cpy * ch} ${cfx * ch - cpx * ch},${cfy * ch - cpy * ch} ${cfx * ch * 1.5},${cfy * ch * 1.5}`}
class="pov-wedge"
/>
<polygon points={facingWedge(povFacing, CELL * 0.26)} class="pov-wedge" />
{/if}
</g>
{/each}
+33 -33
View File
@@ -9,10 +9,10 @@
// here first.
import FirstPerson from "./fpv/FirstPerson.svelte";
import type { FpvTarget } from "./fpv/raycast";
import { tokenArt } from "./art";
import { objectArt } from "./art";
import { objectArt, tokenArt } from "./art";
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
import { castRay, SIDE_ANGLE, OPPOSITE } from "./fpv/raycast";
import { deepestFacing } from "./fpv/director";
import { aimOfEvents, gatherGlides, hurledIn, shortestArc } from "./fpv/director";
import { untrack } from "svelte";
import type { GameEvent, GameView, Side } from "@wizwar/engine";
@@ -108,12 +108,7 @@
seatedMount = possessedId;
preMount = untrack(() => ({ x: cam.x, y: cam.y, facing: cam.facing }));
const mx = possessed.position.x + 0.5, my = possessed.position.y + 0.5;
let deepest = -1, face = 0;
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(view, mx, my, a);
if (h.dist > deepest) { deepest = h.dist; face = a; }
}
cam.x = mx; cam.y = my; cam.facing = face;
cam.x = mx; cam.y = my; cam.facing = deepestFacing(view, mx, my);
});
/** What the ACTIVE body stands in or on: hazards it cannot see below
@@ -164,11 +159,15 @@
let manualUntil = 0;
let turning = false;
/** Manual steering holds the director's idle aims off this long. */
const STEER_HOLD_MS = 4000;
/** Mounting holds them off for the whole ride. */
const RIDE_HOLD_MS = 60000;
const SIDES4 = ["E", "S", "W", "N"] as const;
function manualTurn(dir: -1 | 1) {
if (turning) return;
turning = true;
manualUntil = performance.now() + 4000;
manualUntil = performance.now() + STEER_HOLD_MS;
const from = cam.facing;
const to = from + dir * (Math.PI / 2);
const t0 = performance.now();
@@ -203,7 +202,7 @@
function manualStride(back: boolean) {
if (!canStride) return;
manualUntil = performance.now() + 4000;
manualUntil = performance.now() + STEER_HOLD_MS;
const q = Math.round(cam.facing / (Math.PI / 2));
const side = SIDES4[((q % 4) + 4 + (back ? 2 : 0)) % 4]!;
if (possessed) onCreatureMove?.(possessed.id, side);
@@ -321,15 +320,12 @@
const willCut = !camReady || (dist > 1.6 && !hurled);
cutawayShot = false;
const takeCutaway = (ax: number, ay: number) => {
let best = { d: -1, a: 0 };
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(v, ax, ay, a);
if (h.dist > best.d) best = { d: h.dist, a };
}
const back = Math.min(1.6, Math.max(0.35, best.d - 0.4));
cam.x = ax + Math.cos(best.a) * back;
cam.y = ay + Math.sin(best.a) * back;
cam.facing = best.a + Math.PI;
const a = deepestFacing(v, ax, ay);
const d = castRay(v, ax, ay, a).dist;
const back = Math.min(1.6, Math.max(0.35, d - 0.4));
cam.x = ax + Math.cos(a) * back;
cam.y = ay + Math.sin(a) * back;
cam.facing = a + Math.PI;
camReady = true;
cutawayShot = true;
};
@@ -372,11 +368,7 @@
// A fresh CUT with nothing asking to be watched faces the deepest
// corridor — but a STANDING camera is never "rescued": where you
// pointed your own head is where it stays, brick or no brick.
let deepest = -1;
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(v, tx, ty, a);
if (h.dist > deepest) { deepest = h.dist; targetFacing = a; }
}
targetFacing = deepestFacing(v, tx, ty);
}
if (willCut) {
cam.x = tx; cam.y = ty; cam.facing = targetFacing;
@@ -424,8 +416,8 @@
{#if me}
<div class="live-fp">
<!-- The bezel: instruments frame the windshield instead of covering
it — every canvas pixel stays target glass. -->
<!-- The bezel: instruments live in the frame, never on the glass —
every canvas pixel is target. -->
<div class="bezel-top">
{#if possessed}
<span class="ride-label">riding the {possessed.kind.replace(/-/g, " ")}</span>
@@ -433,7 +425,7 @@
<button class="stamp tiny" onclick={dismount}>back to your eyes</button>
{:else if rideable.length > 0 && (onCreatureMove || onCreatureAttack)}
{#each rideable as c (c.id)}
<button class="stamp tiny" onclick={() => { possessedId = c.id; manualUntil = performance.now() + 60000; }}>
<button class="stamp tiny" onclick={() => { possessedId = c.id; manualUntil = performance.now() + RIDE_HOLD_MS; }}>
👁 ride the {c.kind.replace(/-/g, " ")}
</button>
{/each}
@@ -459,10 +451,7 @@
<button class="drive" onclick={() => manualTurn(1)} aria-label="turn right"></button>
</div>
<div class="bezel-bottom">
{#if canStride}
<button class="drive stride" onclick={() => manualStride(false)} aria-label="step forward">︿ forward</button>
<button class="drive stride" onclick={() => manualStride(true)} aria-label="step back">﹀ back</button>
{/if}
<span class="bezel-zone">
{#if canStride && ((!possessed && atFeet.length > 0) || underfoot.hazard || underfoot.warpHere)}
<span class="feet-label">at your feet</span>
{#if !possessed && onpickup}
@@ -484,8 +473,16 @@
<button class="stamp tiny" onclick={() => onWarpStep?.(possessedId)}> step through the warp</button>
{/if}
{/if}
<span class="bezel-spacer"></span>
<span class="live-fp-hint">← → turn · ↑ ↓ walk</span>
</span>
<span class="bezel-zone bezel-center">
{#if canStride}
<button class="drive stride" onclick={() => manualStride(false)} aria-label="step forward">︿ forward</button>
<button class="drive stride" onclick={() => manualStride(true)} aria-label="step back">﹀ back</button>
{/if}
</span>
<span class="bezel-zone bezel-right">
<span class="live-fp-hint">← → turn · ↑ ↓ walk</span>
</span>
</div>
</div>
{/if}
@@ -508,6 +505,9 @@
.bezel-top { border-bottom: 1px solid #2a2620; }
.bezel-bottom { border-top: 1px solid #2a2620; }
.bezel-spacer { flex: 1; }
.bezel-zone { flex: 1; display: flex; align-items: center; gap: 8px; }
.bezel-center { justify-content: center; }
.bezel-right { justify-content: flex-end; }
.bezel-mid {
display: flex;
align-items: stretch;
+7 -10
View File
@@ -7,7 +7,7 @@
import { scheduleFx, type BoardFx } from "./fx";
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
import { castRay, edgeMid } from "./fpv/raycast";
import { aimOfEvents, gatherGlides, hurledIn, shortestArc } from "./fpv/director";
import { deepestFacing, aimOfEvents, gatherGlides, hurledIn, shortestArc } from "./fpv/director";
import { prefs } from "./prefs.svelte";
import { stackSightTrace } from "@wizwar/engine";
import type { GameEvent, GameView } from "@wizwar/engine";
@@ -249,15 +249,12 @@
* corridor, facing it, holding a beat of the world-before so the
* deed happens ON screen — with the reel's own wizard visible. */
const takeCutaway = (ax: number, ay: number) => {
let best = { d: -1, a: 0 };
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(v, ax, ay, a);
if (h.dist > best.d) best = { d: h.dist, a };
}
const back = Math.min(1.6, Math.max(0.35, best.d - 0.4));
cam.x = ax + Math.cos(best.a) * back;
cam.y = ay + Math.sin(best.a) * back;
cam.facing = best.a + Math.PI;
const a = deepestFacing(v, ax, ay);
const d = castRay(v, ax, ay, a).dist;
const back = Math.min(1.6, Math.max(0.35, d - 0.4));
cam.x = ax + Math.cos(a) * back;
cam.y = ay + Math.sin(a) * back;
cam.facing = a + Math.PI;
camReady = true;
cutawayShot = true;
const i2 = Math.min(idx, steps.length - 1);
+29 -19
View File
@@ -3,9 +3,7 @@
// GameView. Columns of wall shaded by distance and facing; token art
// billboarded for whatever stands in the corridors, occluded per column
// by the same depth buffer the walls wrote.
import {
warpMotion,
type FpvTarget, castRay, billboards } from "./raycast";
import { billboards, castRay, warpMotion, type FpvTarget } from "./raycast";
import { materialTextures } from "./textures";
import { doorOpenness, fxFallback, growProgress, type FpFx } from "./fx3d";
import { terrainFallback, TERRAIN3D } from "./terrain3d";
@@ -59,8 +57,23 @@
W: number; H: number; ex: number; ey: number; facing: number;
zbuf: Float64Array; warpIdCol: Int32Array; warpDistCol: Float64Array;
cols: ({ edge?: string; kind: string; top: number; h: number } | null)[];
/** Depth-sorted nearest-first, ready for hover hit-testing. */
sprites: Projected[];
} | null = null;
/** The one visibility rule a sprite obeys in a column — shared by the
* draw pass and the hover hit test so they can never disagree. */
function spriteVisibleInCol(
sp: Projected, col: number,
zbuf: Float64Array, warpIdCol: Int32Array, warpDistCol: Float64Array,
): boolean {
if (sp.depth >= zbuf[col]!) return false;
if (sp.warped) {
if (warpIdCol[col] !== sp.warpId || sp.depth <= warpDistCol[col]!) return false;
} else if (sp.depth >= warpDistCol[col]!) return false;
if (sp.clampL !== undefined && (col < sp.clampL || col > sp.clampR!)) return false;
return true;
}
/** Crosshair position in canvas pixels, while the pointer is over the
* pane and the pane is an instrument. */
let mouse: { x: number; y: number } | null = null;
@@ -403,7 +416,10 @@
if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art });
}
sprites.sort((a, b) => b.sort - a.sort);
hitFrame = { W, H, ex, ey, facing, zbuf, warpIdCol, warpDistCol, cols: hitCols, sprites };
hitFrame = {
W, H, ex, ey, facing, zbuf, warpIdCol, warpDistCol, cols: hitCols,
sprites: [...sprites].sort((a, b) => a.depth - b.depth),
};
// The nearest sprite each column carries — depth AND vertical span —
// so the overlay passes (veils, ghosts, lintels, risings) can paint
// around a body standing in front of them instead of over it.
@@ -433,11 +449,7 @@
if (s.glow) ctx.globalCompositeOperation = "lighter";
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = s.alpha ?? 1;
for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) {
if (s.depth >= zbuf[col]!) continue;
if (s.warped) {
if (warpIdCol[col] !== s.warpId || s.depth <= warpDistCol[col]!) continue;
} else if (s.depth >= warpDistCol[col]!) continue;
if (s.clampL !== undefined && (col < s.clampL || col > s.clampR!)) continue;
if (!spriteVisibleInCol(s, col, zbuf, warpIdCol, warpDistCol)) continue;
if (s.depth < spriteZ[col]!) {
spriteZ[col] = s.depth;
sprTop[col] = s.top;
@@ -590,6 +602,8 @@
ctx.fillRect(col, c.top, 1, c.h);
}
label = found.label;
} else if (found.shape.kind === "none") {
label = found.label;
} else {
// A ground square: its four corners projected onto the floor plane.
const flen = (f.W / 2) / Math.tan(FOV / 2);
@@ -716,7 +730,7 @@
* (or vault) square the pixel lies on — warp-bent columns mapping
* their virtual ground back to real cells through the warp's own
* rigid motion. */
export function hitTest(px: number, py: number): FpvTarget | null {
function hitTest(px: number, py: number): FpvTarget | null {
return resolveHover(px, py)?.target ?? null;
}
@@ -728,7 +742,8 @@
shape:
| { kind: "rect"; left: number; right: number; top: number; bottom: number }
| { kind: "face"; edge: string }
| { kind: "ground"; cell: { x: number; y: number } };
| { kind: "ground"; cell: { x: number; y: number } }
| { kind: "none" };
} | null {
const f = hitFrame;
if (!f) return null;
@@ -737,15 +752,10 @@
// Sprites first, nearest first, honoring the draw pass's own
// visibility rules for this column.
const byDepth = [...f.sprites].sort((a, b) => a.depth - b.depth);
for (const sp of byDepth) {
for (const sp of f.sprites) {
if (!sp.hit && !sp.cell) continue; // pure spectacle (projectiles, rubble)
if (px < sp.left || px >= sp.right || py < sp.top || py > sp.bottom) continue;
if (sp.clampL !== undefined && (col < sp.clampL || col > sp.clampR!)) continue;
if (sp.depth >= f.zbuf[col]!) continue;
if (sp.warped) {
if (f.warpIdCol[col] !== sp.warpId || sp.depth <= f.warpDistCol[col]!) continue;
} else if (sp.depth >= f.warpDistCol[col]!) continue;
if (!spriteVisibleInCol(sp, col, f.zbuf, f.warpIdCol, f.warpDistCol)) continue;
const shape = { kind: "rect" as const, left: sp.left, right: sp.right, top: sp.top, bottom: sp.bottom };
const label = sp.hit ? (sp.hit.kind === "player" ? sp.hit.id : labelOf(sp)) : labelOf(sp);
if (sp.hit) return { target: sp.hit, label, shape };
@@ -783,7 +793,7 @@
// Warp-bent ground still TARGETS truly, but the highlight quad
// cannot be drawn in this frame's geometry: label it instead.
const pt = groundPoint(f, col, d);
return pt ? { target: { kind: "cell", cell: pt }, label: `through the warp (${pt.x},${pt.y})`, shape: { kind: "face", edge: "\u0000never" } } : null;
return pt ? { target: { kind: "cell", cell: pt }, label: `through the warp (${pt.x},${pt.y})`, shape: { kind: "none" } } : null;
}
const pt = groundPoint(f, col, d);
return pt ? { target: { kind: "cell", cell: pt }, label: "", shape: { kind: "ground", cell: pt } } : null;
+12
View File
@@ -6,6 +6,7 @@
import { edgeMid } from "./raycast";
import { fpFxForEvents } from "./fx3d";
import type { GameEvent, GameView } from "@wizwar/engine";
import { castRay } from "./raycast";
export function shortestArc(from: number, to: number): number {
let d = (to - from) % (2 * Math.PI);
@@ -131,3 +132,14 @@ export function gatherGlides(before: GameView, after: GameView, povId: string):
}
return moves;
}
/** The facing that stares down the longest corridor from a square the
* default view wherever nothing asks to be watched. */
export function deepestFacing(view: GameView, x: number, y: number): number {
let deepest = -1, face = 0;
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(view, x, y, a);
if (h.dist > deepest) { deepest = h.dist; face = a; }
}
return face;
}
+10 -20
View File
@@ -225,7 +225,6 @@ export interface LogLine {
* momentSteps, so a chronicle turn number addresses the same commands. */
const TURN_BOUNDARY = new Set(["turnStarted", "extraTurnStarted", "turnSkipped"]);
const SEAT_KEY = "wizwar-seat";
const SEATS_KEY = "wizwar-seats";
const CHAT_SEEN_KEY = "wizwar-chat-seen";
@@ -318,7 +317,6 @@ class Net {
/** 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;
/** The turn that already carries an instant-replay eye (one per turn). */
/** The turn whose moment reel is open (share links point at it). */
private momentTurn: number | null = null;
private shareResolve: ((url: string) => void) | null = null;
@@ -494,9 +492,8 @@ class Net {
// Prune ONLY seats the server proved dead: the room exists and
// refused this exact token. A seat merely ABSENT from the reply
// stays — a restarting server, a stale restore, or the wrong
// backend all answer with ignorance, and treating ignorance as
// deletion once cost a live player his seat mid-game. The
// wallet is capped by age instead of by trust.
// backend all answer with ignorance, and ignorance is not
// deletion. The wallet is capped by age instead of by trust.
const voided = new Set((msg.voided as string[] | undefined) ?? []);
let kept = this.seats.filter((s) => !voided.has(`${s.roomId}:${s.name}`));
if (kept.length > 50) kept = kept.slice(kept.length - 50);
@@ -510,10 +507,9 @@ class Net {
// "Name is taken" means THIS token was tried and refused: the
// credential itself is dead, and holding it helps nobody. But
// "no such room" proves nothing about the seat — a restarting
// server, a stale restore, or the wrong backend all say it
// and burning credentials on a transient locked a real player
// out of a live game. The seat stays; the error shows; a later
// reconnect against the right server walks back in.
// server, a stale restore, or the wrong backend all say it.
// The seat stays; the error shows; a later reconnect against
// the right server walks back in.
if (this.roomIdPending && /name is taken/.test(msg.message)) {
localStorage.removeItem(SEAT_KEY);
}
@@ -668,8 +664,9 @@ class Net {
} catch { /* blocked at the OS level; the tab title still shows it */ }
}
/** Forget the remembered seat and return to the lobby. */
/** Client-side teardown when the SERVER detached us (kick, abandon). */
/** Client-side teardown when the SERVER detached us (kick, abandon).
* The chronicle survives: the detachment appended its own explanatory
* line, and wiping it would erase the only notice of why. */
leaveLocal(): void {
localStorage.removeItem(SEAT_KEY);
this.roomId = null;
@@ -682,18 +679,11 @@ class Net {
this.audience = 0;
}
/** Walk away from the table (or gallery) by choice. */
leave(): void {
this.send({ type: "leave" }); // detach server-side too (frees a gallery seat)
localStorage.removeItem(SEAT_KEY);
this.roomId = null;
this.roomIdPending = null;
this.view = null;
this.started = false;
this.players = [];
this.leaveLocal();
this.resetChronicle();
this.token = null;
this.spectating = false;
this.audience = 0;
}
start(expansion: boolean): void {