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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
09e33907fa
commit
25d8f04a65
@@ -48,7 +48,7 @@ packs or regenerate art on that machine:
|
||||
```bash
|
||||
npm install # pulls classic-level, used to write the LevelDB packs
|
||||
npm run build # rebuilds packs/ from tools/content.mjs
|
||||
npm run check # the twenty-four guards; the build refuses to run if they fail
|
||||
npm run check # the twenty-five guards; the build refuses to run if they fail
|
||||
```
|
||||
|
||||
To update a deployed server: `git pull` and restart Foundry. If the pull touches
|
||||
@@ -157,7 +157,7 @@ icons/ fonts/ art/ generated art
|
||||
packs/ built LevelDB compendia (committed — see Deploying)
|
||||
tools/
|
||||
content.mjs the catalogue: skills, weapons, armour, gear, vehicles, NPCs
|
||||
build-packs.mjs builds packs/ — runs the twenty-four guards first and refuses on failure
|
||||
build-packs.mjs builds packs/ — runs the twenty-five guards first and refuses on failure
|
||||
make-icons.mjs draws all 231 icons
|
||||
rules-text.mjs generates the rules journal FROM rules.mjs
|
||||
mission.mjs the case generator
|
||||
@@ -178,14 +178,15 @@ stops being identity-equal to what it aliases, if a constant is re-declared as a
|
||||
literal, or if an exported rule has no spot-check. The packs used to be built under
|
||||
one set of numbers and played under another; this makes that impossible to ship.
|
||||
|
||||
The build runs all twenty-four guards before it writes anything:
|
||||
The build runs all twenty-five guards before it writes anything:
|
||||
|
||||
<!-- guards:start -->
|
||||
```
|
||||
check-seam: OK — 74 modules scanned, only all-specs.mjs reads NPCS/PREGENS from content.mjs
|
||||
check-seam: OK — 75 modules scanned, only all-specs.mjs reads NPCS/PREGENS from content.mjs
|
||||
check-generator: OK — 210 posting × tier pairs draw from their own stores, 129 weapons issued, each with the skill it names
|
||||
check-forge: OK — 450 creatures derived across 6 roles × 5 sizes × 5 tiers × 3 species, every one valid, armed with something its body can use, and reproducible from its seed; 90 of them forged identically from the built pack and from content.mjs
|
||||
check-rules: OK — 113 rules, 72 files scanned, 3 aliases + 46 constant sets checked, 417 formulas verified
|
||||
check-armed: OK — 35 creature(s) hold a weapon they have no skill for, every one on the record; no new ones
|
||||
check-rules: OK — 113 rules, 73 files scanned, 3 aliases + 46 constant sets checked, 417 formulas verified
|
||||
check-kits: OK — 25 roles, 10 trades, 226 catalogue items, every kit key resolves, every posting can use what it carries, every loadout distinct
|
||||
check-lang: OK — en.json, 1044 keys, no leaf/branch collisions
|
||||
check-templates: OK — 19 templates compile
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"play": "node tools/playthrough.mjs",
|
||||
"mj": "node tools/mj-queue.mjs",
|
||||
"simulate": "node tools/simulate.mjs",
|
||||
"check": "bun tools/check-seam.mjs && bun tools/check-generator.mjs && bun tools/check-forge.mjs && bun tools/check-rules.mjs && bun tools/check-kits.mjs && bun tools/check-lang.mjs && bun tools/check-templates.mjs && bun tools/check-behaviour.mjs && bun tools/check-scenarios.mjs && bun tools/check-rollable.mjs && bun tools/check-outcomes.mjs && bun tools/check-creatures.mjs && bun tools/check-powers.mjs && bun tools/check-anatomy.mjs && bun tools/check-lethality.mjs && bun tools/check-focus.mjs && bun tools/check-firstblood.mjs && bun tools/check-attackers.mjs && bun tools/check-fight-tail.mjs && bun tools/check-packs.mjs && bun tools/detention-tables.mjs --check && bun tools/bellhouse-plans.mjs --check && bun tools/check-cited.mjs && bun tools/check-figures.mjs && bun tools/check-handouts.mjs && bun tools/check-bestiary.mjs",
|
||||
"check": "bun tools/check-seam.mjs && bun tools/check-generator.mjs && bun tools/check-forge.mjs && bun tools/check-armed.mjs && bun tools/check-rules.mjs && bun tools/check-kits.mjs && bun tools/check-lang.mjs && bun tools/check-templates.mjs && bun tools/check-behaviour.mjs && bun tools/check-scenarios.mjs && bun tools/check-rollable.mjs && bun tools/check-outcomes.mjs && bun tools/check-creatures.mjs && bun tools/check-powers.mjs && bun tools/check-anatomy.mjs && bun tools/check-lethality.mjs && bun tools/check-focus.mjs && bun tools/check-firstblood.mjs && bun tools/check-attackers.mjs && bun tools/check-fight-tail.mjs && bun tools/check-packs.mjs && bun tools/detention-tables.mjs --check && bun tools/bellhouse-plans.mjs --check && bun tools/check-cited.mjs && bun tools/check-figures.mjs && bun tools/check-handouts.mjs && bun tools/check-bestiary.mjs",
|
||||
"test": "bun run check",
|
||||
"readme": "bun tools/update-readme.mjs",
|
||||
"tokens": "python3 tools/make-tokens.py"
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"note": "Creatures holding a weapon they have no skill for. May shrink, never grow.",
|
||||
"cases": [
|
||||
{
|
||||
"key": "anglesmith",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "cutter",
|
||||
"skill": "transposed_weapon"
|
||||
},
|
||||
{
|
||||
"key": "bp_leah",
|
||||
"source": "scenario-starter.mjs STARTER_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "bp_martin",
|
||||
"source": "scenario-starter.mjs STARTER_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "cailleach",
|
||||
"source": "bestiary-folklore.mjs",
|
||||
"weapon": "quarterstaff",
|
||||
"skill": "melee_weapon:club"
|
||||
},
|
||||
{
|
||||
"key": "ceyla",
|
||||
"source": "content.mjs PREGENS",
|
||||
"weapon": "pistol_med",
|
||||
"skill": "firearm:pistol"
|
||||
},
|
||||
{
|
||||
"key": "counterpoint",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "baton",
|
||||
"skill": "melee_weapon:club"
|
||||
},
|
||||
{
|
||||
"key": "drowned_crew",
|
||||
"source": "bestiary-water.mjs",
|
||||
"weapon": "prybar",
|
||||
"skill": "melee_weapon:prybar"
|
||||
},
|
||||
{
|
||||
"key": "five_of_six",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "monoblade",
|
||||
"skill": "melee_weapon:knife"
|
||||
},
|
||||
{
|
||||
"key": "hollow_man",
|
||||
"source": "content.mjs NPCS",
|
||||
"weapon": "dagger",
|
||||
"skill": "melee_weapon:knife"
|
||||
},
|
||||
{
|
||||
"key": "hulk_haunt",
|
||||
"source": "bestiary-water.mjs",
|
||||
"weapon": "boarding_axe",
|
||||
"skill": "melee_weapon:axe"
|
||||
},
|
||||
{
|
||||
"key": "la_edith",
|
||||
"source": "scenario-lastadmission.mjs LA_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "la_nowak",
|
||||
"source": "scenario-lastadmission.mjs LA_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "la_tunnicliffe",
|
||||
"source": "scenario-lastadmission.mjs LA_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "latecomer",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "baton",
|
||||
"skill": "melee_weapon:club"
|
||||
},
|
||||
{
|
||||
"key": "many_hands",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "dagger",
|
||||
"skill": "melee_weapon:knife"
|
||||
},
|
||||
{
|
||||
"key": "net_mender",
|
||||
"source": "bestiary-water.mjs",
|
||||
"weapon": "dagger",
|
||||
"skill": "melee_weapon:knife"
|
||||
},
|
||||
{
|
||||
"key": "od_bewley",
|
||||
"source": "scenario-openday.mjs OPENDAY_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "od_ferrand",
|
||||
"source": "scenario-openday.mjs OPENDAY_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "od_kearsley",
|
||||
"source": "scenario-openday.mjs OPENDAY_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "od_odame",
|
||||
"source": "scenario-openday.mjs OPENDAY_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "od_vasey",
|
||||
"source": "scenario-openday.mjs OPENDAY_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "quarantine_unit",
|
||||
"source": "content.mjs NPCS",
|
||||
"weapon": "baton",
|
||||
"skill": "melee_weapon:club"
|
||||
},
|
||||
{
|
||||
"key": "recurrence",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "cudgel",
|
||||
"skill": "melee_weapon:club"
|
||||
},
|
||||
{
|
||||
"key": "second_pass",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "baton",
|
||||
"skill": "melee_weapon:club"
|
||||
},
|
||||
{
|
||||
"key": "the_teind",
|
||||
"source": "bestiary-folklore.mjs",
|
||||
"weapon": "dagger",
|
||||
"skill": "melee_weapon:knife"
|
||||
},
|
||||
{
|
||||
"key": "the_tideline",
|
||||
"source": "bestiary-anomalous.mjs",
|
||||
"weapon": "monoblade",
|
||||
"skill": "melee_weapon:knife"
|
||||
},
|
||||
{
|
||||
"key": "the_understudy",
|
||||
"source": "content.mjs NPCS",
|
||||
"weapon": "dagger",
|
||||
"skill": "melee_weapon:knife"
|
||||
},
|
||||
{
|
||||
"key": "tt_bewley",
|
||||
"source": "scenario-throughtrain.mjs TT_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "tt_bligh",
|
||||
"source": "scenario-throughtrain.mjs TT_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "tt_kell",
|
||||
"source": "scenario-throughtrain.mjs TT_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "tt_prosser",
|
||||
"source": "scenario-throughtrain.mjs TT_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "tt_reyes",
|
||||
"source": "scenario-throughtrain.mjs TT_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "tt_shard",
|
||||
"source": "scenario-throughtrain.mjs TT_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "tt_vane",
|
||||
"source": "scenario-throughtrain.mjs TT_CAST",
|
||||
"weapon": "punch",
|
||||
"skill": "brawl"
|
||||
},
|
||||
{
|
||||
"key": "uninvited",
|
||||
"source": "bestiary-folklore.mjs",
|
||||
"weapon": "dagger",
|
||||
"skill": "melee_weapon:knife"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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`);
|
||||
Reference in New Issue
Block a user