dyce provides two core primitives for enumeration1.
1
>>>fromdyceimportH,P
H objects represent histograms for modeling discrete outcomes.
They encode finite discrete probability distributions as integer counts without any denominator.
P objects represent pools (ordered sequences) of histograms.
If all you need is to aggregate outcomes (sums) from rolling a bunch of dice (or perform calculations on aggregate outcomes), H objects are probably sufficient.
If you need to select certain histograms from a group prior to computing aggregate outcomes (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
A six-sided die can be modeled as:
12
>>>H(6)H({1:1,2:1,3:1,4:1,5:1,6:1})
H(n) is shorthand for explicitly enumerating outcomes \([{ {1} .. {n} }]\), each with a frequency of 1.
12
>>>H(6)==H({1:1,2:1,3:1,4:1,5:1,6:1})True
Tuples with repeating outcomes are accumulated.
A six-sided “2, 3, 3, 4, 4, 5” die can be modeled as:
12
>>>H((2,3,3,4,4,5))H({2:1,3:2,4:2,5:1})
A fudge die can be modeled as:
12
>>>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 two six-sided dice (2d6) are:
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.
12
>>>2@P(6)2@P(H({1:1,2:1,3:1,4:1,5:1,6:1}))
Pools (in this case, Sicherman dice) can be compared to histograms.
Histograms should be sufficient for most calculations.
However, pools are useful for “taking” (selecting) only some of each roll’s outcomes.
This is done by providing one or more index arguments to the P.h method or the P.rolls_with_counts method.
Indexes can be integers, slices, or a mix thereof.
Outcome indexes are ordered from least to greatest with negative values counting from the right, as one would expect (i.e., [0], [1], …, [-2], [-1]).
Summing the least two faces when rolling three six-sided dice would be:
1234
>>>3@P(6)3@P(H({1:1,2:1,3:1,4:1,5:1,6:1}))>>>(3@P(6)).h(0,1)# see warning below about parenthesesH({2:16,3:27,4:34,5:36,6:34,7:27,8:19,9:12,10:7,11:3,12:1})
Mind your parentheses
Parentheses are needed in the above example because @ has a lower precedence than . and […].
123456
>>>2@P(6).h(1)# equivalent to 2@(P(6).h(1))Traceback(mostrecentcalllast):...IndexError:tupleindexoutofrange>>>(2@P(6)).h(1)H({1:1,2:3,3:5,4:7,5:9,6:11})
Taking the least, middle, or greatest face when rolling three six-sided dice would be:
>>>d10=H(10)-1;d10# a common “d10” with faces [0 .. 9]H({0:1,1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1})>>>h=P(4,6,8,d10,12,20).h(0,-1)>>>print(h.format(scaled=True))avg|13.48std|4.40var|19.391|0.00%|2|0.01%|3|0.06%|4|0.30%|#5|0.92%|#####6|2.03%|###########7|3.76%|####################8|5.57%|##############################9|7.78%|###########################################10|8.99%|##################################################11|8.47%|###############################################12|8.64%|################################################13|8.66%|################################################14|6.64%|####################################15|5.62%|###############################16|5.16%|############################17|5.00%|###########################18|5.00%|###########################19|5.00%|###########################20|5.00%|###########################21|4.50%|#########################22|2.01%|###########23|0.73%|####24|0.18%|
Both histograms and pools support various comparison operations.
The odds of observing all even faces when rolling \(n\) six-sided dice, for \(n\) in \([1..6]\) is:
The odds of scoring at least one nine or higher on any single die when rolling \(n\) “exploding” six-sided dice, for \(n\) in \([1..10]\) is:
1 2 3 4 5 6 7 8 9101112131415161718
>>>fromdyce.evaluationimportexplode>>># By the time we're rolling a third die, we're guaranteed a nine or higher, so we only need to look that far>>>exploding_d6=explode(H(6),limit=2)>>>forninrange(10,0,-1):...d6e_ge_9=exploding_d6.ge(9)...number_of_nines_or_higher_in_nd6e=n@d6e_ge_9...at_least_one_9=number_of_nines_or_higher_in_nd6e.ge(1)...print(f"{n: >2}d6-exploding: {at_least_one_9[1]/at_least_one_9.total: >6.2%}")10d6-exploding:69.21%9d6-exploding:65.36%8d6-exploding:61.03%7d6-exploding:56.15%6d6-exploding:50.67%5d6-exploding:44.51%4d6-exploding:37.57%3d6-exploding:29.77%2d6-exploding:20.99%1d6-exploding:11.11%
Dependent probabilities
Where we can identify independent terms and reduce the dependent term to a calculation solely involving independent terms, dependent probabilities can often be compactly expressed via an expandable-decorated function or callback passed to foreach.
First, we express independent terms as histograms or pools.
Second, we express the dependent term as a function that will be called once for each of the Cartesian product of the results from each independent term.
Results are passed to the dependent function from independent histogram terms as HResult objects or from independent pool terms as PResult objects.
Finally, we pass the dependent function to foreach, along with the independent terms, or, in the alternative, we decorate the function with expandable, and call it with the independent terms.
To illustrate, say we want to roll a d6 and compare whether the result is strictly greater than its distance from some constant.
Instead of a constant, let’s use another die as a second independent term.
We’ll roll a d4 and a d6 and compare whether the d6 is strictly greater than the absolute difference between dice.
1 2 3 4 5 6 7 8 910111213
>>>d4=H(4)# first independent term>>>d6=H(6)# second independent term>>>defsecond_is_strictly_greater_than_first(first:HResult,second:HResult):...returnsecond.outcome>abs(first.outcome-second.outcome)# dependent term>>>h=foreach(second_is_strictly_greater_than_first,first=d4,second=d6)>>>print(h.format())avg|0.83std|0.37var|0.140|16.67%|########1|83.33%|#########################################
In the alternative, one could nest expandable functions, where the innermost holds the dependent term, and the outer functions each establish the scope of their respective independent outcomes.
However, this isn’t very readable, and is often less efficient than using a single function.
This technique also works where the dependent term requires inspection of rolls from one or more pools as independent terms.
Let’s say we have two pools.
A roll from the first pool wins if it shows no duplicates but a roll from the second does.
A roll from the second pool wins if it shows no duplicates but a roll from the first does.
Otherwise, it’s a tie (i.e., if neither or both rolls show duplicates).
Let’s compare how three six-sided dice fair against two four-sided dice.
1 2 3 4 5 6 7 8 910111213141516171819202122
>>>fromdyce.evaluationimportPResult>>>fromenumimportIntEnum>>>classDupeVs(IntEnum):...SECOND_WINS=-1# where second.roll shows no duplicates, but first.roll does...TIE=0# where both rolls show no duplicates or rolls pools have duplicates...FIRST_WINS=1# where first.roll shows no duplicates, but second.roll does>>>defcompare_duplicates(first:PResult,second:PResult):...returnDupeVs((len(set(first.roll))==len(first.roll))-(len(set(second.roll))==len(second.roll)))>>>h=foreach(compare_duplicates,first=P(6,6,6),second=P(4,4));hH({<DupeVs.SECOND_WINS:-1>:12,<DupeVs.TIE:0>:19,<DupeVs.FIRST_WINS:1>:5})>>>print(h.format())avg|-0.19std|0.66var|0.43-1|33.33%|################0|52.78%|##########################1|13.89%|######
Visualization
H objects provide a distribution method and a distribution_xy method to ease integration with plotting packages like matplotlib.
In addition, anydyce provides additional visualization and interactivity conveniences.
(Many of the figures in these docs leverage anydyce in their construction.)
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromanydyce.vizimportplot_barfromdyceimportHdefdo_it(style:str)->None:importmatplotlib.pyplotax=matplotlib.pyplot.axes()text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)plot_bar(ax,[("",3@H(6))])ax.set_title("Distribution for 3d6",color=text_color)
Time to get meta-evil on those outcomes!
Thanks to numerary, dyce offers best-effort support for arbitrary number-like outcomes, including primitives from symbolic expression packages such as SymPy.
Be aware that, depending on implementation, performance can suffer quite a bit when using symbolic primitives.
For histograms and pools, dyce remains opinionated about ordering.
For non-critical contexts where relative values are indeterminate, dyce will attempt a “natural” ordering based on the string representation of each outcome.
This is to accommodate symbolic expressions whose relative values are often unknowable.
SymPy does not even attempt simple relative comparisons between symbolic expressions, even where they are unambiguously resolvable.
Instead, it relies on the caller to invoke its proprietary solver APIs.
dyce, of course, is happily ignorant of all that keenness.
(As it should be.)
In practice, that means that certain operations won’t work with symbolic expressions where correctness depends on ordering outcomes according to relative value (e.g., dice selection from pools).
Anywhere you see a JupyterLite logo , you can click on it to immediately start tinkering with a temporal instance of that example using anydyce.
Just be aware that changes are stored in browser memory, so make sure to download any notebooks you want to preserve.
dyce also provides additional primitives (R objects and their kin) which are useful for producing weighted randomized rolls without the overhead of enumeration.
These are covered seperately. ↩