Opposed Rolls and Delegated Disclosure — ADR-094
Opposed Rolls and Delegated Disclosure — ADR-094
Date: 2026-08-03 · Written in English per the 2026-08-01 convention change.
Provenance: designing the BRP resolution kernel. Building the shared d100 engine underneath
Descent.Vtt.Plugins.CoC7esurfaced a mechanic the disclosure corpus had no position on: an opposed roll necessarily tells the initiator something about a rating the opposition is entitled to keep hidden. ADR-091 had established the shape of the problem for occluders; nothing had asked it about rules.This file continues the
R-numbered sequence for filing purposes only. As with R6, R7 and R8, the title names where the ruling came from rather than implying a ninth review round.
ADR-094 — An Opposed Roll Is Resolved Server-Side, and What It Reveals Is a Delegated Authority Rather Than a Fixed Bound
Status: Accepted · Date: 2026-08-03 · Depends-on: 034 (visibility has one producer, and it is the server), 046 (premise validation), 091 (the disclosure asymmetries, and the precedent that a leak can be a mechanic) · interacts with 017, 071
Context
BRP’s default opposed-roll rule is a comparison of two resolved checks: both parties roll, the higher degree of success wins, and a successful loser downgrades the winner’s degree by one per degree they achieved. Call of Cthulhu 7e reaches the same place by a different route. Either way, the result handed to the initiator is a function of the opposition’s hidden rating.
That is a disclosure surface, and it is not a small one. A player who repeatedly opposes the same NPC — Hide against a sentry’s Sense, Fast Talk against a cultist’s Insight — recovers the opposition’s rating to within a band after a handful of trials, because each trial reports which side of an unknown threshold an unknown roll fell on. The recovery is not defeated by the roll being random; randomness is what makes each trial an independent sample rather than what hides the value. This is the same structure §5.3 already treats as a channel elsewhere: the per-event information is small, the rate is what makes it a leak, and ADR-091 clause 4 records that frequency is a channel amplitude bounding does not cover.
The obvious fix is wrong in the other direction. Emitting only “you won” makes the mechanic unusable at a real table. A keeper who has just narrated a dramatic contest wants to show the dice; players contesting each other openly expect to see both rolls; a published scenario may call for an open opposed roll by name. A system that could not express an open roll would be refusing to run the game in order to protect a number the keeper was about to read aloud anyway.
And the client cannot be the one to decide. ADR-034 rejected client-side culling of hidden occluders because “a culling shader is one line away from being disabled”. A client that receives both rolls and renders one of them is one line away from rendering both — and worse than the visibility case, because the concealed datum is sitting in client memory as a plain integer rather than being inferable from timing. This is ADR-091’s rejected alternative, restated for rules.
Decision
-
An opposed roll is resolved entirely server-side. Both checks are built, drawn and classified inside the room grain. The comparison, the downgrade and the winner are computed there. No client is sent an opposition’s rating, threshold bands, or unresolved roll in order to compute anything. This extends ADR-034 clause 1 and ADR-091 clause 1 from visibility, audibility and leaked illumination to rule resolution: the producer is the server for all four.
-
What the result discloses is set by a GM-delegated disclosure policy, not by a fixed rule. The keeper is the arbiter of what their table knows. The policy is a room-scoped, per-contest-resolvable setting with (at least) two positions:
Position The initiator receives NetOutcome(default)The relative result only — won, lost, or tied — and their own roll and bands. DeclassifiedAdditionally the opposition’s roll, effective target, band boundaries and resolved level. The keeper may declassify a contest before or after it resolves. Declassifying after the fact is explicitly permitted: it is how a table replays a dramatic moment, and the datum has already been computed server-side either way.
-
The default is the restrictive position, and the default is the ruling. A contest resolved without an explicit policy discloses the net outcome and nothing else. The whole risk this ADR addresses is accidental disclosure, and a permissive default converts every un-configured room, every scripted NPC contest and every future cartridge’s first opposed action into a leak that nobody chose. This is the same direction as
Unknown = 0,Incomplete, andRulesetOutcome.Refusethroughout the codebase: the uninitialised state discloses least. -
Declassification is an authority, and it is checked where every other authority is. The policy is carried on the room and evaluated under §9.2 alongside every other keeper power. A player-initiated intent may request an open roll; it may never set the policy. A cartridge may read the policy and may not change it — rules decide what a contest means, not who is allowed to see it.
-
Declassification is recorded in the event stream. The resolved contest event carries the policy in force when it resolved. Without it, a stream replayed later cannot distinguish a roll that was open at the table from one that was not, and a projection rebuilding what a given player legitimately knew — the same question §14.7’s per-viewer property asks — would have to guess.
-
The net outcome is still a channel, and it is still bounded.
NetOutcomereduces the per-contest disclosure; it does not eliminate it, because winning and losing are themselves evidence about the hidden rating. Repeated contests against one opposition are therefore rate-bounded (Q-076), on ADR-091 clause 4’s reasoning and by the same arithmetic: the capacity islog₂(outcomes) × rate, not either factor.Q-076and the outcome vocabulary in clause 2 are one decision, not two. -
A declassified contest is exempt from the rate bound, and the exemption is the keeper’s act rather than a property of the data. Once a keeper has chosen to show the table a rating, bounding the rate at which it is re-shown protects nothing. The exemption attaches to the contest, is recorded under clause 5, and does not generalise to the next contest.
Alternatives Considered and Why Rejected
- Always emit the net outcome only. Rejected — it is safe and it cannot run an open opposed roll, which is a named procedure in the source rules. A system whose security property is “the game may not be played the way the book describes” will be worked around by keepers narrating outside it, and then the disclosure happens with no record at all.
- Always emit full detail and let the client hide it. Rejected — ADR-034’s answer applies unchanged, and the datum is in client memory as a plain value rather than inferable from timing, which makes it strictly worse than the channel ADR-034 closed.
- Let the cartridge decide per action. Rejected — it puts a disclosure decision in the layer that is most likely to be third-party, and makes “may this player know this” a question answered in as many places as there are rulesets. ADR-091’s consequences already record the cost of having two such places; this would make it unbounded.
- Derive the policy from whether the opposition is an NPC. Rejected — it is wrong in both directions. Player-versus-player contests are frequently open, and a scenario’s signature NPC contest is frequently the one that must stay closed. The correlation is real and the exceptions are exactly the dramatic moments the rule would be judged on.
- Bound by rating-band quantisation, as ADR-091 clause 3 does for attenuation. Rejected here, though the shape is right: an opposed roll’s output is already ordinal (five or six degrees), so there is no continuous precision to quantise away. The analogous control is the outcome vocabulary in clause 2, and it is coupled to the rate bound in clause 6 for ADR-091’s reason.
Consequences (including negative)
- Negative: a disclosure decision is now a runtime setting, and settings drift. Every previous disclosure control in this corpus is structural — a field that does not exist, a reference that is absent, a partial set that cannot return an exact answer. This one is a value a keeper can change, which means it can be changed by accident, changed by a UI default, or inherited by a room template nobody reviewed. Clause 3’s restrictive default is the mitigation and it is not a complete one.
- Negative:
Q-076does not exist yet, so clause 6 is currently unenforced. Recorded plainly rather than left to be discovered: until the quantity is measured and registered,NetOutcomebounds the per-contest disclosure and nothing bounds the rate. This is the same posture ADR-091 clause 7 takes towardQ-075, and it carries the same warning — a bound that is described but unset is not a bound. - Negative: the keeper is now a security boundary. Clause 4 makes a human the arbiter of a disclosure decision. That is correct for a game and unusual for this architecture, and it means the audit story for a leak may end at “the keeper chose to”. Clause 5’s recording is what keeps that answerable rather than merely plausible.
- Negative: two players in one room may legitimately hold different knowledge of the same event. A contest declassified to its participants and not to onlookers produces exactly the per-viewer divergence §14.7 exists to reason about, so the opposed-roll payload must be built per viewer and cannot be broadcast. ADR-092 already records that its per-viewer property covers the snapshot and does not automatically extend to other messages; this is a third such message.
- Positive: the mechanic the rules actually describe becomes expressible with no client-side filter anywhere in it — which is what makes it survivable when a client is hostile, and is the same property ADR-091 bought for audibility.
Enforcement
Descent.DisclosureTestsgains a fourth class, alongside the visibility, pathfinding and permeability asymmetries: an opposed contest resolved under the default policy must produce a payload containing no field derived from the opposition’s rating, asserted over the payload’s own shape rather than by inspecting call sites.- The default is asserted directly — a contest resolved with no policy supplied discloses the net outcome — so clause 3 fails the build rather than resting on a configuration file being right.
Descent.ReplayTestsasserts that a resolved-contest event carries its policy, so clause 5 cannot be dropped by an event-shape change without a red suite.- The opposed-roll payload is built per viewer, and the §14.7 generated property is extended to it. ADR-092’s note that per-viewer coverage does not travel between message types is why this is stated rather than assumed.
- Registry rule 4:
Q-076is registered before any part of clause 6 is written into the whitepaper body, and the coupling to clause 2 is recorded with it.
§11 Index Line
ADR-094: Opposed rolls resolve entirely server-side and no client is sent an opposition's rating, threshold or unresolved roll; what the result discloses is a GM-delegated, room-scoped policy defaulting to net outcome only, because the risk being managed is accidental disclosure; declassification is a §9.2 keeper authority that a player may request and never set, a cartridge may read and never change, and that is recorded in the stream so a replay can tell an open roll from a closed one; the net outcome remains a channel whose rate is bounded by Q-076, coupled to the outcome vocabulary as capacity is log₂(outcomes) × rate. (Extends ADR-034, ADR-091.)