Rollin’ with rollers and rolls
Experimental
This functionality should be considered experimental. Be warned that future release may introduce incompatibilities or remove it altogether. Feedback, suggestions, and contributions are welcome and appreciated.
dyce
provides additional primitives for generating and inspecting rolls of weighted random outcomes without requiring the overhead of enumeration.
1 |
|
R
objects represent rollers.
Rollers produce Roll
objects.
Roll
objects are sequences of RollOutcome
objects, which represent weighted random values.
Each object can be a node in a tree-like structure. Rollers, for example, can represent scalars, histograms, pools, operators, etc., and can be assembled into trees representing more complex calculations. Rolls can derive from other rolls, forming trees that are generally analogous to the roller trees that generated them. Similarly, roll outcomes can derive from other roll outcomes.
The simplest roller we can create represents a single value. Each roll it generates has that value as its sole outcome. Let’s see what that looks like (now with tasty entity relationship diagrams).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Hopefully, that’s relatively straightforward. Let’s look at some more substantial examples.
Emulating a hundred-sided die using two ten-sided dice
In many games it is common to emulate a hundred-sided die using a “ones” ten-sided die (faces numbered \([{ {0}, {1}, \ldots , {9} }]\)) and a “tens” ten-sided die (faces numbered \([{ {00}, {10}, \ldots , {90} }]\)). Let’s try to model that as a roller and use it to generate a roll.
We start by creating two histograms1 representing our two ten-sided dice (d00
for our “tens” die and d10
for our “ones“ die).
1 2 3 |
|
Next, we create a roller using the R.from_values
class method.
1 2 3 4 5 6 7 8 |
|
Well, wouldya look at that? That durned class method created a whole roller tree, which is actually three rollers.
- One
ValueRoller
for thed00
histogram; - Another for the
d10
histogram; and - A
PoolRoller
for aggregating them both.
Tip
We could have also composed an identical tree using roller implementations from dyce.r
instead of the R.from_values
convenience method.
1 2 3 |
|
Let’s use our new roller to create a roll and retrieve its total.
1 2 3 |
|
No surprises there. Let’s dig a little deeper and ask for the roll’s outcome values.
1 2 |
|
As we mentioned before, the top level of our roller tree is a PoolRoller
, which aggregates (or “pools”) rolls from its sources.
For our roll, the aggregated outcomes are 60
are 9
.
What does our pooled roll look like?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Let’s break that down so it doesn’t feel like trying to drink from a fire hose.
Calling the R.roll
method on our PoolRoller
resulted in a Roll
object.
Actually, it resulted in a roll tree (analogous to our roller tree).
Each Roll
object in that tree has:
- A reference to the
R
object in the roller tree that generated it, retrieved via itsr
property; - Zero or more
RollOutcome
objects, retrieved by accessing the roll as a sequence (i.e., via__getitem__
,__len__
); and - Zero or more source rolls, retrieved via its
source_rolls
property.
The RollOutcome
objects also form trees (in our case, simple ones).
Each one has:
- A single value, retrieved via its
value
property; - Zero or more source outcomes from which the value was derived, retrieved via its
sources
property; and - A reference back to the roll that generated it, retrieved via its
source_roll
property (omitted from the diagram for the sake of readability).
Tip
You might be wondering to yourself, “Self, one wonders, can one have a pool of pools?” Such questions command the response, “Why the heck not? Try it!”
1 2 3 4 5 6 |
|
So the answer is a resounding, “Of course. What devious entity would prohibit such a thing? Please identify that creature so we may flog it until it achieves enlightenment,” “Yes.”
Composing rollers with arithmetic
Rollers support arithmetic operators.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Dropping dice from prior rolls – keeping the best three of 3d6
and 1d8
The trifecta of roller trees, roll trees, and outcome trees might appear complicated or redundant. Everything serves a purpose.2
Consider excluding (or “dropping”) dice from a roll. How would we account for that? Let’s see how to generate rolls that keep the best three outcomes from rolling three six-sided dice and one eight-sided die.
We start by using the R.from_value
class method to create ValueRoller
s for histograms representing our six- and eight-sided dice.
1 2 3 4 5 6 |
|
For homogeneous pools, we can use the matrix multiplication operator.
1 2 3 4 5 6 |
|
Finally, we’ll create a SelectionRoller
by calling the R.select_from_sources
method on our other rollers.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Oh boy! Aren’t you super excited to try this thing out?
1 2 3 |
|
There are indeed three values, despite starting with four dice.
Given that the lowest value we see is a 1
, we might assume that the eliminated value is also a 1
.
But, we all know what happens when one assumes.
Recall that in roll trees, a roll may have references to other rolls (its “source rolls”) from which it derives. We should be able to get information about the dropped die by traversing that tree. Let’s see if we can validate our assumption by looking at the outcomes from our roll’s direct source.
1 2 3 |
|
Yup!
We were right!
There’s the other 1
, plain as day.
Our work here is do—
What? You want to know which die we eliminated? We can see that, too!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
*snip* ✂️
147 148 |
|
Oof. ☝️ That was … a lot. Let’s visualize!
Holy entangled relationship diagrams, Batman!
One thing you may notice about our top-level roll is that it has four outcomes.
One of those kids is not like the others.
Specifically, it has a value
of None
.
That’s our dropped outcome!
1 2 3 4 5 6 |
|
Info
A roll outcome with a value
of None
is akin to a “tombstone”.
It conveys one whose sources were present in immediately prior rolls but excluded from the current roll.
Such roll outcomes must have at least one source.
1 2 3 4 5 |
|
The RollOutcome.euthanize
method provides a convenient shorthand.
1 2 3 4 5 6 7 8 9 10 |
|
However, because such a roll signals its absence from the current roll, its value is not included by the Roll.outcomes
method.
We can programmatically verify that the excluded outcome originated from one of the six-sided dice.
1 2 3 4 5 6 7 |
|
We can also verify that the 5
came from the eight-sided die.
1 2 3 4 5 |
|
Alternatively, could have also used our old friend the P
object to eliminate the RepeatRoller
for a similar, but structurally simpler result.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
*snip* ✂️
79 80 |
|
In this case, our results are still mostly traceable, since our pool is homogeneous.
However, results from P.roll
are sorted, meaning they lose association with their source histograms.
This risks ambiguity.
Consider:
1 2 |
|
Is the 4
from the d6
or d8
? 🤔💭
No one knows.
1 2 3 4 |
|
Filtering and substitution
dyce
provides two additional rollers for outcome manipulation.
FilterRoller
s euthanize
outcomes that don’t meet provided criteria.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
SubstitutionRoller
s replace or append outcomes based on existing ones.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
Performance
How much overhead do all these data structures contribute?
It obviously depends on the complexity of the structure.
Consider a simple example d20 + d12 + 4
.
Let’s do that 5,000 times, sort the results, and take every other one starting with the highest.
We might use a pool, if we didn’t care about traceability.
Let’s compare that to our roller.
1 2 3 4 5 |
|
Source: perf_pools_vs_rollers.ipy
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
In this particular case, our roller takes approximately ten times longer than our histogram pool.
It is unsurprising that a simple roller is slower than a simple pool, at least in part because the math is deferred until R.roll
time.
In more sophisticated cases, rollers may be more competitive with (or even surpass) their histogram or pool analogies, especially when initialization time is taken into account.
All that being said, for periodic rolls simulating handfuls (not thousands) of operations or dice, such performance disparities probably won’t matter that much. Just use the primitives whose semantics work best for you. If ever performance becomes an issue, let me know, and we can collaborate on how to improve it.
Further exploration
Consider reviewing the roller API.
-
If you’re not already familiar with histograms, consider skimming the counting tutorial. ↩
-
We may still be discovering what those purposes are. We have the utmost faith they exist, even if they have yet to reveal themselves. If you discover one, consider contributing an example. ↩