Credibility pass: one voice, no scars

A three-reviewer sweep for tells of piecemeal machine generation,
every finding verified against the code before touching it. No
behavior changes; the full suite passes unchanged (plus two
strengthened pins).

Engine: removed four void-silenced fossils (a parseEdgeKey call
voided where it stood, stoneEffect's ignored cardId parameter, the
actualTarget remnant in doCast, a voided loop variable in shadow
upkeep); fixed the initialize-then-overwrite narration in
spawnCreature; replaced a filter(() => false) no-op; waterwall now
rides waveFromEdge instead of carrying its own verbatim copy (and the
single-caller washBack wrapper went with it); blind wall-bumps and
LOS blockers each collapsed to one implementation; the wand-id list
and the "permanent" duration sentinel became named constants; the
ambush number local no longer shadows the imported numberValue
function; assorted reviewer-aimed phrasings rewritten as the
constraints they guard.

Server/deploy: the protocol header now documents all eleven message
types; dropped an eslint pragma with no eslint, a test script with no
tests, and an rsync exclude anchored at a path that never existed
(the real data/ dir now excluded); the Caddy vhost has one source of
truth; stale "pending DNS" note removed — the record resolves.

Web: ~90 lines of CSS swallowed verbatim into a mobile media query
deduplicated; the reduced-motion guard on the board now actually
stops the marked-cell pulse; one shared color module replaces two
drifted palettes; an orphaned doc comment rejoined its function.

Tests: the ten-times-pasted helper block became test/helpers.ts;
wave-numbered files renamed for the behaviors they pin; deliberation
comments and void-ed corpses of unwritten assertions deleted; silent
seed-dependent early-returns now fail loudly; one assertion that
compared a value to itself now pins the home-translation it meant to;
the stored-log single-number command form gained the explicit
compatibility test it deserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 11:56:55 -04:00
co-authored by Claude Fable 5
parent f62fcf2510
commit 695307daa8
24 changed files with 240 additions and 716 deletions
+55 -88
View File
@@ -133,6 +133,11 @@ export interface SquareContent {
createdBy: PlayerId;
}
const WAND_CARD_IDS = ["blaster-wand", "shift-wand", "sticky-wand", "warp-wand"] as const;
/** Duration meaning "for the rest of the game" ("This card is permanent."). */
const PERMANENT_TURNS = 1_000_000_000;
/** Which square contents block line of sight. */
export const LOS_BLOCKING_CONTENT: Record<SquareContent["kind"], boolean> = {
stone: true, thornbush: true, rosebush: true, dust: true, slime: true,
@@ -265,17 +270,21 @@ export function sustainedOn(state: GameState, playerId: PlayerId, cardId?: strin
return state.sustained.filter((s) => s.targetId === playerId && (!cardId || s.cardId === cardId));
}
/** LOS including square-filling blockers (stone, thornbushes). */
export function gameLos(state: GameState, from: Cell, to: Cell): boolean {
/** Square-filling sight blockers: stone, bushes — and the BIG MAN, whom no spell passes. */
export function losBlockers(state: GameState): Record<string, true> {
const blockers: Record<string, true> = {};
for (const [key, content] of Object.entries(state.squareContents)) {
if (LOS_BLOCKING_CONTENT[content.kind]) blockers[key] = true;
}
// BIG MAN: you cannot cast spells past him.
for (const p of state.players) {
if (p.alive && sustainedOn(state, p.id, "big-man").length > 0) blockers[cellKey(p.position)] = true;
}
return hasLineOfSight(boardView(state), from, to, blockers);
return blockers;
}
/** LOS including square-filling blockers. */
export function gameLos(state: GameState, from: Cell, to: Cell): boolean {
return hasLineOfSight(boardView(state), from, to, losBlockers(state));
}
/** Parse an edge key back into its north/west cell and side. */
@@ -328,9 +337,7 @@ function perceivedBoard(
}
// Untested: only roll if this sight line would actually cross it.
if (sightLine) {
const { cell, side } = parseEdgeKey(key);
const test = { ...view, edges: { [key]: "wall" as const } };
void cell; void side;
const crossesIt = !hasLineOfSight(test, sightLine.from, sightLine.to);
if (crossesIt) {
if (illusionBelief(state, events, viewerId, key) === "believes") edges[key] = "wall";
@@ -354,10 +361,7 @@ function casterLos(
events: GameEvent[] = [],
): boolean {
const board = perceivedBoard(state, events, caster.id, { from, to });
const blockers: Record<string, true> = {};
for (const [key, content] of Object.entries(state.squareContents)) {
if (LOS_BLOCKING_CONTENT[content.kind]) blockers[key] = true;
}
const blockers = losBlockers(state);
if (hasLineOfSight(board, from, to, blockers)) return true;
if (!displays(caster, "visionstone")) return false;
for (const key of Object.keys(board.edges)) {
@@ -568,7 +572,7 @@ export type Command =
instanceId: string;
/** Number cards powering the cast (two allowed when an ADD is attached). */
numberInstanceIds?: string[];
/** Legacy single-number field; merged into numberInstanceIds. */
/** Single-number form still present in stored command logs; folded into numberInstanceIds. */
numberInstanceId?: string;
/** AMPLIFY cards attached (each doubles power/duration). */
amplifyInstanceIds?: string[];
@@ -1074,7 +1078,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (!target || !target.alive) return "no such living player";
if (!gameLos(state, caster.position, target.position)) return "no line of sight";
// Effectively permanent: broken by the caster attacking the target.
attachSustained(state, events, "buddy", caster.id, target.id, 1_000_000_000);
attachSustained(state, events, "buddy", caster.id, target.id, PERMANENT_TURNS);
return null;
},
},
@@ -1193,29 +1197,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (!losToEdge(view, caster.position, cell, side)) return "no line of sight";
events.push({ type: "waterwallCrashes", caster: caster.id, edge: { cell, side } });
// The two sides of the edge, and the push directions away from it.
const a = cell;
const b = neighbor(cell, side);
const away = (s2: Side): Side => (s2 === "N" ? "S" : s2 === "S" ? "N" : s2 === "E" ? "W" : "E");
const pushes: { start: Cell; dir: Side }[] = [
{ start: a, dir: away(side) },
{ start: b, dir: side },
];
for (const { start, dir } of pushes) {
// Players on the two cells extending away from the edge on this side.
let probe = start;
for (let dist = 0; dist < 2; dist++) {
for (const p of state.players) {
if (!p.alive || cellKey(p.position) !== cellKey(probe)) continue;
washBack(state, events, p, dir);
}
for (const c of [...state.creatures]) {
if (c.kind === "fire-imp" && cellKey(c.position) === cellKey(probe)) {
destroyCreature(state, events, c, "waterwall");
}
}
probe = neighbor(probe, dir);
}
}
waveFromEdge(state, events, cell, side, 2, "waterwall");
checkVictory(state, events);
return null;
},
@@ -1303,11 +1285,11 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
},
},
// --- Magic stones ---------------------------------------------------------
bloodstone: stoneEffect("bloodstone"),
powerstone: stoneEffect("powerstone"),
shadowstone: stoneEffect("shadowstone"),
soulstone: stoneEffect("soulstone"),
speedstone: stoneEffect("speedstone", (state, _events, caster) => {
bloodstone: stoneEffect(),
powerstone: stoneEffect(),
shadowstone: stoneEffect(),
soulstone: stoneEffect(),
speedstone: stoneEffect((state, _events, caster) => {
// "Your movement rate is increased by 1" — starting now, not next turn.
// A delta (not a recompute) so number cards already played stay counted.
// No bump while SLOW forces 1, or when this turn's movement is already
@@ -1318,9 +1300,9 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
state.turn.movementAllowance += 1;
}
}),
shieldstone: stoneEffect("shieldstone"),
visionstone: stoneEffect("visionstone"),
brainstone: stoneEffect("brainstone", (state, events, caster) => {
shieldstone: stoneEffect(),
visionstone: stoneEffect(),
brainstone: stoneEffect((state, events, caster) => {
// "Draw two more cards, now."
const drawn: CardInstance[] = [];
for (let i = 0; i < 2; i++) {
@@ -1339,7 +1321,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
// "This is permanent. Once SLOW DEATH is on, it can't be turned off."
onResolved: (ctx) => {
if (ctx.fullyStopped || !ctx.defender.alive) return;
attachSustained(ctx.state, ctx.events, "slow-death", ctx.attacker.id, ctx.defender.id, 1_000_000_000);
attachSustained(ctx.state, ctx.events, "slow-death", ctx.attacker.id, ctx.defender.id, PERMANENT_TURNS);
},
},
blind: { kind: "attack", requiresLos: true, baseDamage: () => 0, sustains: true },
@@ -1470,7 +1452,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
kind: "neutral",
resolve: (state, events, caster) => {
if (state.players.filter((p) => p.alive).length <= 2) return "not applicable in a 2-player game";
attachSustained(state, events, "lifesaver", caster.id, caster.id, 1_000_000_000);
attachSustained(state, events, "lifesaver", caster.id, caster.id, PERMANENT_TURNS);
return null;
},
},
@@ -1543,7 +1525,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
resolve: (state, events, caster, cmd) => {
const wanted = cmd.params?.cardId;
if (!wanted) return "name the card to retrieve";
if (["blaster-wand", "sticky-wand", "shift-wand", "warp-wand"].includes(wanted)) {
if ((WAND_CARD_IDS as readonly string[]).includes(wanted)) {
return "deja-vu cannot retrieve a magic wand";
}
for (let i = state.discard.length - 1; i >= 0; i--) {
@@ -1680,7 +1662,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
return "both squares must hold an item";
}
if (itemsA.length > 0 || itemsB.length > 0) {
if (itemsA.length > 0) state.groundObjects[kb] = [...itemsB.filter(() => false), ...itemsA];
if (itemsA.length > 0) state.groundObjects[kb] = [...itemsA];
else delete state.groundObjects[kb];
if (itemsB.length > 0) state.groundObjects[ka] = [...itemsB];
else delete state.groundObjects[ka];
@@ -1773,7 +1755,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
// "1/2 point of damage for every space moved. This spell is permanent."
onResolved: (ctx) => {
if (ctx.fullyStopped || !ctx.defender.alive) return;
attachSustained(ctx.state, ctx.events, "walking-dead", ctx.attacker.id, ctx.defender.id, 1_000_000_000);
attachSustained(ctx.state, ctx.events, "walking-dead", ctx.attacker.id, ctx.defender.id, PERMANENT_TURNS);
},
},
disease: {
@@ -2040,7 +2022,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (ctx.fullyStopped || !ctx.defender.alive) return;
const hasTreasureOut = ctx.state.treasures.some((t) => t.owner === ctx.defender.id && t.position);
if (!hasTreasureOut) return; // "ends if both treasures are being carried"
attachSustained(ctx.state, ctx.events, "idiot", ctx.attacker.id, ctx.defender.id, 1_000_000_000);
attachSustained(ctx.state, ctx.events, "idiot", ctx.attacker.id, ctx.defender.id, PERMANENT_TURNS);
},
},
"big-man": {
@@ -2268,7 +2250,7 @@ function terrainEffect(kind: SquareContent["kind"]): NeutralEffect {
}
/** A collapsing waterwall wave from an edge: wash players back `range`. */
function waveFromEdge(state: GameState, events: GameEvent[], cell: Cell, side: Side, range: number): void {
function waveFromEdge(state: GameState, events: GameEvent[], cell: Cell, side: Side, range: number, reason = "rushing water"): void {
const away = (s2: Side): Side => (s2 === "N" ? "S" : s2 === "S" ? "N" : s2 === "E" ? "W" : "E");
const pushes: { start: Cell; dir: Side }[] = [
{ start: cell, dir: away(side) },
@@ -2282,7 +2264,7 @@ function waveFromEdge(state: GameState, events: GameEvent[], cell: Cell, side: S
}
for (const c of [...state.creatures]) {
if (c.kind === "fire-imp" && cellKey(c.position) === cellKey(probe)) {
destroyCreature(state, events, c, "rushing water");
destroyCreature(state, events, c, reason);
}
}
probe = neighbor(probe, dir);
@@ -2334,8 +2316,7 @@ function summonEffect(kind: CreatureState["kind"]): NeutralEffect {
}
/** A displayable stone: casting it turns it face-up; its power is passive. */
function stoneEffect(cardId: string, onDisplay?: (state: GameState, events: GameEvent[], caster: PlayerState) => void): NeutralEffect {
void cardId;
function stoneEffect(onDisplay?: (state: GameState, events: GameEvent[], caster: PlayerState) => void): NeutralEffect {
return {
kind: "neutral",
keepInHand: true,
@@ -2398,10 +2379,6 @@ function washBackN(state: GameState, events: GameEvent[], p: PlayerState, dir: S
}
}
function washBack(state: GameState, events: GameEvent[], p: PlayerState, dir: Side): void {
washBackN(state, events, p, dir, 2);
}
// ---------------------------------------------------------------------------
// Creatures
@@ -2439,15 +2416,13 @@ function spawnCreature(
damage: 0,
maxDamage: stats.maxDamage,
movesPerTurn: stats.moves,
movementUsed: stats.moves, // no movement on the creation turn's remainder...
movementUsed: 0, // "It may move on that turn." (Exp1 sheet)
attackUsed: true, // "cannot attack the turn they are created"
justCreated: true,
wallPassesPerTurn: stats.wallPasses,
wallPassUsed: 0,
scorchedThisTurn: [],
};
// "...but may move on that turn." (Exp1 sheet) — movement allowed at once.
creature.movementUsed = 0;
state.creatures.push(creature);
events.push({ type: "creatureCreated", creatureId: creature.id, kind, controller: controllerId, at });
return creature;
@@ -3157,6 +3132,13 @@ function takeFromHand(p: PlayerState, instanceId: string): CardInstance | null {
// --- Movement ---------------------------------------------------------------
/** A blind wizard's wasted lurch into a wall still costs a movement point. */
function blindBump(state: GameState, events: GameEvent[], p: PlayerState, direction: Side): CommandResult {
state.turn.movementUsed++;
events.push({ type: "moveBumped", player: p.id, direction });
return { ok: true, state, events };
}
function doMove(prev: GameState, direction: Side): CommandResult {
const blocked = requireActionsAvailable(prev);
if (blocked) return err(blocked);
@@ -3188,11 +3170,7 @@ function doMove(prev: GameState, direction: Side): CommandResult {
const key = edgeKey(p.position, direction);
if (state.illusionWalls[key] &&
illusionBelief(state, events, p.id, key) === "believes") {
if (isBlinded(state, p)) {
state.turn.movementUsed++;
events.push({ type: "moveBumped", player: p.id, direction });
return { ok: true, state, events };
}
if (isBlinded(state, p)) return blindBump(state, events, p, direction);
return err("blocked by wall");
}
}
@@ -3209,11 +3187,7 @@ function doMove(prev: GameState, direction: Side): CommandResult {
const edge = view.edges[key] ?? "open";
const dest = neighbor(p.position, direction);
if (!view.cells[cellKey(dest)]) {
if (isBlinded(state, p)) {
state.turn.movementUsed++;
events.push({ type: "moveBumped", player: p.id, direction });
return { ok: true, state, events };
}
if (isBlinded(state, p)) return blindBump(state, events, p, direction);
return err("blocked");
}
if (edge === "door" && (state.doorStates[key] === "removed" || state.openDoorEdges.includes(key))) {
@@ -3235,10 +3209,7 @@ function doMove(prev: GameState, direction: Side): CommandResult {
p.position = dest;
via = "passWall";
} else if (isBlinded(state, p)) {
// Blind bump: the wasted lurch costs a movement point.
state.turn.movementUsed++;
events.push({ type: "moveBumped", player: p.id, direction });
return { ok: true, state, events };
return blindBump(state, events, p, direction);
} else {
return err(`blocked by ${target.by}`);
}
@@ -3739,7 +3710,7 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
// Magic wands: charged on first use by the number card(s) played; one
// charge per use, one use per turn; discarded when the last charge goes.
const WANDS = new Set(["blaster-wand", "sticky-wand", "shift-wand", "warp-wand"]);
const WANDS = new Set<string>(WAND_CARD_IDS);
const isWand = WANDS.has(inHand.cardId);
if (isWand && state.turn.wandsUsed.includes(inHand.instanceId)) {
return err("any wand operates a maximum of once per turn");
@@ -3877,7 +3848,6 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
// BLIND: casts at others fly in a rolled direction. "Misdirected spells
// go intended distance" — if the die disagrees with the true direction,
// the spell hits whoever lies that way, or dissipates.
let actualTarget = target;
if (isBlinded(state, caster) &&
cellKey(target.position) !== cellKey(caster.position)) {
const dx = target.position.x - caster.position.x;
@@ -3904,7 +3874,6 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
rolledDirection: rolled, newTarget: along?.id ?? null,
}];
if (!along) return { ok: true, state, events: missEvents }; // dissipates
actualTarget = along;
state.stack = {
attackerId: caster.id,
defenderId: along.id,
@@ -3926,7 +3895,6 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
return { ok: true, state, events: missEvents };
}
}
void actualTarget;
consumeCast(state, caster, inHand, mods, effect.keepInHand ?? false);
if (state.turn.attackUsed) state.turn.secondAttackUsed = true;
@@ -3994,7 +3962,7 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
state.lastSpellUsed[caster.id] = inHand.cardId;
}
const result = (effect as NeutralEffect).resolve(state, events, caster, cmd, mods.magnitude);
if (result) return err(result); // unreachable after preview
if (result) return err(result); // non-null here means resolve and preview disagree
return { ok: true, state, events };
}
@@ -4107,7 +4075,7 @@ function checkAmbushes(
state.ambushes = state.ambushes.filter((a) => a.id !== ambush.id);
state.discard.push(ambush.via, ambush.spell, ...ambush.numbers);
const numberValue = ambush.numbers.length > 0
const numberTotal = ambush.numbers.length > 0
? ambush.numbers.reduce((t, c) => t + (cardDef(c.cardId).value ?? 0), 0)
: null;
events.push({
@@ -4118,7 +4086,7 @@ function checkAmbushes(
attackerId: owner.id,
defenderId: actor.id,
attackCard: ambush.spell,
numberValue,
numberValue: numberTotal,
amplifyFactor: 1,
extendFactor: 1,
powerAttackPoints: 0,
@@ -4129,7 +4097,7 @@ function checkAmbushes(
};
events.push({
type: "spellCast", caster: owner.id, card: ambush.spell, cardId: ambush.spell.cardId,
numberCards: ambush.numbers, numberValue,
numberCards: ambush.numbers, numberValue: numberTotal,
from: owner.position, target: actor.id, targetCell: actor.position,
});
return; // one ambush per check; others may spring on later steps
@@ -4152,7 +4120,7 @@ function doCounteract(prev: GameState, playerId: PlayerId, instanceId: string):
if (playerId === stack.defenderId) {
if (card.cardId === "absorb-spell") {
if (stack.kind !== "spell") return err("absorb spell only works against spells");
if (stack.attackCard && ["blaster-wand", "sticky-wand", "shift-wand", "warp-wand"].includes(stack.attackCard.cardId)) {
if (stack.attackCard && (WAND_CARD_IDS as readonly string[]).includes(stack.attackCard.cardId)) {
return err("Absorb Spell has no effect on magic wands");
}
takeFromHand(player, instanceId);
@@ -4225,7 +4193,7 @@ function doCounteract(prev: GameState, playerId: PlayerId, instanceId: string):
}
if (playerId === stack.attackerId) {
if (card.cardId !== "anti-anti") return err("only ANTI-ANTI can counteract a counteraction (for now)");
if (card.cardId !== "anti-anti") return err("only ANTI-ANTI can counteract a counteraction");
const targetCounter = [...stack.counters].reverse().find((c) => !c.nullified);
if (!targetCounter) return err("no counteraction to nullify");
takeFromHand(player, instanceId);
@@ -4601,8 +4569,7 @@ function doPickUpObject(prev: GameState, instanceId: string): CommandResult {
* charges are keyed by instance, so they travel automatically).
*/
const MOVABLE_OBJECT_CARD_IDS = new Set([
"dagger", "large-rock", "wizardblade",
"blaster-wand", "shift-wand", "sticky-wand", "warp-wand",
"dagger", "large-rock", "wizardblade", ...WAND_CARD_IDS,
]);
export function isMovableObject(cardId: string): boolean {
@@ -4751,13 +4718,13 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi
}
// SHADOW upkeep: 1 life per turn, even during lost turns (handled where
// turns are skipped too).
for (const c of state.creatures.filter((c) => c.kind === "shadow" && c.controllerId === player.id)) {
const shadows = state.creatures.filter((c) => c.kind === "shadow" && c.controllerId === player.id).length;
for (let i = 0; i < shadows; i++) {
player.life -= 1;
events.push({ type: "shadowUpkeep", player: player.id, lifeAfter: player.life });
if (player.life <= 0) {
applyDamage(state, events, player, 0, "shadow upkeep", null); // triggers death path at <=0
}
void c;
}
// Duration spells expire at the start of their CASTER's turns.