Commit Graph
7 Commits
Author SHA1 Message Date
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 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 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 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 414dbd7037 simulate --spread: one number was the wrong instrument
THROUGH TRAIN Act Three publishes a single figure for its standoff - "the party is
wiped in 24% of runs" - and tells the GM not to soften it. Measured against every party
the duty roster can field, that fight runs from 0% to 100%.

It is not that the published number is wrong. It is that no single number can be right
for this fight, because the answer was settled at character selection: four armed
postings walk it, four trades are massacred, and the GM reading one figure is reading
somebody else's session.

--spread measures the encounter against every C(16,4) party - 1820 of them, exhaustive
rather than sampled, about a minute - and reports the floor, the median and the ceiling
with the parties that produce them. Exhaustive on purpose: a GM planning a session
wants the actual worst case, not an estimate of it.

It also prints each agent's effect on the wipe rate averaged over every party they
appear in, which is the line that gets used at the table. For the Act Three standoff:

  okonkwo   -43.2 points        ashcroft  +16.1
  sandoval  -17.7               nkemdirim +13.5
  holloway  -15.9               ferriby   +12.4

Okonkwo is worth forty-three points of wipe rate on his own. That is a scene-shaping
fact about the encounter that no amount of re-measuring the median would surface.

The published tables are NOT rewritten here. What to do about a 100-point spread is a
design decision - constrain the party, print a range, or rebalance the fight - and it
belongs to whoever wrote the scenario.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:17:28 +01:00
slaguru666andClaude Opus 5 94479641b3 simulate: locate the damage
The harness measured a game in which every blow went to a general pool. That was
tolerable while every creature was a baseline human and merely wrong once the bestiary
had a Cadence in it: a creature whose entire mechanical identity is being whittled down
body by body was being measured as a person with an odd portrait, and the number it
printed was about nobody.

locationFor and resolveLocationHit move from ringbrp.mjs to rules.mjs, and the
category mapping inside woundPenaltyFor becomes woundPenaltyFrom beside them, for the
reason applyDifficulty and resolveBands moved before them: node cannot load the engine,
and the harness may not own a second copy of a rule. All three arrive with the
spot-checks they never had - 103 rules now, 346 formulas verified.

What the loop does now: 1d20 against the DEFENDER's own species table, melee finding
limbs and shooting finding centre of mass; damage to that location and to the general
pool, so the two agree; disabled at the location maximum and destroyed at twice it,
cumulatively. Then the consequences actually apply - a destroyed head is unconscious
immediately (conditionFor has always accepted that flag and was never passed it, so a
headshot used to leave the target swinging), a ruined arm costs -30 to every attack
including shooting, a ruined leg costs -30 to dodge, and each Cadence body lost costs
the whole creature -10 to everything.

Verified as mechanics rather than as numbers that moved: no species can return a
location it does not have across all 40 rolls; vesh and cadence have no head at any
roll and cadence has no vital either; a vesh ridge is hit 3/20 where a human head is
1/20, which is the "long target rather than a small one" the tables were designed for.
A built Cadence has five bodies at 4 points each and no vital; a built Vesh has six
locations with the ridge the largest.

Measured consequences, seed 11, 400 runs, four duty-roster agents:

  keepers x6      31.0% -> 36.5% wiped
  the_choir x11    0.3% ->  0.0% wiped, and now degrading as bodies go rather than
                   being a person who cannot be shot in the head
  long_walker      42.0% wiped, which nothing had ever measured

Still no bleeding, panic, Coherence, cover, range bands or fire modes. Still a floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:05:25 +01:00
slaguru666andClaude Opus 5 2c547efe72 simulate: a lethality harness that uses the real rules
THROUGH TRAIN publishes "MEASURED, 400 runs each" and a 24% wipe rate; the starter
publishes its own table. Nothing in this repository could produce either number, so both
were claims rather than results and neither could be re-checked after a rule moved.

The harness knows no rules of its own. Everything comes out of rules.mjs, because a
harness with its own copy of the combat loop measures a game nobody is playing and
drifts silently the first time a rule changes. Two things had to move for that to be
possible:

  applyDifficulty, resolveBands and gradeRoll -> rules.mjs. The whole of d100 resolution
  lived in ringbrp.mjs, which node cannot import. All three are pure, so they moved and
  the engine re-exports them; no caller changes. check-rules then refused them for having
  no spot-check, so they now have fifteen, covering the 1% floor, the 96-99 rule and
  00-always-fumbles. They had none in their entire life inside the engine.

  expandFromRegister -> tools/expand-spec.mjs. A roster agent is written as a job and a
  rank and carries no skills or kit; everything it can do is derived at build time by a
  function locked inside build-packs.mjs, which cannot be imported because importing it
  runs the build. A simulated agent expanded by a second copy of that logic is not the
  agent that gets packed. Moved verbatim; both sides import one copy.

Deterministic throughout: same creature, party and seed, same numbers, which is what
lets a published table be verified rather than remembered.

What it finds: for the Act Three standoff the seed barely matters (0.5-2.8% wipe across
five seeds, eight rounds throughout) and party composition dominates everything. Four
support agents against the six miners wipe 92.8% of the time; swap in the heavy and the
warden and it is 0.3%. The published 24% is not a property of the encounter, and no
single figure can be.

Models Reaction re-rolled each round, banding, graded defences, the stacking defence
penalty, damage by band, major wounds and the dying clock. Does NOT yet model hit
locations, bleeding, panic, cover, range bands or fire modes, all of which make a losing
fight worse. Read the output as a floor, never a ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:10:05 +01:00