diff --git a/tools/playthrough.mjs b/tools/playthrough.mjs index 9d23892..8f57606 100644 --- a/tools/playthrough.mjs +++ b/tools/playthrough.mjs @@ -13,6 +13,9 @@ * node tools/playthrough.mjs redcap a named creature * node tools/playthrough.mjs redcap 20260913 a chosen seed * node tools/playthrough.mjs redcap 20260913 focus one mode only + * node tools/playthrough.mjs --creature quiet_neighbours_npc --count 6 \\ + * --party ashcroft,bhattacharya,renshaw,braithwaite,pollard,okonkwo --seed 11 + * a scenario's own encounter, against its own cast * * The pack size comes from focus-baseline.json, so the fight you read is the fight * check-focus measures. Creatures it records as pinned — no pack size makes them an even @@ -28,20 +31,33 @@ const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace const base = JSON.parse(readFileSync(path.join(ROOT, "tools", "lethality-baseline.json"), "utf8")); const focus = JSON.parse(readFileSync(path.join(ROOT, "tools", "focus-baseline.json"), "utf8")); -const KEY = process.argv[2] ?? "redcap"; -const SEED = Number(process.argv[3] ?? 20260913); +/* Flags mirror tools/simulate.mjs, so a fight measured there can be read here with the + same words. The positional form still works: . */ +const argv = process.argv.slice(2); +const opt = (n, d) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; }; +const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1].startsWith("--"))); + +const KEY = opt("--creature", positional[0] ?? "redcap"); +const SEED = Number(opt("--seed", positional[1] ?? 20260913)); const spec = NPCS.find(n => n.key === KEY); if (!spec) { console.error(`playthrough: no creature keyed "${KEY}". Try one of: ` + NPCS.slice(0, 6).map(n => n.key).join(", ") + ", ..."); process.exit(1); } -const party = base.party.map(k => ROSTER.find(r => r.key === k || r.key === "pc_" + k)); +/* The frozen four by default — the party every recorded number is measured against — or a + named cast, for reading a scenario's own encounter rather than the guard's. */ +const partyKeys = (opt("--party", "") || "").split(",").map(x => x.trim()).filter(Boolean); +const party = (partyKeys.length ? partyKeys : base.party).map(k => { + const found = ROSTER.find(r => r.key === k || r.key === "pc_" + k); + if (!found) { console.error(`playthrough: "${k}" is not on the duty roster`); process.exit(1); } + return found; +}); const rec = focus.creatures[KEY]; /* A pinned row records the pack size that was TRIED, not one worth playing: the header above promises solo for those, and a tool that contradicts its own documentation is the defect this file exists to help find. Solo also matches what check-lethality records. */ -const N = rec?.gain !== undefined ? rec.n : 1; +const N = Number(opt("--count", 0)) || (rec?.gain !== undefined ? rec.n : 1); /* The same generator shape the guards use, so a seed here names the same fight there. */ const mk = seed => { let s = seed >>> 0; return () => ((s = (1664525 * s + 1013904223) >>> 0) / 4294967296); }; @@ -92,7 +108,8 @@ function play(mode) { return { r, lines }; } -const MODES = process.argv[4] ? [process.argv[4]] : (N > 1 ? ["random", "focus"] : ["random"]); +const MODE = opt("--mode", positional[2]); +const MODES = MODE ? [MODE] : (N > 1 ? ["random", "focus"] : ["random"]); for (const mode of MODES) { const { r, lines } = play(mode); const how = N === 1 ? "" : mode === "focus" ? " — party CONCENTRATING FIRE" @@ -100,7 +117,7 @@ for (const mode of MODES) { console.log(`\n${"=".repeat(78)}\n${N} × ${spec.name.toUpperCase()}${how}\n${"=".repeat(78)}`); console.log(lines.join("\n")); console.log(`\n → ${r.won ? "PARTY WINS" : r.wiped ? "PARTY WIPED" : "neither side finished it"}` - + ` in ${r.rounds} rounds · ${r.partyDown} of 4 down · ${r.dead} dead`); + + ` in ${r.rounds} rounds · ${r.partyDown} of ${party.length} down · ${r.dead} dead`); } if (rec?.gain !== undefined)