Files
RingBRP/tools/check-powers.mjs
T
slaguru666andClaude Opus 5 32ca982369 R-275: the harness had never read a creature's power
41 statblocks carry a POWER in their tactics and simulate.mjs read none of them,
so a redcap that ignores the cumulative defence penalty has been measured as a
creature that tires -- in check-lethality, in check-focus, and in every figure
published about it. The defect was not that the powers were unimplemented, it was
that nothing said they were not.

powers.mjs classifies all 41: 2 wired, 14 notSimulable with a stated reason, 25
not fight rules. check-powers refuses an unclassified POWER and refuses a
notSimulable without a reason -- and it does not test that the harness imports a
power, it fights the creature with and without and requires the two to disagree.

Moved: the courier 9.4% to 1.0% wiped (it attacks at half while carrying), the
redcap 0.7% to 0.9% (small, because these fights rarely spend a second defence).

ARGENT AND GULES was wired and then un-wired: it tripled the supporter's wipe rate
to 75.2% because the harness has no ground and applied the borough-ground condition
unconditionally. Same reason THE PULL is not wired. I had wired one and refused the
other on identical facts.

Lethality and focus re-recorded, bestiary regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:05:11 +01:00

112 lines
4.9 KiB
JavaScript

/**
* Every creature's POWER must be classified, and every wired one must actually reach the fight.
*
* 41 statblocks carry a POWER in their tactics prose and, until R-275, not one of them
* reached `simulate.mjs`. The harness read characteristics and weapons and nothing else,
* so a redcap — whose entry says it ignores the cumulative defence penalty — was measured
* as a creature that tires, in check-lethality, in check-focus, and in every figure this
* project has published about it. The defect was not that the powers were unimplemented.
* It was that nothing anywhere said they were not.
*
* So this guard asserts two things, and the second is the one that matters:
*
* 1. Every POWER in content.mjs is classified in powers.mjs — wired, notSimulable with
* a reason, or outOfCombat. A creature added tomorrow cannot arrive unclassified.
*
* 2. Every WIRED power changes a fight. Not "is referenced in the harness" — measured:
* the creature is fought with its power and again with it stripped, and the two must
* disagree. A power the simulator reads and does not act on is the defect this whole
* entry is about, one layer in, and a guard that checked for the import would have
* passed on it.
*/
import { readFileSync } from "node:fs";
import path from "node:path";
import { NPCS } from "./content.mjs";
import { ROSTER } from "./roster.mjs";
import { POWERS, EFFECT_KINDS } from "./powers.mjs";
import { runFight, makeRng, seedFor } from "./simulate.mjs";
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), "..");
const RUNS = 400;
const SEED = 11;
const PARTY_KEYS = ["pc_holloway", "pc_okonkwo", "pc_nkemdirim", "pc_ferriby"];
const party = PARTY_KEYS.map(k => {
const found = ROSTER.find(r => r.key === k);
if (!found) { console.error(`check-powers: the frozen party names "${k}", not on the roster`); process.exit(1); }
return found;
});
const problems = [];
/* 1. Classification. The prose is the authority for what a power SAYS; powers.mjs is the
authority for what the harness does about it, and every one must be in both. */
const withPower = NPCS.filter(n => /POWER:/.test(n.tactics ?? ""));
for (const spec of withPower) {
const p = POWERS[spec.key];
if (!p) {
problems.push(` ${spec.key} carries a POWER in its tactics and is not classified in powers.mjs — `
+ `add it as wired, notSimulable (with a reason) or outOfCombat`);
continue;
}
if (!["wired", "notSimulable", "outOfCombat"].includes(p.kind)) {
problems.push(` ${spec.key}: unknown classification "${p.kind}"`);
continue;
}
if (p.kind === "wired") {
const keys = Object.keys(p.effect ?? {});
if (!keys.length) problems.push(` ${spec.key}: wired but carries no effect`);
for (const k of keys) {
if (!EFFECT_KINDS.includes(k)) {
problems.push(` ${spec.key}: effect "${k}" is not one the harness knows (${EFFECT_KINDS.join(", ")})`);
}
}
}
if (p.kind === "notSimulable" && !p.why) {
problems.push(` ${spec.key}: notSimulable without a reason. A gap nobody wrote down is the defect this guard exists for.`);
}
}
for (const key of Object.keys(POWERS)) {
if (!withPower.some(s => s.key === key)) {
problems.push(` ${key} is classified in powers.mjs but no longer carries a POWER in content.mjs`);
}
}
/* 2. Every wired power must MOVE something. Fought with its power and without it. */
const winRate = (spec, strip) => {
let won = 0;
const seed = seedFor(SEED, spec.key);
for (let i = 0; i < RUNS; i++) {
const foe = strip ? { ...spec, key: `${spec.key}__nopower` } : spec;
if (runFight(makeRng(seed + i * 2654435761), party, [foe, foe]).won) won++;
}
return (100 * won) / RUNS;
};
const moved = [];
for (const key of Object.keys(POWERS)) {
if (POWERS[key].kind !== "wired") continue;
const spec = NPCS.find(n => n.key === key);
if (!spec) continue;
const withIt = winRate(spec, false);
const without = winRate(spec, true);
if (withIt === without) {
problems.push(` ${key}: ${POWERS[key].name} is wired and changes nothing — the party wins `
+ `${withIt}% of fights either way over ${RUNS} runs. Either the harness is not applying it `
+ `or the effect does not bite on this creature. A power that is read and not acted on is `
+ `exactly what R-275 was about.`);
} else {
moved.push(`${key} ${withIt.toFixed(1)}% -> ${without.toFixed(1)}% without it`);
}
}
if (problems.length) {
console.error(`check-powers: FAILED — ${problems.length} problem(s)`);
problems.forEach(p => console.error(p));
process.exit(1);
}
const by = { wired: 0, notSimulable: 0, outOfCombat: 0 };
for (const p of Object.values(POWERS)) by[p.kind]++;
console.log(`check-powers: OK — ${withPower.length} powers classified, ${by.wired} wired and each one `
+ `measurably changes its fight, ${by.notSimulable} recorded as beyond the harness with a reason, `
+ `${by.outOfCombat} not fight rules`);