check-lethality: read the shared seam, not content.mjs

It imported NPCS directly, so once the bestiary moved into its own files it
measured 47 of 147 and printed OK. A guard blind to two thirds of what it
guards is not a guard, and this one failed silently — the worse kind.

Now reads collectSpecs(), the list check-creatures, make-portraits and
mj-queue already share. Scenario casts come in with the bestiary: they were
never measured, and they should have been, since the Act Three standoff
this repository publishes wipe rates for IS a scenario cast. 184 specs at
roughly 47ms each, about nine seconds.

Switched BEFORE re-recording, to prove the change moved nothing: the run
against the old baseline reported all 47 existing creatures fighting
exactly as recorded. The --update is then verifiably additive — 47 to 184,
137 added, 0 changed, 0 lost.

Still fail-open on arrival: an unbaselined creature is a note, not a
failure, so this printed "OK, 137 new" while 137 creatures were unguarded.
Left as the author wrote it rather than changed under a content drop, but
it is the same shape as the three classifier bugs in 00f6a1e and worth
closing.

First thing it found, invisible until now: march_stone is authored "it
kills perhaps one person a century" and measures 43.5% wipe, 2.72 of 4
down, over 22 rounds. naturalArmour 14 is the highest in the game — the
Apex tier is 7 — so the party cannot hurt it and it grinds them down. The
prose and the statblock describe different creatures.

All 23 guards pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-22 23:14:37 +01:00
co-authored by Claude Opus 5
parent 8a591b7853
commit 4d591f635d
2 changed files with 703 additions and 3 deletions
+18 -3
View File
@@ -30,7 +30,7 @@
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { NPCS } from "./content.mjs";
import { collectSpecs } from "./all-specs.mjs";
import { ROSTER } from "./roster.mjs";
import { measure, seedFor } from "./simulate.mjs";
@@ -77,9 +77,24 @@ const party = PARTY_KEYS.map(k => {
return found;
});
/* WHAT IS MEASURED.
*
* Every npc-kind spec in the repository, read through collectSpecs() — the one list that
* check-creatures, make-portraits and mj-queue already share. This file used to import
* NPCS from content.mjs directly, which was the same thing until the 100-creature
* bestiary landed in its own files: after that it measured 47 of 147 and reported OK.
* A guard that cannot see two thirds of the creatures it guards is not a guard, and it
* failed silently, which is worse than failing loudly.
*
* Scenario casts come in with them. They were never measured before and they should have
* been: the Act Three standoff this repository publishes wipe rates for IS a scenario
* cast, and a rules change reaches a ticket inspector exactly as it reaches a barghest.
* 184 specs at roughly 47ms each is about nine seconds. */
const SPECS = (await collectSpecs()).filter(r => r.kind === "npc").map(r => r.spec);
/** Solo, because a creature is the unit under test — counts are an encounter's business. */
const current = {};
for (const spec of NPCS) {
for (const spec of SPECS) {
const r = measure(party, [spec], { runs: RUNS, seed: seedFor(SEED, spec.key) });
current[spec.key] = {
wipe: Number((r.wipeRate * 100).toFixed(1)),
@@ -123,7 +138,7 @@ if (base.runs !== RUNS || base.seed !== SEED
change reaches for Math.random or a Set iteration order, this says so instead of
letting the whole guard degrade into noise-chasing. */
{
const probe = NPCS[0];
const probe = SPECS[0];
const a = measure(party, [probe], { runs: RUNS, seed: seedFor(SEED, probe.key) });
const b = measure(party, [probe], { runs: RUNS, seed: seedFor(SEED, probe.key) });
const shape = r => JSON.stringify([r.wipeRate, r.downMean, r.roundsMedian, r.hurtMean]);
+685
View File
@@ -244,6 +244,691 @@
"wipe": 0,
"down": 0.05,
"rounds": 3
},
"hunt_master": {
"wipe": 22.4,
"down": 2.27,
"rounds": 11
},
"gabriel_hounds": {
"wipe": 0,
"down": 0.05,
"rounds": 2
},
"grey_mare": {
"wipe": 1.9,
"down": 0.89,
"rounds": 4
},
"black_annis": {
"wipe": 21.6,
"down": 2,
"rounds": 7
},
"cailleach": {
"wipe": 0,
"down": 0.01,
"rounds": 2
},
"jenny_greenteeth": {
"wipe": 0,
"down": 0.15,
"rounds": 2
},
"gwrach": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"hunky_punk": {
"wipe": 0.7,
"down": 0.68,
"rounds": 5
},
"sin_eater": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"lych_waiter": {
"wipe": 0,
"down": 0.13,
"rounds": 3
},
"crossroads_man": {
"wipe": 0.1,
"down": 0.41,
"rounds": 3
},
"march_stone": {
"wipe": 43.5,
"down": 2.72,
"rounds": 22
},
"foundation_child": {
"wipe": 0,
"down": 0,
"rounds": 3
},
"bog_offering": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"hearth_hob": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"cauld_lad": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"the_teind": {
"wipe": 0,
"down": 0.04,
"rounds": 4
},
"changeling": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"uninvited": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"dullahan": {
"wipe": 9.3,
"down": 1.52,
"rounds": 6
},
"pooka": {
"wipe": 9.7,
"down": 1.41,
"rounds": 4
},
"cu_sidhe": {
"wipe": 1.5,
"down": 0.91,
"rounds": 5
},
"tatterfoal": {
"wipe": 0,
"down": 0.03,
"rounds": 2
},
"dun_cow": {
"wipe": 82.9,
"down": 3.64,
"rounds": 10
},
"boobrie": {
"wipe": 4.5,
"down": 1.08,
"rounds": 4
},
"harbour_dog": {
"wipe": 0.1,
"down": 0.24,
"rounds": 3
},
"sea_mare": {
"wipe": 8.6,
"down": 1.36,
"rounds": 5
},
"tangie": {
"wipe": 3.1,
"down": 1.25,
"rounds": 7
},
"selkie": {
"wipe": 0,
"down": 0.07,
"rounds": 3
},
"wrack_gull": {
"wipe": 0,
"down": 0.01,
"rounds": 2
},
"sea_crow": {
"wipe": 0,
"down": 0.01,
"rounds": 1
},
"shellycoat": {
"wipe": 0.1,
"down": 0.23,
"rounds": 3
},
"peg_powler": {
"wipe": 0.1,
"down": 0.39,
"rounds": 3
},
"greenteeth": {
"wipe": 0.1,
"down": 0.4,
"rounds": 3
},
"fen_hound": {
"wipe": 0,
"down": 0.15,
"rounds": 3
},
"cut_walker": {
"wipe": 0,
"down": 0.13,
"rounds": 2
},
"wrackman": {
"wipe": 0.2,
"down": 0.52,
"rounds": 5
},
"net_mender": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"mudlark_child": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"hulk_haunt": {
"wipe": 0,
"down": 0.07,
"rounds": 4
},
"drowned_crew": {
"wipe": 0,
"down": 0.02,
"rounds": 2
},
"lock_keeper": {
"wipe": 0.1,
"down": 0.41,
"rounds": 2
},
"the_undertow": {
"wipe": 52.8,
"down": 2.94,
"rounds": 14
},
"the_sump": {
"wipe": 12.5,
"down": 1.72,
"rounds": 8
},
"drowned_parish": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"the_fathom": {
"wipe": 88.5,
"down": 3.73,
"rounds": 14
},
"eelmother": {
"wipe": 15.6,
"down": 1.78,
"rounds": 8
},
"lamprey_host": {
"wipe": 0.1,
"down": 0.33,
"rounds": 3
},
"washland_cattle": {
"wipe": 4.7,
"down": 1.15,
"rounds": 5
},
"the_heron": {
"wipe": 0,
"down": 0.18,
"rounds": 2
},
"foldwalker": {
"wipe": 0.1,
"down": 0.34,
"rounds": 4
},
"anglesmith": {
"wipe": 0.1,
"down": 0.38,
"rounds": 4
},
"halfpast": {
"wipe": 0,
"down": 0.1,
"rounds": 3
},
"returned_surveyor": {
"wipe": 0,
"down": 0.03,
"rounds": 2
},
"unmeasured": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"patient_grasp": {
"wipe": 8,
"down": 1.76,
"rounds": 11
},
"the_tideline": {
"wipe": 0.9,
"down": 0.73,
"rounds": 6
},
"glass_auditor": {
"wipe": 0,
"down": 0.32,
"rounds": 6
},
"quorate": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"five_of_six": {
"wipe": 0.4,
"down": 0.65,
"rounds": 5
},
"the_remainder": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"counterpoint": {
"wipe": 0,
"down": 0,
"rounds": 3
},
"the_tally": {
"wipe": 0,
"down": 0,
"rounds": 3
},
"many_hands": {
"wipe": 0,
"down": 0.07,
"rounds": 4
},
"the_iteration": {
"wipe": 0.4,
"down": 0.53,
"rounds": 6
},
"backscatter": {
"wipe": 22.1,
"down": 2.42,
"rounds": 17
},
"afterimage": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"second_pass": {
"wipe": 0.1,
"down": 0.35,
"rounds": 4
},
"turnback": {
"wipe": 0.1,
"down": 0.25,
"rounds": 4
},
"the_overflight": {
"wipe": 14.1,
"down": 1.75,
"rounds": 6
},
"recurrence": {
"wipe": 0.2,
"down": 0.44,
"rounds": 5
},
"inside_out": {
"wipe": 91.9,
"down": 3.83,
"rounds": 10
},
"latecomer": {
"wipe": 0.8,
"down": 0.7,
"rounds": 5
},
"mirrorline": {
"wipe": 0.2,
"down": 0.62,
"rounds": 6
},
"carried_over": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"bull_azure": {
"wipe": 21.4,
"down": 1.98,
"rounds": 8
},
"enfield": {
"wipe": 0,
"down": 0.21,
"rounds": 3
},
"yale": {
"wipe": 1.4,
"down": 0.73,
"rounds": 4
},
"white_hart": {
"wipe": 0,
"down": 0.04,
"rounds": 4
},
"man_tyger": {
"wipe": 2.2,
"down": 1.03,
"rounds": 6
},
"melusine": {
"wipe": 0.1,
"down": 0.16,
"rounds": 3
},
"eagle_displayed": {
"wipe": 0.1,
"down": 0.24,
"rounds": 3
},
"gryphon": {
"wipe": 23.5,
"down": 2.15,
"rounds": 8
},
"sign_wyvern": {
"wipe": 1.2,
"down": 0.77,
"rounds": 5
},
"cockatrice": {
"wipe": 0,
"down": 0.01,
"rounds": 2
},
"martlet": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"piety_pelican": {
"wipe": 0,
"down": 0.01,
"rounds": 2
},
"drain_heron": {
"wipe": 0,
"down": 0.03,
"rounds": 2
},
"talbot": {
"wipe": 0.1,
"down": 0.34,
"rounds": 3
},
"gazehound": {
"wipe": 0,
"down": 0.04,
"rounds": 2
},
"bearward_bear": {
"wipe": 5.9,
"down": 1.41,
"rounds": 6
},
"heath_cow": {
"wipe": 64.2,
"down": 3.2,
"rounds": 10
},
"tup": {
"wipe": 0.1,
"down": 0.41,
"rounds": 3
},
"cade_lamb": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"hollow_hog": {
"wipe": 2.5,
"down": 1.01,
"rounds": 5
},
"hinny": {
"wipe": 0.1,
"down": 0.31,
"rounds": 3
},
"water_bull": {
"wipe": 63.2,
"down": 3.19,
"rounds": 13
},
"moor_cat": {
"wipe": 0.1,
"down": 0.24,
"rounds": 3
},
"gaunt_grey": {
"wipe": 0.1,
"down": 0.23,
"rounds": 3
},
"menagerie_ape": {
"wipe": 0.7,
"down": 0.6,
"rounds": 4
},
"tt_bewley": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"tt_prosser": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"tt_rance": {
"wipe": 0,
"down": 0.07,
"rounds": 2
},
"tt_shard": {
"wipe": 0,
"down": 0.03,
"rounds": 1
},
"tt_cordera_man": {
"wipe": 0,
"down": 0.02,
"rounds": 2
},
"tt_tom_doyle": {
"wipe": 0,
"down": 0.06,
"rounds": 2
},
"tt_kell": {
"wipe": 0,
"down": 0,
"rounds": 2
},
"tt_vane": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"tt_bligh": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"tt_reyes": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"tt_merritt": {
"wipe": 0,
"down": 0.06,
"rounds": 2
},
"tt_ambrose": {
"wipe": 0,
"down": 0.02,
"rounds": 2
},
"tt_coat": {
"wipe": 0,
"down": 0.02,
"rounds": 2
},
"tt_writman": {
"wipe": 0,
"down": 0.06,
"rounds": 2
},
"bp_leah": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"bp_martin": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"bp_inspector": {
"wipe": 0,
"down": 0.07,
"rounds": 2
},
"bp_conductor": {
"wipe": 0,
"down": 0.25,
"rounds": 4
},
"bp_nia": {
"wipe": 0.1,
"down": 0.11,
"rounds": 2
},
"bp_callum": {
"wipe": 6.4,
"down": 1.4,
"rounds": 14
},
"bp_mara": {
"wipe": 0.1,
"down": 0.22,
"rounds": 2
},
"bp_owen": {
"wipe": 0,
"down": 0.02,
"rounds": 2
},
"bp_elin": {
"wipe": 0,
"down": 0.01,
"rounds": 2
},
"bp_sennen": {
"wipe": 0,
"down": 0.02,
"rounds": 2
},
"la_enderby": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"la_sammy": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"la_marrow": {
"wipe": 0.1,
"down": 0.35,
"rounds": 3
},
"la_edith": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"la_nowak": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"la_tunnicliffe": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"od_kearsley": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"od_odame": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"od_vasey": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"od_ferrand": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"od_bewley": {
"wipe": 0,
"down": 0,
"rounds": 1
},
"so_warden": {
"wipe": 0,
"down": 0.04,
"rounds": 2
},
"so_teague": {
"wipe": 0,
"down": 0.07,
"rounds": 2
}
}
}