4d7d93abc8a00cc916004c51fdc84c3d44dfea7d
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |