From 19ed0b5f0f9c73dd9325a1a0b92aba9c15b4de04 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sun, 13 Sep 2026 09:36:05 +0100 Subject: [PATCH] R-266: the coin is the first disabling blow, and it lands in round two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/REVIEW_LOG.md | 45 ++++++++++++++++++++++++++ tools/first-blood.mjs | 73 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 tools/first-blood.mjs diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index 07b0788..8e36325 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -5809,3 +5809,48 @@ fights that get there. 68% of fights reach round five with none of them down. **The lesson is the one this log keeps recording, one layer out.** A number that cannot be reproduced is a claim, whoever holds it. It was caught because another session tried the citation rather than trusting it, which is exactly what a citation is for. + +## R-266 — the coin is the first disabling blow, and it lands in round two + +R-265 left a finding standing: the four-player cut of CLEAN GROUND is bimodal — 23.1% of +fights end with nobody down, 45.0% with everybody down, and only 11% near the published +mean. It said the fight is *decided by round five*. That was true and it was the wrong +frame: round five is where the outcome is already legible, not where it is decided. + +**Reading one fight from each end says why.** Seed 5 is the clean sweep. Neil puts 5 into +a leg on the first exchange; 5 of 10 is exactly the major-wound threshold, so one of the +three is disabled before anybody else has landed anything. A is down in round 2, C in +round 3 on a critical, B in round 4 before it acts. **The party is hit once in the whole +fight** — 4 points through Neil's armour. Seed 2 is the wipe, and its first two rounds +produce a single 3-point scratch; the column lands three blows in round 3, Kate goes 12 to +0 and Arun is disabled by a critical, and the cut is two effective attackers against three. + +**Both sides disable on one good hit.** The major-wound threshold is half of max HP and it +is the same rule in both directions — the neighbours have 10, the agents 11 to 13 — so an +average blow takes a fighter out of the fight. Every disable removes an attacker, which +thins the return fire, which makes the next disable likelier. Nothing pulls a fight back +toward the middle once it has tipped, which is the whole of the bimodality. + +**Measured, `tools/first-blood.mjs`, 2000 runs at seed 11:** + +| first fighter out | share of fights | mean round | wipes among those | +|---|---|---|---| +| the party strikes first | 49.7% | 1.7 | **23.1%** | +| the column strikes first | 50.3% | 1.7 | **66.5%** | + +A true coin, settled in round two, worth **43 points of wipe rate**. Six against six of the +column is the control: first blood still matters there and matters far less — 8.6% against +22.4%, a 14-point swing, at mean round 1.3. One fighter is a sixth of that line and a +quarter of the cut, so the same blow compounds less. This is a property of the cut, which +is what the histogram said by a different route. + +**Dominic should be read before anyone is handed him.** In both narrations he attacks at +`vs 1` — four attempts in the seed 5 fight, all of them hopeless. The four-player cut is +three effective attackers, not four, and a table that assigns Ashcroft to a player without +saying so has quietly made the coin worse. + +**`tools/first-blood.mjs` is a reading tool, not a guard.** It records nothing and no +figure from it belongs on a generated page — the bestiary and the scenarios read guarded +artifacts only. It is in the repo rather than in a scratch directory for one reason: the +number above was quoted to another session, and R-265's lesson is that a number the +person quoted it cannot re-run is a claim, not a result. diff --git a/tools/first-blood.mjs b/tools/first-blood.mjs new file mode 100644 index 0000000..0a714c7 --- /dev/null +++ b/tools/first-blood.mjs @@ -0,0 +1,73 @@ +/** + * Who lands the first disabling blow, and what the fight does afterwards. + * + * `check-focus` and `check-lethality` record how a fight ENDS. Neither records how it + * turns, and the four-player cut of CLEAN GROUND turns early: both sides disable on one + * good hit, so the first one to land it removes an attacker, which thins the return fire, + * which makes the next one likelier. This counts that first blow and reports the outcome + * on each side of it. + * + * node tools/first-blood.mjs the four-player cut + * node tools/first-blood.mjs --creature redcap --count 3 \ + * --party ashcroft,bhattacharya,renshaw,braithwaite --runs 2000 --seed 11 + * + * This is a reading tool, not a guard: nothing here is recorded to a baseline and no + * figure from it belongs on a generated page. It exists so a number quoted between + * sessions can be re-run by whoever was quoted it, which is the whole of R-265's lesson. + */ +import { ROSTER } from "./roster.mjs"; +import { NPCS } from "./content.mjs"; +import { runFight, makeRng } from "./simulate.mjs"; + +const argv = process.argv.slice(2); +const opt = (n, d) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; }; + +const KEY = opt("--creature", "quiet_neighbours_npc"); +const COUNT = Number(opt("--count", 3)); +const RUNS = Number(opt("--runs", 2000)); +const SEED = Number(opt("--seed", 11)); + +const spec = NPCS.find(n => n.key === KEY); +if (!spec) { console.error(`first-blood: no creature keyed "${KEY}"`); process.exit(1); } + +const partyKeys = (opt("--party", "ashcroft,bhattacharya,renshaw,braithwaite")) + .split(",").map(x => x.trim()).filter(Boolean); +const party = partyKeys.map(k => { + const found = ROSTER.find(r => r.key === k || r.key === "pc_" + k); + if (!found) { console.error(`first-blood: "${k}" is not on the duty roster`); process.exit(1); } + return found; +}); +const foes = Array.from({ length: COUNT }, () => spec); + +/* A fighter leaves the fight when a hit disables or destroys a location, or drops them — + the same three conditions the narration marks, so the count matches what a reader sees. */ +const rng = makeRng(SEED); +const by = { party: { n: 0, wiped: 0, rounds: 0 }, column: { n: 0, wiped: 0, rounds: 0 } }; +let undecided = 0; + +for (let i = 0; i < RUNS; i++) { + let first = null, firstRound = 0, round = 0; + const r = runFight(rng, party, foes, { say: e => { + if (e.kind === "round") round = e.round; + if (first) return; + if (e.kind === "hit" && (e.disabled || e.destroyed || e.defender.hp <= 0)) { + first = e.defender.side === "enemy" ? "party" : "column"; + firstRound = round; + } + }}); + if (!first) { undecided++; continue; } + const b = by[first]; + b.n++; b.rounds += firstRound; + if (r.wiped) b.wiped++; +} + +console.log(`\nfirst fighter out of the fight — ${RUNS} runs, seed ${SEED}`); +console.log(`${party.length} of the roster vs ${COUNT} × ${spec.name ?? KEY}\n`); +for (const side of ["party", "column"]) { + const b = by[side]; + if (!b.n) continue; + console.log(` ${side.padEnd(7)} strikes first in ${String((100 * b.n / RUNS).toFixed(1)).padStart(5)}% of fights` + + ` (mean round ${(b.rounds / b.n).toFixed(1)})` + + ` → ${String((100 * b.wiped / b.n).toFixed(1)).padStart(5)}% of those end in a wipe`); +} +if (undecided) console.log(` ${undecided} fights with nobody disabled at all`);