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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6627e91558c892bbbecdebac69fef51578e9990b037eff0ebf7ec1ba672bb639
|
|
| MD5 |
5a0d6ef142e76b7ceb83e7d01442241f
|
|
| BLAKE2b-256 |
a4f7d6f30e3cbd9272e0c1d5ac3b5ce55187b078432927763c23454c3b6ee8f1
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
714c82cfb36aee1b57250a1e89b86d65e0e778f350335c1828ae36bce054b9f4
|
|
| MD5 |
347067cc74ac781b5948adc0bce5e3e6
|
|
| BLAKE2b-256 |
14eafb8f60ea2b14650653e7d7919ed2367804cbebf16888b5d74060f212bdc0
|