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:
co-authored by
Claude Fable 5
parent
7bd8eff11c
commit
3bd7e3c81d
@@ -757,7 +757,7 @@ describe("the thief-chase holds one goal per turn", () => {
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 4 }, "S")] = "open";
|
||||
// A number card and no attacks: the blow the chase serves cannot land.
|
||||
bot.hand = [];
|
||||
bot.hand.push({ cardId: "number-2", instanceId: "N2" } as never);
|
||||
bot.hand.push({ cardId: "number-2", instanceId: "N2" });
|
||||
const visited = [cellKey(bot.position)];
|
||||
for (let guard = 0; guard < 40; guard++) {
|
||||
const view = viewFor(state, "bot");
|
||||
@@ -776,8 +776,9 @@ describe("the thief-chase holds one goal per turn", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the archmage's sharpened instincts", () => {
|
||||
function botTurn(seed = 42) {
|
||||
describe("the archmage's key, shield, and race discipline", () => {
|
||||
function botTurn() {
|
||||
const seed = 42;
|
||||
let { state } = createGame({ playerIds: ["foe", "bot"], seed, sets: ["basic", "expansion1"] });
|
||||
while (state.turn.round < 2 || state.players[state.turn.activeIndex]!.id !== "bot") {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
@@ -790,7 +791,7 @@ describe("the archmage's sharpened instincts", () => {
|
||||
it("a MASTER KEY drawn goes straight on display", () => {
|
||||
const state = botTurn();
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
bot.hand.push({ cardId: "master-key", instanceId: "MK" } as never);
|
||||
bot.hand.push({ cardId: "master-key", instanceId: "MK" });
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toEqual({ type: "cast", instanceId: "MK" });
|
||||
});
|
||||
@@ -806,8 +807,8 @@ describe("the archmage's sharpened instincts", () => {
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
bot.position = { ...foe.position };
|
||||
bot.life = 2;
|
||||
bot.hand = [{ cardId: "full-shield", instanceId: "FS" } as never];
|
||||
foe.hand.push({ cardId: "fireball", instanceId: "FB" } as never);
|
||||
bot.hand = [{ cardId: "full-shield", instanceId: "FS" }];
|
||||
foe.hand.push({ cardId: "fireball", instanceId: "FB" });
|
||||
const r = applyCommand(state, "foe", {
|
||||
type: "cast", instanceId: "FB", target: { kind: "player", playerId: "bot" },
|
||||
});
|
||||
@@ -833,15 +834,20 @@ describe("the archmage's sharpened instincts", () => {
|
||||
gold[1]!.position = null;
|
||||
bot.carriedTreasureId = gold[1]!.id;
|
||||
foe.position = { ...bot.home }; // a foe in sight: the war chest would hoard
|
||||
bot.hand = [
|
||||
{ cardId: "fireball", instanceId: "FB" } as never,
|
||||
{ cardId: "number-3", instanceId: "N3" } as never,
|
||||
];
|
||||
// Stand the bot a straight, open four squares from home.
|
||||
// Legs only: an attack in hand would fire at the foe in sight and
|
||||
// open a stack this single-seat loop cannot answer.
|
||||
bot.hand = [{ cardId: "number-3", instanceId: "N3" }];
|
||||
// Carve a straight, open four-square run to home; the rig owns its
|
||||
// geometry rather than praying the seed provides it.
|
||||
const home = bot.home;
|
||||
const column = [0, 1, 2, 3, 4].map((d) => ({ x: home.x, y: home.y + d }));
|
||||
if (!column.every((c) => viewFor(state, "bot").board.cells[cellKey(c)])) return;
|
||||
bot.position = { ...column[4]! };
|
||||
const column = [4, 3, 2, 1, 0].map((d) => ({ x: home.x, y: home.y - d }));
|
||||
for (const c of column) {
|
||||
if (!viewFor(state, "bot").board.cells[cellKey(c)]) {
|
||||
throw new Error("setup: the column north of home runs off the board");
|
||||
}
|
||||
delete state.squareContents[cellKey(c)];
|
||||
}
|
||||
bot.position = { ...column[0]! };
|
||||
for (const c of column.slice(0, 4)) state.edgeOverrides[edgeKey(c, "S")] = "open";
|
||||
for (let guard = 0; guard < 12; guard++) {
|
||||
const view = viewFor(state, "bot");
|
||||
@@ -857,8 +863,9 @@ describe("the archmage's sharpened instincts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("lessons from the human duels", () => {
|
||||
function rig(players = ["foe", "bot"]) {
|
||||
describe("bank guarding, teleport delivery, and turn theft", () => {
|
||||
function rig() {
|
||||
const players = ["foe", "bot"];
|
||||
let { state } = createGame({ playerIds: players, seed: 42, sets: ["basic", "expansion1"] });
|
||||
while (state.turn.round < 2 || state.players[state.turn.activeIndex]!.id !== "bot") {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
@@ -878,9 +885,28 @@ describe("lessons from the human duels", () => {
|
||||
foe.position = { x: bot.home.x, y: bot.home.y + 1 };
|
||||
bot.position = { ...foe.home };
|
||||
bot.hand = [];
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
// Whatever step it picks, the march must be TOWARD home, not the gold map.
|
||||
expect(cmd?.type).toBe("move");
|
||||
// The march may wind through the maze; what matters is that it ENDS
|
||||
// at the threatened bank, not out on the gold map.
|
||||
let cur = state;
|
||||
for (let guard = 0; guard < 60; guard++) {
|
||||
const seat = actingSeat(cur);
|
||||
if (seat !== "bot") {
|
||||
// The foe answers stacks with a pass and burns its turns.
|
||||
let r2 = applyCommand(cur, seat, { type: "pass" });
|
||||
if (!r2.ok) r2 = applyCommand(cur, seat, { type: "endTurn", draw: 0 });
|
||||
if (!r2.ok) throw new Error(`foe stuck: ${r2.error}`);
|
||||
cur = r2.state;
|
||||
continue;
|
||||
}
|
||||
const view = viewFor(cur, "bot");
|
||||
const cmd = automatonCommand(view, "hunter", "archmage") ?? automatonFallback(view, "archmage");
|
||||
const r = applyCommand(cur, "bot", cmd);
|
||||
if (!r.ok) throw new Error(`refused: ${JSON.stringify(cmd)} — ${r.error}`);
|
||||
cur = r.state;
|
||||
const at = cur.players.find((p) => p.id === "bot")!;
|
||||
if (cellKey(at.position) === cellKey(bot.home)) return;
|
||||
}
|
||||
throw new Error("the clockwork never came home to its threatened bank");
|
||||
});
|
||||
|
||||
it("carrying with home a blink away, it teleports the delivery", () => {
|
||||
@@ -895,7 +921,7 @@ describe("lessons from the human duels", () => {
|
||||
for (const side of ["N", "S", "E", "W"] as const) {
|
||||
state.edgeOverrides[edgeKey(bot.position, side)] = "wall";
|
||||
}
|
||||
bot.hand = [{ cardId: "teleport", instanceId: "TP" } as never];
|
||||
bot.hand = [{ cardId: "teleport", instanceId: "TP" }];
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toEqual({ type: "cast", instanceId: "TP", target: { kind: "cell", cell: { ...bot.home } } });
|
||||
});
|
||||
@@ -908,15 +934,15 @@ describe("lessons from the human duels", () => {
|
||||
const banked = state.treasures.find((t) => t.owner === "bot")!;
|
||||
banked.position = { ...foe.home };
|
||||
const carried = state.treasures.filter((t) => t.owner === "bot")[1];
|
||||
if (!carried) return;
|
||||
if (!carried) throw new Error("setup: the bot owns fewer than two treasures");
|
||||
carried.carriedBy = "foe";
|
||||
carried.position = null;
|
||||
foe.carriedTreasureId = carried.id;
|
||||
foe.position = { ...bot.position };
|
||||
bot.hand = [
|
||||
{ cardId: "fireball", instanceId: "FB" } as never,
|
||||
{ cardId: "lightning-blast", instanceId: "LB" } as never,
|
||||
{ cardId: "number-4", instanceId: "N4" } as never,
|
||||
{ cardId: "fireball", instanceId: "FB" },
|
||||
{ cardId: "lightning-blast", instanceId: "LB" },
|
||||
{ cardId: "number-4", instanceId: "N4" },
|
||||
];
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toMatchObject({ type: "cast", instanceId: "LB" });
|
||||
@@ -924,10 +950,10 @@ describe("lessons from the human duels", () => {
|
||||
});
|
||||
|
||||
describe("the denial planner offers only castable blocks", () => {
|
||||
it("tacks at range are never proposed — the RNRX coma", () => {
|
||||
// Room RNRX froze Automaton II for three turns: pathDenial proposed
|
||||
// scattering tacks four squares away, the engine refused ("you must
|
||||
// be adjacent"), and the refusal-fallback loop read as a coma.
|
||||
it("tacks are offered only at the caster's feet", () => {
|
||||
// TACKS demand adjacency; a planner that proposes them at range has
|
||||
// its cast refused every turn, and the refusal-fallback loop reads
|
||||
// as an idle bot.
|
||||
let { state } = createGame({ playerIds: ["foe", "bot"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
while (state.turn.round < 2 || state.players[state.turn.activeIndex]!.id !== "bot") {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
@@ -941,14 +967,13 @@ describe("the denial planner offers only castable blocks", () => {
|
||||
const gold = state.treasures.find((t) => t.owner === "bot" && t.position)!;
|
||||
foe.position = { ...gold.position! };
|
||||
bot.position = { ...bot.home };
|
||||
bot.hand = [{ cardId: "handful-of-tacks", instanceId: "HT" } as never];
|
||||
bot.hand = [{ cardId: "handful-of-tacks", instanceId: "HT" }];
|
||||
for (let guard = 0; guard < 6; guard++) {
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
if (!cmd) break;
|
||||
const r = applyCommand(state, "bot", cmd);
|
||||
// Whatever the brain proposes, the engine must accept it.
|
||||
expect(r.ok, `refused: ${JSON.stringify(cmd)} — ${!r.ok ? r.error : ""}`).toBe(true);
|
||||
if (!r.ok) break;
|
||||
if (!r.ok) throw new Error(`brain proposed a refused command: ${JSON.stringify(cmd)} — ${r.error}`);
|
||||
state = r.state;
|
||||
if (cmd.type === "endTurn") break;
|
||||
}
|
||||
|
||||
@@ -1186,12 +1186,13 @@ describe("the bent sight-trace shows AROUND THE CORNER's legs", () => {
|
||||
powerAttackPoints: 0, params: null, kind: "spell",
|
||||
counters: [], waitingOn: d.id, bentCorner: true,
|
||||
};
|
||||
// Wall the straight diagonal shut so only the bend can answer.
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 2 }, "S")] = "wall";
|
||||
state.edgeOverrides[edgeKey({ x: 2, y: 3 }, "E")] = "wall";
|
||||
const traced = stackSightTrace(viewFor(state, d.id));
|
||||
expect(traced).not.toBeNull();
|
||||
if (traced) {
|
||||
// Whether sight ran straight (free-angle found a gap) or bent, the
|
||||
// overlay has something to draw; a bend names its middle square.
|
||||
if (traced.bend) expect(traced.bend.mid).toBeDefined();
|
||||
}
|
||||
// The engine promises A workable corner, not which one.
|
||||
expect(traced?.bend).toBeDefined();
|
||||
expect(traced?.bend?.mid).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame } from "../src/game";
|
||||
import {
|
||||
type CreatureState, applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame } from "../src/game";
|
||||
import { cellKey, edgeKey, SIDES, stepTarget, type Cell, type Side } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { newExpansionGame as newGame, must, giveCard, toRound2, emptyNeighborCell, pushSustained } from "./helpers";
|
||||
@@ -709,7 +710,7 @@ describe("fear holds off monsters and unwilling feet alike", () => {
|
||||
damage: 0, maxDamage: 6, movesPerTurn: 3, movementUsed: 0,
|
||||
wallPassesPerTurn: 0, wallPassUsed: 0, attackUsed: false, justCreated: false,
|
||||
scorchedThisTurn: [],
|
||||
} as never);
|
||||
});
|
||||
for (const y of [5, 6, 7, 8]) state.edgeOverrides[edgeKey({ x: 2, y }, "S")] = "open";
|
||||
while (state.players[state.turn.activeIndex]!.id !== "a") {
|
||||
const r = applyCommand(state, state.players[state.turn.activeIndex]!.id, { type: "endTurn", draw: 0 });
|
||||
@@ -749,7 +750,6 @@ describe("fear holds off monsters and unwilling feet alike", () => {
|
||||
// The closer step is the only way out: permitted.
|
||||
const r = applyCommand(state, "a", { type: "move", direction: "S" });
|
||||
if (!r.ok) throw new Error("escape step refused: " + r.error);
|
||||
expect(r.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -760,9 +760,9 @@ describe("creatures and the dimensional warp", () => {
|
||||
state.dimWarps.push({ a: { x: 1, y: 1 }, b: { x: 3, y: 8 } });
|
||||
state.creatures.push({
|
||||
id: "troll-w", kind: "troll", controllerId: you, position: { x: 1, y: 1 },
|
||||
life: 6, movesPerTurn: 2, movementUsed: 0, attackUsed: false,
|
||||
justCreated: false, wallPassesPerTurn: 0, wallPassUsed: 0,
|
||||
} as never);
|
||||
damage: 0, maxDamage: 6, movesPerTurn: 2, movementUsed: 0, attackUsed: false,
|
||||
justCreated: false, wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [],
|
||||
});
|
||||
const r = applyCommand(state, you, { type: "creatureWarpStep", creatureId: "troll-w" });
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) {
|
||||
@@ -779,9 +779,9 @@ describe("creatures and the dimensional warp", () => {
|
||||
state.squareContents[cellKey({ x: 3, y: 8 })] = { kind: "stone", damage: 0, createdBy: "b" };
|
||||
state.creatures.push({
|
||||
id: "troll-w", kind: "troll", controllerId: you, position: { x: 1, y: 1 },
|
||||
life: 6, movesPerTurn: 2, movementUsed: 0, attackUsed: false,
|
||||
justCreated: false, wallPassesPerTurn: 0, wallPassUsed: 0,
|
||||
} as never);
|
||||
damage: 0, maxDamage: 6, movesPerTurn: 2, movementUsed: 0, attackUsed: false,
|
||||
justCreated: false, wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [],
|
||||
});
|
||||
const r = applyCommand(state, you, { type: "creatureWarpStep", creatureId: "troll-w" });
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.error).toContain("stone");
|
||||
|
||||
@@ -488,7 +488,9 @@ describe("a held door is an open doorway to the eye", () => {
|
||||
// no longer on their threshold; clear their path to the doorway.
|
||||
const near = neighbor(cell, side);
|
||||
const far = neighbor(near, side);
|
||||
if (!boardView(state).cells[cellKey(far)]) return; // the maze ends here; geometry unavailable
|
||||
if (!boardView(state).cells[cellKey(far)]) {
|
||||
throw new Error("setup: seed 42 lost the hallway this rig stands in");
|
||||
}
|
||||
state.players.find((p) => p.id === pursuer)!.position = far;
|
||||
state.edgeOverrides[edgeKey(near, side)] = "open";
|
||||
const rm = giveCard(state, holder, "remove-lock");
|
||||
|
||||
Reference in New Issue
Block a user