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