User guide All lessons Cookbook Function reference Open playground

D&D 5e d20 checks with bucket

The roll

At the table you roll a d20, add your modifier, and compare that total to the DC (difficulty class or armor class). You succeed when:

natural + MOD ≥ DC

where natural is the number showing on the d20 you keep—not the total after adding the modifier.

Two details are about the die face, not the math:

Face on the kept d20Usual 5e meaning (attacks; similar idea for many checks)
Natural 1Automatic failure (critical miss)
Natural 20Automatic success (critical hit on attacks)

Advantage means roll 2d20 and keep the higher face (2d20kh1). Disadvantage keeps the lower (2d20kl1). A normal roll is 1d20. Advantage and disadvantage apply to which natural you keep; the modifier is still added afterward at the table.

Lesson 11 labels fixed ranges on 1d20 (e.g. 1–5 crit fail). That teaches ordered outcomes, but a real check also depends on DC and MOD. This lesson keeps the roll on the natural die and encodes pass/fail from those two numbers—so nat 1 and nat 20 stay on the die you model.

Pieces of a .dice file

Think of the script in four parts:

  1. ParametersDC and MOD for the situation you care about (AC 15, +5 to hit, and so on).
  2. Outcome labels — the four results players care about: critical fail, fail, success, critical success. Build them with scale().step(...) (same ladder idea as lesson 11).
  3. The roll1d20, 2d20kh1, or 2d20kl1; do not write 1d20 + MOD if you want true nat-1 / nat-20 behavior on the kept die.
  4. Classificationnatural.bucket(scale) turns the roll into exact probabilities for each label; p_at_least("SUCCESS") is “success or critical success.”

What number do I need on the d20? Players often do that mental step: with DC 15 and +5, you need 10 or higher on the die because 10 + 5 = 15. In the script that need is T = DC - MOD: faces below T fail (except nat 1 is still its own crit-fail band), faces T and above succeed (except nat 20 is still its own crit-success band).

Put the scale in a Starlark def (e.g. check_scale(DC, MOD)) so one definition works for any DC and modifier. Names that start with d20 right after def break parsing (d20test becomes d(20) + test); use check_scale or similar.

Crit steps use early=True so nat 1 and nat 20 match their crit labels even though they also fall in the fail and success ranges—without reordering the ladder so p_at_least("SUCCESS") still counts ordinary and critical successes. Declaration order: crit fail → fail → success → crit success; both crit steps use early=True and are listed before fail and success.

The script

In the playground, open this lesson and click Run (or press Shift+Enter).

def check_scale(DC, MOD):
    T = DC - MOD
    return (
        scale()
        .step("CRITICAL_FAIL", 1..1, early=True)
        .step("FAIL", at_most(T - 1))
        .step("SUCCESS", at_least(T))
        .step("CRITICAL_SUCCESS", 20..20, early=True)
    )

DC = 15
MOD = 5
natural = 2d20kh1
out = natural.bucket(check_scale(DC, MOD))
output("advantage_check", out)
output("p_hit_or_better", out.p_at_least("SUCCESS"))

Reading the result

The text tab shows a four-row table—one row per outcome label—in ladder order. The json tab uses "kind": "ordinal" with the same labels.

p_hit_or_better is p_at_least("SUCCESS"): ordinary successes plus critical successes (nat 20 on the kept die). That matches “did I succeed?” at the table, including crits.

Try this

Builtin details: standard library reference (scale, Scale.step, bucket, at_most, at_least, keep_highest, keep_lowest). Use classify when house rules do not fit four face bands.

What’s next

Powered by the Apocalypse (2d6 + stat)—miss / partial / full on the total with bucket, the usual PbtA pattern.

See the user guide index.

advantage_check · Outcomes

outcome%fracX/400
CRITICAL_FAIL0.251/4001
FAIL20.01/580
SUCCESS70.07/10280
CRITICAL_SUCCESS9.7539/40039

p_hit_or_better · Prob

outcome%fracX/400
p_hit_or_better79.8319/400319