Skip to content

Core concepts

dyce provides two core primitives for basic finite discrete probability computations.

H objects represent finite discrete probability distributions as histograms. They map outcomes to integer counts. Each positive outcome’s probability is its count by the histogram’s total count. Outcomes with zero counts have probabilities of zero.

P objects represent pools (sorted sequences) of histograms. If all you need is aggregate outcomes (sums) from rolling a bunch of dice, H objects are probably sufficient. If you need to narrow outcomes prior to computing aggregates (e.g., taking the highest and lowest of each possible roll of \(n\) dice), that’s where P objects come in.

As a wise person whose name has been lost to history once said: “Language is imperfect. If at all possible, shut up and point.” So with that illuminating (or perhaps impenetrable) introduction out of the way, let’s dive into some examples!

Basic examples

Histograms

H(n) is shorthand for explicitly enumerating outcomes \([{ {1} .. {n} }]\), each with a frequency of 1. A fair, six-sided die (d6) can be modeled as:

1
2
3
>>> from dyce import H
>>> H(6)
H({1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1})

Tuples with repeating outcomes are accumulated. A six-sided “2, 3, 3, 4, 4, 5” die can be modeled as:

1
2
>>> H((2, 3, 3, 4, 4, 5))
H({2: 1, 3: 2, 4: 2, 5: 1})

A fudge die can be modeled as:

1
2
>>> H((-1, 0, 1))
H({-1: 1, 0: 1, 1: 1})

Python’s matrix multiplication operator (@) is used to express the number of a particular die (roughly equivalent to the “d” operator in common notations). The outcomes of rolling and summing two six-sided dice (2d6) are:

1
2
>>> 2 @ H(6)
H({2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2, 12: 1})

That shows one way to make 2, two ways to make 3, three ways to make 4, etc.

dyce.d has some convenient abbreviations for commonly found dice:

1
2
3
4
5
>>> from dyce.d import d6, h2d6
>>> d6 == H(6)
True
>>> h2d6 == 2 @ d6
True

Pools

A pool of two six-sided dice is:

1
2
3
>>> from dyce import P
>>> P(d6, d6)
2@P(H({1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1}))

Where n is an integer, P(n, ...) is shorthand for P(H(n), ...). Python’s matrix multiplication operator (@) can also be used with pools. The above can be expressed more succinctly.

1
2
3
>>> from dyce.d import p2d6
>>> 2 @ P(6) == P(d6, d6) == p2d6
True

When compared with a histogram, a pool is treated as the distribution of its summed outcomes. We can validate that a pool representing a pair of Sicherman dice is equivalent to a histogram representing 2d6.

1
2
3
>>> d_sicherman = P(H((1, 2, 2, 3, 3, 4)), H((1, 3, 4, 5, 6, 8)))
>>> d_sicherman == h2d6
True

Sicherman dice

Sicherman dice … are a pair of 6-sided dice with non-standard numbers—one with the sides 1, 2, 2, 3, 3, 4 and the other with the sides 1, 3, 4, 5, 6, 8. They are notable as the only pair of 6-sided dice that are not [standard 6-sided] dice, bear only positive integers, and have the same probability distribution for [their] sum as [two standard 6-sided] dice.

Arithmetic and formatting

Both histograms and pools support arithmetic operations. 3×(2d6+4) is:

1
2
>>> 3 * (2 @ H(6) + 4)
H({18: 1, 21: 2, 24: 3, 27: 4, 30: 5, 33: 6, 36: 5, 39: 4, 42: 3, 45: 2, 48: 1})

A pool can be “flattened” to a histogram via its P.h method.

1
2
>>> p2d6.h()
H({2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2, 12: 1})

Arithmetic operations implicitly flatten pools into histograms.

1
2
>>> 3 * (2 @ P(6) + 4)
H({18: 1, 21: 2, 24: 3, 27: 4, 30: 5, 33: 6, 36: 5, 39: 4, 42: 3, 45: 2, 48: 1})

Histograms provide rudimentary text formatting for convenience.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> print(h2d6.format_short())
{avg: 7.00, 2:  2.78%, 3:  5.56%, 4:  8.33%, 5: 11.11%, 6: 13.89%, 7: 16.67%, 8: 13.89%, 9: 11.11%, 10:  8.33%, 11:  5.56%, 12:  2.78%}
>>> print(h2d6.format(width=65))
avg |    7.00
std |    2.42
  2 |   2.78% |#
  3 |   5.56% |##
  4 |   8.33% |####
  5 |  11.11% |#####
  6 |  13.89% |######
  7 |  16.67% |########
  8 |  13.89% |######
  9 |  11.11% |#####
 10 |   8.33% |####
 11 |   5.56% |##
 12 |   2.78% |#

Outcome selection

Histograms should be sufficient for most calculations. Pools are useful for “keeping” or “taking” (selecting) some of each roll’s outcomes. P.at sums the selected outcomes into a histogram. P.rolls_with_counts yields the selected rolls and their counts. Outcomes in rolls are ordered from least to greatest. Provide one or more integers, slices, or a mix thereof to those methods to select outcomes by their ordered positions.

Taking the least two, least and greatest, or greatest two faces when rolling three six-sided dice (3d6):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> p3d6 = 3 @ P(6)
>>> print(p3d6.at(0, 1).format(width=65))  # least two
avg |    5.54
std |    2.21
  2 |   7.41% |###
  3 |  12.50% |######
  4 |  15.74% |#######
  5 |  16.67% |########
  6 |  15.74% |#######
  7 |  12.50% |######
  8 |   8.80% |####
  9 |   5.56% |##
 10 |   3.24% |#
 11 |   1.39% |
 12 |   0.46% |
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> print(p3d6.at(0, -1).format(width=65))  # least and greatest
avg |    7.00
std |    1.85
  2 |   0.46% |
  3 |   2.78% |#
  4 |   6.02% |###
  5 |  11.11% |#####
  6 |  17.13% |########
  7 |  25.00% |############
  8 |  17.13% |########
  9 |  11.11% |#####
 10 |   6.02% |###
 11 |   2.78% |#
 12 |   0.46% |
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> print(p3d6.at(slice(-2, None)).format(width=65))  # greatest two
avg |    8.46
std |    2.21
  2 |   0.46% |
  3 |   1.39% |
  4 |   3.24% |#
  5 |   5.56% |##
  6 |   8.80% |####
  7 |  12.50% |######
  8 |  15.74% |#######
  9 |  16.67% |########
 10 |  15.74% |#######
 11 |  12.50% |######
 12 |   7.41% |###

Mind your parentheses

Parentheses are often needed when constructing a pool and making selections in the same expression. This is because @ has a lower precedence than other operators like ..

1
2
3
4
5
6
>>> 3 @ P(6).at(2)  # equivalent to 3 @ (P(6).at(2))
Traceback (most recent call last):
  ...
IndexError: tuple index out of range
>>> (3 @ P(6)).at(2)
H({1: 1, 2: 7, 3: 19, 4: 37, 5: 61, 6: 91})

Pools are immutable, ordered sequences. Like histograms are grouped, and groups are sorted. Slicing creates a new pool with the selected histograms.

1
2
>>> (2 @ P(4, 6, 8))[:3]
P(2@P(H({1: 1, 2: 1, 3: 1, 4: 1})), H({1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1}))

A pool’s histogram order is distinct from its rolls’ order

Histograms are sorted in a pool.

1
2
>>> list(P(4, 6, 20)) == list(P(20, 6, 4))
True

Rolls are sorted least-to-greatest, irrespective of the histogram’s position in the pool from which each outcome came.

1
2
3
4
>>> odds = H({1: 1, 3: 1})
>>> evens = H({2: 1, 4: 1})
>>> list(P(odds, evens).rolls_with_counts())
[((1, 2), 1), ((2, 3), 1), ((1, 4), 1), ((3, 4), 1)]

Indexes used in roll selection, such as with P.at and P.rolls_with_counts, reference positions in sorted rolls.

1
2
>>> list(P(odds, evens).rolls_with_counts(-1))
[((2,), 1), ((3,), 1), ((4,), 2)]

Comparisons and counting

Outcome comparisons produces a histogram with False and True outcomes. Because ==, !=, <, etc. compare whole histograms, H provides H.eq, H.ne, H.lt, etc. for comparing outcomes.

1
2
3
4
5
6
>>> H({1: 1, 2: 1, 3: 1}) >= 2  # type: ignore[operator]
Traceback (most recent call last):
  ...
TypeError: '>=' not supported between instances of 'H' and 'int'
>>> H({1: 1, 2: 1, 3: 1}).ge(2)
H({False: 1, True: 2})

False and True behave as zero and one in arithmetic. These allow us to take advantage of a counting trick. Summing \(n\) of these histograms tells us how many of the \(n\) original dice meet the condition. Calculating the probability that all of 3d6’s outcomes are even:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> from dyce.d import d6
>>> d6_even = (d6 % 2).eq(0)
>>> d6_even
H({False: 3, True: 3})
>>> number_of_evens_in_3d6 = 3 @ d6_even  # our counting trick
>>> number_of_evens_in_3d6
H({0: 27, 1: 81, 2: 81, 3: 27})
>>> all_three_are_even = number_of_evens_in_3d6.eq(3)
>>> print(all_three_are_even.format_short())
{avg: 0.12, False: 87.50%, True: 12.50%}

Visualization

H.probability_items yields outcome and probability pairs for use with plotting packages. If Matplotlib is installed, dyce.viz.matplotlib provides functions for plotting histograms. dyce.viz.plotly provides figure specifications for use with Plotly. dyceum provides additional interactive visualization tools.

Visualization source code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from dyce.d import p3d6
from dyce.viz.matplotlib import plot_ridge

ax = plot_ridge(
    p3d6.at(0, 1),
    p3d6.at(0, -1),
    p3d6.at(slice(-2, None)),
    labels=("least two", "least and greatest", "greatest two"),
)
ax.set_title("Distributions for various selections from 3d6")

Plot: Distribution for 3d6

Run this example in JupyterLite: Try <code>dyce</code>!

Visualization source code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from dyce.d import p3d6
from dyce.viz.plotly import ridge_spec

spec = ridge_spec(
    p3d6.at(0, 1),
    p3d6.at(0, -1),
    p3d6.at(slice(-2, None)),
    labels=("least two", "least and greatest", "greatest two"),
    colors=["#348abd", "#a60628", "#7a68a6"],
)
spec.layout.update(
    {
        "title": {"text": "Distributions for various selections from 3d6"},
        "margin": {"l": 55, "r": 20, "t": 50, "b": 45},
        "paper_bgcolor": "rgba(0,0,0,0)",
        "plot_bgcolor": "rgba(0,0,0,0)",
    }
)

Further exploration

See the examples for worked calculations, or consult the API reference.