Skip to main content

Library for simulating dice, dice rolls, and stats on dice.

Project description

A dice library.

For use in dice mechanics explorations, probability experiments, and little tools for Table Top RPGs and Board Games. Not designed or optimized to be used in computer games, high CPU simulations or the like.

Installing

via uv:

uv add die

via pip:

pip install die

Quickstart

from die import d6

d6() or d6.roll()       # roll a ready-made six-sided die
# Die, also return result of roll when mathed, compared, or coerced
result = d6 + 3         # adds 3 to result of rolling d6
if d6 > 3:              # if roll is greater than 3, prints 'lucky!'
    print('lucky!')
d6 == 6                 # True if roll equals 3
print(f'rolled a {d6}') # conversion to string rolls d6
result = float(d6)      # rolls d6, coerces to float

import die

die.roll('3d6+2')           # parse dice notation, roll it once
d8d6 = die.d8.plus(die.d6)  # compose dice (d8+d6)

Usage

See examples/ for runnable walk throughs of everything die library has to offer: demo_die.py (dice), demo_rolls.py (rolls, notation, composing, odds), demo_stats.py (statistics and charts); demo.py runs all three.

Standard Dice

Standard(sides) is the common numeric die. Ready-made instances (d1…``d20``, d24, d30, d100, d1000, d10000) are importable directly:

from die import d6, d20 # ready-made dice
d6()                    # -> single roll, e.g. 4

import die
d69 = die.Standard(69)  # or construct your own
d6(3)                   # -> sum of three rolls
d6(3, max)              # -> max of three rolls
d6(3, func=list)        # -> return individual rolls as a list

After a roll, the die remembers it:

d6.roll()
d6.result          # -> (face, value) of the most recent roll
d6.face            # -> face text, e.g. '4'
d6.value           # -> value, e.g. 4

The die retains every roll; subscript it to reach older ones (this does not roll):

d6.roll(3)         # roll three times
d6[0]              # -> first roll's value
d6[-1]             # -> most recent value, same as d6.value
list(d6)           # -> all rolled values, in order
sum(d6)            # -> total of every retained roll
d6.history         # -> full Result(face, value) records, in order
len(d6)            # -> number of retained rolls
d6.clear()         # empty the history

Exploding dice

Exploding rolls again and adds when it rolls into the explode range, recursively. Ready-made: d2x…``d10000x`` (explode on max), d2p…``d100p`` (Hackmaster penetrating), d100rm (Rolemaster double-ended):

d20x = die.Exploding(20)                 # explodes on 20 (the max), rolls itself again
d6x = die.Exploding(6, explode_range=2)  # explodes on 5-6

# Hackmaster style: math correct (subtracts 1 per explosion), may explode to a smaller die.
d6p = die.Exploding(6, math_correct=True)
d20p = die.Exploding(20, explode_to=d6p, math_correct=True)

# Rolemaster style double-ended: explodes up on 96-100 and down (subtracts) on 1-5.
d100rm = die.Exploding(100, explode_range=5, explode_range_low=5)

Rerolling dice

Rerolling rolls again (replacing original roll) when it rolls into the reroll range:

d6r = die.Rerolling(6)                          # rerolls 1s once, keeps the second roll
d6rr2 = die.Rerolling(6, reroll_range=2, once=False)  # rerolls 1-2 until it rolls 3+

Custom dice

Dice need not be sequential, numeric, or even mappable to numbers:

# Numeric: non-sequential numeric faces, e.g. a backgammon doubling die.
doubling = die.Numeric('doubling', (2, 4, 8, 16, 32, 64))
# Ready-made: die.doubling (2/4/8/16/32/64), die.averaging (2/3/3/4/4/5, mean
# 3.5), die.d09 (d10 read 0-9, '0' counts as 0 not 10).

# NumericBased: textual faces with numeric values; (face, value, count) spec.
fudge = die.NumericBased('dF', (('+', 1, 2), (' ', 0, 2), ('-', -1, 2)))
# Ready-made: die.dF and die.coin (heads=1, tails=0).

# Symbolic: values need not be numeric; roll() returns the value
# (a list of them when count > 1).
pet = die.Symbolic('pet', (('bark', 'dog', 2), ('meow', 'cat', 1)))
pet.roll()         # -> 'dog' or 'cat'
# Ready-made: Warhammer 40k die.scatter (HIT/arrow) and die.blast (2/4/6/8/10/MISFIRE).

# Die: the base class, roll your own from (face, value) pairs.
d6 = die.Die('d6', (('1', 1), ('2', 2), ('3', 3), ('4', 4), ('5', 5), ('6', 6)))

Gensys Narrative Dice

Ready-made FFG narrative dice (Genesys, Star Wars): gsys_boost, gsys_setback, gsys_ability, gsys_difficulty, gsys_proficiency, gsys_challenge, gsys_force. Each Symbolic face value is a collections.Counter of symbols (success/advantage/failure/threat/triumph/despair/light/dark); sum a pool’s rolls to total its symbols (cancellation is left to you):

from collections import Counter

die.gsys_proficiency.roll()   # -> e.g. Counter({'success': 1, 'advantage': 1})
pool = (die.gsys_proficiency, die.gsys_ability, die.gsys_difficulty)
sum((d.roll() for d in pool), Counter())  # -> totalled symbols across the pool

Rolls

Roll groups dice and applies func (default sum) to their rolled values:

roll = die.Roll((die.d6, die.d6))
roll.notation      # -> '2d6', derived from the dice
roll.roll()        # -> 2d6
roll.roll(3)       # -> 2d6, three times, summed
roll.roll(3, func=list)  # -> 2d6, three times, as a list

best = die.Roll((die.d20, die.d20), func=max, name='advantage')
best.roll()        # -> higher of two d20

# A Roll's dice are fixed at construction; compose a new Roll to change them.
bigger = roll.plus(die.d4)
bigger.notation    # -> '2d6+d4'

Like Die, a Roll remembers its most recent roll:

roll.roll()
roll.results       # -> ((face, value), ...) one per die
roll.value         # -> the aggregated (func-applied) result

It retains every roll too; subscripting yields aggregate values (no roll):

roll.roll(3)
roll[-1]                  # -> most recent value, same as roll.value
roll[0]                   # -> first roll's value
roll.history[-1]          # -> Rolled(results=((face, value), ...), value=...)
roll.history[0].results   # -> first roll's per-die (face, value) tuples

Dice Notation

parse() builds a Roll from standard dice notation: NdS terms, NdF Fudge dice, integer constants, + - * / // %, parentheses, standard precedence, keep/drop suffixes kh kl dh dl (optional count, default 1), counting suffixes >=N/<=N, and exploding/penetrating/reroll suffixes x/p/r/rr on a dice term (not valid on Fudge dice):

attack = die.parse('2d6+3')
attack.roll()
die.parse('4dF')              # four Fudge dice
die.parse('(d6*2+3)/d6')      # full expressions
die.parse('4d6dl')            # 4d6, drop the lowest
die.parse('2d20kh')           # 2d20, keep the highest
die.parse('3d6x')             # three exploding d6, summed
die.parse('d6p')              # one penetrating (Hackmaster math-corrected) d6
die.parse('4d6r')             # four d6, each rerolling 1s once
die.parse('6d10>=7')          # six d10, count of dice rolling 7+
die.roll('3d6+2')             # parse and roll once, in one call

Composing

Die and Roll compose in code via plus, minus, times, div, mod — each returns a new Roll instance (nothing rolls until you say so); chains bind tight, left to right:

combo = die.d6.times(2).plus(3).div(die.d6)   # the Roll (d6*2+3)/d6
combo.roll()

div() is float division, rounded half up to an int by default:

die.d6.div(2)                    # fractions round half up
die.d6.div(2, rounding='down')   # fractions round down (floor, //)
die.d6.div(2, rounding='up', ndigits=2)  # fractions round up at 2 decimal places

keep_highest, keep_lowest, drop_highest, drop_lowest keep/drop the n (default 1) highest/lowest die values and sum the kept:

die.parse('4d6').drop_lowest()      # the Roll 4d6dl
die.d20.plus(die.d20).keep_highest()     # the Roll (d20+d20)kh

successes, failures count die values >= / <= a target instead of summing (dice pools):

die.parse('6d10').successes(7)      # the Roll 6d10>=7
die.parse('6d10').failures(1)       # the Roll 6d10<=1

Odds

Every numeric Roll knows its exact probabilities; chance() (on both Die and Roll) answers “what are the odds of rolling N or better?”:

roll = die.parse('3d6')
roll.odds            # -> [Odds(value, ways, probability), ...] one per distinct result
roll.odds[0]         # -> Odds(value=3, ways=1, probability=0.00463)
roll.chance(gte=15)  # -> 9.26, percent chance of rolling 15 or higher
die.d20.chance(lte=2)  # -> 10.0, percent chance of rolling 2 or lower

odds is a Roll property; for a lone Die, compose or parse it into a Roll first (die.parse('d6').odds). Odds are computed from face values, so they are wrong for dice whose rolls don’t come straight from their faces (Exploding, Rerolling).

Reproducible rolls

Set your own seed for all Die. And/or, override specific Die:

import random
die.Die.rng = random.Random(42)                  # reproducible rolls, all dice
seeded = die.Standard(6, rng=random.Random(42))  # reproducible rolls, one die

Statistics

Statistic rolls a numeric Roll many times and reports empirical statistics (a non-numeric Roll raises ValueError). run() returns self, so statistics chain; it also accumulates — repeated calls extend the rolled values — and clear() empties them:

die.Statistic(die.parse('3d6')).run(10_000).mean

stat = die.Statistic(die.d3d6)
stat.run(1000)
stat.run(1000)     # now 2000 accumulated rolls
stat.clear()       # back to none

Properties (all read only; every one but count raises RuntimeError until run() has been called):

stat.roll          # -> the Roll statistics are calculated on
stat.count         # -> number of rolls made; 0 until run(), never raises
stat.values        # -> tuple of every rolled value, in roll order
stat.sum           # -> total of all rolled values
stat.mean          # -> arithmetic mean of rolled values
stat.min, stat.max # -> smallest / largest rolled value
stat.median        # -> median of rolled values
stat.mode          # -> most common rolled value (first rolled wins ties)
stat.stdev         # -> sample standard deviation (StatisticsError if < 2 rolls)
stat.counts        # -> collections.Counter of rolled values
stat.frequencies   # -> observed Odds(value, ways, probability) per distinct
                   #    value, sorted; the empirical counterpart of Roll.odds

Charts

die.charts charts dice probabilities and rolled results (usable directly after import die). Chart functions take a Roll or Die (theoretical probabilities, from odds) or a Statistic (observed frequencies, from a simulation).

ASCII charts return multi-line strings for terminals:

print(die.charts.histogram(die.parse('3d6')))        # theoretical probability curve
stat = die.Statistic(die.d3d6).run(10_000)
print(die.charts.histogram(stat, width=40))          # observed, one bar per value
print(die.charts.scatter(stat, width=70, height=10)) # rolled results, roll order vs value

Vega-Lite emitters return spec dicts; json.dumps() one and render it with any Vega-Lite tool (https://vega.github.io/editor, VSCode, JupyterLab, vl-convert for PNG/SVG):

die.charts.vega(die.parse('3d6'))     # bar chart of probability per value
die.charts.vega(stat, kind='line')    # same chart as a line
die.charts.vega_scatter(stat)         # scatter of rolled results, roll order vs value
die.charts.vega_compare(stat)         # observed frequency bars overlaid with theoretical odds line

vega_compare’s theoretical line carries the odds caveat: it is wrong for Exploding/Rerolling dice; the observed bars show the real distribution.

Warnings

Every operator and coercion rolls the die — including equality and str():

die.d6 + 3         # rolls, then adds 3
die.d6 == die.d6   # rolls BOTH dice, compares the results!
str(die.d6)        # rolls, returns the result as a string, same as int()/float()

Dice are not hashable, and comparing or sorting them gives unstable results. Compare Die.items / Roll.dice for structural equality. Use the notation property, not str(), to get a die/roll’s textual name. Or, set your own via name property.

The roll-state properties (result, face, value, results) raise RuntimeError until the first roll; subscripting into the roll history (obj[i]) instead raises IndexError while empty, history is just empty, and neither rolls.

Testing

ruff check .
pytest

pre-commit runs ruff check and ruff format automatically before each commit:

pre-commit install
pre-commit run --all-files

Documentation

Full docs at Read the Docs.

Build locally:

uv sync --group docs
uv run sphinx-build -b html docs/source docs/build

Then open docs/build/index.html in a browser.

History

See CHANGELOG for release history and breaking changes.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

die-3.0.0.tar.gz (79.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

die-3.0.0-py3-none-any.whl (33.0 kB view details)

Uploaded Python 3

File details

Details for the file die-3.0.0.tar.gz.

File metadata

  • Download URL: die-3.0.0.tar.gz
  • Upload date:
  • Size: 79.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for die-3.0.0.tar.gz
Algorithm Hash digest
SHA256 6627e91558c892bbbecdebac69fef51578e9990b037eff0ebf7ec1ba672bb639
MD5 5a0d6ef142e76b7ceb83e7d01442241f
BLAKE2b-256 a4f7d6f30e3cbd9272e0c1d5ac3b5ce55187b078432927763c23454c3b6ee8f1

See more details on using hashes here.

File details

Details for the file die-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: die-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 33.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for die-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 714c82cfb36aee1b57250a1e89b86d65e0e778f350335c1828ae36bce054b9f4
MD5 347067cc74ac781b5948adc0bce5e3e6
BLAKE2b-256 14eafb8f60ea2b14650653e7d7919ed2367804cbebf16888b5d74060f212bdc0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page