Commit Graph
69 Commits
Author SHA1 Message Date
slaguru666andClaude Opus 5 1a2ca44c52 R-284: report a wrong figure as a wrong figure, and read the right entry
R-283 left the intact-sentence-wrong-number case falling through to omission, so
the guard said BESTIARY "does not state" a rating the page was stating. reads()
now has a shape tier between strict and loose: the strict pattern with its value
slot loosened, reporting which of the two numbers moved.

Scoped the per-creature rules while adding it. They read the whole page, and each
is the only rule of its kind today, so a page-wide match found the right line by
luck; a second attackFactor creature would have let the courier's rule match that
creature's sentence and report the courier correct. They now read the creature's
own "### Name" entry -- proved by deleting the courier's line and planting an
identical one under the redcap: still omission, where before it would have passed.

Five discriminations plus the decoy, proved in a worktree with the message read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:15:24 +01:00
slaguru666andClaude Opus 5 327d060eb1 R-283: tell a reworded bestiary sentence apart from a missing one
R-282 matched the page with String.includes, so rewording the exemption sentence
reported "BESTIARY never says it is off the stripping ladder" -- sending a
maintainer after a sentence that is sitting right there, and never naming the
real problem, which is a pattern that has silently stopped reading.

Each textual rule now reads twice. Strict is the sentence as it stands and is
tighter than before (the bold and the full stop, not the bare clause a substring
accepted); loose is the same claim in any wording. Strict passes, loose-only is
reported as a reword with the line quoted and the page presumed right, neither is
the omission.

The loose anchor was wrong on its first pass in the way that matters: "a sentence
with 40% and 80%" also matched the courier's own statblock line, so deleting the
sentence reported a reword and quoted the statblock back. It now excludes that
generated marker, which makes it a test of the claim and not of the digits, and
degrades to omission rather than to a false reword.

Four discriminations proved in a worktree with the message read in each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:12:33 +01:00
slaguru666andClaude Opus 5 1a80d3637c 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 <noreply@anthropic.com>
2026-09-13 15:01:04 +01:00
slaguru666andClaude Opus 5 fa484071cd R-281: a ratchet, because "helps in all of them" survives the advice decaying
R-280 left the claim satisfiable by a gain of 0.2. The artifact now records how
many measurable packs clear their own noise -- reliable: {aboveNoise: 36, of: 38}
-- and the check refuses if that share falls. It may rise freely.

A ratchet rather than a threshold: any threshold here would be a number I chose,
and choosing one just under the current value is what produced MEASURABLE =
[15,85]. A share rather than a count, so widening admission cannot pay it off.

Proved three ways in worktrees: making focus fire actively bad fires the drift
check first, which is correct; making it unreliable and re-recording fires the
older claim at 37 of 38; and claiming a better past, 38 of 38, is refused by the
ratchet itself. The ratchet bites exactly where the old claim does not -- between
"still helps everywhere" and "helps as reliably as it did", which is where a slow
degradation lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:34:10 +01:00
slaguru666andClaude Opus 5 3d83fbde93 R-280: the band replaced by the test it was standing in for
MEASURABLE = [15,85] proxied for "can this fight move at all". The direct form,
in the units the guard already uses: the spread rate must stand clear of both
ends by more than its own noise. Deliberately blind to the gain -- admitting the
sizes where focus fire clears its noise would make the guard's claim true by
construction. Size is still picked on nearest-an-even-fight.

31 measurable became 38. the_arrears returns at 6.3, and switchboard arrives at
8.1 -- the second largest gain in the artifact, thrown away for being one point
past a round number. Four of the eight carry effects larger than most rows the
band already admitted.

Two of them do not clear their own noise: the_choir has 0.7 points of headroom
and used 0.2, the_stanchion has six and used 0.2. Above-noise falls 31/31 to
36/38 and the bestiary prints 36. That is two measurements reporting no
detectable effect, which the band suppressed by refusing to take them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:27:13 +01:00
slaguru666andClaude Opus 5 a7e4dd7754 R-279: what the_arrears was claiming, and why dropping it is the wrong kind of right
It claimed focus fire is worth 6.3 points against two of them, 14.7% to 21%.
Measured independently at 4000 runs x 3 seeds: 14.2% to 20.4%, gain 6.2, which is
2.4x its own noise and 44% of the base. The claim was true and reproduces.

What failed is a threshold. MEASURABLE is [15,85] and the scan's estimate of a
boundary value moved 14.7 to 14.4. And the creature is a step -- 85.7% at one,
14.2% at two, 0.4% at three -- so no pack size gives an even fight and the band's
endpoints fall in the gap. The band records nothing about a creature whose
defining property is having no middle.

Not moving the band to 14: fitting a threshold to the datum it excludes is how a
guard stops being a test. But the band is a proxy for "can this fight move", and
the direct test -- does the gain clear its own noise -- is already in the
artifact and answers yes. Replacing the proxy is a decision about all 47, not a
fix, and not mine to take unasked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:21:31 +01:00
slaguru666andClaude Opus 5 a7eca91a9c R-278: raising SCAN_RUNS found that SCAN_RUNS had never been read
Asked to raise the scan until the redcap's pack size stopped flipping. Measured
the threshold -- unstable at 3000 and 4000, stable across twenty seeds at 6000 --
raised it, and the re-record took fifteen seconds, which was impossible.

winRate takes three parameters and pickSize passed SCAN_RUNS as a fourth.
JavaScript discards it, so every scan has always run at RUNS and SCAN_RUNS has
never been read by anything. The fix I was asked to make was inert in the same
way as the thing it was fixing.

winRate takes runs now. The redcap is still n=3 with gain 6.3, arrived at stably
rather than luckily; the_arrears drops out of the measurable band at an honest
scan, 31 packs to 30; the_committee moves 2 to 6 and stays pinned. Claim check
still passes, bestiary regenerated.

The only signal was a number being too small. A fifteen-second re-record is good
news, and good news is what nobody investigates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:18:46 +01:00
slaguru666andClaude Opus 5 38012d56ff R-277: n=3 is right for the redcap, and R-276 was wrong about why
check-focus picks the pack size nearest an even fight. For the redcap that is
n=3 at 28.0% against n=2's 73.4% -- correct, and also where focus fire is worth
most. But the margin is 1.4 points and the scan is 400 runs: run across eight
seeds it picks 3 seven times and 2 once, and the recorded gain would move 6.3 to
4.9 with it.

R-276's explanation was wrong. Its table was measured against the CLEAN GROUND
cut, which it never named. Against the frozen party a lone redcap is worth 0.4
rather than 6.1, because that party wins 98.9% and nothing shows against a
ceiling. The power is worth most where the fight is in doubt -- 3.8 at n=2, 3.1
at n=3, nothing at either end. Not outnumbered. Undecided.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:11:20 +01:00
slaguru666andClaude Opus 5 7fc0da3ecf R-276 correction: check-lethality fights solo, and I read the wrong column
Asked to re-record the lethality baseline against a lone redcap, I read the tool
and found it already does: measure(party, [spec]), with a comment saying "Solo,
because a creature is the unit under test". My claim that it fights packs came
from memory of check-focus, whose redcap is n: 3.

The lethality figure was also not hiding the power. Wipe rate moved 0.7% to 0.9%
because one redcap cannot wipe four agents whatever it ignores -- that column is
at its floor. Agents down moved 0.55 to 0.74 of 4, a 35% relative increase, which
is the column I did not look at.

No baseline re-recorded: it is already solo and was re-recorded in R-275.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:08:04 +01:00
slaguru666andClaude Opus 5 8fa691c4aa R-276: NOT TIRED is visible in play, and worth nothing where we measure it
Played four agents against one redcap. Two dodges in one round, both at 75 --
DEFENCE_STEP is -30, so the second would have been 45 and the roll of 70 would
have failed. The wiring fires.

Measured with and without, 2000 x 3 seeds: the power costs the party 6.1 points
against a lone redcap and 0.1 against three of them. It is a rule about being
outnumbered -- a lone defender spends four defences a round, a pack spends one
each -- which is why check-lethality moved only 0.7% to 0.9%. The baseline fights
redcaps in a pack, the configuration where the power is worth nothing, so that
figure is the floor rather than the effect.

Not presented as evidence: at seed 3 the party wins with the power on and is wiped
with it off, which is stream divergence rather than direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:02:54 +01:00
slaguru666andClaude Opus 5 32ca982369 R-275: the harness had never read a creature's power
41 statblocks carry a POWER in their tactics and simulate.mjs read none of them,
so a redcap that ignores the cumulative defence penalty has been measured as a
creature that tires -- in check-lethality, in check-focus, and in every figure
published about it. The defect was not that the powers were unimplemented, it was
that nothing said they were not.

powers.mjs classifies all 41: 2 wired, 14 notSimulable with a stated reason, 25
not fight rules. check-powers refuses an unclassified POWER and refuses a
notSimulable without a reason -- and it does not test that the harness imports a
power, it fights the creature with and without and requires the two to disagree.

Moved: the courier 9.4% to 1.0% wiped (it attacks at half while carrying), the
redcap 0.7% to 0.9% (small, because these fights rarely spend a second defence).

ARGENT AND GULES was wired and then un-wired: it tripled the supporter's wipe rate
to 75.2% because the harness has no ground and applied the borough-ground condition
unconditionally. Same reason THE PULL is not wired. I had wired one and refused the
other on identical facts.

Lethality and focus re-recorded, bestiary regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:05:11 +01:00
slaguru666andClaude Opus 5 dd3d818893 R-274: the opposed roll works and the combat never reaches it
Played the hollow man against the cut. THE FILE IS YOURS NOW never fires --
simulate.mjs reads statblocks and weapons and has no concept of tactics, so
R-273 closed a gap in rules.mjs and left the same gap one layer out.

Resolved by hand it behaves: every roll pair enumerated for both beats. The
tie-break carries it -- at POWx5 100 against 60 the aggressor still only takes
them 48% of the time, because equal bands go to whoever is being acted upon.

Act Four prices THE OFFER: accepting it moves Ashcroft from the safest person in
the room to the least safe, 21.9% to 48.7%, past Braithwaite's 37.6%. And that
once-only row no-ops 14.5% of the time against him, which Act Three can absorb
and a permanent countdown beat may not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:45:32 +01:00
slaguru666andClaude Opus 5 cdbb2e34ee R-273: the opposed roll, built where the authority lives
The content has called for 'opposed POWx5' since before the rule existed -- the
hollow man's tactics, CLEAN GROUND twice, slice-text three times -- and rules.mjs
defined none. A scenario carried a local ruling that said in its own text it was a
ruling and not a rule.

Generalises that ruling rather than inventing another: both sides roll, the better
band wins, only the ladder the game already has. Ties go to whoever is being acted
upon, which is what defenceOutcomeFor has always said; the scenario's 'favour the
agent' gave the same answer only because no agent ever initiates one. Neither side
succeeding leaves the contest unsettled rather than won, which the two beats need
in opposite directions.

Two exports at the scenario session's request: opposedOutcomeFor compares graded
levels and carries both, so a caller can price a fumbled attempt without this file
deciding what a fumble costs; opposedContestFor runs it from ratings and rolls with
per-side difficulty, so 'resists at Difficult' does not put applyDifficulty back
into a document. Spot-checked against the real Act Four beat: 55 against 85 at
Difficult, which is 42.

Page 1 states it by asking it -- the tie-break and the margin are computed from the
rule at build time, so the book cannot drift from the engine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:42:17 +01:00
slaguru666andClaude Opus 5 8279cdfda8 R-272: guard the tail, and stop prose drifting from its artifacts
Three things Tim asked for together: measure the long end of a fight, make the
swing check bite on stale prose, and record in rules.mjs the coincidence that
hid an invented rule from two readers.

GUARD 15 — check-fight-tail, on tools/fight-tail.mjs.

Fourteen guards measured this scenario's combat and none could see a long
fight, because every one of them averages. The cap is the measurement here, so
the tool passes its own (400, not runFight's default 40) and --update refuses
to record anything reaching it. Verified by lowering it back to 40: the cut
loses 20 fights to the ceiling, the six-a-side 117, 107 of those ending
neither won nor wiped, and the recorder stops.

Two claims, both read from a fresh measurement rather than the baseline, so
--update cannot silence them. "The long end is twice the median" was rejected
as a claim because it is true of both encounters and so distinguishes nothing.
Instead: a fight past 15 rounds wipes the party materially more often than a
short one, and the SIX-A-SIDE fight is the longer one (median 14 vs 10.7) --
R-270 showing up as duration, since a disabled fighter keeps fighting 30
points down. The cut is shorter because it is decisive, not safer.

GUARD 16 — check-cited, on tools/check-cited.mjs.

check-firstblood and check-attackers catch the game changing; neither reads
the document. Re-record after a re-cast and the artifact updates, the guard
goes green, and the prose keeps printing the old number under a citation
saying where the new one lives. So citations are now machine-readable --
**40**<!-- cite: first-blood cut.swing --> -- and resolved on every build. 25
of them. It failed three times on its first runs, all real: a config keyed
"column" that the prose called "line", two figures rounded 32.3 -> 32, and a
vacuous pass on zero citations, now fatal in its own right.

It also refuses citation of unstable fields. fight-tail.longest may not reach
prose: same party, same seeds, same runs, and renaming a config moved it 71 ->
90 rounds, because seedFor derives the stream from the id. Across seven
labels -- median spread 0, p95 1, p99 3, longest 21. A sample maximum reads
like a bound and is a property of the label. EXPOSURE states p99 instead.
Same discipline on the deadlier ratio: 2.42 with seed spread 1.1, so the
document gives its direction and declines to quote its size.

RULES.MJS — one comment, no rule change.

Over resolveLocationHit: its two thresholds are unrelated and usually agree.
disabled is a fraction of the pool per location; majorWound is ceil(hp/2) and
feeds only dyingLimitFor; neither removes anyone from a fight, which is
conditionFor at 2 hit points or a destroyed head. At 10 hp, leg/abdomen/chest
capacity is 5 and majorWoundFor is 5, and those locations take 12 of 20 melee
results and 15 of 20 ranged -- so two readers reconstructed a rule that does
not exist, checked it against the log, and were confirmed by it. The note
says to test an arm, the only place the difference shows.

Guards verified to bite, not assumed: drift, re-cast, censoring, the longest
refusal, the rounding catch and the vacuous-pass catch were each forced and
each failed the build with the right guidance, then restored.

npm run check: 16 guards, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:20:25 +01:00
slaguru666andClaude Opus 5 14c34449e3 R-271: measure the tail, and a round cap that scores a long fight as a draw
Fourteen guards all average, so none can see how long a fight might run. 6000
fights per configuration: the cut runs a median of 11 and a 95th of 22, the
six-a-side line a median of 14 and a 95th of 32, and the longest seen are 71 and
79. The line is the LONGER fight, because more bodies means more of them fighting
on at reduced skill rather than dropping. Length predicts death: 61.6% of cut
fights past fifteen rounds are wipes against 42.0% of shorter ones.

maxRounds = 40 is the default every guard runs at and a fight reaching it is
scored as neither wipe nor win -- censoring 0.2% of cut fights and 1.87% of the
line's. Measured before proposing anything: uncensored, the swing moves 39.8 to
39.9 and 14.8 to 15.1, against a recorded noise of 2.1. The cap stays, documented
rather than corrected, because the cure is four re-recorded baselines.

Also R-270 addendum: the hit table is weighted toward the locations where the two
thresholds coincide -- 12 of 20 melee results, 15 of 20 ranged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:08:09 +01:00
slaguru666andClaude Opus 5 74e45b15e7 R-270: a rule I invented, and the coincidence that hid it
R-266 said a fighter goes down at half maximum hit points and called it one rule
in both directions. conditionFor puts someone down at hp <= 2; majorWoundFor is
ceil(hp/2) and feeds only how long the dying last; a location is disabled by
resolveLocationHit at its own locationMaxHp capacity.

The reason it survived reading: for a 10-point neighbour majorWoundFor is 5 and a
leg, abdomen or chest holds exactly 5, and for a 12-point agent both are 6. On the
locations that get hit most the invented rule returns the real one's answer, and
the narration prints MAJOR WOUND and disabled on the same line. Arms and heads are
where they part, and I had not looked at an arm.

Found by the scenario session going to rules.mjs to verify a different correction
of mine and reading the next function along. Both errors made the fight look
easier than it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:03:34 +01:00
slaguru666andClaude Opus 5 765b3518db R-269: the fourth quadrant, and a disable does not remove anybody
Seed 16 is the missing corner -- party takes first blood in round 1 and is wiped
anyway -- and it runs 24 rounds. Its last five are Neil dropped by a critical
through armour, then Dominic alone at 1%, failing three times, then dying. That
is what three effective attackers costs at a table when a fight goes long.

Corrects a mechanism I had written twice: a disabling hit does not remove an
attacker, it charges 30 points off physical or manipulation per locationEffectsFor.
Bhattacharya keeps swinging at 10 for four rounds. A slope, not a cliff.

Neither guard fired and neither was wrong: the fight sits in the 25.5% the swing
does not cover, and no guard measures the tail because all of them average.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:59:06 +01:00
slaguru666andClaude Opus 5 0b86c5ffc3 R-268: guard the sentence that names a player's character
CLEAN GROUND prints that the four-player cut is three effective attackers and
that Ashcroft is not one. Nothing checked it, and a GM reads it aloud to decide
who a real player spends four hours being.

effective-attackers.mjs measures per attack, not per fight: counted per fight
Braithwaite leads on disables, but only because his armour buys him a third more
swings -- per attack he is the weakest of the three. Both rates are recorded and
only the per-attack one is reasoned from. Asserts exact drift, then the sentence:
three clear 5% of attacks disabling, one does not, and that one is Ashcroft.
Re-recording does not silence the claim check; verified in a worktree.

declared-cast.mjs holds the cast marker reading both scenario guards need, rather
than a copy in each. It was briefly named scenario-cast.mjs, which check-scenarios
sweeps into the scenario corpus -- its own example marker was read as a real cast.

update-readme dropped any guard that exited non-zero, so check-rollable vanished
from the README and the count word fell to thirteen while fourteen guards ran. A
missing line is now fatal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:56:01 +01:00
slaguru666andClaude Opus 5 60995b28c2 R-267 addendum: read the cast, do not copy it
I told the scenario session that this guard turns a silent re-cast into a build
failure, and it accepted the coupling on that basis. The cast was hardcoded, so
a re-cast would have left it measuring the old six and reporting success.

Reads the <!-- cast: --> marker check-rollable established, drops the two the
scaling note drops by name rather than by position, and refuses to run if the
marker is gone. Verified by re-casting CLEAN GROUND in a throwaway worktree and
watching the guard name the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:45:38 +01:00
slaguru666andClaude Opus 5 ce1103ec0d R-267: a guarded home for the first disabling blow
Another session verified R-266's measurement, agreed the frame was better than
its own, and declined to publish it because the figure had no guarded lineage a
doubter could re-derive. It was right, and the fix costs 0.7s of build time --
which is the number I should have checked before calling it a reading tool and
not a guard.

first-blood.mjs gains --update/--check and is now both the reader and the
measurement of record; check-firstblood.mjs is a thin wrapper over it, the same
shape as check-bestiary. Compares the recorded figures exactly, then asserts only
what a page would claim: first blood lands within 5 points of even in the cut,
and its swing exceeds the six-a-side line's by more than both noises.

Recording it moved the swing from the scratch run's 43 to 40 against a
seed-to-seed spread of 2.1 -- high by more than its own noise, which is the
argument in miniature. update-readme's COUNT_WORD could not reach thirteen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:42:31 +01:00
slaguru666andClaude Opus 5 19ed0b5f0f R-266: the coin is the first disabling blow, and it lands in round two
Reading both ends of the four-player cut — seed 5's sweep and seed 2's wipe —
says the fight is not decided by round five but by whoever lands the first
disabling hit, on average in round two. Both sides disable on one good blow, so
each one thins the return fire and makes the next likelier; nothing pulls a
fight back toward the middle.

tools/first-blood.mjs measures it: 49.7/50.3 on who strikes first, and 23.1% vs
66.5% wipes on either side of that. Six against six is the control at 8.6% vs
22.4%. A reading tool, not a guard — it records nothing, and no figure from it
goes on a generated page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:36:05 +01:00
slaguru666andClaude Opus 5 e84c85be02 R-265: name the denominator the two-down row is zero out of
The round-five race line read "zero in 2000 fights", which invites the reading
that no fight in the run was a wipe. It is zero out of the 84 that reach round
five with two of the three down — a small bucket, and the sentence should say so
before the figure goes to print in a scenario.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 01:17:51 +01:00
slaguru666andClaude Opus 5 8627fa3536 One generator, so a citation names one fight (R-265)
tools/playthrough.mjs exists to show a reader why a measured number is what it is. I cited
it to another session — read seed 2, see the wipe the 45% row is made of — and the
reproduction failed in front of them. They reported different outcomes at both seeds and
guessed the cause correctly from outside: the two tools were not drawing from the same
stream.

They were not. simulate.mjs used a private mulberry32 makeRng; playthrough.mjs had its own
LCG written to look like it. Both deterministic, both reproducible alone, and "seed 2"
named a different fight in each — which breaks the only thing the tool is for. Its own
comment claimed a seed here names the same fight there. check-focus carried a third copy of
that LCG, so the two guards described the same game with different dice.

makeRng is exported and both files use it. A playthrough seed is now exactly the first
fight of simulate.mjs --seed <n>: --runs 1 --seed 2 and the playthrough give 9 rounds, 4 of
4 down, 1 dead, both.

The other half was my citation rather than the code: the command I sent omitted --mode, so
it plays both targeting arms and prints two fights. They read the last line, I quoted the
first. The summary line now names the arm.

check-focus re-recorded under the shared stream; figures move a point or two. What it buys
is that the bimodality analysis now reproduces the published means exactly — 2.31, 3.03,
0.40, 0.42 against the four rows CLEAN GROUND publishes. Under the old LCG it agreed to
within a decimal, which looked like corroboration and was two experiments landing near each
other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 01:11:41 +01:00
slaguru666andClaude Opus 5 ad5fd57568 The harness was fighting a different animal (R-264)
Reading a narrated supporter fight showed it being hit on armR and head. It has wings; it
has never had arms. Three defects, all in the pipeline every published number comes from.

1. It located hits by species, not body plan — simulate.mjs read defender.species where
   the game writes spec.bodyPlan ?? spec.species ?? "baseline" into speciesProfile
   (build-packs.mjs:294). The barghest, kelpie and church grim were fought on two legs
   with arms, and the supporter with no wings, so the fight its tactics call the one the
   agents can win could not occur in a measured fight.
2. Armour was one scalar for the whole creature, and the harness's locations had no
   armour field. Bare wings, the grounded halving and the vital exemption — R-254 and
   R-255 — were invisible to every number. Worn armour was summed the same way, which put
   a stab vest on a cleaner's head.
3. Nothing was ever grounded: a wing could be ruined and the creature kept flying.

Fixed in the game's order — locate, then apply what that location carries, every term
imported from rules.mjs. A ruined wing calls groundedPlanFor and the wounds carry across
by severity through remapLocationDamage, the function _preUpdate uses.

A bug of mine no guard would have caught: remapLocationDamage returns { damage, moved,
rescaled } and my first draft passed the whole object where a damage map was expected, so
every wound a creature carried was forgiven the moment it came down. check-lethality would
have passed it — fewer wounds means a longer fight, which reads as a number moving, and
this commit moves numbers. Found by probing a landing by hand.

20 of 47 creatures moved, 18 deadlier and 2 less. The supporter goes 69.3% -> 25.1% wiped,
3.38 -> 2.16 down, second deadliest to fourth: it was being measured as a 30-hit-point
creature in uniform armour 9 that could not be grounded. The small rises elsewhere are the
party's armour no longer covering locations it never protected.

check-focus then caught the page overclaiming, which is what it is for: focus fire still
helps in all 30 but only 28 clear their own noise where 31 of 31 did. The guard was
asserting more than the page needs — the bestiary prints that count from the artifact and
cannot overstate it — so it now checks only what the page asserts outright, and the page
rewrote itself to "in 28 of them".

Both baselines re-recorded. Minor version, not a patch: the published numbers changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:42:52 +01:00
slaguru666andClaude Opus 5 5dcaaf519a The simulator can be read now, not just totalled (R-263)
Every guard here reports a number per creature, and a number cannot say why a fight went
the way it did. That gap is what produced R-258 through R-260: each written from a side
harness built to watch a fight, each modelling something slightly different from the game,
two of the three wrong in ways no guard could catch because no guard was involved.

runFight takes an optional `say` sink. It reports what the simulator already decided — the
attack roll and its target, a defence and the penalty it was made at, the landing level
after a dodge downgrades it, damage against armour before and after armourAgainst, the
location, major wounds, disablement, death. It never touches the generator, so a narrated
fight and a silent one are the same fight; check-lethality and check-focus both still
match their baselines exactly with the hook in place.

tools/playthrough.mjs (npm run play) is its consumer, in the same commit deliberately: a
hook with no reader is the exact defect this project keeps finding in its own rules, and
adding one to the measurement pipeline with only a scratchpad file calling it would have
been committing the thing I have spent the session removing. Pack size comes from
focus-baseline.json so the fight you read is the fight check-focus measures; creatures
recorded as pinned are played solo.

Three redcaps, seed 20260913, same seed both ways. Spread fire: wiped in 10 rounds, 3
dead, and all three redcaps still standing — 39 hit points spread three ways so that none
of it finished anything. Focus fire: same opening, diverging at one target choice in round
1, party wins in 20 with two up. The +13.2 points check-focus records, seen once instead
of averaged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:30:14 +01:00
slaguru666andClaude Opus 5 104b588e86 R-262: the second R-261, renumbered, deferring to the one that was committed
Two sessions numbered an entry against the same committed log, which ended at
R-260, and both picked R-261. Mine was written first but was uncommitted, so the
other session could not have seen it and had every reason to think the number was
free. When it staged docs/REVIEW_LOG.md my entry was sitting in the file, so it
went out inside 1d915c5 under that commit's message, and HEAD reached origin with
two R-261 headings in it.

Mine renumbers to R-262 and moves below theirs so the log stays monotonic. Theirs
keeps R-261 because theirs is the one that was committed; this diff moves and
renumbers nothing but my own prose, and their entry appears in it only as context.

The hazard is now a matter of record in both directions: their note at the foot of
R-261 caught check-rollable arriving from another session mid-build and handled it
without clobbering anything, and this is the same collision on a file whose
convention is to append to the end. A shared working tree makes the end of
REVIEW_LOG.md the likeliest place for two sessions to meet, and an uncommitted
entry there has no claim on its own number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:16:04 +01:00
slaguru666andClaude Opus 5 1d915c55cf check-focus: the focus-fire claim is an artifact, not a sentence (R-261)
R-258 put a tactic on a GM-facing page from an unguarded harness, R-259 found it worth
nothing, R-260 found the replacement right for the wrong reason. Three corrections, each
landing as prose with nothing checking it — the arrangement that let ddc4f99 describe a
game nobody was playing.

tools/focus-baseline.json records, per creature, the pack size and the spread-fire and
focus-fire win rates. check-focus re-measures all of it on every build and compares
exactly, with the party, seeds, run count and seeding scheme recorded alongside so numbers
taken under different conditions are refused rather than compared. The bestiary READS the
artifact instead of restating it, and check-bestiary refuses a page that has fallen behind
it. Page, guard and simulator cannot disagree.

The pack size is recorded rather than re-chosen: a fight at 0% or 100% cannot show an
effect, and a guard that picked again each run would let a changed creature move quietly
to a different question and pass. 31 of 47 creatures land in the measurable band; the
other 16 are recorded as pinned, with the rate that pinned them.

It guards the claim as well as the numbers. The page says focus fire helps in every fight
in doubt; check-focus fails if any row's gain reaches zero or stops clearing its own
noise. That failure means rewrite the page, not re-record the baseline.

The run count was chosen by evidence. 1000 x 3 seeds costs 8.5s and takes the suite from
2.5s to 13.7s. I tried 500 to halve it and the claim-check failed — at 500 runs one row
no longer clears its noise, so "without exception" is not supported by that much
sampling. Recording twice at 1000 gives byte-identical files.

Negative-tested four ways, all firing: a creature quietly made nimbler (redcap dodge
75 -> 85, spread 40.6% -> 27.6%), a baseline under different seeds, a hand-edited page,
and the claim failing at 500 runs.

Guard eleven (check-rollable) arrived from another session mid-build; this is twelve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:13:28 +01:00
slaguru666andClaude Opus 5 48216bae10 Focus fire across all 47, and the same confound one turn later (R-260)
Measured in simulate.mjs. For each creature, the pack size whose spread-fire win rate
lands nearest 50% — a fight at 0% or 100% cannot show an effect, which is why R-259's
first sample was three-quarters useless — then focus versus spread, 2000 fights across
three seeds.

31 of 47 creatures had a pack size that could show anything. Focus fire helped 31 of 31,
by more than that row's own noise 31 of 31 times, mean +12.8 points. Best is 6x The
margin, 40.4% -> 61.1%. The other 16 are unmeasurable rather than unaffected: eleven are
trivial six at a time, five are hopeless in pairs.

R-259's decomposition was confounded, and I wrote it while correcting a confound. It
credited the defence ladder with 4-5 points by comparing dodge <=30% against dodge >=55%
— but the low-dodge samples were PAIRS and the high-dodge ones TRIOS. Held at a fixed
pack size, dodge is worth 1-2 points. Pack size is the driver: 8.0 points for a pair,
14-15 for three or more, then flat. What focus fire buys is that a dead creature stops
attacking and a half-dead one does not attack any less.

Two failures, one cause: comparing groups that differ in more than the thing being
measured. The control is cheap and I did not run it until the third time.

The bestiary advised concentrating fire and gave the ladder as the reason — right advice,
wrong reason. It now gives the reason that survives measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:03:02 +01:00
slaguru666andClaude Opus 5 995f3fa94e The firing order was worth nothing, measured properly (R-259)
R-258 put a tactic in the bestiary on the strength of a table-side harness — cheapest
weapon first, best weapon last, 10.7 points of win rate across 24 orderings. Measured in
simulate.mjs, the pipeline every published number comes from, it is worth +0.27 points,
with per-seed differences of -1.5 to +1.8 against 2.6 points of noise on a single arm.
Zero.

The toy lied for two reasons, both mine. Its party had one uniquely valuable weapon (an
anti-materiel rifle the borough does not own, so unhalved, worth double anything else) so
there was something to sequence around; the frozen party is 4.09, 3.98, 1.43, 1.43 —
two near-identical pairs. And the toy fixed the order every round, while the game
re-rolls Reaction every round and has no rule for holding an action, which I checked
before measuring. The order is not a decision the rules offer, and I had written table
advice for it.

The page is corrected: the ladder stays, because it is derived and true, and it now says
you cannot choose who goes first.

What is worth doing, measured the same way: committing a round's attacks to one target,
in fights that can actually move (a fight at 0.2% or 100% cannot show an effect and three
of my first four samples were pinned there). Against dodge <= 30% focus fire gains 7.6
points, which is just killing attackers sooner; against dodge >= 55% it gains 12.2, so
about 4-5 points is the defence ladder and the rest is arithmetic unrelated to dodging.
Separating them needed contested fights at both ends of the dodge range — without that
control I would have credited all 13.8 points against three redcaps to the ladder, which
is R-255's mistake again.

runFight gained two hooks, off by default and unused by the baseline: partySequence
rearranges the party within the slots initiative already gave them, leaving enemies where
they fell so the experiment isolates agent order from who acts before the creature; and
partyTargets "focus" concentrates fire. Both roll the same dice, and check-lethality
confirms all 47 creatures still fight exactly as recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:58:22 +01:00
slaguru666andClaude Opus 5 f932a79121 The firing order, in the bestiary, generated (R-258)
DEFENCE_STEP is -30, so a creature's Dodge is not a percentage it has for a round — it is
one it has once or twice. The median dodge in the book is 45% and goes 45 -> 15 -> 1
inside a single round. No player-facing document said so, which hid a real decision:
what order the party shoots in.

The bestiary now has a section on it. Only a landing hit spends a defence, so a miss
strips nothing; each landing hit makes the next attack of that round easier; therefore
the cheapest reliable weapon fires first and the one you most need to land fires last —
against the supporter, the weapon the borough does not own.

Everything it states is generated from DEFENCE_STEP, resolveBands and the statblocks: the
step, the ladder, the median, the spread (13 of 47 creatures stripped by one landing hit,
25 by two, 9 by three) and the list of the hardest to strip. Negative-tested by setting
the step to -15 and -20; the page rewrites itself both times. No win rates are published:
the ordering measurements come from a table-side harness that ignores wound penalties,
bleeding and dying, so the page states the mechanism and quotes no percentages.

My first draft used the dodgiest creature in the book as the worked example — 80%, three
landing hits to strip — directly beneath a claim that the first attack of the round eats
the dodge. True of a modest dodge, false of a good one, and both were on the same page.

tools/bestiary.mjs --check has existed since the document did, with the comment "(for the
guards)", and no guard ever ran it: a check written and wired to nothing, the same shape
as every rule found computed and never read. That was tolerable while the page restated
statblocks and is not now that it derives from rules — change DEFENCE_STEP and the
document lies to a GM. check-bestiary.mjs is the tenth guard, negative-tested both ways
it can fail: a rule change that staler the page, and a hand-edit of the page.
update-readme flagged it as "run by the build, not advertised" before I listed it, and
the README's count word went from nine to ten by itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:51:04 +01:00
slaguru666andClaude Opus 5 c3a337314f One armourAgainst, in the authority, shared with the harness (R-257)
Left open by R-256. Two rules decided how a hit meets armour — armourAgainst (a critical
ignores it, a special halves it, rounded down) and damageAfterArmour (what is left never
goes below zero). Both lived in ringbrp.mjs, which is the game. Neither lived in
rules.mjs, which is the authority, so the guard that forbids redefining a rule had
nothing to forbid. tools/simulate.mjs applied armour from its own inline copies of both,
as it had since it was written.

They agreed, which is the whole point. Nothing published was wrong and no guard could
have said they were two things rather than one. But every number in BESTIARY.md, every
row of the lethality baseline and every measurement quoted from R-251 onward comes out of
that harness: change how a special hit meets armour and the game changes, the numbers
describing the game do not, and all nine guards still pass. Same shape as ddc4f99, with
no tolerance to blame and no reason it would ever have surfaced.

Both rules now live in rules.mjs. ringbrp.mjs imports and re-exports them because they
are public API at game.ringbrp. The simulator imports them. Verified live in the world
that game.ringbrp.armourAgainst and the rules.mjs export are the same function object,
not two that agree.

Proof it changed nothing: check-lethality replays 47 creatures over 2000 fights each and
every one fights exactly as recorded — no tolerance, no drift — across roughly four
million resolved attacks, all of which now go through the moved rule.

check-rules fails any file outside rules.mjs that halves armour or subtracts it inline,
naming file and line. Negative-tested by restoring the harness's original three lines
verbatim (both patterns fire) and by redefining armourAgainst in ringbrp.mjs, which the
name-based guard now catches because the rule finally lives somewhere it belongs to.
Arriving in rules.mjs also tripped the spot-check requirement immediately: eight new
spot-checks, and what a special does to hide 9 is now written down once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:33:58 +01:00
slaguru666andClaude Opus 5 f847cb2171 The sheet header said 9 while the body said 4 (R-256)
Verifying R-255 on a live sheet found the rule working everywhere it was built and
contradicted in the first place a GM looks. The header's ARMOUR field is the hide the
creature HAS; on a grounded beast the hit-location table under it read 4 and the header
still read 9. Nothing computes off that field, so nothing in the code was wrong. It was
wrong at the table.

The field stays editable and stays 9 — that is the creature's hide and what a GM types
into. Under it, when system.grounded is set, the sheet now says what is in play:
DOWN: 4 · CHEST 9, derived from exposedHideFor rather than from a second opinion, with
the reason on the tooltip. Verified live: no badge in the air, and grounded the badge and
the location table agree at 4 with the forequarters at 9.

check-rules forbids redefining a rule by NAME, which misses how this goes wrong in
practice — nobody redefines exposedHideFor, they write Math.floor(hide / 2) in the sheet
because it is cheaper than an import. I wrote that version first and check-rules passed
it. It now fails any file outside rules.mjs that halves a hide inline, naming file and
line; check files are exempt because stating the expected value independently is what a
test is for.

That guard also turned up something I have not fixed, recorded in R-256: armourAgainst
lives in ringbrp.mjs rather than rules.mjs, and tools/simulate.mjs has its own inline copy
of it. They agree today so no published number is wrong, but every figure in the bestiary
and the lethality baseline comes out of that simulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:29:41 +01:00
slaguru666andClaude Opus 5 c0d33ee81d Hide 9, grounded 4, vital exempt — the borough is worth 56 points again
R-254 shipped the belly rule and called +6.4 points "modest on purpose". That read the
wrong statistic. +6.4 measured whether grounding helps; the question was whether the
borough still matters, and the gap between fighting the supporter on borough ground and
off it had fallen from 60.6 points to 44.9. Half damage from every departmental weapon is
meant to be the fight's central problem, and I had quietly taken sixteen points off it
while reporting a small number about something else. Same error as R-251.

Three changes that only work together:

  - hide 7 -> 9, restoring the weight on the axis that should hurt
  - grounded still halves it, 9 -> 4, which an ordinary round beats
  - exposedHideFor now takes the location's kind and leaves `vital` unhalved: a beast on
    its belly is not presenting its chest to the floor

Measured: 28.4% on borough ground flying, 68.4% grounded, 84.6% off it — a 56.2 point gap
against 44.9 before. Predicted 28.4 / 84.3 / 55.9 before building.

The composition of the three hide functions lives in prepareDerivedData where no test
could reach it, and check-anatomy's guard 6 is deliberately one-sided so it would not
have caught the vital losing its exemption. check-behaviour now composes the same
expression from the real supporter spec and the real tables. Negative-tested four ways:
removing the exemption, un-baring the wings, removing the halving, and inflating the hide
past what a round can beat each fail it with the right message.

Cost: the supporter goes from fifth to second deadliest creature in the book (2.84 -> 3.38
down, 46.8% -> 69.3% wiped). check-lethality refused the build until that row was
deliberately re-recorded.

BESTIARY.md printed the new hide and said nothing about it halving, because the body-plan
blurb predates the rule. The README said "eight guards" in three sentences above a
generated block listing nine. Both are the signature defect in prose: a number written
down that nothing reads. Both now generate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:15:35 +01:00
slaguru666andClaude Opus 5 ca040b7926 The belly is not armoured (R-254)
R-95 made losing a wing change the animal. R-96 took the hide off the
wing so losing one was possible. Separately each was right and each was
measured. Together they gave the party a soft target and deleted it the
moment the party hit it:

  winged     6/20 ranged faces land on armour 0 (a bare wing)
  quadruped  0/20

A service round on borough ground averages 3.75 after halving — 3.75
through a bare wing, 0 through hide 7 — so grounding the creature took
the party's chance of winning from 44.9% to 1.4%, while its own tactics
told the GM that grounding it was the fight they could actually win.

Found by measuring something else. Every aimed-shot configuration made
the party worse, and the most aggressive was the worst: aiming at a wing
at -20% grounds it in 99% of fights and HALVES the win rate to 10.1%.
Buying the objective reliably was the fastest way to lose, which only
makes sense if the objective is a trap. So no aimed-shot rule; the
problem was never the lack of one.

It also retires the "+31 points" I reported for grounding at 1.7.3. That
was win-rate-if-grounded against win-rate-if-not — selection bias, since
the parties that grounded it were the ones shooting well. Forcing the
grounding gives the causal value and it was -43. A correlation of +31
and a causation of -43 out of the same mechanic.

The fix: a heraldic beast is armoured the way it is drawn, across the
back and the flanks. On its belly half that hide is no longer in the
way, so exposedHideFor halves natural armour while system.grounded is
set. Hide 7 becomes 3.

                      flying   grounded   worth
  on borough ground    45.3%     51.7%    +6.4
  off borough ground   90.6%     97.9%    +7.3

Modest deliberately. Grounding should help, not decide — the borough is
still the real answer. Losing its damage modifier was worth +26.5 and
would have made the wing the whole fight.

check-anatomy gains a sixth property: a flyer must not be harder to hurt
on the ground than in the air. The first version probed at a single
4-point round and declared the fix broken, because 4 sits inside the one
band where a bare wing beats a halved hide — below 5 damage the creature
really is better off grounded, above it much worse. Integrating across
1-12 gives 2.83 flying against 3.75 grounded and reads it correctly.
Negative-tested against 1.7.8's behaviour, which it names.

The lethality baseline is unchanged: its simulation never grounds
anything, so system.grounded never comes up there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:57:31 +01:00
slaguru666andClaude Opus 5 97fe2704e8 Measure creatures over 2000 runs, not 200 (R-253)
R-252 established that a recorded figure carried several points of slack
at 200 runs and that only more runs narrows it. This is that.

Measured by taking every creature under four different base seeds and
looking at how far the four answers sat apart, which is the noise a GM
reading the bestiary is unknowingly trusting:

                   mean spread    worst creature
  200 runs          1.28 points     11.5 points
  2000 runs         0.36 points      2.7 points

The worst case is what mattered. One creature's published wipe rate
could be eleven points from the same creature rolled with different
dice, on a figure printed in docs/BESTIARY.md for somebody to plan an
evening around. Under three now.

It corrected 19 of 47 published figures, mean 0.35 points: the arrears
6% -> 8.9%, the supporter 44% -> 46.8%, the nuckelavee 13.5% -> 16%.
Each was a sampling artefact printed as a property.

The cost is the part worth recording, because it is why this was never
done. I guessed "maybe a minute of build time" when I suggested it,
which was wrong by a factor of thirty and would have been a fair reason
to say no:

  check-lethality alone   0.29s -> 1.88s
  whole nine-guard suite           2.55s

BESTIARY.md regenerates from the baseline, so its run count follows
automatically; its header now also says the seeds are derived per
creature, since "at seed 11" stopped being the whole truth in R-252.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:33:40 +01:00
slaguru666andClaude Opus 5 2dbea88d4b Seed the lethality sim per creature (R-252)
Each creature now derives its own seed from its key, FNV-1a mixed into
the base seed, instead of every one of them being measured against seed
11.

This is the second reading of the request and the one I had not tested.
What I checked before was position independence — whether a creature's
numbers depend on its neighbours — and that was already true and still
is: inserting a creature ahead of the barghest changes 0 of 47 existing
rows under the new scheme, same as the old. What was NOT true is that
each creature had its own dice. All forty-seven faced the same two
hundred sequences.

Measured, because the argument for doing it is better than the result:

                                  shared 11   per-creature
  mean wipe rate across bestiary     5.67%        5.67%
  mean agents down                   0.538        0.522
  rows changed                         --        34 of 47

Seed 11 was not biasing the book. There was no systematic luck to
remove, and the aggregate is unmoved to two decimal places. What the
change buys is decorrelation: the error in each row no longer comes from
the same draw as every other row.

The useful number fell out of the comparison rather than the change.
Individual creatures moved up to four points of wipe rate purely from
being handed different dice — the courier 3.5% -> 7.5%, the long walker
66.5% -> 62.5%, quarantine unit 84.5% -> 88.5%. That is the sampling
noise inside any single recorded figure at 200 runs, and it means these
numbers are an exact regression fingerprint and a loose description of a
creature at the same time. Only more runs narrows the second; more seeds
does not. R-252 says so on the page.

seedMode is recorded alongside the numbers and checked, because changing
how a seed is derived moves every row without changing SEED itself. A
baseline from the old scheme is now refused rather than compared against
this one silently and wrongly — verified by running the new code against
the old file before re-recording.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:28:45 +01:00
slaguru666andClaude Opus 5 e54ce9130d R-251: measure what ddc4f99 actually did, four commits late
The baseline needed no re-recording — e5dc9b5 already captured these
numbers, it matches HEAD's code exactly, and it differs from 4b71859's
in precisely the 26 entries ddc4f99 moved. What was missing was any
record of WHAT that change did, since it was folded silently into a
commit about wings.

Isolated by replaying 322389b and ddc4f99 against the same party, seed
and counts, so the figures are ddc4f99's own and not contaminated by the
four commits after it:

  creatures whose numbers moved          26 of 46
  deadlier                                3
  less deadly                             8
  unchanged wipe rate, moved elsewhere   15
  mean change in wipe rate               -0.17 points

Routing every blow through a hit location REDISTRIBUTED lethality
without raising it. Creatures with one big attack got slightly less
dangerous, because a blow that used to come off a single pool now has to
beat the armour covering wherever it landed; creatures that attack often
got slightly more dangerous, because they now accumulate damage in a
limb and disable it. Across the bestiary those cancel to within a fifth
of a point. Largest moves: the courier 6% -> 3.5%, the arrears 7.5% ->
9.5%, the stanchion 26% -> 28%, the long walker 68% -> 66.5%.

The log entry also records the two corrections I owe this thread, both
from explaining before measuring: the seeding was always per creature,
and the claim in check-lethality's header that I called untrue was true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:24:34 +01:00
slaguru666andClaude Opus 5 7f8355f0d0 Add the player primer and cheat sheet, and brand every journal
Ten pages a player reads before session one, in their own compendium at OBSERVER
ownership: nine of setting, one cheat sheet.

The draft called the organisation "the Bureau"; everything else in the game calls it
the department or the agency, so that is what it is called. Thin areas filled rather
than padded: the seven character principles as a list, the real Coherence band table
instead of a paragraph describing one, and "what continuity is not" pulled out of the
middle of a page because it is the most important sentence for a new player.

The cheat sheet's numbers are computed from rules.mjs and lang/en.json at build time.
A hand-typed one would have been the sixth time this project shipped a number that
disagreed with the code, and the one the players were holding.

R-250: the design tokens were never on :root. Journals rendered as default parchment,
and four things were wrong in sequence — v14's hook is renderJournalEntrySheet; its
`element` is the node, so `[0]` returns the first CHILD and put the class on a header
button; there is no .journal-page-content in v14; and then the real one: with the
selector matching, font-size, line-height and padding all applied while every colour
did not, because the palette is declared on .ringbrp-sheet/.ringbrp-chat/.ringbrp-window
and never on :root. A journal inherited no tokens, so every var() silently became
nothing. A rule that HALF applies is not a specificity problem — I spent three attempts
treating it as one, including a version bump on a cache theory that was wrong.

Branded: primer, rulebook, starter GM text and handouts. Scoped by flag, so other
modules' journals are untouched. Version 0.9.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 10:19:41 +01:00
slaguru666andClaude Opus 5 d9bce12c27 Replace the starter with BLACK PLATFORM, and make the dialogs ask questions
The starter is now an action case: a train running through a station whose rails were
lifted in 1976, two engineers aboard, eleven minutes until it returns. Descend, take the
platform, board, rescue, fight forward along a moving train, stop it.

Simpler than PAPER WINDOW on purpose — one clue scene, no social subsystem — which also
means it cannot reproduce R-248, because there are no groups or standings to leave
unwired.

Every agent can fight, deliberately: floors of firearm 42%, melee 40%, Brawl 40%,
Dodge 40% so nobody is a spectator on their first evening. Floors only RAISE, so a
posting already better stays better — Callum's shield lands at 53, not the floor's 45.

The builder now handles combat floors, extra weapons issued for a case, and a scenario
with NO Borrowed Authority at all; the last was assumed to exist and would have thrown.
Two new tests: every floor names a real skill, and every agent clears the fighting
floors the handout promises.

Sixty-two dialog strings reworded. They were naming mechanics at people: "Situational
modifier %" is now "Anything else helping or hurting? (%)", "Target cover" is "Is the
target behind anything?", "Difficult (÷2)" is "Difficult — half your skill". Labels ask
the question; hints say what it does to the number; notifications say what to do next
rather than what went wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:08:07 +01:00
slaguru666andClaude Opus 5 4e28de2a64 Play PAPER WINDOW with the shipped pregens; fix four silent defects
Ran the starter end to end. It closed, and it found four defects in what I shipped an
hour earlier — all in the scenario data, all silent.

R-247: the Borrowed Authority did not work. I wrote scope "operate", which is not one
of the five scope tiers; an unrecognised scope ranks 0 and every action came back
"impossible" with no error. The first scene of the starter is the Borrowed Authority
tutorial. Fixed, guarded in check-kits, and covered by a test that documents the
failure mode.

R-248: the contact subsystem was unreachable in the scenario built around it. The case
file carried a standing for the Returned Clerks and no GROUP ITEM; resolveContact
returns null without one, so READ, SIGNAL and OFFER did nothing. Groups are data now,
and a test asserts every standing on a shipped case file has a group behind it.

R-249: legs wrote `kindId` where resolveLegDialog reads `leg.kind`. It worked only
because the terrain text happens to begin with "Extraction".

Not a defect: phase four went Difficult because Resources hit 0 and destitution makes
every remaining phase Difficult. Best beat in the session.

All four phases failed and the case still closed — Containment 8 to 2, Resources 8 to
0 — which is what the scenario is built to survive. Two of six closing requirements
succeeded and the settlement still completed. The Cancellation Man opened with a
critical against a raised riot shield and went through 10 points of armour, teaching
the shield and graded-defence lessons in one roll.

Also documented: nobody on the starter team can aim the return, because only the Anchor
Officer trains Transposition. The extraction page now says to narrate it rather than
roll six dice that will all fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:39:43 +01:00
slaguru666andClaude Opus 5 49c2e9fc7b Add PAPER WINDOW as the starter scenario Adventure
The six-player introductory case ships as a Foundry Adventure in a new `starter` pack,
because a scenario that arrives as nine loose documents a GM has to assemble is a
scenario that gets read rather than run.

One import brings in: the GM scenario in twelve pages; the seven player handouts as
their own journal at OBSERVER ownership so they can be revealed one at a time without
the GM editing permissions first, while the GM text stays hidden; the six pregens built
through the same buildActor path the shipped pregens use, so they arrive with full skill
lists, kit, hit points and linked tokens; the cast, including the Cancellation Man; the
Borrowed Authority as a real credential with scope, Trust 2 and its three red flags; and
the case file already at Containment 8 / Resources 8 / Clearance 6 with its four phases
loaded at the stated difficulties and modifiers.

Pregen ratings are fixed at mid-band rather than rolled — a pregen that varies between
installs is not a pregen. The expansion from posting or trade into a sheet reuses the
register and the induction rule rather than restating them, so the starter cannot drift
from the generator.

Verified by importing it: no errors, the roster resolves through stable keys, handouts
arrive player-visible, Nia has the team's highest Tradecraft as the text requires,
Callum's shield is slung so raising it is still a decision, the three probationers show
the trade/induction split, and phase one runs out of the box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:25:10 +01:00
slaguru666andClaude Opus 5 43508259ac Consolidation pass after a Codex review
Four real defects, each verified against the source before acting on it.

Trade expertise was coupled to agency rank. A probationary burglar came out at 35-50%
and a veteran at 65-80%, as though fifteen years of picking locks were something the
department conferred. TRADE_BANDS is now fixed at 50-65/30-50 at every rank, and
INDUCTION scales with service instead (20-32 probationary to 45-60 veteran) because
that half genuinely is the department's. The two axes were the wrong way round. This
is the thing I flagged myself after PAPER HARBOUR and then left alone — flagging a
defect is not the same as fixing it.

Bonus points were sprayed across every skill in the game. grantFullSkillList runs
immediately before the loop and puts all fifty-nine skills on the sheet, and the loop
picked from actor.items, so a veteran's ninety points landed anywhere. The comment two
hundred lines above has always said "over the core skills"; the code never did.

"Random species" passed "baseline" into generators that support random perfectly well.

No behavioural tests. The strongest point in the review: the three guards are static
and none of them can tell whether a rule is READ, which is the only kind of defect this
project has ever shipped. tools/check-behaviour.mjs now runs 28 deterministic tests,
gated into the build, covering R-231/232/243/245/246, graded defences and the lamp
rule. Proved it bites by re-breaking R-246.

The register moved to postings.mjs, a Foundry-neutral module the engine, rulebook,
guards and tests all import normally — they were previously pulled out of the engine
with a regex and eval, which worked and was a trap. My first version of that used
`export … from`, which re-exports without local bindings, so ROLES was undefined and
the system threw on init: node --check passes that, loading it in Foundry does not.

Also: the contact doc comment still said "only OFFER moves standing"; check-kits
accepted mas/app, which are the DISPLAY names of siz/cha, so a posting declaring one
would have passed and produced an undefined characteristic; package.json disagreed with
the manifest; README counts were three revisions stale and are now generated at build.

Deferred with reasons: splitting the 6,700-line engine into modules. Right, and a
multi-session refactor whose risk is exactly the silent breakage demonstrated above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:57:20 +01:00
slaguru666andClaude Opus 5 c60485de72 Play PAPER HARBOUR with the criminal, and fix R-246
A pier that closed in 1974, a beacon still transmitting, and a probationer recruited
out of a city with a caution the department has quietly lost. The generator's gap —
somebody uses that agent's name before it is given — landed on him in phase 2, and he
held it on Fast Talk 14 against 48 while the Field Lead missed the read.

The case turned on a warrant card: the keepers of the ground want relieving by somebody
official enough to take the duty on, and every agent carries exactly that. Bargain 27,
favourable, rolled 33 — impression -1 to +1. The one unambiguously his moment was the
log the first team locked in behind them: 11 against Sleight of Hand 50.

R-246: a fumbled greeting cost nothing. contactOutcomes has said fumble/signal -1 since
the social pillar was built, and impression was adjusted only inside the `offer` branch
— so the -1 was computed on every botched greeting and thrown away. Impression now
falls on a fumble at any stage and still only rises on an offer; the narrower fix
matters because read's delta counts FACTS LEARNED, not standing, so applying the delta
always would have paid the team impression for doing research.

Also logged: a probationary criminal is out-sneaked by a career Field Lead (47 to 60)
because trade skills use the agency tier bands — defensible, flagged, not changed. And
one invented step of my own: I rolled Transposition to get them home after the
extraction phase had already put them out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:35:37 +01:00
slaguru666andClaude Opus 5 b8430c284b Play PAPER MARGIN with a trade character
A farm in Angus, a person in two places, and a probationary doctor recruited from A&E
alongside two career officers. The generated case turned out to be its own best
argument for trades: the truth is that there is no original left, both are copies, and
telling two identical people apart is a medical question — Medicine on that team reads
42, 1 and 1.

Both halves of the trade design showed up. She was the only one carrying the
department's own vocabulary (Anomaly Lore 37 against 0 and 0, Tradecraft 31 against 5
and 5), and she was hopeless at closing: Persuade 23, rolled 95, read on Insight 60.

The examination failed by three and the bloods came back critical on 02 — both samples
are the same sample. Neither career officer could have found it.

R-226 fired visibly: trained Tradecraft 31 lost the phase lead to trained Navigate 34.
The riot shield issued to Containment two commits ago turned up unprompted and took
Kobe from 7 everywhere to 10 on torso and arms. Coverage applied to the opposition too
— the duplicate died through its bare leg.

One probe error recorded, not a defect: endOfCase returned nothing because my ad-hoc
rolls passed no skillFamilyId, so nothing ticked — the same omission R-222 found in
resolveLegDialog, reproduced by hand. Named properly, Medicine ticked and improved
42 -> 48%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:55:36 +01:00
slaguru666andClaude Opus 5 79acc8227f Ten trades, an induction layer, and rolling the stats yourself
TRADES: ten ordinary jobs the department recruits from — construction worker, doctor,
professor, engineer, law enforcement, IT specialist, scientist, linguist, entertainer,
criminal. They are not postings. A posting is what the department made of you; a trade
is what you were before any of it.

A trade character is built in two halves. The trade is genuine expertise on the same
bands a posting's skills use — a doctor is a proper doctor. INDUCTION goes on top at
25-40%: Tradecraft, Anomaly Lore, First Aid, Firearm (Pistol), Dodge, plus a sidearm, a
vest, cordon kit, a cover identity and a ward. The band is low on purpose; six weeks is
not a career, and that gap is most of what it feels like to play one.

Induction never demotes: a former police officer keeps pistol 58 from the trade rather
than being re-taught it at 26.

The creation dialog gains a "Recruited as" switch with its own trade list and card
(which shows both halves), and check-kits now holds trades and induction to the same
standard as postings — both new guard paths verified by breaking them.

Rolling them yourself: a tickbox that walks the eight characteristics one at a time,
each roll announced to chat so the dice actually fall, "Take them" locked until all
eight are down. A hand-rolled character is taken exactly as it fell — no best-of-three
on the key stat, no species shift. Verified both ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:48:06 +01:00
slaguru666andClaude Opus 5 53eb72c100 Issue riot shields to Containment and the Night Warden
Both postings draw a riot shield, and both are trained to use it — issuing one without
the skill would have been R-212 again with a different item, the Antiquities Officer
who drew a flintlock and rolled 5% with it.

melee_weapon:shield is SWAPPED into each support list rather than added, so five core
and five support are preserved and the shield arrives without a free power bump.
Containment drops stealth (a posting whose job is being the thing in the doorway was
always the least stealthy in the department); the Night Warden drops navigate (they sit
with one site all night).

check-kits now requires the Shield skill for an issued shield, the same way it has
always required training for an issued weapon. Verified by removing the skill and
watching the build refuse.

Four generated agents: shield skill at 25-30%, nobody encumbered, though a Containment
officer lands at 32.9 of 33 kg. The shield ships slung — raising it is the choice, and
for the Warden it is the difference between 2 points of armour and 5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:37:46 +01:00
slaguru666andClaude Opus 5 0c2636d543 Play the bell tower stair: shields, and what they cost
Two officers, identical kit — riot shield, stab vest, 12 hp, Shield 65% — and one
difference: one holds the shield up, one keeps it slung and shoots. Raised it is most
of their armour (torso 5, arms 3 against torso 2, arms 0 with it down).

Four ordinary blows turned aside cost the shield nothing, which is the right call: at a
point per parry a 5-wear riot shield would have been kindling before the fight started.
Round 8 a CRITICAL halberd was parried, dragged down one step, and the shield took 2
wear for absorbing it while the reduced blow still landed on the covered arm for 7.

Both officers were killed through the gaps a riot shield does not cover — one through
both legs, one through the head for 10 against a location maximum of 5, dead outright
rather than dying (R-227 holding).

The ranged penalty then decided it. Shield up, the survivor shot at 65% less 20% and
rolled 93, 89, 73, 65. That last number is the rule in one figure: 65 hits at 65% and
misses at 45%. The redcap was on 3 of 13 and walked away.

Nothing needed tuning. Full log in docs/REVIEW_LOG.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 21:08:12 +01:00
slaguru666andClaude Opus 5 c61700566c Shields: armour you raise, and the only armour you can defend with
A shield is the one piece of armour you hold, and it protects you exactly as much as
you are currently holding it up. Raised, its points apply to what it covers and it can
parry on a new Shield skill. Lowered, it is weight. The price of keeping it up is that
it occupies a hand and you are shooting around it: -20% to your own ranged attacks,
printed on the card like everything else. Only one shield goes up at a time.

Seven of them, 2-point buckler to 6-point ballistic shield; the pavise and barrier
plate cover legs too because they are walls you carry.

`degradation` finally does something — it has been on every armour item since the packs
were first built and was read by nothing. A blow costs the shield a point of wear for
every step it stood above an ordinary success, so an ordinary hit costs nothing, a
special 1 and a critical 2. At its limit it comes apart, drops, and will not be raised
again.

Two defects found by driving the real functions rather than reading the diff, both
introduced by this change:

- Every shield defence quietly rolled DODGE. "shield" passed through
  defenceTypeAllowed unchanged, so neither parry branch matched and it fell through.
  Invisible in the card; it showed up as eight rolls at a supposed 85% returning six
  failures. Taking a blow on the shield IS a parry, so it is now a flag on a parry
  rather than a defence type of its own.
- The wear rule was unreachable: it fired only on a clean stop, but under graded
  defences only a critical fully stops a critical, so a shield would never have taken
  a scratch. Wear is for what the shield ABSORBED — dragging a critical down to a
  special is exactly when shields splinter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:25:30 +01:00
slaguru666andClaude Opus 5 016ea1c4ab Play the chantry ruin: coverage measured against points
Two officers from the same pregen, same hit points, same Sword and Dodge, armoured so
that the one carrying MORE points carried it in FEWER places: a breaching suit at 7
points on all seven locations against a cuirass and helmet at 9 points on three.
Against a redcap with an 80% halberd, where fourteen of twenty melee locations are
limbs.

The cuirass and helmet stopped fifteen points across three connecting blows and it did
not matter: the two hits that found a bare leg did every point they rolled, destroyed
the leg (12 against a maximum of 6), and started a six-round dying clock. Under the old
rule that leg carried 9 points and both blows would have done nothing.

The suited officer took the same weapon on a limb for zero and went nine rounds
untouched — then a CRITICAL halberd against a successful dodge landed one step down as
a special, halved the armour, and put 9 through a 7-point suit. Full coverage is
resilience, not immunity.

Full log in docs/REVIEW_LOG.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:56:31 +01:00
slaguru666andClaude Opus 5 6ed9ded362 Armour covers what it covers, and Coherence moves to the vitals
Asking for the type of armour per location turned out not to be a display request.
Every equipped piece added its points to EVERY location, so a Brodie helmet protected
your shins and a cuirass protected your skull — with a catalogue containing sallets,
lobster-tail helmets and three cuirasses, that could not be printed without the sheet
saying something visibly false.

Armour now declares coverage as a named set rather than a list of locations, because a
piece has to fit whatever body wears it: all, torso, torsoArms, torsoLimbs, head. The
sets expand to location KINDS, so a cuirass covers a baseline torso and a vesh trunk
and ridge without either knowing the other exists. Unstated coverage is `all`, so
anything that does not declare behaves as everything did before. Eighteen of the
twenty-five catalogue pieces are not whole-body and now say so.

The trap was double counting: hitLocationRoll subtracted the global worn total PLUS
the body's own armour at that location, which was right while armour was global and
becomes a double count the moment a location knows its own pieces. It reads the
location's total and nothing else, verified by firing fourteen hits of 10 damage
across every location and asserting damage applied == 10 - that location's armour.

A single ARMOUR number no longer exists, so the header shows the torso with "0-8 by
location" beside it when the rest of you is not that. This is a real difficulty
change: limbs are frequently bare and limbs are 12 of 20 on the melee table.

Coherence moves out of the derived strip into the vitals at hit-point size, with its
band under it and the box reddening as it falls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:33:04 +01:00