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
+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.