Files
wizwar6e/packages/engine/test/automaton.test.ts
T
Eric WagonerandClaude Fable 5 5a407a1e4c The clockwork learns numbers, summons, creatures — and temperament
The automaton now plays number cards where they matter: attached to
value-scaled attacks (waterbolt, powerthrust) choosing the biggest
for damage, and played for movement when the goal is just out of
stride. It raises its monsters when no wizard is in spell range and
commands them every turn — creatures march at enemies by the same
hazard-shy BFS and maul whoever shares their square. Targets are
picked by lowest life.

And it has moods. The HUNTER plays the classic game: gold first,
violence when convenient. The BERSERKER hunts wizards over treasure
and finishes games by last-wizard-standing. The WORRIER paths around
enemies, counters at a lower threshold, shields against chaos, and
never brawls. Hosts pick the temperament when seating one (or the
workshop assigns at random); it persists with the seat, shows in the
roster, and the tournament harness proves every pairing finishes.
Across ten seeds of berserker versus hunter: six treasure wins to
four kills — the moods play genuinely different games.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:29:36 -04:00

101 lines
3.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
applyCommand,
createGame,
type GameState,
type PlayerId,
} from "../src/game";
import { viewFor } from "../src/view";
import { automatonCommand, automatonFallback, type AutomatonStyle } from "../src/automaton";
/** Whose input does the maze want right now? */
function actingSeat(state: GameState): PlayerId {
return (
state.stack?.waitingOn ??
state.pendingDiscard ??
state.chaosPending?.queue[0] ??
state.outOfTurnWindow?.playerId ??
state.players[state.turn.activeIndex]!.id
);
}
/** Drive a full bot-vs-bot game; returns the final state and command count. */
function playOut(seed: number, players: number, expansion = true, styles: AutomatonStyle[] = []) {
const ids = Array.from({ length: players }, (_, i) => `bot${i + 1}`);
const styleOf = new Map(ids.map((id, i) => [id, styles[i] ?? "hunter"]));
let { state } = createGame({
playerIds: ids,
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
deckRev: 8,
});
let commands = 0;
let stuck = 0;
const CAP = 4000;
while (state.phase === "playing" && commands < CAP) {
const seat = actingSeat(state);
const view = viewFor(state, seat);
const cmd = automatonCommand(view, styleOf.get(seat)) ?? automatonFallback(view);
let r = applyCommand(state, seat, cmd);
if (!r.ok) {
const fb = automatonFallback(view);
r = applyCommand(state, seat, fb);
if (!r.ok) {
stuck++;
if (stuck > 3) {
throw new Error(
`automaton stuck at seat ${seat} after ${commands} commands: ` +
`${JSON.stringify(cmd)} -> ${JSON.stringify(fb)} both refused (${r.error})`,
);
}
// Last-ditch: burn the turn structure forward.
r = applyCommand(state, seat, { type: "endTurn", draw: 0 });
if (!r.ok) r = applyCommand(state, seat, { type: "pass" });
if (!r.ok) throw new Error(`unrecoverable at ${seat}: ${r.error}`);
}
} else {
stuck = 0;
}
state = r.state;
commands++;
}
return { state, commands };
}
describe("automaton vs automaton", () => {
it("two clockwork wizards fight a game to its end", () => {
const { state, commands } = 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", () => {
let finished = 0;
for (const seed of [1, 2, 3, 5, 8, 13, 21, 34]) {
const { state } = playOut(seed, 2);
if (state.phase === "finished") finished++;
}
// Cautious clockwork can stall a maze; most games must still conclude.
expect(finished).toBeGreaterThanOrEqual(6);
});
it("a full table of four automatons concludes", () => {
const { state } = playOut(7, 4);
expect(state.phase).toBe("finished");
});
it("every temperament finishes its wars", () => {
for (const styles of [
["berserker", "hunter"], ["worrier", "hunter"], ["berserker", "worrier"],
] as AutomatonStyle[][]) {
let finished = 0;
for (const seed of [3, 17, 29]) {
const { state } = playOut(seed, 2, true, styles);
if (state.phase === "finished") finished++;
}
expect(finished).toBeGreaterThanOrEqual(2);
}
});
});