diff --git a/README.md b/README.md index 74f92d8..60783eb 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ check-creatures: OK — 102 actor specs across 8 sources, every characteristic, check-powers: OK — 41 powers classified, 2 wired and each one measurably changes its fight, 14 recorded as beyond the harness with a reason, 25 not fight rules check-anatomy: OK — 5 body plans, 36 locations, d20 complete in both modes, every location drawn, every kind handled and destructible, 20 plan changes carried 432 wounds with their severity intact, 1 flyer(s) with a survivable landing check-lethality: OK — 47 creatures, every one fighting exactly as recorded against the frozen party -check-focus: OK — focus fire measured against 31 packs, helps in all of them, 31 of them by more than their own noise +check-focus: OK — focus fire measured against 30 packs, helps in all of them, 30 of them by more than their own noise check-firstblood: OK — the first disabling blow is worth 40 points of wipe rate in the four-player cut, 15.4 in the six-a-side line check-attackers: OK — three of the four-player cut disable on 11.1% of their attacks or better, and one of them on 0.1% check-fight-tail: OK — a fight past 15 rounds wipes the party 1.4x as often in the four-player cut and 2.4x at six a side, and nothing was truncated diff --git a/docs/BESTIARY.md b/docs/BESTIARY.md index 981db32..44252e6 100644 --- a/docs/BESTIARY.md +++ b/docs/BESTIARY.md @@ -88,7 +88,7 @@ measured in the simulator and came to nothing distinguishable from noise. **What you can do is kill them one at a time.** Against a pack, every agent putting their attacks into the same creature beats spreading them — in **every one** of the -31 fights in this book whose outcome was ever in doubt, without exception, and by more +30 fights in this book whose outcome was ever in doubt, without exception, and by more than the measurement’s own noise each time. It is worth about **8 points** of win rate against a pair and **11 points** against three or more; the best case in the book is diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index 7395375..89e80a8 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -6403,3 +6403,48 @@ fights solo, read the wipe column when the agents-down column was the one that m explained a power by a mechanism that does not hold. Each was found by somebody asking me to act on it. The entry is worth keeping in that state rather than tidying: it is the clearest example in this log of confident, internally consistent, repeatedly wrong. + +## R-278 — raising SCAN_RUNS found that SCAN_RUNS had never been read + +R-277 left the redcap's recorded pack size one seed in eight from being 2 instead of 3, so +the instruction was to raise the scan until it stopped flipping. Measured rather than +guessed: still unstable at 3000 and 4000, stable across twenty seeds at 6000. Raised it. + +**Then the re-record took fifteen seconds, which was impossible.** Fifteen times the scan +work cannot cost what 400 runs cost, so I read the call: + +```js +const winRate = (foes, targets, seed) => { for (let i = 0; i < RUNS; i++) ... +... +const w = winRate(Array(n).fill(spec), "random", seedFor(...), SCAN_RUNS); +``` + +**Three parameters, four arguments.** JavaScript discards the fourth in silence, so every +scan this guard has ever run used `RUNS`, and `SCAN_RUNS` — declared, commented, passed — +has never been read by anything. Changing it from 400 to 6000 also did nothing, which is +how it surfaced: the fix I was asked to make was inert in exactly the way the thing it was +fixing was. + +That is the defect this project names as its own signature — a value computed and never +read — living inside a guard, undetected by seventeen other guards, for as long as the +scan has existed. **No guard can catch it.** The constant is used at its one call site and +the call site looks correct; only the clock disagreed. + +**What the honest scan changed.** `winRate` takes `runs` now, and the re-record costs 49 +seconds instead of 15: + +- **the redcap is still n=3, gain still 6.3** — the answer R-277 gave was right, and is now + arrived at stably rather than luckily. +- **the_arrears drops out of the measurable band**, 31 packs to 30. Its best size was chosen + on 1000 runs and does not survive 6000, which means it has been carrying a recorded focus + gain that the scan would not now stand behind. +- **the_committee moves n 2 to 6** and stays pinned either way, so nothing is measured there + and nothing changes. + +**The claim check still passes**: focus fire helps in all 30, all of them clear of their own +noise. Bestiary regenerated, and it prints 30 where it printed 31. + +**The lesson is not about arity.** It is that the only signal was a number being too small, +and I nearly did not look — a fifteen-second re-record is good news, and good news is what +nobody investigates. The same instinct that made the README quieter as the build got worse +(R-268) made this cheaper as the scan got weaker. diff --git a/tools/check-focus.mjs b/tools/check-focus.mjs index 99e8806..31071a4 100644 --- a/tools/check-focus.mjs +++ b/tools/check-focus.mjs @@ -51,7 +51,13 @@ const RUNS = 1000; const SEEDS = [11, 4242, 90210]; const SEED_MODE = "per-creature"; const SIZES = [2, 3, 4, 5, 6]; -const SCAN_RUNS = 400; +/* R-278. 400 was enough to find a size and not enough to find the SAME size twice. For + the redcap the criterion is near-tied — |win-50| is about 25 at two of them and 23 at + three — so a 400-run scan picked 2 instead of 3 roughly one seed in eight, and the + recorded gain moved 6.3 to 4.9 with it. Measured rather than guessed: the pick is still + unstable at 3000 and at 4000, and stable across twenty seeds at 6000. This is --update + only, so the build pays nothing; re-recording pays about half a minute. */ +const SCAN_RUNS = 6000; /* A pack whose spread-fire rate falls outside this cannot move enough to measure, and a row that cannot move is a row that would pass this guard whatever happened to it. */ const MEASURABLE = [15, 85]; @@ -71,12 +77,16 @@ const party = PARTY_KEYS.map(k => { to roll its own LCG, so the two guards described the same game with different dice and no figure here could be reproduced from simulate.mjs. */ const rngFor = makeRng; -const winRate = (foes, targets, seed) => { +/* R-278: `runs` was not a parameter. pickSize has always passed SCAN_RUNS as a fourth + argument to a three-argument function, so JavaScript discarded it and every scan ran at + RUNS instead — the constant was declared, documented, passed and never read, which is + the defect this project keeps finding in itself, sitting inside a guard. */ +const winRate = (foes, targets, seed, runs = RUNS) => { let won = 0; - for (let i = 0; i < RUNS; i++) { + for (let i = 0; i < runs; i++) { if (runFight(rngFor(seed + i * 2654435761), party, foes, { partyTargets: targets }).won) won++; } - return (100 * won) / RUNS; + return (100 * won) / runs; }; const mean = a => a.reduce((x, y) => x + y, 0) / a.length; diff --git a/tools/focus-baseline.json b/tools/focus-baseline.json index 0c6ef2c..6649217 100644 --- a/tools/focus-baseline.json +++ b/tools/focus-baseline.json @@ -94,11 +94,11 @@ }, "supporter": { "n": 2, - "pinned": 4 + "pinned": 4.7 }, "hollow_man": { "n": 6, - "pinned": 91.8 + "pinned": 92.3 }, "the_tenant": { "n": 2, @@ -106,7 +106,7 @@ }, "the_choir": { "n": 6, - "pinned": 99.4 + "pinned": 99 }, "precedent": { "n": 3, @@ -138,7 +138,7 @@ }, "quarantine_unit": { "n": 2, - "pinned": 0.2 + "pinned": 0.1 }, "cuckoo_array": { "n": 2, @@ -149,7 +149,7 @@ }, "long_walker": { "n": 2, - "pinned": 0.1 + "pinned": 0.6 }, "auditor": { "n": 4, @@ -175,7 +175,7 @@ }, "knockers": { "n": 6, - "pinned": 99.7 + "pinned": 99.8 }, "spriggan": { "n": 3, @@ -186,11 +186,11 @@ }, "nuckelavee": { "n": 2, - "pinned": 8.2 + "pinned": 8.5 }, "hedley_kow": { "n": 6, - "pinned": 97.8 + "pinned": 97.3 }, "apple_man": { "n": 2, @@ -200,8 +200,8 @@ "noise": 0.8 }, "the_committee": { - "n": 2, - "pinned": 100 + "n": 6, + "pinned": 99.9 }, "night_registry": { "n": 4, @@ -244,7 +244,7 @@ }, "switchboard": { "n": 6, - "pinned": 86.9 + "pinned": 85.9 }, "the_census": { "n": 3, @@ -269,7 +269,7 @@ }, "the_stanchion": { "n": 2, - "pinned": 5.8 + "pinned": 5.2 }, "the_yield": { "n": 2, @@ -287,10 +287,7 @@ }, "the_arrears": { "n": 2, - "spread": 14.7, - "focus": 21, - "gain": 6.3, - "noise": 1.5 + "pinned": 14.4 }, "the_margin": { "n": 5,