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:
co-authored by
Claude Opus 5
parent
f932a79121
commit
995f3fa94e
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user