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 d20 | Usual 5e meaning (attacks; similar idea for many checks) |
|---|---|
| Natural 1 | Automatic failure (critical miss) |
| Natural 20 | Automatic 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:
- Parameters —
DCandMODfor the situation you care about (AC 15, +5 to hit, and so on). - 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). - The roll —
1d20,2d20kh1, or2d20kl1; do not write1d20 + MODif you want true nat-1 / nat-20 behavior on the kept die. - Classification —
natural.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"))
T = DC - MOD— lowest natural that still meets or beats the DC after addingMOD.at_most(T - 1)/at_least(T)— fail and success on the natural die; use these whenTcomes from variables (literal range sugar like..(T - 1)only works with numeric literals in source).2d20kh1— advantage on the example; use1d20or2d20kl1for a normal roll or disadvantage.check_scale(DC, MOD)— reuse anywhere (1d20.bucket(check_scale(12, 4)), loops over DCs, etc.).
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
- Change
DCandMOD(or loop over pairs) and callcheck_scale(DC, MOD)for each combination—watch how the fail and success rows move while crit fail and crit success stay on 1 and 20. - Swap
2d20kh1for1d20or2d20kl1and compare the table; only the roll changes, not the scale. - Compare with lesson 11’s fixed bands on
1d20—samebucketmachinery, but a real check needsDCandMOD, not one static partition of the die.
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 | % | frac | X/400 |
|---|---|---|---|
| CRITICAL_FAIL | 0.25 | 1/400 | 1 |
| FAIL | 20.0 | 1/5 | 80 |
| SUCCESS | 70.0 | 7/10 | 280 |
| CRITICAL_SUCCESS | 9.75 | 39/400 | 39 |
p_hit_or_better · Prob
| outcome | % | frac | X/400 |
|---|---|---|---|
| p_hit_or_better | 79.8 | 319/400 | 319 |