From 1a80d3637cca2d320641f34bbecd21a92deef31c Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sun, 13 Sep 2026 15:01:04 +0100 Subject: [PATCH] R-282: guard the bestiary against the powers, including the next one check-bestiary proves the page matches its generator, and the generator had never heard of powers.mjs -- so the redcap sat in "the ones that take the most stripping" under eighteen green guards while its own entry said it never spends a defence. Two files agreeing with each other while both disagree with the engine is a quorum, not a check. check-powers now asserts per effect kind what the page must say: defenceStacking requires the creature off the stripping list, named as exempt, and the "across N creatures that spend defences" count reconciled against powers.mjs; attackFactor requires the rating the simulator actually uses printed as a number, which the courier's entry now carries. The clause that matters is the failure on an unknown effect kind -- a wired effect with no DOCUMENT_RULE fails the build, so the next one cannot arrive without somebody deciding what the document owes it. Without that this would guard the mistake already made and nothing else. Proved three ways in a worktree, exit codes read directly. Co-Authored-By: Claude Opus 5 --- docs/BESTIARY.md | 2 + docs/REVIEW_LOG.md | 37 ++++++++++++++++++ tools/bestiary.mjs | 10 +++++ tools/check-powers.mjs | 86 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 130 insertions(+), 5 deletions(-) diff --git a/docs/BESTIARY.md b/docs/BESTIARY.md index ae13804..7a8035a 100644 --- a/docs/BESTIARY.md +++ b/docs/BESTIARY.md @@ -654,6 +654,8 @@ The far side, and what it sends. This is where the non-human anatomies are: a Ve **THE CONSIGNMENT.** Everything it carries is in the single grasp limb and it will not use that limb to fight while it is carrying — so it attacks with the brace limb at HALF the rating above until the consignment is taken from it. Disable the grasp limb and it drops the consignment and fights properly, at full rating and furious. +*In play that is **40%**, not 80% — the simulator fights it this way and every figure below is measured with the power on.* + *Measured alone against the frozen party: 0.80 of 4 down, 1% wiped, 9 rounds.* ### Precedent diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index cbe8553..262e51c 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -6573,3 +6573,40 @@ have been paid off by the widening. The middle case is the useful one to understand: **the ratchet only bites where the existing claim does not**, in the band between "still helps everywhere" and "helps as reliably as it did". That band is exactly where a slow degradation lives, and it was unguarded until now. + +## R-282 — the bestiary is now checked against the powers, including the next one + +R-275 wired NOT TIRED into the simulator and the bestiary went on listing the redcap among +"the ones that take the most stripping" — the exact opposite of what its own entry says +three pages down — with **eighteen guards green over it**. `check-bestiary` proves the page +matches its generator; the generator had never heard of `powers.mjs`. Two files agreeing +with each other while both disagree with the engine is not a check, it is a quorum. + +**check-powers now asserts what the page must say, per effect kind.** + +- `defenceStacking` — the creature must not appear in the stripping list, must be named as + off the ladder, and the section's "across N creatures that spend defences" count must + equal the dodgers minus the exempt. Arithmetic tying prose to `powers.mjs`. +- `attackFactor` — the page must print the rating the simulator actually uses, as a number. + "HALF the rating above" is a rule a GM has to do arithmetic on and a guard cannot read; + **40%** is both. The courier's entry now carries it. + +**The part that matters is the failure on an UNKNOWN kind.** A wired effect with no rule in +`DOCUMENT_RULES` fails the build by itself: + +> `effect "reachBonus" has no rule about what BESTIARY must say. Add one to DOCUMENT_RULES: +> an effect the harness applies and the page never mentions is how the redcap came to be +> listed under advice that does not work on it.` + +Without that clause this guard would cover the mistake already made and nothing else — +which is the failure mode of most guards written the day after a defect. The next effect +kind cannot reach the simulator without somebody deciding what the document owes it. + +**Proved three ways** in a throwaway worktree: the redcap put back on the ladder fails with +both the list and the missing exemption named; the courier's real rating removed fails; a +new `reachBonus` effect fails for having no page rule. Exit codes read directly. + +**What this does not cover, stated so it is a decision rather than a silence.** The +assertions are textual — they check that the page says a thing, not that what it says is +true. A `damageFactor` rule is registered as a no-op because nothing wires one today, and +it will need writing when something does; the guard will demand it, but it cannot write it. diff --git a/tools/bestiary.mjs b/tools/bestiary.mjs index f51a057..8ea5885 100644 --- a/tools/bestiary.mjs +++ b/tools/bestiary.mjs @@ -258,6 +258,16 @@ for (const tag of TAG_ORDER) { md += `**Attacks with** ${r.arm ? `${r.arm.w.name} at ${r.arm.rating}%, ${r.arm.w.dmg}` : "nothing"}` + `${r.c.dodge ? ` · **dodges** ${r.c.dodge}%` : ""}\n\n`; if (r.power) md += `**${r.power.name}.** ${esc(r.power.rule)}\n\n`; + /* What the simulator does about that power, stated as a number rather than left in the + prose. "HALF the rating above" is a rule a GM must do arithmetic on and a guard + cannot check; 40% is both. check-powers requires this line for every factored + attack, so the page cannot go back to describing the unmodified creature. */ + const fx = combatPowerFor(r.spec.key); + if (fx?.attackFactor && r.arm) { + md += `*In play that is **${Math.floor(r.arm.rating * fx.attackFactor)}%**, not ` + + `${r.arm.rating}% — the simulator fights it this way and every figure below is ` + + `measured with the power on.*\n\n`; + } if (r.measured) { md += `*Measured alone against the frozen party: ${pad2(r.measured.down)} of 4 down, ` + `${r.measured.wipe}% wiped, ${r.measured.rounds} rounds.*\n\n`; diff --git a/tools/check-powers.mjs b/tools/check-powers.mjs index ffe6017..4d387c0 100644 --- a/tools/check-powers.mjs +++ b/tools/check-powers.mjs @@ -19,12 +19,12 @@ * 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 { existsSync, 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"; +import { POWERS, EFFECT_KINDS, combatPowerFor } from "./powers.mjs"; +import { runFight, makeRng, seedFor, buildCombatant } from "./simulate.mjs"; const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), ".."); const RUNS = 400; @@ -98,6 +98,82 @@ for (const key of Object.keys(POWERS)) { } } +/* 3. The document must not describe a fight the power overrides. + R-275 wired NOT TIRED and the bestiary went on listing the redcap among "the ones + that take the most stripping" — the opposite of what its own entry said three pages + down — and eighteen guards were green over it, because check-bestiary only proves the + page matches its generator and the generator had never heard of powers.mjs. Two files + agreeing with each other while both disagree with the engine is not a check. + + So each wired EFFECT KIND carries an assertion about the page here. A new effect kind + arrives with no assertion and check-powers says so, which is the part that makes this + cover the next one rather than only the last one. */ +const PAGE = path.join(ROOT, "docs", "BESTIARY.md"); +const page = existsSync(PAGE) ? readFileSync(PAGE, "utf8") : null; +if (!page) { + problems.push(` docs/BESTIARY.md is missing, and the powers are checked against it`); +} else { + const dodgers = NPCS.filter(n => (n.skills ?? []).some(k => k.fam === "dodge" && k.val > 0)); + const strippingList = (page.split("The ones that take the most stripping:")[1] ?? "").split("\n\n")[1] ?? ""; + + const DOCUMENT_RULES = { + /* A creature that never spends a defence is not on the stripping ladder, must not be + counted among those that are, and must be named — because the advice the section + gives is exactly wrong for it. */ + defenceStacking: (spec) => { + const out = []; + if (strippingList.includes(spec.name)) { + out.push(` ${spec.key}: BESTIARY lists it among "the ones that take the most stripping", ` + + `and ${POWERS[spec.key].name} means it never spends a defence. That is the opposite ` + + `of the advice, printed for a GM to act on.`); + } + if (!page.includes(`${spec.name} is not on this ladder at all`)) { + out.push(` ${spec.key}: BESTIARY never says it is off the stripping ladder. A reader ` + + `who takes the section's advice against it is being told to do the one thing that ` + + `does not work.`); + } + const exemptDodgers = dodgers.filter(n => combatPowerFor(n.key)?.defenceStacking === "ignores").length; + const m = page.match(/Across \*{0,2}(\d+)\*{0,2} creatures that spend defences/); + if (!m) out.push(` BESTIARY no longer says how many creatures spend defences at all`); + else if (Number(m[1]) !== dodgers.length - exemptDodgers) { + out.push(` BESTIARY counts ${m[1]} creatures that spend defences; the game has ` + + `${dodgers.length} that dodge and ${exemptDodgers} exempt, so it should be ` + + `${dodgers.length - exemptDodgers}`); + } + return out; + }, + /* A rating the simulator halves must appear halved, as a number. */ + attackFactor: (spec, effect) => { + const c = buildCombatant(spec, { side: "enemy" }); + const arm = c.arms?.[0]; + if (!arm) return []; + const effective = Math.floor(arm.rating * effect.attackFactor); + return page.includes(`**${effective}%**, not ${arm.rating}%`) ? [] + : [` ${spec.key}: BESTIARY does not state the rating the simulator actually uses ` + + `(${effective}%, not ${arm.rating}%). Without it the page prints the statblock and ` + + `every measured figure beside it is from a different creature.`]; + }, + damageFactor: () => [] + }; + + for (const key of Object.keys(POWERS)) { + const p = POWERS[key]; + if (p.kind !== "wired") continue; + const spec = NPCS.find(n => n.key === key); + if (!spec) continue; + for (const [kind, value] of Object.entries(p.effect)) { + const rule = DOCUMENT_RULES[kind]; + if (!rule) { + problems.push(` effect "${kind}" has no rule about what BESTIARY must say. Add one to ` + + `DOCUMENT_RULES: an effect the harness applies and the page never mentions is how ` + + `the redcap came to be listed under advice that does not work on it.`); + continue; + } + problems.push(...rule(spec, p.effect)); + } + } +} + if (problems.length) { console.error(`check-powers: FAILED — ${problems.length} problem(s)`); problems.forEach(p => console.error(p)); @@ -107,5 +183,5 @@ if (problems.length) { 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`); + + `measurably changes its fight and is accounted for in the bestiary, ${by.notSimulable} recorded ` + + `as beyond the harness with a reason, ${by.outOfCombat} not fight rules`);