R-273: the opposed roll, built where the authority lives

The content has called for 'opposed POWx5' since before the rule existed -- the
hollow man's tactics, CLEAN GROUND twice, slice-text three times -- and rules.mjs
defined none. A scenario carried a local ruling that said in its own text it was a
ruling and not a rule.

Generalises that ruling rather than inventing another: both sides roll, the better
band wins, only the ladder the game already has. Ties go to whoever is being acted
upon, which is what defenceOutcomeFor has always said; the scenario's 'favour the
agent' gave the same answer only because no agent ever initiates one. Neither side
succeeding leaves the contest unsettled rather than won, which the two beats need
in opposite directions.

Two exports at the scenario session's request: opposedOutcomeFor compares graded
levels and carries both, so a caller can price a fumbled attempt without this file
deciding what a fumble costs; opposedContestFor runs it from ratings and rolls with
per-side difficulty, so 'resists at Difficult' does not put applyDifficulty back
into a document. Spot-checked against the real Act Four beat: 55 against 85 at
Difficult, which is 42.

Page 1 states it by asking it -- the tie-break and the margin are computed from the
rule at build time, so the book cannot drift from the engine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 10:42:17 +01:00
co-authored by Claude Opus 5
parent d03b3208e5
commit cdbb2e34ee
5 changed files with 161 additions and 3 deletions
+25
View File
@@ -506,6 +506,31 @@ const checks = [
["defenceOutcomeFor(special, special)", RULES.defenceOutcomeFor("special","special").turnedAside, true],
["defenceOutcomeFor(success, success)", RULES.defenceOutcomeFor("success","success").turnedAside, true],
["defenceOutcomeFor(success, fumble)", RULES.defenceOutcomeFor("success","fumble").landsAt, "success"],
// Opposed rolls. The better band wins; ties go to whoever is being acted upon, which is
// the same answer defenceOutcomeFor gives a defender; and if nobody reached a success
// the contest is unsettled rather than won, so it can be tried again.
["opposedOutcomeFor(crit, success).winner", RULES.opposedOutcomeFor("critical","success").winner, "active"],
["opposedOutcomeFor(crit, success).margin", RULES.opposedOutcomeFor("critical","success").margin, 2],
["opposedOutcomeFor(success, crit)", RULES.opposedOutcomeFor("success","critical").winner,"resisting"],
["opposedOutcomeFor(success, success)", RULES.opposedOutcomeFor("success","success").winner, "resisting"],
["opposedOutcomeFor(crit, crit)", RULES.opposedOutcomeFor("critical","critical").winner,"resisting"],
["opposedOutcomeFor(success, success).margin", RULES.opposedOutcomeFor("success","success").margin, 0],
["opposedOutcomeFor(special, success)", RULES.opposedOutcomeFor("special","success").winner, "active"],
["opposedOutcomeFor(success, failure)", RULES.opposedOutcomeFor("success","failure").winner, "active"],
["opposedOutcomeFor(failure, failure).settled", RULES.opposedOutcomeFor("failure","failure").settled, false],
["opposedOutcomeFor(failure, failure).winner", RULES.opposedOutcomeFor("failure","failure").winner, null],
["opposedOutcomeFor(fumble, failure).settled", RULES.opposedOutcomeFor("fumble","failure").settled, false],
["opposedOutcomeFor(success, fumble)", RULES.opposedOutcomeFor("success","fumble").winner, "active"],
// Both sides' levels travel with the verdict, so a caller can price a fumbled attempt
// without this file deciding what fumbling costs.
["opposedOutcomeFor(fumble, failure).activeLevel", RULES.opposedOutcomeFor("fumble","failure").activeLevel, "fumble"],
["opposedOutcomeFor(fumble, failure).resistingLevel", RULES.opposedOutcomeFor("fumble","failure").resistingLevel, "failure"],
// The whole contest, each side at its own difficulty. CLEAN GROUND's Act Four beat:
// the understudy at POWx5 55 against Ashcroft's 85 resisting at Difficult, which is 42.
["opposedContestFor(...).resisting.bands.effective", RULES.opposedContestFor({rating:55,roll:12},{rating:85,roll:40,difficulty:"difficult"}).resisting.bands.effective, 42],
["opposedContestFor(...).winner", RULES.opposedContestFor({rating:55,roll:12},{rating:85,roll:40,difficulty:"difficult"}).winner, "resisting"],
["opposedContestFor(aggressor specials)", RULES.opposedContestFor({rating:55,roll:8},{rating:85,roll:40,difficulty:"difficult"}).winner, "active"],
["opposedContestFor(both miss).settled", RULES.opposedContestFor({rating:55,roll:90},{rating:85,roll:90,difficulty:"difficult"}).settled, false],
// Hit locations. Moved out of ringbrp.mjs so the harness can locate damage; these are
// the spot-checks they never had while the engine held the only copy. A species that
// has no head must not be able to return one, which is the whole point of the tables.