The archmage cuts off a winning delivery and aims at the leader when no kill is on the table
Reuben's three wins today were walked home unseen: the clockwork fought whoever stood in front of it. Now a delivery that would end the game pulls every automaton toward that wizard's home (where the banked gold is lifted off the doorstep) or the road to it, and among visible targets the thief still dies first, a kill is still taken, and only then does the aim shift to whoever has more treasures banked. Measured with deploy/brain-trial.mjs, which seats the working tree's brain against an older checkout's at one table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
1b18dffd28
commit
be96dd6047
@@ -0,0 +1,76 @@
|
||||
// Two brains at one table: the working tree's automaton against an older
|
||||
// checkout's, over seeded games, to measure a brain change before it
|
||||
// ships. The older engine only chooses; the working tree's engine rules
|
||||
// the table, so both play the same game under the same law.
|
||||
//
|
||||
// npx tsx deploy/brain-trial.mjs <old-checkout> [seeds=30] [--table new,old,old,old] [--style hunter] [--basic]
|
||||
//
|
||||
// Make the old checkout with `git worktree add <dir> <commit>`. Each seed
|
||||
// is played once per rotation of the table so seat order cancels out.
|
||||
|
||||
import { applyCommand, automatonFallback, createGame, viewFor } from "../packages/engine/src/index.ts";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const oldDir = args[0];
|
||||
if (!oldDir || oldDir.startsWith("--")) { console.error("first argument: the old checkout's directory"); process.exit(2); }
|
||||
const seeds = Number(args.find((a, i) => i > 0 && /^\d+$/.test(a)) ?? 30);
|
||||
const table = (args.includes("--table") ? args[args.indexOf("--table") + 1] : "new,old").split(",");
|
||||
const style = args.includes("--style") ? args[args.indexOf("--style") + 1] : "hunter";
|
||||
const sets = args.includes("--basic") ? ["basic"] : ["basic", "expansion1"];
|
||||
const old = await import(pathToFileURL(resolve(oldDir, "packages/engine/src/index.ts")).href);
|
||||
const brains = {
|
||||
new: (await import("../packages/engine/src/index.ts")).automatonCommand,
|
||||
old: old.automatonCommand,
|
||||
};
|
||||
|
||||
function actingSeat(state) {
|
||||
return state.wardPending?.ownerId ?? state.pushPending?.pusheeId ?? state.slowDeathPending?.playerId
|
||||
?? state.pendingDiscard ?? state.chaosPending?.queue[0] ?? state.stack?.waitingOn
|
||||
?? state.outOfTurnWindow?.playerId ?? state.players[state.turn.activeIndex].id;
|
||||
}
|
||||
|
||||
function playOut(seed, order) {
|
||||
const ids = order.map((b, i) => `${b}${i + 1}`);
|
||||
const brainOf = Object.fromEntries(ids.map((id, i) => [id, brains[order[i]]]));
|
||||
let { state } = createGame({ playerIds: ids, seed, sets });
|
||||
let commands = 0, stuck = 0;
|
||||
while (state.phase === "playing" && commands < 6000) {
|
||||
const seat = actingSeat(state);
|
||||
const view = viewFor(state, seat);
|
||||
const chosen = brainOf[seat](view, style, "archmage");
|
||||
let r = applyCommand(state, seat, chosen ?? automatonFallback(view, "archmage"));
|
||||
if (!r.ok) {
|
||||
if (chosen) r = applyCommand(state, seat, automatonFallback(view, "archmage"));
|
||||
if (!r.ok) {
|
||||
if (++stuck > 3) throw new Error(`stuck at ${seat} after ${commands}: ${r.error}`);
|
||||
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;
|
||||
}
|
||||
|
||||
const wins = { new: 0, old: 0, none: 0 };
|
||||
const reasons = {};
|
||||
const t0 = Date.now();
|
||||
let games = 0;
|
||||
for (let seed = 1; seed <= seeds; seed++) {
|
||||
for (let rot = 0; rot < table.length; rot++) {
|
||||
const order = [...table.slice(rot), ...table.slice(0, rot)];
|
||||
const state = playOut(seed, order);
|
||||
games++;
|
||||
const w = state.winner;
|
||||
if (!w) wins.none++;
|
||||
else { wins[w.replace(/\d+$/, "")]++; reasons[state.winReason] = (reasons[state.winReason] ?? 0) + 1; }
|
||||
}
|
||||
}
|
||||
const share = (b) => table.filter((x) => x === b).length / table.length;
|
||||
console.log(`${games} games (${table.join(" vs ")}, ${style}, ${sets.length === 1 ? "basic" : "expansion"}) in ${((Date.now() - t0) / 1000).toFixed(0)}s`);
|
||||
console.log(`new ${wins.new} (${(100 * wins.new / games).toFixed(0)}%, seats ${(100 * share("new")).toFixed(0)}%) old ${wins.old} (${(100 * wins.old / games).toFixed(0)}%, seats ${(100 * share("old")).toFixed(0)}%) unfinished ${wins.none}`);
|
||||
console.log(`won by ${JSON.stringify(reasons)}`);
|
||||
Reference in New Issue
Block a user