Dice Stats

Introduction

If you’re a new to this library you may want to look at the Tutorial.

If you’ve gone through that then the Terminology section would help understand the inner mechanics of the Dice class, but otherwise this document explains the public interface.

Public library

Advanced Dice statistics without the headaches.

More description to come.

class dice_stats.Dice(chances=None, total_chance=Fraction(1, 1))

Dice class.

apply_dice(chances, default=None)

Change chances to provided dice.

This changes the values of the host dice, to the values of the provided dice. It does so by changing the provided dice to the correct chance.

Parameters
Return type

Dice

Returns

A new Dice that acts as if these multiple dice rolls happen in one throw.

Say we’re playing a game where you roll a die to attack people. You normally do 1-6 damage, however sometimes there’s a chance to do 1-4 - say you had bad footing. To determine what dice you use use throw a d3 before using either the d6 or the d4. On a 1 you use the d4, otherwise it’s the d6. To find the chances is easy with this function.

print(
    Dice.from_dice(3)
        .apply_dice(
            {(1,): Dice.from_dice(4)},
            Dice.from_dice(6),
        )
)
Dice[1](
  1: 19.4%  7/36
  2: 19.4%  7/36
  3: 19.4%  7/36
  4: 19.4%  7/36
  5: 11.1%  1/9
  6: 11.1%  1/9
)
apply_functions(chances, default=None, apply=<function Dice.<lambda>>)

Apply a callback to the provided chances.

This is the more complex version dice_stats.Dice.apply_dice(). With this version you can pass a callback that mutates the segment of the dice provided.

This callback is given a segment of the host dice, this means that the callback has all the information that it should need, as this dice would have all the Chances Value and Chances Chance it needs. The Total Chance will also be the correct for that segment of the dice results.

The callback then has to return a dice object that that segment gets mutated to. For example if you’re using this to reroll dice, then one function would return the segment provided, where the other would return the host dice of sorts.

Parameters
  • chances (Dict[Iterable[Union[Real, Integral]], Callable[[Dice], Dice]]) – The mapping that holds the outputs.

  • default (Optional[Callable[[Dice], Dice]]) – The function to apply if there are missing values.

  • apply (Callable[[Dice], Dice]) – A function to mutate each dice segment before they reach the chances callback.

Return type

Dice

Returns

New dice that’s been fully mutated by the callbacks.

To expand on the previous example, if we have a d6 and we’re allowed to reroll 1s once, then we could use the following code.

Note

This functionality is already builtin to the dice_stats.Dice class. You can instead use the dice_stats.Dice.reroll() method to do this.

print(
    Dice.from_dice(6)
        .apply_functions(
            {(1,): lambda d: Dice.from_dice(6) @ d},
            lambda d: d,
        )
)
Dice[1](
  1:  2.8%  1/36
  2: 19.4%  7/36
  3: 19.4%  7/36
  4: 19.4%  7/36
  5: 19.4%  7/36
  6: 19.4%  7/36
)
as_total_chance(total_chance)

Change Total Chance.

This adjusts each Chances Chance by the scale of the old and new Total Chance.

Parameters

total_chance (Union[Fraction, Dice, int]) – Desired Total Chance.

Return type

Dice

Returns

A new dice with the specified Total Chance.

For example, say we have a d3 which has a 100% Total Chance, however it should be a 200% Total Chance. Then we use this function to change it. It will also change all the Chances Chance from 1/3 to 2/3.

d3 = Dice.from_dice(3)
print(d3)
print(d3.as_total_chance(Fraction(2, 1)))
Dice[1](
  1: 33.3%  1/3
  2: 33.3%  1/3
  3: 33.3%  1/3
)
Dice[2](
  1: 66.7%  2/3
  2: 66.7%  2/3
  3: 66.7%  2/3
)
copy()

Create a copy of the current dice.

This allows other methods to easily mutate a copy of a provided dice without mutating the dice a user provides. This helps keep the purity of a lot of the functions in this library.

Returns

A copy of this dice, including its type.

classmethod from_dice(sides, total_chance=Fraction(1, 1))

Make a dice from a number of sides.

This is a very useful convenience function, this is the most common way to build dice as it builds dice with an equal chance to land on all faces.

Parameters
Return type

Dice

Returns

A fair n-sided dice.

Below is an example of some of the smaller dice you can create with this.

print(Dice.from_dice(1))
print(Dice.from_dice(2))
print(Dice.from_dice(3))
print(Dice.from_dice(6))
Dice[1](
  1: 100.0%  1/1
)
Dice[1](
  1: 50.0%  1/2
  2: 50.0%  1/2
)
Dice[1](
  1: 33.3%  1/3
  2: 33.3%  1/3
  3: 33.3%  1/3
)
Dice[1](
  1: 16.7%  1/6
  2: 16.7%  1/6
  3: 16.7%  1/6
  4: 16.7%  1/6
  5: 16.7%  1/6
  6: 16.7%  1/6
)
classmethod from_empty(value=0)

Create an empty chance.

A Chances Value must be specified and so defaults to 0 as the unwanted value.

Parameters

value (Union[Real, Integral]) – Chances Value of the failure value.

Returns

An empty dice.

classmethod from_external(chances, total_chance)

Create a dice from the Chances form.

This doesn’t use the Internal Chances form, and so eases use when not using that form.

Parameters
Return type

Dice

Returns

A dice with the specified chances.

classmethod from_full(chances, total_chance=Fraction(1, 1))

From full Internal Chances.

This is a helper function to build dice from the Internal Chances. Allowing a different Total Chance.

Parameters
Returns

A new dice with the provided chances.

classmethod from_partial(chances=None, total_chance=Fraction(1, 1))

From partial Internal Chances.

This is a helper function to build dice from the Internal Chances, where the chance’s always total 1. By defaulting all undefined Chances Chance to the chance of 0, rather than erroring.

This may become deprecated in the future, as it seems like there is no use for this method.

Parameters
Returns

A new dice with the provided chances.

classmethod from_prev_total_chance(chances, chance, prev_chance)

Change a dice from one Total Chance to another.

This is a deprecated function, this is likely better expressed by using dice_stats.Dice.from_external() and dice_stats.Dice.as_total_chance().

Parameters
Return type

Dice

Returns

A dice with new chances.

get(k[, d]) → D[k] if k in D, else d. d defaults to None.
items() → a set-like object providing a view on D's items
keys() → a set-like object providing a view on D's keys
max(other=None)

Get the maximum result from two dice.

Say you’re playing a game which allows you to roll two dice and take the highest as your result. To do this you would need a cartesian max, which is what this function performs.

Parameters

other (Optional[Dice]) – Another dice to get the maximum value from. If unset then it uses itself.

Return type

Dice

Returns

A new dice of the chances of getting the maximum.

Say you have can roll two d6s and use the maximum, that would simply be:

print(Dice.from_dice(6).max())
Dice[1](
  1:  2.8%  1/36
  2:  8.3%  1/12
  3: 13.9%  5/36
  4: 19.4%  7/36
  5: 25.0%  1/4
  6: 30.6% 11/36
)
property non_repeat

Build a NonRepeat object.

Return type

NonRepeat

partition(values)

Remove multiple Chances Value from the dice.

Split the dice into two. This removes the values provided in the values argument out of the dice, and then returns the rest of the values in a separate dice.

Parameters

values (Iterable[Union[Real, Integral]]) – Selection of Chances Value to remove from the dice.

Return type

Tuple[Dice, Dice]

Returns

Returns two different dice, one with the wanted values, the other with the unwanted values.

For the most part this is used internally when using one of the apply functions or the reroll function. However its existence helps makes these functions possible and easy to implement.

To showcase this we’ll define a basic reroll function using this function.

Note

This functionality is already builtin to the dice_stats.Dice class. You can instead use the dice_stats.Dice.reroll() method to do this.

def reroll(dice, values):
    rerolled, kept = dice.partition(values)
    return Dice.sum([
        kept,
        dice.as_total_chance(rerolled.total_chance),
    ])

print(reroll(Dice.from_dice(6), (1,)))
Dice[1](
  1:  2.8%  1/36
  2: 19.4%  7/36
  3: 19.4%  7/36
  4: 19.4%  7/36
  5: 19.4%  7/36
  6: 19.4%  7/36
)
reroll(values)

Reroll specified values.

Since this is a common operation on dice this is some sugar for it. It should be noted that whilst this handles the simple instances of rerolling, there are some more complex situations that can only be handled through custom functions or the dice_stats.Dice.apply_functions() method.

For the simple situation of being able to reroll a range of values once then this is all you need.

Parameters

values (Iterable[Union[Real, Integral]]) – Collection of Chances Value.

Return type

Dice

Returns

A new dice factoring in rerolls.

print(Dice.from_dice(6).reroll((1,)))
Dice[1](
  1:  2.8%  1/36
  2: 19.4%  7/36
  3: 19.4%  7/36
  4: 19.4%  7/36
  5: 19.4%  7/36
  6: 19.4%  7/36
)
classmethod sum(dice)

Add multiple dice together, increasing their Total Chance.

This is mostly used when you have multiple partial dice that you need to add together only adding the chances together. For example lets say we have a 2/3 chance for our outcome to be a d6 or a 1/3 chance that the outcome is a d4 then the total outcome would be the result of those two dice added together, but not in the common form - d6 + d4.

print(Dice.sum([
    Dice.from_dice(6, total_chance=Fraction(2, 3)),
    Dice.from_dice(4, total_chance=Fraction(1, 3)),
]))

Which correctly results in:

Dice[1](
  1: 19.4%  7/36
  2: 19.4%  7/36
  3: 19.4%  7/36
  4: 19.4%  7/36
  5: 11.1%  1/9
  6: 11.1%  1/9
)

As we can see, the result totals to 100%, and there is a significantly higher chance to get a 1-4 then 5 or 6. This shows pretty clearly that the outcome is only adding the chances together.

For the most part you can normally use a different function than this one if you need to get this sort of output. Lets say the game we’re playing says to roll a d3 to decide between either the d4 or the d6 to be rolled to get the result. On a 1 you roll the d4 otherwise it’s the d6, which could be expressed as:

print(
    Dice.from_dice(3)
        .apply_dice(
            {(1,): Dice.from_dice(4)},
            Dice.from_dice(6)
        )
)
Dice[1](
  1: 19.4%  7/36
  2: 19.4%  7/36
  3: 19.4%  7/36
  4: 19.4%  7/36
  5: 11.1%  1/9
  6: 11.1%  1/9
)
Return type

Dice

property total_chance

Total Chance of the values of this dice.

Return type

Fraction

Returns

The Total Chance of the dice.

values() → an object providing a view on D's values
class dice_stats.Range(start, stop, step)

Range object allowing floating point and infinite ranges.

classmethod from_range(range_)

Build a range from mathematical notation.

Return type

Range

Terminology

Chances

These are made up of pairs of two items, a Chances Value and a Chances Chance. These are contained in a mapping where the mapping’s key is the Chances Value, and the value is the Chances Chance.

This swap of terminology may cause some confusion so I think it should be noted again that the mappings value is not the Chances Value.

Chances Value

This is the value on the dice. For a standard d6 this would mean that the dice has 6 values ranging from 1 to 6. And so the the Chances would have six Chances Value.

Chances Chance

This is the chance of the value being an outcome. For a standard d6 this would mean that all the chances are 1/6. And so the the Chances would have six Chances Chance all with the same value.

These are all provided as fractions.Fraction so that there are no floating point issues when calculating the chances. This also allows high precision in the chances we calculate.

Note

The Internal Chances Chance is slightly different to the public one.

Total Chance

This is the total chance of the dice, this is important as the Chances and the Internal Chances total to different amounts, as described in the latter. Whilst this may at first seem bizarre, some games allow dice to have an over, or under, 100% chance. Say we have a game where you roll a d10;

  • on a 1 you miss,

  • on a 10 you hit twice, otherwise

  • you hit once.

This could be inferred as hitting 100% of the time, and missing 10%. Making the total chance of the dice 110%. Whilst interpreting the rules this way are entirely down to you. It leaves the option open for you to go down this route if you want to.

This attribute is also used on some of the more high level functions. This is as they split a dice into say two results, ones that are successful and ones that are unsuccessful. Having this attribute contained within the dice makes it simpler to handle the splitting and then combining of those dice.

Internal Chances

These are the internal chances of the dice object. These are stored in a dict where the Internal Chances Chance total 1. This has two major benefits:

  • Allows us to at runtime check if there’s a critical error with the dice object and exit gracefully, rather than fail silently.

    Whilst I’m confident the code works due to tests. I also know that this feature helped development and debugging when I messed up different functions. It’s an extra fail safe to know the code isn’t completely broken.

    It may also help you if you start doing some fancy things with this library, as if you break the state of the Internal Chances then you know straight away, not after hours of debugging.

  • Keeping a mutable dictionary private, and keeping the amount of code that touch it to a minimum helps keep the logic of the Dice object reasonable. Passing a dice object to a function shouldn’t mutate the object. At every step of development I’ve kept immutability to be a core concept for the dice class.

    Given that creating dice statistics are complex enough, this can keep me, and hopefully you, to be at peace when making complex dice statistic functions. And should prevent easy to overlook issues with a mutable class.

Internal Chances Chance

This is slightly different to the Chances Chance. This, the internal one, totals to 1, allowing some runtime validity checks. However the public Chances Chance total to whatever Total Chance is.

For the most part, these are likely to be the same.