Curious about the following Anydice formula.

I’d like to know how to calculate what it would look like to roll a d20 UNDER a specific target number (lets say 14 or 10), but also have that d20 still get HIGHER than a opposed die roll (lets say d8, d10, or d12).

I feel like the math should be pretty straight forward, but just want to be able to look at an Anydice calculation.

So again. I’d pick up 2 dice (d20 + another die). I’d want the d20 to be lower than a static number (10-15) but still roll higher than the opposed die.

  • \$\begingroup\$ Do you want to check if the d20 roll lies in the close interval (i.e. extrema included) or open interval (i.e. extrema excluded)? \$\endgroup\$
    – Eddymage
    Commented Jun 27, 2023 at 5:47
  • \$\begingroup\$ Is this Pendragon-related? It sounds Pendragon-related. \$\endgroup\$
    – From
    Commented Jun 27, 2023 at 8:50

4 Answers 4


This is a simple implementation:


output d{1..(TARGET-1), 0:(20-TARGET+1)} > OPPOSED

It creates a 20 sided dice with 0s for any number greater than or equal to the TARGET and then compares it to the OPPOSED die. 1 means you win, 0 means you don't.


Math provides a short, easy and elegant solution.

Given a target T and the opposed die D, then the probability of a d20 roll to lie between any roll of the opposed die and the target T is $$ P(D<d20<T) = \frac{{\rm min} \{D,T-1\}}{40D} \Big(2T-3-{\rm min} \{D,T-1\} \Big) $$

In most case (namely for \$d \neq 10,12, \, T\neq 10,11,12\$) the minimum is equal to D and hence we have an elegant formula which reads as $$ P(D<d20<T) = \frac{2T-D-3}{40} $$

For example, for the target T=15 and the opposed die d8 the probability of success is $$ P(d8<d20<15) = \frac{2\cdot 15 - 8 -3}{40} = \frac{19}{40}=0.475 $$ which is exactly the value provided in Dale M's answer.

Using the above formula, we can get the table below.

target d4 d6 d8 d10 d12
10 0.325 0.275 0.225 0.180 0.150
11 0.375 0.325 0.275 0.225 0.188
12 0.425 0.375 0.325 0.275 0.229
13 0.475 0.425 0.375 0.325 0.275
14 0.525 0.475 0.425 0.375 0.325
15 0.575 0.525 0.475 0.425 0.375

Mathematical Derivation

Consider the die D and let be \$d\in\{1,2,\dots,D\}\$ one outcome of the roll: hence the probability of the d20 roll to lie in the interval between d and T is $$ P(d<d20<T) = \frac{1}{D} \frac{T-1-d}{20} $$ where the first factor accounts for the probability of the outcome d to occur, while the second one is simply the definition of probability (number of success cases over the number of possible cases). The case in which the roll on the opposite die provides an outcome equal or larger than the target has probability 0.

For obtaining the total probability, one has to sum over the possible outcomes of the die D, taking into account that for some particular cases the sum is up to the minimum between T-1 and D, for avoiding negative (nonsense) terms, as in the case \$T=10\$, \$D=10\$ and the roll on the opposed die is exactly 10. \begin{eqnarray*} \newcommand{\Q}{{\text{min}\{D,T-1\}}} P(D<d20<T) &=& \sum_{d=1}^\Q \frac{1}{D}\frac{T-1-d}{20}\\ &&\\ &=& \frac{1}{D}\sum_{d=1}^\Q\frac{T-1}{20} - \frac{1}{20\,D}\sum_{d=1}^\Q d\\&&\\ &=& \frac{\Q(T-1)}{20\,D}- \frac{1}{20\,D} \frac{(\Q+1)(\Q)}{2}\\&&\\ &=& \frac{\Q}{40\,D}\Big( 2T-2 - \Q-1\Big) \\&&\\ &=& \frac{\Q}{40\,D}\Big( 2T -\Q-3\Big) \end{eqnarray*}


This AnyDice program should do it:

function: foo DIE:n OPPOSED:n TARGET:n {
  if DIE <= OPPOSED { result: -1 }
  if DIE >= TARGET { result: 1 }
  result: 0

MY_DIE: 1d20


That program will output one of three values:

  • -1 means that your d20 rolled under the opposition die
  • 1 means that your d20 rolled above the "target" fixed number
  • 0 means that your d20 rolled between the two (ie., that it succeeded)

You can change any of the three config lines in the middle (MY_DIE, OPPONENT_DIE, and TARGET) to see how different dice and/or target values will change the outcomes.


This is not an AnyDice-based solution, but is otherwise directly responsive. I made a dyce-based¹ interactive notebook that provides extended visualization options beyond those AnyDice supports and that you can use to tinker with various inputs to your mechanic. You can play around with it in your browser: Try dyce [source]


While a matter of taste, I find anydyce's² "burst" graphs (above) often help give insight into the feel for distributions like these. But distribution is a small part of mechanic "feel", on which I'll elaborate and opine further below. For now, though, the core mechanic implementation used to create the above graphs is as follows:

class ResultType(IntEnum):
    IMPOSSIBLE = auto()  # no roll can succeed because target <= challenge die
    TARGET_MISS = auto()  # failure because player die >= target
    CHALLENGE_MISS = auto()  # failure because player die <= challenge die
    HIT = auto()  # success

def expected_result(
    player_die: H,
    challenge_die: H,
    target: int,
) -> H:
    def _dependent_term(player_die: HResult, challenge_die: HResult):
        if target <= challenge_die.outcome:
            return ResultType.IMPOSSIBLE
        elif player_die.outcome >= target:
            return ResultType.TARGET_MISS
        elif player_die.outcome <= challenge_die.outcome:
            return ResultType.CHALLENGE_MISS
            return ResultType.HIT

    # Start with zero counts for all possible outcomes
    result_base = H((outcome, 0) for outcome in ResultType)
    # Accumulate those that came up in our calculation
    return result_base.accumulate(foreach(_dependent_term, player_die, challenge_die))

If I'm reading your original description accurately, one has to roll less than (not less-than-or-equal-to) the target number and greater than (not greater-than-or-equal-to) the challenge die outcome. If I've misread, then the above code (and it's place in the linked interactive notebook) can be modified accordingly (changing >= to > and <= to <).

If you don't want the fidelity of whether the die failed because of the target or because the challenge die, there's a simpler approach akin to Dale M's answer:

def expected_result_low_fidelity(
    player_die: H,
    challenge_die: H,
    target: int,
) -> H:
    # Build a die that has just the faces that are below the target
    # (or zero where they were at or above)
    player_target_die = H(
        (outcome, count) if outcome < target else (0, count)
        for outcome, count in player_die.items()
    # Then compute a histogram for where those faces are greater than
    # the challenge die
    return player_target_die.gt(challenge_die)

That being said, I think understanding where/when failures can occur provides important insight into how the mechanic might feel at the table, akin to how the popular Advantage mechanic feels a lot different than merely adding or subtracting a statistically equivalent static modifier:

What matters is how Advantage and Disadvantage feel at the table. Rolling two dice and taking the higher — or being forced to take the lower — feels way better — or way worse — than rolling one die and adding — or subtracting — an extra number. Moreover, the play dynamic creates all these interesting little emotional outcomes. If you have Advantage and roll two ones, that’s a f$&%ing gut punch, for example. And if you roll a one and a seventeen, it feels like Advantage saved your a$& from disaster, even if, statistically, there’s no way to know whether you’d have gotten the one or the seventeen if you’d just rolled a single die.

"The Best and Worst of D&D 5E" (Rehm, Dec 2022)

Revealing the target and challenge roll to the player or changing the order rolls are made could elicit significantly different reactions. Depending on parameters, there could be situations where no roll the player makes would succeed. Consider rolling the challenge die first, which would reveal those helpless situations without player action. Consider instead naming the target, then having the player roll a die, then rolling the challenge die (or maybe even having the player roll the challenge die) if the player rolls under the target. The player could be relieved by beating the target after rolling a two, only to realize the implications that the challenge die is very likely to change the tide for the worse.

Note that neither implementation above has an opinion about the player die (other than it having exclusively positive outcomes), so you can experiment with that, too (e.g., if you wanted to see how 2d10 or 3d6 worked in lieu of a d20).

¹ dyce is my Python dice probability library.

² anydyce is my visualization layer for dyce meant as a rough stand-in for AnyDice.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .