Files
RingBRP/tools/check-bestiary.mjs
T
slaguru666andClaude Opus 5 f932a79121 The firing order, in the bestiary, generated (R-258)
DEFENCE_STEP is -30, so a creature's Dodge is not a percentage it has for a round — it is
one it has once or twice. The median dodge in the book is 45% and goes 45 -> 15 -> 1
inside a single round. No player-facing document said so, which hid a real decision:
what order the party shoots in.

The bestiary now has a section on it. Only a landing hit spends a defence, so a miss
strips nothing; each landing hit makes the next attack of that round easier; therefore
the cheapest reliable weapon fires first and the one you most need to land fires last —
against the supporter, the weapon the borough does not own.

Everything it states is generated from DEFENCE_STEP, resolveBands and the statblocks: the
step, the ladder, the median, the spread (13 of 47 creatures stripped by one landing hit,
25 by two, 9 by three) and the list of the hardest to strip. Negative-tested by setting
the step to -15 and -20; the page rewrites itself both times. No win rates are published:
the ordering measurements come from a table-side harness that ignores wound penalties,
bleeding and dying, so the page states the mechanism and quotes no percentages.

My first draft used the dodgiest creature in the book as the worked example — 80%, three
landing hits to strip — directly beneath a claim that the first attack of the round eats
the dodge. True of a modest dodge, false of a good one, and both were on the same page.

tools/bestiary.mjs --check has existed since the document did, with the comment "(for the
guards)", and no guard ever ran it: a check written and wired to nothing, the same shape
as every rule found computed and never read. That was tolerable while the page restated
statblocks and is not now that it derives from rules — change DEFENCE_STEP and the
document lies to a GM. check-bestiary.mjs is the tenth guard, negative-tested both ways
it can fail: a rule change that staler the page, and a hand-edit of the page.
update-readme flagged it as "run by the build, not advertised" before I listed it, and
the README's count word went from nine to ten by itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:51:04 +01:00

30 lines
1.5 KiB
JavaScript

/**
* The bestiary document must agree with the game it describes.
*
* `tools/bestiary.mjs --check` has existed since the document did, and nothing ever ran
* it — a check written, wired to nothing, which is the exact defect this project keeps
* finding in its own rules. It mattered little while the page only restated statblocks.
* It matters now: R-258 added a section whose numbers come from DEFENCE_STEP and
* resolveBands, so changing a rule silently falsifies the page a GM reads at the table.
*
* This is a thin guard on purpose. The comparison lives in bestiary.mjs, which is the
* thing that knows how to build the document; re-deriving it here would be the
* duplication check-rules exists to forbid.
*/
import { spawnSync } from "node:child_process";
const r = spawnSync(process.execPath, [new URL("bestiary.mjs", import.meta.url).pathname, "--check"],
{ encoding: "utf8" });
if (r.status !== 0) {
process.stdout.write(r.stdout ?? "");
process.stderr.write(r.stderr ?? "");
console.error("check-bestiary: FAILED — docs/BESTIARY.md no longer matches the game. Run `npm run bestiary`.");
process.exit(1);
}
// Speak in the same format as the other nine: update-readme reads the line that starts
// with this file's own name, and a guard that reports under a different name is a guard
// the README cannot quote.
const n = (r.stdout.match(/(\d+) creatures/) ?? [])[1] ?? "?";
console.log(`check-bestiary: OK — ${n} creatures, the document matches the game`);