User guide Tutorial Cookbook Open playground

Dice standard library

This reference helps you turn a tabletop rule into a .dice script. You do not need to know Rust or Starlark to use the examples. For a first walk-through, start with the tutorial.

The playground calculates possible results and their chances; it does not pick one random roll. Each dice example below is a complete script you can copy into the playground and run on its own.

Reading the examples

↗roll = 2d6 + 3
output("My roll", roll)
output("Chance of ten or more", roll.p_ge(10))

The def ... -> ... lines in this reference describe how to call an operation; they are signatures, not scripts to paste into the playground. A signature such as d(sides: int) -> DieRoll says that d takes a whole number and returns a numeric roll. You write d(6), not the type names.

Signature notationWhat it tells you
intA whole number, such as 6.
floatA number that can have a decimal part. Probabilities use 0–1: 0.5 means 50%.
strText in quotes, such as "Hit".
list[int]Whole numbers in square brackets, such as [1, 2, 2, 3].
name=2You may leave this argument out; its default is 2.
*args, *spec, or *bandsExtra arguments are accepted; the entry explains which ones. Do not type the star in an ordinary call.
/ or a bare *Arguments before / are positional; arguments after a bare * must be named, such as early=True. These markers are not values to pass.
-> TypeThe kind of value the operation gives back.

Core ideas

ValueMeaning at the tableWhat to do with it
DieRollA numeric roll or total, with a chance for every possible result.Show it with output, add a modifier, or ask for the chance of reaching a target.
DicePoolSeveral dice whose individual faces still matter.Count successes, choose a die, or call .sum() when you need the total.
ScaleA ladder of labels, from worst to best, optionally with number ranges.Build it with scale().step(...), then use it to label a roll.
OutcomesChances for named results such as miss, partial, and hit.Show the results or ask for “partial or better”.
IntBandA range describing which whole numbers match.Use it in face matching or a scale; it is not a roll or a probability itself.
ProbTableSeveral labeled probability questions in one table.Show it with output.

Operations such as .keep(...) and .step(...) return new values rather than changing the old ones. Keep the new value by giving it a name or passing it to another operation.

Combining numeric rolls

You writeMeaning at the table
a + bAdd two independent numeric rolls. Even roll + roll means two independent rolls, not doubling the same face.
roll + 5Add a flat bonus to every result.
roll - 3Subtract a penalty from every result.
a - bSubtract one independent numeric roll from another.
roll * 10Multiply each result by 10. The multiplier must be a positive whole number.
roll // 2Divide each result by 2 and round down. The divisor must be a positive whole number.

A bare assignment such as pool = 2d6 keeps the dice as a DicePool. To store a finished total, write roll = dice_pool(2, 6).sum(). Dice notation is shortened according to context; explicit .sum() makes your intent clear.

Pools have their own joining rules: pool + pool puts dice into a larger pool rather than adding their faces. See API conventions for face matching, pool counts, and the difference between keep and ignore.

Dice notation such as 4d6dl1 is a shorter way to call some of the functions below. See the dice notation lesson.

Building dice and totals

Start here for ordinary dice, custom faces, and totals that keep or drop dice.

d

def d(sides: int) -> DieRoll

Make one fair die: every face has the same chance.

Parameters

Details

d(6) means a six-sided die, just like 1d6 in a .dice script. It returns a DieRoll: all possible results and their chances, not one random roll. Give that result to output to see its table.

↗output("One six-sided die", d(6))

die_faces

def die_faces(faces: list[int]) -> DieRoll

Make a die with numbers of your choosing on its faces.

Parameters

Details

Write the numbers inside square brackets, separated by commas. This is a list. Each entry is one equally likely face, so repeating a number makes it more likely. The example has four faces: 2 is twice as likely as either 1 or 3. The result is a DieRoll you can add to other rolls or show with output.

↗output("Custom die", die_faces([1, 2, 2, 3]))

dice_pool

def dice_pool(count: int, sides: int) -> DicePool

Make several dice that you can examine separately before adding them.

Parameters

Details

The result is a DicePool. Use a pool when your rule counts successes or chooses the highest die. The dice have not been added together yet. Write .sum() after the pool to turn it into a DieRoll that output can show.

↗pool = dice_pool(3, 6)
output("Total of three dice", pool.sum())
output("Number of sixes", pool.count(6))

sum

def sum(value) -> DieRoll

Add the dice in a pool to get the chances for each possible total.

Parameters

Details

The result is a DieRoll; for four d6, the totals run from 4 to 24. You can also write pool.sum(): the dot means “use this operation on this pool”. This dice helper does not add an ordinary list of numbers.

↗pool = dice_pool(4, 6)
output("Four dice added together", sum(pool))

drop_lowest

def drop_lowest(count: int, sides: int, drop: int) -> DieRoll

Leave out the lowest dice, then add the rest.

Parameters

Details

For the familiar ability-score rule, roll four d6 and leave out the lowest one. The result is a DieRoll of totals from 3 to 18, just like 4d6dl1. Tied low dice are still separate dice; dropping one removes only one of them.

↗output("Ability score", drop_lowest(4, 6, 1))

drop_highest

def drop_highest(count: int, sides: int, drop: int) -> DieRoll

Leave out the highest dice, then add the rest.

Parameters

Details

For example, roll four d6, remove the highest one, and add the other three. The result is a DieRoll of totals, also written 4d6dh1. If several dice tie for highest, each still counts as a separate die.

↗output("Four d6, leave out the highest", drop_highest(4, 6, 1))

keep_highest

def keep_highest(count: int, sides: int, keep: int) -> DieRoll

Choose the highest dice and add them, leaving the others out.

Parameters

Details

Keeping three of four d6 is the same as dropping the lowest one. The result is a DieRoll of totals, also written 4d6kh3 in dice notation. Keeping just one of two d20 models advantage.

↗output("Advantage", keep_highest(2, 20, 1))

keep_lowest

def keep_lowest(count: int, sides: int, keep: int) -> DieRoll

Choose the lowest dice and add them, leaving the others out.

Parameters

Details

Keeping one of two d20 models disadvantage. The result is a DieRoll of the kept total. In dice notation, this example is 2d20kl1.

↗output("Disadvantage", keep_lowest(2, 20, 1))

explode

def explode(dist: DieRoll, max_depth: int = 2) -> DieRoll

Roll again on the highest result and add the extra roll to the total.

Parameters

Details

This is an exploding die. On a d6, a 6 earns another d6; another 6 can earn another roll. The result is a DieRoll of totals, including those above 6. To keep the calculation finite, extra rolls stop at max_depth even if the last die is a 6. This models a capped rule, not an unlimited chain.

↗output("Exploding d6, at most two extra rolls", explode(d(6), max_depth=2))

open_ended_d100

def open_ended_d100(max_chain: int = 8) -> DieRoll

Make a Rolemaster-style d100 roll that can finish below 1 or above 100.

Parameters

Details

A first roll of 6–95 stands as it is. On 1–5, roll again and subtract; on 96–100, roll again and add. Further rolls continue only on 96–100, keeping the same subtracting or adding direction. A printed 00 means 100 here. The result is a DieRoll of final totals.

↗output("Open-ended d100", open_ended_d100())

shift

def shift(dist: DieRoll, delta: int) -> DieRoll

Add the same bonus or penalty to every possible total.

Parameters

Details

The returned DieRoll keeps the same chances but moves the numbers. Adding 3 to a d20 changes its results from 1–20 to 4–23. Usually roll + 3 is the simplest way to write this; shift(roll, 3) does the same thing.

↗output("d20 with a +3 bonus", shift(d(20), 3))

Inclusive ranges

Describe which numbers match. Both endpoints count; these ranges are not rolls by themselves.

through

def through(lo: int, hi: int) -> IntBand

Describe a range of whole numbers, including both ends.

Parameters

Details

through(5, 6) means “5 or 6”, also written 5..6 in a .dice script. It returns an IntBand, a range for matching faces or defining outcome bands. It does not roll a die or calculate a chance on its own.

↗output("Number of 5s and 6s", dice_pool(3, 6).count(through(5, 6)))

at_most

def at_most(hi: int) -> IntBand

Describe “this number or lower”, including the number itself.

Parameters

Details

at_most(2) matches 2, 1, 0, and every lower whole number. It returns an IntBand range, also written ..2 in a .dice script. Use it to match faces or define outcome bands, not to ask for a probability directly.

↗output("Number of low dice", dice_pool(3, 6).count(at_most(2)))

at_least

def at_least(lo: int) -> IntBand

Describe “this number or higher”, including the number itself.

Parameters

Details

at_least(5) matches 5, 6, 7, and every higher whole number. It returns an IntBand range, also written 5.. in a .dice script. This describes which faces match; count or a probability method works out their chances.

↗output("Number of successes on 5+", dice_pool(3, 6).count(at_least(5)))

Pool rules (faces still matter)

Use these when a rule examines individual dice rather than just the total.

count

def count(pool: DicePool, spec) -> DieRoll

Count how many dice show a matching face, rather than adding their values.

Parameters

Details

If each 5 or 6 is a success, three dice can give 0, 1, 2, or 3 successes. The returned DieRoll gives the chance of each count. pool.count(spec) is another way to write the same operation.

↗pool = dice_pool(3, 6)
output("Successes on 5 or 6", count(pool, [5, 6]))

order_stat

def order_stat(pool: DicePool, k: int) -> DieRoll

Choose one die by its position from highest to lowest.

Parameters

Details

Use 1 for the highest die, 2 for the second-highest, and so on. Ties still occupy separate positions: in 6, 6, 2 the second-highest is 6. The result is a DieRoll of the chosen value, not a sum. You can also write pool.order_stat(k).

↗output("Highest of three d6", order_stat(dice_pool(3, 6), 1))

middle_of

def middle_of(pool: DicePool, keep: int) -> DieRoll

Add the middle dice, leaving out the lowest and highest results.

Parameters

Details

With three dice and keep=1, this chooses the middle die. With five dice and keep=3, it adds the middle three. The result is a DieRoll of totals. If the number left out is odd, one more die is removed from the high end than from the low end. You can also write pool.middle_of(keep).

↗output("Middle of three d6", middle_of(dice_pool(3, 6), 1))

pool_map

def pool_map(pool: DicePool, map_fn) -> DieRoll

Work out the chances for a custom rule that looks at all the dice together.

Parameters

Details

Try helpers such as count and order_stat first. When they do not fit, write a small function: def names your rule, and return supplies its answer. The engine calls it for every possible combination and collects the answers into a DieRoll. Large pools can take a long time because every combination is checked.

Here doubles score 2 and everything else scores 0. Square brackets pick a die from the list: [0] is the first, [1] the second. == asks whether they are equal. Keep the indentation shown: it marks which lines belong to the rule and the if.

↗def score_doubles(faces):
    if faces[0] == faces[1]:
        return 2
    return 0

output("Doubles score", pool_map(dice_pool(2, 6), score_doubles))

success_pool

def success_pool(count: int, sides: int, mode: str = "baseline") -> DieRoll

Count successes for a specific rule: even faces and the highest face succeed; highest faces also roll again.

Parameters

Details

On a d6, 2, 4, and 6 each earn one success, and each 6 adds another d6. On a d5, 2, 4, and 5 succeed, and each 5 adds another d5. The result is a DieRoll of success counts, never below 0. This is not a general target-number system: for “5 or higher succeeds”, use count instead.

ModeWhat happens to rolled 1s?
"baseline"Nothing extra.
"ones_cancel"Any 1 stops all extra dice earned in that round; successes still count.
"ones_remove"Each 1 subtracts one success from the final count.
"implode"Each 1 earns a penalty roll of the same die; an odd result subtracts one success. Penalty rolls do not trigger more rolls.

Extra dice are processed in rounds, capped at 48 rounds including the first. This is a finite approximation to unlimited explosions. Start with small pools.

↗output("Even faces succeed, sixes explode", success_pool(1, 6))

Named outcomes

Turn numbers or custom rules into labels such as miss, partial, and hit.

scale

def scale() -> Scale

Start a ladder of named results, such as miss, partial success, and full success.

scale() takes no arguments and returns an empty Scale. Add labels from worst to best with .step(...). Each step returns the longer ladder, so you can write one step after another. Quotes mark labels as text. A scale describes the rule; use bucket to find its chances for a roll.

↗results = scale().step("Miss", at_most(6)).step("Partial", through(7, 9)).step("Hit", at_least(10))
output("Move result", bucket(2d6, results))

You can omit the number ranges when using classify to choose labels yourself, or when supplying the ranges separately to bucket.

bucket

def bucket(dist: DieRoll, scale: Scale, *spec) -> Outcomes

Group numeric totals into named results, such as miss, partial success, and hit.

Parameters

Details

First build a scale with a number range for each label. bucket then returns Outcomes: the chance of each label, rather than each individual total. You can also write roll.bucket(results).

↗results = scale().step("Miss", at_most(6)).step("Partial", through(7, 9)).step("Hit", at_least(10))
output("Move result", bucket(2d6, results))

Every possible total must match a band, or the script reports an error. If bands overlap, steps marked early=True are checked first, then the others; within each group, the first matching step wins.

You can instead supply cut points: for three labels, [6, 9] means 6 or less, 7–9, and 10 or more. These replace any ranges already on the scale.

↗results = scale().step("Miss").step("Partial").step("Hit")
output("Move result using cut points", bucket(2d6, results, [6, 9]))

classify

def classify(dist: DieRoll, scale: Scale, classify) -> Outcomes

Choose a label for each possible roll using a rule you write yourself.

Parameters

Details

Use bucket for simple number bands. Use classify when a special result, such as a natural 20, needs its own rule. The returned Outcomes contains the chance of each label. Your rule sees the value of the roll you pass in: pass the unmodified die if natural faces matter, not a total with a bonus already added.

def starts your rule, if tests a condition, and return gives its label. == means “is equal to”. Keep the indentation shown.

↗def name_roll(face):
    if face == 20:
        return "Critical"
    if face >= 10:
        return "Hit"
    return "Miss"

results = scale().step("Miss").step("Hit").step("Critical")
output("d20 check", classify(d(20), results, name_roll))

joint_classify

def joint_classify(
    d1: DieRoll,
    d2: DieRoll,
    scale: Scale,
    classify,
) -> Outcomes

Choose a named result by looking at two independent rolls together.

Parameters

Details

For an opposed check, compare one player's roll with another's. Your function receives both numbers and returns a label; the engine checks every possible pair. The result is Outcomes, with a chance for each label. Even if you pass the same roll variable twice, the two rolls are independent, not two references to the same rolled face.

↗def compare(left, right):
    if left > right:
        return "Win"
    if left == right:
        return "Tie"
    return "Lose"

results = scale().step("Lose").step("Tie").step("Win")
output("Opposed d6 rolls", joint_classify(d(6), d(6), results, compare))

def names the rule, and return supplies its answer. The indented lines belong to the rule; > means “greater than” and == means “equal to”.

Showing results

Use output to put a result in the report, and prob_table to compare several chances.

output

def output(*args) -> None

Show a result in the playground report.

Parameters

Details

Call output at least once so you have a result to read. Put a helpful title in quotes, followed by the result. You can show a full numeric roll (DieRoll), named results (Outcomes), one probability, or a table made with prob_table. A DicePool must first become a result, for example with .sum() or .count(6).

↗roll = 2d6 + 3
output("Total with bonus", roll)
output("Chance of 10 or more", roll.p_ge(10))

roll = ... gives the calculation a name so you can use it on later lines. A probability is a number from 0 to 1: 0.5 means 50%. Do not multiply it by 100 before passing it to output. The call records the result and returns None (no value); do not use it to build another roll.

prob_table

def prob_table(rows: list) -> ProbTable

Put several probability questions into one table for comparison.

Parameters

Details

Each row has a text label and a probability. Rows answer separate questions; they may overlap and do not have to add to 100%. The result is a ProbTable; pass it to output to show it.

Square brackets make the list of rows. Parentheses group each label with its chance. Here .p_ge(...) asks for the chance of meeting or beating a target.

↗roll = dice_pool(2, 6).sum()
rows = [("Need 7+", roll.p_ge(7)), ("Need 10+", roll.p_ge(10))]
output("Compare targets", prob_table(rows))

DieRoll methods

Use these on a numeric roll or total, such as roll = dice_pool(2, 6).sum(). Write roll.p_ge(7), not DieRoll.p_ge(7).

DieRoll.mean

def DieRoll.mean() -> float

Find the average total you would get over many rolls.

A d6 has a mean of 3.5 even though no face shows 3.5. This is an average, not the most likely result and not a probability. The result is a decimal number. The report for a full roll already includes its mean; use this method when you need the number in a calculation.

↗roll = d(6)
average = roll.mean()  # 3.5; a number you can use in later calculations.
output("The roll, with its mean in the report", roll)

DieRoll.pmf

def DieRoll.pmf(value: int) -> float

Find the chance of rolling exactly one number.

Parameters

Details

For two d6, a total of 7 has a chance of 6 out of 36, about 16.7%. The returned probability is a number from 0 to 1, not a percentage from 0 to 100. pmf is short for “probability mass function”; think “chance of this exact result”. Use .p_ge(7) instead if you mean “7 or higher”.

↗roll = dice_pool(2, 6).sum()
output("Exactly seven", roll.pmf(7))

DieRoll.p_ge

def DieRoll.p_ge(value: int) -> float

Find the chance of meeting or beating a target number.

Parameters

Details

Use this for “need 15 or more” checks. The letters ge mean “greater than or equal to”, so the target itself counts. The result is a probability from 0 to 1. This checks the finished total, not each separate die.

↗roll = 2d10 + 3
output("Meet a target of 15", roll.p_ge(15))

DieRoll.cdf

def DieRoll.cdf(value: int) -> float

Find the chance of this total or anything lower.

Parameters

Details

Use this for roll-under rules, where low numbers are good. .cdf(8) includes 8 itself. It returns a probability from 0 to 1. cdf means “cumulative distribution function”: add up the chances from the bottom.

↗roll = dice_pool(2, 6).sum()
output("Eight or less", roll.cdf(8))

DieRoll.clamp

def DieRoll.clamp(min: int, max: int) -> DieRoll

Set a floor and a ceiling for the final result.

Parameters

Details

Anything below the floor becomes the floor; anything above the ceiling becomes the ceiling. Those chances are kept, not thrown away. The returned DieRoll models a rule such as “damage is at least 1, at most 6”.

↗damage = d(6) + 2
output("Damage capped at six", damage.clamp(1, 6))

DieRoll.support_size

def DieRoll.support_size() -> int

Count how many different numeric results are possible.

Two d6 have 11 possible totals, 2 through 12, even though there are 36 ways the dice can land. The returned whole number counts totals, not combinations. “Support” is the mathematical name for the set of possible results.

↗roll = dice_pool(2, 6).sum()
possible_totals = roll.support_size()  # 11, counting totals 2 through 12.
output("Two dice: eleven possible totals", roll)

DieRoll.keep

def DieRoll.keep(spec) -> DieRoll

Keep only matching results and recalculate their chances to add up to 100%.

Parameters

Details

On a d6, keeping 5 and 6 makes each of them 50% likely. This describes a roll restricted to those results, not the chance of rolling them on an ordinary d6. Use .p_ge(5) for that probability. The returned DieRoll leaves the original unchanged. Keeping no possible results is an error.

↗output("Only fives and sixes", d(6).keep([5, 6]))

DieRoll.remove

def DieRoll.remove(spec) -> DieRoll

Remove matching results and share all the probability among those left.

Parameters

Details

Removing 1 from a fair d6 leaves 2–6, each with a 20% chance. This is different from .ignore(1), which keeps the chance of rolling 1 but makes that result worth 0. The result is a new DieRoll; removing every possible result is an error.

↗output("A die without ones", d(6).remove(1))

DieRoll.convert

def DieRoll.convert(spec, to: int) -> DieRoll

Change matching results to a new number without changing how often they happen.

Parameters

Details

For a “sixes count double” rule, turn 6 into 12. Other results stay as they are. If several results become the same number, their chances add together. The result is a new DieRoll; the original is unchanged.

↗output("Sixes count as twelve", d(6).convert(6, 12))

DieRoll.ignore

def DieRoll.ignore(spec) -> DieRoll

Make matching results worth zero, while keeping their chance of happening.

Parameters

Details

On a d6, ignoring 1–4 gives a 4-in-6 chance of 0, plus the usual chances of 5 and 6. Unlike .remove(...), it does not rule out those rolls. It returns a new DieRoll and is shorthand for .convert(spec, 0).

↗output("Only high faces add points", d(6).ignore(through(1, 4)))

DieRoll.bucket

def DieRoll.bucket(scale: Scale, *bands) -> Outcomes

Turn this roll's totals into named results using your scale.

Parameters

Details

This is the same as bucket(roll, results). It returns Outcomes, which you can show as a table or ask about with .p_at_least("Hit"). Each possible total must be covered by a band.

↗results = scale().step("Miss", at_most(6)).step("Hit", at_least(7))
roll = dice_pool(2, 6).sum()
output("Move result", roll.bucket(results))

DicePool methods

Use these on a pool, such as pool = dice_pool(3, 6). Face operations work on each die separately; .sum() turns the pool into a total.

DicePool.sum

def DicePool.sum() -> DieRoll

Add all the dice in this pool to get one total.

The returned DieRoll gives the chance of each total; for three d6, these run from 3 to 18. Use it when you no longer need to examine individual faces. The original pool is unchanged, so you can still ask it other questions.

↗pool = dice_pool(3, 6)
output("Three dice added together", pool.sum())

DicePool.keep

def DicePool.keep(spec) -> DicePool

Restrict every die to matching faces, then recalculate each die's chances.

Parameters

Details

Keeping 5 and 6 on three d6 makes every die show either 5 or 6 with equal chances. It does not roll ordinary dice and discard the low ones: the pool still has three dice. The returned DicePool is new; the original pool is unchanged. It is an error if any die has no possible face left.

↗pool = dice_pool(3, 6).keep([5, 6])
output("Only fives and sixes: total 15–18", pool.sum())

DicePool.remove

def DicePool.remove(spec) -> DicePool

Remove matching faces from each die, not dice from the pool.

Parameters

Details

Removing 1 leaves faces 2–6 on each d6, with equal chances. The result is a new DicePool with the same number of dice. Use .ignore(1) instead if ones should still happen but contribute 0. It is an error if any die loses all its possible faces.

↗output("Three dice with no ones", dice_pool(3, 6).remove(1).sum())

DicePool.convert

def DicePool.convert(spec, to: int) -> DicePool

Give matching faces a different value on every die.

Parameters

Details

For “sixes count double”, change each 6 to 12 before adding the dice. The faces keep their original chances. This returns a new DicePool and leaves the original unchanged.

↗pool = dice_pool(3, 6).convert(6, 12)
output("Total with double sixes", pool.sum())

DicePool.ignore

def DicePool.ignore(spec) -> DicePool

Make matching faces contribute zero when you add the dice.

Parameters

Details

Low faces still happen; they just score no points. This differs from .remove(...), which makes those faces impossible. The result is a new DicePool with the same number of dice, equivalent to .convert(spec, 0).

↗pool = dice_pool(3, 6).ignore(through(1, 4))
output("Add only fives and sixes", pool.sum())

DicePool.count

def DicePool.count(spec) -> DieRoll

Count one success for each die that shows a matching face.

Parameters

Details

Three dice can give 0, 1, 2, or 3 successes. The returned DieRoll shows how likely each count is. It counts dice, not their face values: a 5 and a 6 give two successes, not eleven points.

↗pool = dice_pool(3, 6)
output("Successes on 5+", pool.count(at_least(5)))

DicePool.order_stat

def DicePool.order_stat(k: int) -> DieRoll

Choose the highest die, second-highest die, or another position from the top.

Parameters

Details

1 means highest; 2 means second-highest. Ties count separately: in 6, 6, 2 the second-highest is 6. The result is a DieRoll of that one value. This is the same as order_stat(pool, k).

↗output("Best of three dice", dice_pool(3, 6).order_stat(1))

DicePool.middle_of

def DicePool.middle_of(keep: int) -> DieRoll

Add the middle dice after setting aside low and high results.

Parameters

Details

With three dice, keeping one chooses the middle die. With five dice, keeping three adds the middle three. If an odd number of dice must be left out, one more is removed from the high end. Returns a DieRoll, just like middle_of(pool, keep).

↗output("Middle die", dice_pool(3, 6).middle_of(1))

DicePool.p_any

def DicePool.p_any(*spec) -> float

Find the chance that at least one die shows a matching face.

Parameters

Details

Use this for “any six is a success”. It returns a probability from 0 to 1; 0.5 means 50%. Two or more matching dice still count as success, not as extra probability.

↗output("At least one six", dice_pool(3, 6).p_any(6))

DicePool.p_none

def DicePool.p_none(*spec) -> float

Find the chance that none of the dice show a matching face.

Parameters

Details

Use this for “avoid rolling any ones”. It returns a probability from 0 to 1 and is the opposite of .p_any(...): their answers add up to 1.

↗output("No ones", dice_pool(3, 6).p_none(1))

DicePool.p_at_least

def DicePool.p_at_least(k: int, *spec) -> float

Find the chance of getting enough matching dice.

Parameters

Details

For “at least two successes, with each 5 or 6 succeeding”, ask for two matches to at_least(5). The result is a probability from 0 to 1, not a success count. Use .count(...) instead to see every possible count and its chance.

↗output("Two or more successes", dice_pool(3, 6).p_at_least(2, at_least(5)))

DicePool.bucket

def DicePool.bucket(scale: Scale, *spec) -> Outcomes

Add the dice, then group the total into named results.

Parameters

Details

This is shorthand for pool.sum().bucket(results). It returns Outcomes, not a count of how many individual dice match a band. Use .count(...) when each die earns a success separately.

↗results = scale().step("Miss", at_most(6)).step("Hit", at_least(7))
pool = dice_pool(2, 6)
output("Result of the total", pool.bucket(results))

Outcomes methods

Use these on named results returned by bucket or classify. Higher and lower mean later and earlier on the scale you built.

Outcomes.pmf

def Outcomes.pmf(label: str) -> float

Find the chance of exactly one named result.

Parameters

Details

.pmf("Partial") counts only partial successes, not full hits as well. The result is a probability from 0 to 1. pmf is the mathematical name for the chance of one exact result.

↗results = scale().step("Miss", at_most(6)).step("Partial", through(7, 9)).step("Hit", at_least(10))
move = bucket(2d6, results)
output("Exactly a partial success", move.pmf("Partial"))

Outcomes.p_at_least

def Outcomes.p_at_least(label: str) -> float

Find the chance of this result or any higher step on your ladder.

Parameters

Details

If the scale runs from miss to partial to hit, “at least partial” includes both partial and hit. Higher means added later with .step(...), not alphabetical order. Put your labels from worst to best when building the scale. The result is a probability from 0 to 1.

↗results = scale().step("Miss", at_most(6)).step("Partial", through(7, 9)).step("Hit", at_least(10))
move = bucket(2d6, results)
output("Partial success or better", move.p_at_least("Partial"))

Outcomes.p_at_most

def Outcomes.p_at_most(label: str) -> float

Find the chance of this result or any lower step on your ladder.

Parameters

Details

On a miss, partial, hit scale, “at most partial” includes miss and partial. Lower means added earlier with .step(...). The chosen label itself is included. The result is a probability from 0 to 1.

↗results = scale().step("Miss", at_most(6)).step("Partial", through(7, 9)).step("Hit", at_least(10))
move = bucket(2d6, results)
output("Partial success or worse", move.p_at_most("Partial"))

Scale methods

Use .step(...) on a scale to build your ladder of named results.

Scale.step

def Scale.step(label: str, *band, early: bool = False) -> Scale

Add the next named result to your ladder, working from worst to best.

Parameters

Details

Each call returns a new Scale; it does not change the old one. Save the result with results = results.step(...), or put several .step(...) calls one after another. Give each label a number range when using bucket. You can leave ranges out for classify, or supply them separately to bucket.

↗results = scale().step("Miss", at_most(6))
results = results.step("Partial", through(7, 9))
results = results.step("Hit", at_least(10))
output("Move result", bucket(2d6, results))

For overlapping ranges, early=True means “check this step before ordinary steps”. True is Starlark's word for yes. Early steps are checked in the order added, then ordinary steps in their order; the first match wins. This does not change the ladder order used by .p_at_least(...) and .p_at_most(...).