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:
co-authored by
Claude Opus 5
parent
8f3e1ff48e
commit
6e3ea8fa19
+23
-6
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user