Files
RingBRP/tools/check-armed.mjs
slaguru666andClaude Opus 5 25d8f04a65 check-armed: a creature must be able to use what it carries
check-kits has asserted this for the 36 postings since it was written — "every
posting can use what it carries" is in its success line. Nothing asserted it
for the creatures, and 35 of them hold a weapon they have no skill for. The
hollow man has a dagger and no knife. The hulk haunt has a boarding axe and
no axe. One PREGEN carries a pistol she cannot fire.

It is not cosmetic. simulate.mjs sorts a combatant's arms by expected damage
and an unskilled weapon sits at the 1% floor, so it loses to the creature's
own fists every time: the statblock advertises a dagger and the thing
punches. That is R-310's defect written down rather than generated.

Recorded, not fixed. Giving each of the 35 the missing skill means choosing a
rating, and a rating is a balance decision that moves check-lethality's
numbers for every creature touched — that belongs to whoever owns the game.
The 35 are held at a ratchet that may shrink and may never grow, so no new
creature can be written holding something it cannot use.

Specs are expanded through the register first. A spec written as a job has no
skills of its own, and judging one unexpanded reports a Field Officer as
unable to fire the pistol her own posting issues her.

--update refused to create the record it needs on its first run, because with
no file on disk all 35 existing cases counted as new. The first recording is
the baseline; only later ones are held to the ratchet.

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

103 lines
5.4 KiB
JavaScript

/**
* A creature must be able to use what its statblock says it carries.
*
* check-kits asserts this for the 36 postings — "every posting can use what it carries"
* is in its success line. Nothing asserted it for the creatures, and 35 of them hold a
* weapon they have no skill for: the hollow man has a dagger and no knife skill, the
* hulk haunt a boarding axe and no axe, and one PREGEN carries a pistol she cannot fire.
*
* It is not a cosmetic mismatch. tools/simulate.mjs sorts a combatant's arms by expected
* damage, and an unskilled weapon sits at the 1% floor, so it loses to the creature's own
* fists every time. The statblock advertises a dagger; the thing punches. That is the
* same defect R-310 found in the NPC generator, one layer further out — written down
* rather than generated.
*
* WHY A RATCHET AND NOT A FIX. Giving each of the 35 the missing skill means choosing a
* rating, and a rating is a balance decision that moves check-lethality's recorded
* numbers for every creature touched. That belongs to whoever owns the game, not to the
* guard that found it. So the existing 35 are recorded by name and held: the count may
* fall, and it may never rise. A new creature cannot be written holding something it
* cannot use.
*
* bun tools/check-armed.mjs fail on anything not on the record
* bun tools/check-armed.mjs --update re-record (only ever to LOWER the count)
*/
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { collectSpecs } from "./all-specs.mjs";
import { WEAPONS } from "./content.mjs";
import { expandFromRegister } from "./expand-spec.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const RECORD = path.join(ROOT, "tools", "armed-baseline.json");
const UPDATE = process.argv.includes("--update");
const WEAPON_BY_KEY = new Map(WEAPONS.map(w => [w.key, w]));
const skillOf = w => (w.spec ? `${w.fam}:${w.spec}` : w.fam);
/* Expanded exactly as the build expands it. A spec written as a job carries no skills of
its own — they come out of the register at build time — and judging one unexpanded
would report a Field Officer as unable to fire the pistol her posting issues her. */
const found = [];
for (const row of await collectSpecs()) {
let spec = row.spec;
if (!spec.skills && (spec.role || spec.trade)) {
spec = expandFromRegister(spec, { where: "check-armed" });
}
if (!spec.skills) continue;
const has = new Set(spec.skills.map(s => (s.spec ? `${s.fam}:${s.spec}` : s.fam)));
for (const key of [...(spec.weapons ?? []), ...(spec.extraWeapons ?? [])]) {
const w = WEAPON_BY_KEY.get(key);
if (!w) continue; // check-creatures owns "is that a real weapon"
const id = skillOf(w);
if (!has.has(id)) found.push({ key: spec.key, source: row.label, weapon: key, skill: id });
}
}
found.sort((a, b) => `${a.key}${a.weapon}`.localeCompare(`${b.key}${b.weapon}`));
const idOf = f => `${f.key}:${f.weapon}`;
if (UPDATE) {
/* The FIRST recording is the baseline and is allowed to contain anything. Every later
one may only shrink. Without that distinction --update refused to create the record
it needs, because with no file on disk all 35 existing cases count as new — which is
what happened on the first run of this tool. */
const first = !existsSync(RECORD);
const before = first ? { cases: [] } : JSON.parse(await readFile(RECORD, "utf8"));
const prev = new Set((before.cases ?? []).map(idOf));
const added = first ? [] : found.filter(f => !prev.has(idOf(f)));
if (added.length) {
console.error(`check-armed: refusing to --update — ${added.length} NEW case(s) would be `
+ `recorded, and this record may only ever shrink:`);
for (const f of added) console.error(` ${f.key} carries ${f.weapon} and has no ${f.skill}`);
console.error(`\n Give the creature the skill, or take the weapon off it.`);
process.exit(1);
}
await writeFile(RECORD, JSON.stringify({ note: "Creatures holding a weapon they have no skill for. May shrink, never grow.", cases: found }, null, 2) + "\n");
console.log(`check-armed: recorded ${found.length} case(s) (was ${before.cases?.length ?? 0})`);
process.exit(0);
}
if (!existsSync(RECORD)) {
console.error(`check-armed: no record at tools/armed-baseline.json — run with --update once`);
process.exit(1);
}
const record = JSON.parse(await readFile(RECORD, "utf8"));
const known = new Set((record.cases ?? []).map(idOf));
const novel = found.filter(f => !known.has(idOf(f)));
const fixed = (record.cases ?? []).filter(c => !found.some(f => idOf(f) === idOf(c)));
if (novel.length) {
console.error(`check-armed: FAILED — ${novel.length} creature(s) carry a weapon they cannot use`);
for (const f of novel) {
console.error(` ${f.key} (${f.source}) carries ${f.weapon}, which is fired by ${f.skill}, and has no such skill`);
}
console.error(`\n simulate.mjs sorts arms by expected damage and an unskilled weapon sits at the`);
console.error(` 1% floor, so this creature will punch instead and the statblock will lie about it.`);
console.error(` Give it the skill or take the weapon away. Do not re-record.`);
process.exit(1);
}
console.log(`check-armed: OK — ${found.length} creature(s) hold a weapon they have no skill for, `
+ `every one on the record${fixed.length ? `, and ${fixed.length} since fixed` : ""}; no new ones`);