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>
This commit is contained in:
slaguru666
2026-09-12 23:58:22 +01:00
co-authored by Claude Opus 5
parent f932a79121
commit 995f3fa94e
6 changed files with 110 additions and 25 deletions
+1 -1
View File
@@ -186,7 +186,7 @@ check-kits: OK — 25 roles, 10 trades, 218 catalogue items, every kit key resol
check-lang: OK — en.json, 1018 keys, no leaf/branch collisions
check-templates: OK — 19 templates compile
check-behaviour: OK — 76 behavioural tests
check-scenarios: OK — 11 scenario files, 78 mechanic tags, every skill named resolves against 62 catalogue entries
check-scenarios: OK — 11 scenario files, 79 mechanic tags, every skill named resolves against 62 catalogue entries
check-creatures: OK — 102 actor specs across 8 sources, every characteristic, skill, kit key, talent, species and style resolves, no duplicate keys
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
+15 -10
View File
@@ -73,18 +73,23 @@ and is down to the floor after 2 landing hits.
Across 47 creatures: **13** are stripped by 1 landing hit, **25** are stripped by 2 landing hits, **9** are stripped by 3 landing hits.
Three things follow, and together they decide what order a team shoots in:
Two things follow:
- **Only a hit spends a defence.** A miss strips nothing, so the agent who leads off
wants a reliable attack rather than a big one.
- **Each landing hit makes the next one easier**, in the same round only. Attacks made
after the defence is gone are attacking something that cannot get out of the way.
- **So the cheapest reliable weapon fires first and the one you most need to land fires
last.** The opening shots are bait; they exist to make the thing move. Against
anything that halves damage from departmental kit, the weapon the department did not
issue is the one that fires last of all.
- **Only a hit spends a defence.** A miss strips nothing, so a round in which the
party misses twice is one where the creature meets the third shot at full value.
- **Each landing hit makes the next one easier**, in the same round only. The second
agent to connect is worth more than the first, and against the heavy dodgers below
the third is worth more still. Commit a round’s attacks to one target; spreading
them lets every defence reset at full value.
The ones that need the most stripping before your best shot is worth taking:
**What you cannot do is choose who goes first.** Reaction is re-rolled every round, so
over a fight every agent takes the opening slot about equally often, and there is no
rule for holding an action. Arranging the party so its best weapon fires last was
measured in the simulator over six independent seeds and came to nothing
distinguishable from noise. The ladder is a reason to commit a round to one target,
not a reason to choreograph the order.
The ones that take the most stripping:
- **The margin** — dodge 80%, 3 landing hits to strip
- **Redcap** — dodge 75%, 3 landing hits to strip
+55
View File
@@ -5481,3 +5481,58 @@ Negative-tested both ways it can fail: a rule change that makes the page stale,
hand-edit of the page itself. `update-readme` caught it as "run by the build, not
advertised" before I had added it to the list, and the README's count word went from nine
to ten on its own, which is R-256 paying for itself.
## R-259 — the firing order was worth nothing, and R-258 shipped it anyway
R-258 put a tactic in the bestiary on the strength of a table-side harness: cheapest
weapon first, best weapon last, worth 10.7 points of win rate across 24 orderings. Run in
`simulate.mjs` — the pipeline that produces every published number — it is worth
**+0.27 points**, with per-seed differences from −1.5 to +1.8 against 2.6 points of
seed-to-seed noise on a single arm. It is zero.
**Why the toy lied.** Two reasons, both mine.
- That party had one uniquely valuable weapon — an anti-materiel rifle the borough does
not own, so unhalved and worth roughly double anything else. Reordering around it had
something to reorder around. The frozen party's four are worth 4.09, 3.98, 1.43 and
1.43 expected damage: two near-identical pairs, and nothing to gain by sequencing them.
- The toy used a fixed order every round. **The game re-rolls Reaction every round**, so
over a fight every agent takes the opening slot about equally often. There is no rule
for holding an action — I checked before measuring — so the order is not a choice a
player gets to make. I had written table advice for a decision the rules do not offer.
**The page is corrected.** The ladder stays, because it is derived and true. What it now
says is that you cannot choose who goes first, and why.
**What IS worth doing, measured the same way.** Committing a round's attacks to one
target rather than spreading them, in fights that can actually move (spread-fire win rate
between 20% and 80% — a fight at 0.2% or 100% cannot show an effect and three of my first
four samples were pinned there):
| pack | dodge | spread | focus | gain |
|---|---|---|---|---|
| 2× Cuckoo array | 5% | 65.7% | 73.9% | +8.2 |
| 2× The apple tree man | 10% | 45.0% | 52.0% | +7.0 |
| 3× Spriggan | 45% | 48.9% | 64.9% | +16.0 |
| 2× The yield | 60% | 55.8% | 67.4% | +11.6 |
| 3× Precedent | 65% | 70.9% | 82.2% | +11.3 |
| 3× Redcap | 75% | 38.9% | 52.7% | +13.8 |
Against low dodge the gain is 7.6 points and is simply killing attackers sooner. Against
high dodge it is 12.2, so about **4–5 points is the defence ladder** and the rest is
arithmetic that has nothing to do with dodging. Splitting it needed contested fights at
both ends of the dodge range; without that control I would have credited the whole 13.8
against the redcaps to the ladder, which is the same error as R-255's +6.4.
**The instrument.** `runFight` gained two hooks, both off by default and unused by the
baseline: `partySequence` rearranges the party among the slots initiative already gave
them — leaving enemies where they fell, so the experiment isolates "what order the agents
act in" from "who acts before the creature" — and `partyTargets: "focus"` concentrates
fire. Both roll the same dice as before, so the stream is unchanged, and check-lethality
confirms all 47 creatures still fight exactly as recorded with the hooks present.
**The pattern.** Third time this session that my explanation was wrong and the experiment
settled it, and the first where the wrong explanation had already shipped to a document a
GM would read. The rule I keep relearning: a measurement taken outside the guarded
pipeline is a hypothesis, not a result, and it does not go in a document until the
pipeline agrees.
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "ringbrp",
"title": "The Custodians",
"description": "A Basic Roleplaying game of agents who cross the crossing to contain what should not be there — and who come back a little less from here each time.",
"version": "1.7.13",
"version": "1.7.14",
"compatibility": {
"minimum": 13,
"verified": "14.364"
+14 -10
View File
@@ -168,16 +168,20 @@ md += `\n**V** = Vesh, **C** = Cadence. Everything else is baseline-human anatom
+ `Across ${all.length} creatures: `
+ Object.keys(spread).sort().map(k => `**${spread[k]}** are stripped by ${k} landing hit${k === "1" ? "" : "s"}`).join(", ")
+ `.\n\n`
+ `Three things follow, and together they decide what order a team shoots in:\n\n`
+ `- **Only a hit spends a defence.** A miss strips nothing, so the agent who leads off\n`
+ ` wants a reliable attack rather than a big one.\n`
+ `- **Each landing hit makes the next one easier**, in the same round only. Attacks made\n`
+ ` after the defence is gone are attacking something that cannot get out of the way.\n`
+ `- **So the cheapest reliable weapon fires first and the one you most need to land fires\n`
+ ` last.** The opening shots are bait; they exist to make the thing move. Against\n`
+ ` anything that halves damage from departmental kit, the weapon the department did not\n`
+ ` issue is the one that fires last of all.\n\n`
+ `The ones that need the most stripping before your best shot is worth taking:\n\n`
+ `Two things follow:\n\n`
+ `- **Only a hit spends a defence.** A miss strips nothing, so a round in which the\n`
+ ` party misses twice is one where the creature meets the third shot at full value.\n`
+ `- **Each landing hit makes the next one easier**, in the same round only. The second\n`
+ ` agent to connect is worth more than the first, and against the heavy dodgers below\n`
+ ` the third is worth more still. Commit a round\u2019s attacks to one target; spreading\n`
+ ` them lets every defence reset at full value.\n\n`
+ `**What you cannot do is choose who goes first.** Reaction is re-rolled every round, so\n`
+ `over a fight every agent takes the opening slot about equally often, and there is no\n`
+ `rule for holding an action. Arranging the party so its best weapon fires last was\n`
+ `measured in the simulator over six independent seeds and came to nothing\n`
+ `distinguishable from noise. The ladder is a reason to commit a round to one target,\n`
+ `not a reason to choreograph the order.\n\n`
+ `The ones that take the most stripping:\n\n`
+ hardest.map(x => `- **${esc(x.name)}** — dodge ${x.d}%, ${stripsIn(x.d)} landing hits to strip\n`).join("")
+ `\nIt runs the other way too: agents defend on the same ladder, which is why a second\n`
+ `attacker in a round is worth so much more than the first, and why anything that gets\n`
+24 -3
View File
@@ -303,7 +303,8 @@ function attack(rng, attacker, defender) {
}
/** One fight to a conclusion. Returns which side stood, and how long it took. */
export function runFight(rng, partySpecs, enemySpecs, { maxRounds = 40 } = {}) {
export function runFight(rng, partySpecs, enemySpecs,
{ maxRounds = 40, partySequence = null, partyTargets = "random" } = {}) {
const party = partySpecs.map(s => buildCombatant(s, { side: "party" }));
const enemy = enemySpecs.map(s => buildCombatant(s, { side: "enemy" }));
const all = [...party, ...enemy];
@@ -318,13 +319,33 @@ export function runFight(rng, partySpecs, enemySpecs, { maxRounds = 40 } = {}) {
c.defencesThisRound = 0;
c.initiative = c.reactionBase + d(rng, REACTION.die);
}
const order = [...all].sort((a, b) => b.initiative - a.initiative);
let order = [...all].sort((a, b) => b.initiative - a.initiative);
/* R-259. An experiment hook, off by default and never used by the baseline: it
rearranges the PARTY among the slots initiative already gave them, leaving every
enemy where it fell. That isolates "what order the agents act in" from "who acts
before the creature does" — reordering the whole line would measure both at once
and credit the answer to the wrong one. The initiative dice are still rolled
either way, so the random stream is identical with the hook on or off. */
if (partySequence) {
const slots = order.map((c, i) => [c, i]).filter(([c]) => c.side === "party").map(([, i]) => i);
const seq = partySequence(order.filter(c => c.side === "party"));
order = [...order];
slots.forEach((slot, k) => { order[slot] = seq[k]; });
}
for (const actor of order) {
if (!isFighting(actor)) continue;
const foes = (actor.side === "party" ? enemy : party).filter(isFighting);
if (!foes.length) break;
attack(rng, actor, foes[Math.floor(rng() * foes.length)]);
/* R-259, second hook, also off by default: "focus" makes the party concentrate on
one enemy — the most hurt of them — instead of each agent picking at random.
The die is rolled either way so the stream does not shift. */
const pick = Math.floor(rng() * foes.length);
const focus = actor.side === "party" && partyTargets === "focus"
? foes.reduce((a, b) => (b.hp < a.hp ? b : a))
: foes[pick];
attack(rng, actor, focus);
}
// End of round: anyone at or below zero loses ground on the dying clock.