playthrough: read a scenario's own encounter, against its own cast

The tool could only narrate the guard's fight — the frozen four, at the pack size
focus-baseline records. CLEAN GROUND's EXPOSURE table measures something else entirely: a
declared cast of six at counts of 1, 3, 6, 10 and 15, which is the fight a GM running that
case will actually referee and the one whose numbers we have just re-pinned.

--creature, --count, --party, --seed and --mode now mirror tools/simulate.mjs, so a row
measured there can be read here in words. Positional form still works.

  node tools/playthrough.mjs --creature quiet_neighbours_npc --count 6 \
    --party ashcroft,bhattacharya,renshaw,braithwaite,pollard,okonkwo --seed 11

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 00:55:45 +01:00
co-authored by Claude Opus 5
parent 8f3e1ff48e
commit 6e3ea8fa19
+23 -6
View File
@@ -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: <key> <seed> <mode>. */
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)