dice.py

A Pythonic definition of a DSL for dice expressions.

3 * D6 + 2

This creates an object with methods for rolling the specified dice, or computing the min, max, mean, and standard deviation.

Implementation

A module to model polyhedral dice.

While this isn’t hugely useful for TTRPG design or literary work, it does do some predictions of the expected distribution of a few simple dice mechanics.

Used as a library

In Rogue-like games, Gold is generally uniform distribution between \(2l\) and \(16l\) for a given level, \(l\). This module defines a UniformValue class for this.

>>> import random
>>> random.seed(42)
>>> level = 1
>>> gold_u = UniformValue(2 * level, 16 * level)
>>> gold_u.min, gold_u.max, 2*level, 16*level
(2, 16, 2, 16)
>>> gold_u.roll()
12

A slightly more colorful approach is to use a more normal distribution.

>>> level = 1
>>> gold_1 = (3 * level) * D6 - level
>>> gold_1.min, gold_1.max, 2*level, 16*level
(2, 17, 2, 16)
>>> str(gold_1)
'3D6-1'
>>> repr(gold_1)
'3 * D6 -1'
>>> level = 2
>>> gold_2 = (3 * level) * D6 - level
>>> gold_2.min, gold_2.max, 2*level, 16*level
(4, 34, 4, 32)
>>> level = 3
>>> gold_3 = (3 * level) * D6 - level
>>> gold_3.min, gold_3.max, 2*level, 16*level
(6, 51, 6, 48)

Using the library directly

PYTHONPATH=src python -c 'from dice import *; print((3*D6+2).roll())'

CLI

python tools/src/dice.py '3*D6+2'

The shell ' apostrophes are required.

Interactive

python tools/src/dice.py -i
Enter a Python-syntax dice expressions, like 3*D6+2.
[dice] (4*D6).kh(3)
6
[dice] count 6
[6 rolls] (4*D6).kh(3)
7
14
13
14
11
12
[6 rolls]
10
14
12
15
16
10
[6 rolls]

Die

class dice.Die(d: int, *, n: int | None = None, adj: int | None = None, keep: int | None = None)[source]

Creates a useful source of random numbers from a “die expression”. A die expression can also provide some expected value information for most (not all) cases. The syntax uses Python objects and operators.

Examples:

>>> from random import seed
>>> seed(42)

The base objects, D4, D6, etc.

>>> D6.roll()
6

A dice expression in Python syntax.

>>> d = 3 * D6
>>> d.roll()
8
>>> d.pool()
[2, 2, 3]
>>> str(d)
'3D6'

A set of dice rolls. Note d = 3 * D6.

>>> roll10 = [(d - 3).roll() for i in range(10)]
>>> roll10
[6, 14, 7, 0, 6, 8, 11, 12, 8, 3]

Expected values.

>>> d.min
3
>>> d.max
18
>>> d.mean
10.5
>>> d.stdev
2.958...

The “roll 4d6 keep the highest 3” expression. The ()’s are required.

>>> char = (4 * D6).kh(3)
>>> char.pool()
[3, 4, 6]

Expected Values

The expected values are exact, and can do things like transform a dice value into a z-score: (d.roll() - d.mean) / d.stdev. This will have value from about -3 to about +3 and can be used to turn a number into a descriptive summary.

The predictions do not account for kh() modifiers.

See https://rpubs.com/Avijit0616/698613

The expected values are the basis for comparison among two Die instances.

>>> D8 > D6
True

Note

A thing that “seems” to behave inconsistently in unit tests.

>>> seed(42)
>>> [r for r in range(10) if D20.roll() >= 13]
[7, 9]
kh(keep: int | None = None) Self[source]

Creates new Die with the keep highest “keep” values.

property mean: float

The mean of this dice expression.

The mean, or expected value is

\[E(F) = \frac{1}{f}\sum\limits_{1 \leq x < f} x = \frac{f + 1}{2}\]

For “keep high” rules, we could enumerate all possible rolls. It’s this:

rolls = chain(*self.n * [range(1, 1 + self.faces)])
rolls_kh = (sorted(r)[-self.keep:] for r in rolls)
domain = [sum(r) + self.adj for r in rolls_kh]

d_m = mean(domain)
d_s = stdev(domain)
pool() list[int][source]

A collection of dice with the “keep-high” applied.

roll() int[source]

The sum of the dice pool with the adjustment applied.

property stdev: float

The standard deviation of this dice expression.

The variance, \(\sigma^2\), or \(\text{var}\), is

\[\text{Var}(F) = E(F^2) - E(F)^2\]

The expected value, \(E(F)\), is the mean.

The sum of the squares, \(E(F^2)\), is

\[E(F^2) = \frac{1}{f}\sum\limits_{1 \leq x < f} x^2 = \frac{\frac{f^{3}}{3} + \frac{f^{2}}{2} + \frac{f}{6}}{f}\]

We can compute the variance as follows:

\[\begin{split}\text{Var}(F) &= E(F^2) - E(F)^2 \\ &= \frac{\frac{f^{3}}{3} + \frac{f^{2}}{2} + \frac{f}{6}}{f} - \frac{f + 1}{2}^2 \\ &= \frac{\frac{f^{3}}{3} + \frac{f^{2}}{2} + \frac{f}{6}}{f} - \left(\frac{f}{2} + \frac{1}{2}\right)^{2}\end{split}\]

The mean and variance scale linearly for the number of dice, \(d\). The mean of multiple dice, \(d\), is \(\mu(F, d) = E(F) \times d\). The variance, of multiple dice, \(d\), similarly is \(\text{Var}(F, d) = \text{Var}(F) \times d\).

The standard deviation doesn’t scale linearly; it is \(\sigma = \sqrt{\text{Var}(F) \times d}\).

Wild Die

class dice.WildDie(d: int, *, n: int | None = None, adj: int | None = None, keep: int | None = None)[source]

The “Wild Die” mechanic for OpenD6 games.

A 1 is a critical failure. A 6 is a critical success, roll again and accumulate the total.

>>> seed(42)
>>> dice = 5 * WildDie(6)
>>> dice.roll()
30
>>> dice.wild
'success'
>>> dice.roll()
32
>>> dice.wild
'success'
>>> dice.roll()
18
>>> dice.wild
''
kh(keep: int | None = None) Self[source]

Creates new Die with the keep highest “keep” values.

pool() list[int][source]

A collection of dice.

roll() int[source]

Apply the OpenD6 wild die mechanic.

UniformValue

class dice.UniformValue(low: int, high: int)[source]

A uniformly distributed value on the closed interval. Both ends included. Used with Aggregates that have a uniform distribution of instances.

This is compatible with Die to permit aggregation of distinct domains.

This isn’t obviously useful, really.

property mean: float

Mean of the uniform distribution.

roll() int[source]

The sum of the dice pool with the adjustment applied.

property stdev: float

Standard deviation of the uniform distribution.

Interaction

class dice.Interaction(completekey='tab', stdin=None, stdout=None)[source]

An Interactive dice roller.

default(line: str) None[source]

Parse this line, assuming it’s a dice expression.

do_EOF(arg: str) bool | None

Exit the the dice-roller.

do_count(arg: str) bool | None[source]

Set the number of times to roll the dice expression.

do_exit(arg: str) bool | None

Exit the the dice-roller.

do_expect(arg: str) bool | None[source]

Display expected ranges of values of a dice expression.

do_help(arg: str) bool | None[source]

Provide some guidance.

do_quit(arg: str) bool | None[source]

Exit the the dice-roller.

emptyline() bool[source]

Roll the most recently-parsed dice expression.

postcmd(stop: bool, line: str) bool[source]

Hook method executed just after a command dispatch is finished.

preloop() None[source]

Hook method executed once when the cmdloop() method is called.

CLI

dice.main(interactive: ~typing.Annotated[bool, <typer.models.OptionInfo object at 0x1042982d0>] = False, expected_value: ~typing.Annotated[bool, <typer.models.OptionInfo object at 0x104298410>] = False, count: ~typing.Annotated[int, <typer.models.OptionInfo object at 0x104298690>] = 1, expression: ~typing.Annotated[str, <typer.models.ArgumentInfo object at 0x104278ec0>] = '', seed_value: ~typing.Annotated[str | None, <typer.models.OptionInfo object at 0x104298910>] = None)[source]

CLI for dice.

Either provide a Python-syntax dice expression or start an interactive command loop. The Python syntax means the * is required for ‘3*D6’.