I asked for this on a false premise of my own: I reported that adding a creature shifted a shared random stream and perturbed every other creature's recorded number. That is not true and the code never did it. measure() builds its own mulberry32 from the seed on every call, and check-lethality calls it once per creature, so a creature's numbers do not depend on its neighbours or its position. Measured rather than argued: inserting a creature ahead of the barghest changes 0 of 47 existing entries. The file's own claim — "the only thing that can move the number is a change to the rules or to the creature" — was accurate all along, and my last commit message says otherwise. It is wrong. The real cause, found by replaying each commit against the baseline as committed at4b71859:4b71859baseline recorded 0 of 46 differ322389bbestiary 0 of 46 differddc4f99hit locations reach combat 26 of 46 differ <-- herea90c4f3..e5dc9b526 of 46 differddc4f99routed every ordinary blow through the hit location table. That is the largest change the combat system has had and it moved 26 of 46 creatures, which is correct and expected. What is not correct is that nobody noticed for four commits: each creature moved by one or two points, the guard allowed six, and it reported OK while describing a game nobody was playing. So the tolerance goes. It exists for sampling noise and there is no sampling noise here — same party, same seed, same counts, and two recordings of unchanged code are byte-identical. Anything that moves is a real change, which is the entire point of the file. `rounds` is now compared too; it was recorded and then never read, so a creature could take a round longer to kill forever without a word. Because exactness only means something if the measurement is exact, the guard now proves it instead of assuming it: one creature measured twice must come back identical, and it says so plainly if a future change reaches for Math.random. Negative-tested. A 2% change to locationMaxHp now trips 8 creatures at +1.5 and +0.5 points of wipe rate — every one of which the old tolerance would have passed. Breaking determinism is caught and named. Also fixes update-readme, which advertised 8 guards while the build ran 9: check-anatomy was added without touching the list, which is precisely what the comment above that list already warned had happened once. The list is no longer trusted — it is checked against the `check` script in package.json, and refuses to write a README advertising a different set than the build runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
5.2 KiB
JavaScript
97 lines
5.2 KiB
JavaScript
/**
|
|
* Rewrite the README's counted sections from the real data.
|
|
*
|
|
* Those tables have been wrong three times, because a catalogue that grows every
|
|
* session and a number typed by hand are not the same kind of thing. A stale count in
|
|
* a README is the same defect as a stale comment in the engine: a claim nothing checks.
|
|
*
|
|
* Only the marked regions are touched; the prose around them is left alone.
|
|
*/
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { SKILL_CATALOGUE, WEAPONS, ARMOURS, GEAR, VEHICLES, NPCS, TALENTS }
|
|
from "./content.mjs";
|
|
import { ROLES, TRADES } from "../postings.mjs";
|
|
import { ICON_MAP } from "./icons.mjs";
|
|
import { RULES_PAGES } from "./rules-text.mjs";
|
|
|
|
const root = new URL("../", import.meta.url);
|
|
const readme = new URL("README.md", root);
|
|
let text = await readFile(readme, "utf8");
|
|
|
|
const items = WEAPONS.length + ARMOURS.length + GEAR.length + VEHICLES.length;
|
|
|
|
// The guard lines are quoted from the guards themselves rather than paraphrased.
|
|
const runGuard = name => {
|
|
try {
|
|
return execFileSync(process.execPath, [fileURLToPath(new URL(name, import.meta.url))],
|
|
{ encoding: "utf8" }).split("\n").find(l => l.startsWith(name.replace(".mjs", "")));
|
|
} catch { return null; }
|
|
};
|
|
/* All of them, in the order `npm run check` runs them. This list was four long while the
|
|
suite was seven, so the README advertised a subset and silently stopped mentioning
|
|
every guard added after it was written — including the three that catch the most. */
|
|
const GUARDS = ["check-rules.mjs", "check-kits.mjs", "check-lang.mjs",
|
|
"check-templates.mjs", "check-behaviour.mjs", "check-scenarios.mjs",
|
|
"check-creatures.mjs", "check-anatomy.mjs", "check-lethality.mjs"];
|
|
|
|
/* The list above went stale the moment a guard was added without touching this file —
|
|
which is what the comment above it already warned about, and which happened again
|
|
with check-anatomy. So it is no longer trusted: the suite of record is the `check`
|
|
script in package.json, and this refuses to write a README that advertises a
|
|
different set than the one the build actually runs. */
|
|
{
|
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
const actual = [...(pkg.scripts?.check ?? "").matchAll(/(check-[\w-]+\.mjs)/g)].map(m => m[1]);
|
|
const missing = actual.filter(g => !GUARDS.includes(g));
|
|
const phantom = GUARDS.filter(g => !actual.includes(g));
|
|
if (missing.length || phantom.length) {
|
|
console.error("update-readme: FAILED — the README's guard list does not match `npm run check`");
|
|
if (missing.length) console.error(" run by the build, not advertised: " + missing.join(", "));
|
|
if (phantom.length) console.error(" advertised, not run by the build: " + phantom.join(", "));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const guardLines = GUARDS.map(runGuard).filter(Boolean);
|
|
|
|
const box = `| | |
|
|
|---|---|
|
|
| **Postings** | ${Object.keys(ROLES).length}, each with a key characteristic, trained skills, talents and its own kit |
|
|
| **Trades** | ${Object.keys(TRADES).length} ordinary jobs the department recruits from, plus induction |
|
|
| **Catalogue** | ${items} items — ${GEAR.length} gear, ${WEAPONS.length} weapons, ${ARMOURS.length} armour, ${VEHICLES.length} vehicles |
|
|
| **Skills / talents** | ${SKILL_CATALOGUE.length} skills, ${TALENTS.length} talents |
|
|
| **Bestiary** | ${NPCS.length} statblocks across folklore, horror and the far side |
|
|
| **Eras** | modern, antique, future, anomalous — the agency is old and never abolishes anything |
|
|
| **Icons** | ${Object.keys(ICON_MAP).length}, all generated from \`tools/icons.mjs\`; no external art |
|
|
| **Starter scenario** | **BLACK PLATFORM**, a six-player action starter, importable as a single Adventure |
|
|
| **Compendia** | pregens, NPCs, skills, weapons, armour, gear, vehicles, powers, tables, rules, the starter |
|
|
| **Rules journal** | ${RULES_PAGES.length} pages, generated from \`rules.mjs\` so the book cannot drift from the code |`;
|
|
|
|
const replaceRegion = (src, marker, body) => {
|
|
const open = `<!-- ${marker}:start -->`, close = `<!-- ${marker}:end -->`;
|
|
const i = src.indexOf(open), j = src.indexOf(close);
|
|
if (i < 0 || j < 0) return null;
|
|
return src.slice(0, i + open.length) + "\n" + body + "\n" + src.slice(j);
|
|
};
|
|
|
|
// First run: wrap the existing hand-written regions in markers.
|
|
if (!text.includes("<!-- box:start -->")) {
|
|
text = text.replace(/\| \| \|\n\|---\|---\|\n(?:\|.*\n)+/,
|
|
m => `<!-- box:start -->\n${m}<!-- box:end -->\n`);
|
|
text = text.replace(/```\ncheck-rules:[\s\S]*?```/,
|
|
m => `<!-- guards:start -->\n${m}\n<!-- guards:end -->`);
|
|
}
|
|
|
|
const withBox = replaceRegion(text, "box", box);
|
|
if (!withBox) { console.error("update-readme: box markers missing"); process.exit(1); }
|
|
const withGuards = replaceRegion(withBox, "guards",
|
|
"```\n" + guardLines.join("\n") + "\n```");
|
|
if (!withGuards) { console.error("update-readme: guard markers missing"); process.exit(1); }
|
|
|
|
await writeFile(readme, withGuards, "utf8");
|
|
console.log(`update-readme: OK — ${items} items, ${Object.keys(ROLES).length} postings, `
|
|
+ `${Object.keys(TRADES).length} trades, ${guardLines.length} guard lines`);
|