Credibility pass: sweep the workshop floor
Scoped to everything since the last pass (695307d). The residue of
fast iteration, removed: a reduced-motion media query that had
swallowed a full copy of the .faq-seal rules; doc comments orphaned
from their functions by inserted methods; the FAQ scrape's seams
(section headings run into ruling bodies under the wrong topics, a
next-page heading shipped as a ruling, an amputated "h", and rulings
filed under alphabetically-nearest strangers — Large Rock/Dagger now
lives on those cards; Book of Spells and Torquemada describe no card
in this set and are gone); the write-only aisle flag left behind by
the reverted rev 7; a linter-silenced dead destructure; a duplicated
median lookup; dead casts; and the fallback path that ignored the
apprentice's one-card draw.
Tests now typecheck (tsconfig includes test/), which surfaced the
missing type imports and a drifted creature literal hiding under an
as-cast. Also: a no-op self-assignment, mid-file imports hoisted, a
dynamic-import habit made static, a hedge comment replaced with a
loud setup failure, the fallback-retries-itself dead rung removed
from both bot drivers, and the twin peek scrims merged.
Deliberately kept: the TIERS lookup guard (ledger JSON is untrusted),
the wand-cost stanzas (they differ on the power-attack trade — a
rules question, not a dedup), and rollDie's coexistence with rollD4
(migrating changes visible logs; future work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
73150a89b5
commit
3308850bb6
@@ -39,11 +39,14 @@ function playOut(
|
||||
while (state.phase === "playing" && commands < CAP) {
|
||||
const seat = actingSeat(state);
|
||||
const view = viewFor(state, seat);
|
||||
const cmd = automatonCommand(view, styleOf.get(seat), tierOf.get(seat)) ?? automatonFallback(view);
|
||||
const tier = tierOf.get(seat);
|
||||
const chosen = automatonCommand(view, styleOf.get(seat), tier);
|
||||
const cmd = chosen ?? automatonFallback(view, tier);
|
||||
let r = applyCommand(state, seat, cmd);
|
||||
if (!r.ok) {
|
||||
const fb = automatonFallback(view);
|
||||
r = applyCommand(state, seat, fb);
|
||||
// Retry with the fallback only if the fallback was not what just failed.
|
||||
const fb = automatonFallback(view, tier);
|
||||
if (chosen) r = applyCommand(state, seat, fb);
|
||||
if (!r.ok) {
|
||||
stuck++;
|
||||
if (stuck > 3) {
|
||||
@@ -68,10 +71,9 @@ function playOut(
|
||||
|
||||
describe("automaton vs automaton", () => {
|
||||
it("two clockwork wizards fight a game to its end", () => {
|
||||
const { state, commands } = playOut(11, 2);
|
||||
const { state } = playOut(11, 2);
|
||||
expect(state.phase).toBe("finished");
|
||||
expect(state.winner).not.toBeNull();
|
||||
expect(commands).toBeLessThan(4000);
|
||||
});
|
||||
|
||||
it("holds up across many seeds without wedging", () => {
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
type SectorPlacement,
|
||||
} from "../src/board";
|
||||
import { buildDeck } from "../src/cards";
|
||||
import { activePlayer, applyCommand, createGame } from "../src/game";
|
||||
import { createRng } from "../src/rng";
|
||||
import { setupBoard } from "../src/setups";
|
||||
import { giveCard } from "./helpers";
|
||||
|
||||
describe("card data", () => {
|
||||
it("basic deck is exactly 125 cards (official 6e rulebook)", () => {
|
||||
@@ -109,9 +113,7 @@ describe("board assembly", () => {
|
||||
});
|
||||
|
||||
describe("player counts 2-6", () => {
|
||||
it("assembles a coherent board for every supported count", async () => {
|
||||
const { setupBoard } = await import("../src/setups");
|
||||
const { createRng } = await import("../src/rng");
|
||||
it("assembles a coherent board for every supported count", () => {
|
||||
for (const n of [2, 3, 4, 5, 6]) {
|
||||
const { board } = setupBoard(n, createRng(n * 7));
|
||||
expect(board.homes.length).toBe(n);
|
||||
@@ -127,8 +129,7 @@ describe("player counts 2-6", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("full games start at every count", async () => {
|
||||
const { createGame } = await import("../src/game");
|
||||
it("full games start at every count", () => {
|
||||
for (const n of [3, 5, 6]) {
|
||||
const ids = Array.from({ length: n }, (_, i) => `w${i}`);
|
||||
const { state } = createGame({ playerIds: ids, seed: 99 + n, sets: ["basic", "expansion1"] });
|
||||
@@ -158,12 +159,7 @@ describe("sight and the wraparound openings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
import { createRng } from "../src/rng";
|
||||
import { setupBoard as setupBoardForPins } from "../src/setups";
|
||||
|
||||
describe("setup diagram pairings (rulebook Set-Up Diagram)", () => {
|
||||
const setupBoard = setupBoardForPins;
|
||||
|
||||
function onEdge(c: { x: number; y: number }, origin: { x: number; y: number }, side: string): boolean {
|
||||
if (side === "N") return c.y === origin.y && c.x >= origin.x && c.x < origin.x + 5;
|
||||
if (side === "S") return c.y === origin.y + 4 && c.x >= origin.x && c.x < origin.x + 5;
|
||||
@@ -203,17 +199,16 @@ describe("setup diagram pairings (rulebook Set-Up Diagram)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("relocation discards the aisle warp: only opposite edges connect after", async () => {
|
||||
const { createGame, applyCommand, activePlayer } = await import("../src/game");
|
||||
it("relocation discards the aisle warp: only opposite edges connect after", () => {
|
||||
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 7, sets: ["basic"], deckRev: 5 });
|
||||
const bent = (warps: typeof state.board.warps) =>
|
||||
warps.filter((w) => w.from.cell.x !== w.to.cell.x && w.from.cell.y !== w.to.cell.y);
|
||||
expect(bent(state.board.warps).length).toBeGreaterThan(0); // the arc exists
|
||||
const me = activePlayer(state);
|
||||
me.hand[0] = { instanceId: "rel#1", cardId: "relocate-sector" };
|
||||
const rel = giveCard(state, me.id, "relocate-sector");
|
||||
// Slide the top sector from (5,0) across to (0,0): an L becomes a block.
|
||||
const r = applyCommand(state, me.id, {
|
||||
type: "cast", instanceId: "rel#1",
|
||||
type: "cast", instanceId: rel.instanceId,
|
||||
target: { kind: "cell", cell: { x: 0, y: 0 } }, params: { cell: { x: 7, y: 2 } },
|
||||
});
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
@@ -223,8 +218,10 @@ describe("setup diagram pairings (rulebook Set-Up Diagram)", () => {
|
||||
|
||||
describe("the aisle warp treats its corner as adjacent — sight included", () => {
|
||||
it("sight passes through the AUTO WARP, diagonals and all", () => {
|
||||
const { board } = setupBoardForPins(3, createRng(7));
|
||||
const aisle = board.warps.find((w) => w.aisle)!;
|
||||
const { board } = setupBoard(3, createRng(7));
|
||||
const aisle = board.warps.find(
|
||||
(w) => w.from.cell.x !== w.to.cell.x && w.from.cell.y !== w.to.cell.y,
|
||||
)!;
|
||||
expect(aisle).toBeDefined();
|
||||
// From the aisle mouth, squares beyond the corner are visible — including
|
||||
// at least one OFF the straight axis (the adjacency promise).
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyCommand, activePlayer, boardView, createGame } from "../src/game";
|
||||
import { cellKey, edgeKey, hasLineOfSight, neighbor, type Side, SIDES, stepTarget } from "../src/board";
|
||||
import { applyCommand, activePlayer, boardView, createGame, type GameState } from "../src/game";
|
||||
import { cellKey, edgeKey, hasLineOfSight, neighbor, type Cell, type Side, SIDES, stepTarget } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { viewFor } from "../src/view";
|
||||
import { newGame, must, giveCard, toRound2, faceOff } from "./helpers";
|
||||
|
||||
/** Test surgery: put a specific card into a player's hand (swapping one out). */
|
||||
@@ -277,8 +278,7 @@ describe("speedstone", () => {
|
||||
});
|
||||
|
||||
describe("the post-game reveal", () => {
|
||||
it("shows an eliminated player's hand as they fell, not their emptied one", async () => {
|
||||
const { viewFor } = await import("../src/view");
|
||||
it("shows an eliminated player's hand as they fell, not their emptied one", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const attacker = activePlayer(state);
|
||||
@@ -350,7 +350,7 @@ describe("attacking walls and doors", () => {
|
||||
expect(applyCommand(state, me.id, { type: "punchWall", cell, side }).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("a powered fireball brings a wall down at 20 accumulated damage", () => {
|
||||
it("repeated fireballs bring a wall down at 20 accumulated damage", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
let me = activePlayer(state);
|
||||
@@ -388,7 +388,7 @@ describe("attacking walls and doors", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rules revision 3", () => {
|
||||
describe("ward arming and chaos shields (rules rev 3)", () => {
|
||||
function rev3Game(seed = 42) {
|
||||
return createGame({ playerIds: ["alice", "bob", "cara"], seed, sets: ["basic", "expansion1"], deckRev: 3 });
|
||||
}
|
||||
@@ -409,7 +409,6 @@ describe("rules revision 3", () => {
|
||||
expect(s2.players.find((p) => p.id === owner.id)!.hand.some((c) => c.cardId === "ward")).toBe(true);
|
||||
|
||||
// Armed (on the owner's own turn): the trap bites for 3.
|
||||
state.players[state.turn.activeIndex] = state.players[state.turn.activeIndex]!;
|
||||
const ownerTurnState = (() => {
|
||||
let s = state;
|
||||
while (activePlayer(s).id !== owner.id) s = must(s, activePlayer(s).id, { type: "endTurn", draw: 0 });
|
||||
@@ -735,7 +734,7 @@ describe("answering counteractions (FAQ rulings)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rules revision 8", () => {
|
||||
describe("speed and warp-token creation (rules rev 8)", () => {
|
||||
it("a SPEED bonus turn burns a turn of durations on the hastened wizard", () => {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 8 });
|
||||
state = toRound2(state);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame } from "../src/game";
|
||||
import { cellKey, SIDES, stepTarget, type Side } from "../src/board";
|
||||
import { cellKey, SIDES, stepTarget, type Cell, type Side } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { newExpansionGame as newGame, must, giveCard, toRound2, emptyNeighborCell } from "./helpers";
|
||||
|
||||
@@ -402,8 +402,7 @@ describe("big man (rules rev 5)", () => {
|
||||
expect(r.ok).toBe(false);
|
||||
return;
|
||||
}
|
||||
// Seed offered no walled pocket beside the home; that is fine — the
|
||||
// pushable case above pins the mechanism.
|
||||
throw new Error("setup: seed offered no walled pocket beside the home");
|
||||
});
|
||||
|
||||
it("monsters cannot enter the giant's square", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { applyCommand, activePlayer, boardView, gameLos } from "../src/game";
|
||||
import { cellKey, SIDES, stepTarget, type Cell } from "../src/board";
|
||||
import type { CardInstance } from "../src/cards";
|
||||
import { eligibleCellsFor, viewFor } from "../src/view";
|
||||
import { newExpansionGame as newGame, must, giveCard, emptyNeighborCell } from "./helpers";
|
||||
|
||||
describe("expansion terrain", () => {
|
||||
@@ -163,8 +164,7 @@ describe("expansion terrain", () => {
|
||||
});
|
||||
|
||||
describe("eligibility dimming mirrors the engine", () => {
|
||||
it("boobytrap tokens go anywhere but solid stone, sight be damned", async () => {
|
||||
const { viewFor, eligibleCellsFor } = await import("../src/view");
|
||||
it("boobytrap tokens go anywhere but solid stone, sight be damned", () => {
|
||||
let { state } = newGame();
|
||||
const caster = activePlayer(state).id;
|
||||
const view = viewFor(state, caster);
|
||||
@@ -174,8 +174,7 @@ describe("eligibility dimming mirrors the engine", () => {
|
||||
expect(lit.size).toBe(Object.keys(view.board.cells).length);
|
||||
});
|
||||
|
||||
it("glue lights only sighted squares that hold something", async () => {
|
||||
const { viewFor, eligibleCellsFor } = await import("../src/view");
|
||||
it("glue lights only sighted squares that hold something", () => {
|
||||
let { state } = newGame();
|
||||
const caster = activePlayer(state);
|
||||
// Drop a dagger at the caster's feet — the one guaranteed-sighted object.
|
||||
@@ -184,8 +183,8 @@ describe("eligibility dimming mirrors the engine", () => {
|
||||
const lit = eligibleCellsFor(viewFor(state, caster.id), "glue")!;
|
||||
expect(lit.has(cellKey(here))).toBe(true);
|
||||
// Empty squares stay dim — glue needs something to glue down.
|
||||
const view = viewFor(state, caster.id);
|
||||
for (const k of lit) {
|
||||
const view = viewFor(state, caster.id);
|
||||
const held =
|
||||
(view.groundObjects[k] ?? []).length > 0 ||
|
||||
view.treasures.some((t) => t.position && cellKey(t.position) === k);
|
||||
|
||||
@@ -409,7 +409,8 @@ describe("relocation past the origin (rules rev 11)", () => {
|
||||
id: "c1", kind: "troll", controllerId: me.id, position: { ...spot },
|
||||
damage: 0, maxDamage: 5, movesPerTurn: 3, movementUsed: 0,
|
||||
attackUsed: false, justCreated: false,
|
||||
} as (typeof state.creatures)[number]);
|
||||
wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [],
|
||||
});
|
||||
state.gluedCells[cellKey(spot)] = true;
|
||||
state.dimWarps.push({ a: { ...spot }, b: { x: otherOrigin.x + 1, y: otherOrigin.y + 1 } });
|
||||
state.boobytraps.push({ casterId: me.id, cells: [{ ...spot }], realKey: cellKey(spot) });
|
||||
@@ -424,10 +425,8 @@ describe("relocation past the origin (rules rev 11)", () => {
|
||||
const finalMine = state.board.placements[idx]!.origin;
|
||||
const finalOther = state.board.placements[idx === 0 ? 1 : 0]!.origin;
|
||||
const moved = { x: spot.x + finalMine.x - myOrigin.x, y: spot.y + finalMine.y - myOrigin.y };
|
||||
const staticShifted = {
|
||||
x: otherOrigin.x + 1 + finalOther.x - otherOrigin.x,
|
||||
y: otherOrigin.y + 1 + finalOther.y - otherOrigin.y,
|
||||
};
|
||||
// The warp's far token sat one square inside the static sector.
|
||||
const staticShifted = { x: finalOther.x + 1, y: finalOther.y + 1 };
|
||||
expect(state.creatures[0]!.position).toEqual(moved);
|
||||
expect(state.gluedCells[cellKey(moved)]).toBe(true);
|
||||
expect(state.dimWarps[0]!.a).toEqual(moved);
|
||||
|
||||
Reference in New Issue
Block a user