Concepts
Dice Expression Syntax
The ndice package supports dice expressions made up of one or more terms.
dice-expression := term [term ...]
Some examples of valid dice expressions are d6+2, 3d6-3x10 and -1.
term := die | dice | mod
A term is either a single die, a number of dice of the same kind, or a mod (modifier).
die := OP 'd' S
OP = operator, one of ('+' | '-' | 'x')
S = number of sides, integer >= 0
Some examples of die terms are d4, -d6 and xd10. When a die term is the first term in an expression, the + operator is implied. A die term indicates that a single die is rolled.
dice := OP N 'd' S
OP = operator, one of ('+' | '-' | 'x')
N = number of dice, integer >= 0
S = number of sides, integer >= 0
Some examples of dice terms are 2d6, -2d8 and x3d10. When a dice term is the first term in an expression, the + operator is implied. A dice term indicates that a number of dice of the same kind are rolled and added together.
mod := OP N
OP = operator, one of ('+' | '-' | 'x')
N = mod value, integer >= 0
Some examples of mod terms are +1, -3 and x10. Unlike die and dice terms, the operator is always written for a mod term, even when the mod is the first term in an expression. A mod term always has a constant value.
Evaluation Order
Dice expressions are evaluated in left-to-right order. Unlike normal algebraic expressions, the times operation “x” does not have higher precedence than the plus “+” and minus “-” operations. For example, for the expression 3-2x10 has a different total when evaluated strictly left-to-right than when evaluated algebraically.
dice expression order:
+3 -2 x10 --> (+3 -2) x10 --> 1 x10 --> 10
algebraic order:
+3 -2 x10 --> +3 (-2 x10) --> 3 -20 --> -17
Explicit Random Number Generators
Since the goal is to roll dice, a random number generator is ultimately needed
when evaluating dice expressions. As a design choice, ndice makes the use of
a random number generator explicit as close to the point of use as possible.
Thus, the Dice class defines a term in a dice expression, but doesn’t know how
to “roll itself”. Instead, the roll_each_die() function takes a random number
generator (RNG) and a Dice object as parameters, returning a list of values
from the RNG, one for each die rolled. Similarly, the roll() function is
built on roll_each_die(), taking an RNG and one or more Dice objects that
make up a dice expression, returning the total.
This achieves two goals. First, the Op and Dice types in ndice are
immutable and stateless, making them easy to reason about. Second, it enables
and encourages a design pattern where the random number generator is defined at
a high level and explicitly passed down through the code. This pattern makes it
easy to substitute in a deterministic or fake random number generator, which
greatly simplifies testing and reproducibility.
Some applications don’t need this strict separation. You can easily write your
own wrappers around roll() and roll_each_die() that always uses the global
ndice.rng random number generator, if you like.
# example wrapper functions
import ndice
def my_roll(*dice_expression: ndice.Dice) -> int:
return ndice.roll(ndice.rng, *dice_expression)
def my_roll_each_die(dice: ndice.Dice) -> list[int]:
return ndice.roll_each_die(ndice.rng, dice)
Library Components
Operations (Op Enum)
The Op enum defines the three operations used in dice expressions: Op.PLUS
(+), Op.MINUS (-) and Op.TIMES (x).
Dice Class
A Dice object represents a single term in a dice expression like d8,
2d6 or -2. A single die, multiple dice and mods are all represented by
Dice objects. For die and dice terms, Dice.number and Dice.sides contain the
obvious values. For a mod, Dice.number contains the mod value and Dice.sides
is set to 1.
Note that zero is a legal value for both Dice.number and Dice.sides. Such
Dice objects always evaluate to zero.
Random Number Generators (RNG)
Rolling dice requires a “random” number generator. A number generator is a
function or “callable” that take an int with the max die roll (i.e. the number
of sides) and returns an int value in the range [1, sides]. For example, the
rng global is defined this way:
# definition of `ndice.rng` random number generator
from random import randrange
def rng(sides: int) -> int:
return randrange(sides) + 1
The type alias RNG can be used to annotate number generator variables.
The rng global returns a true random number. A number of deterministic
generators and generator constructors are also included in ndice.
Roll Function
The roll() function takes a number generator and zero or more Dice objects
making up a dice expression. It returns the total of the dice expression, or
zero if no dice are given.
In some cases, the individual die rolls are needed. The roll_each_die()
function takes a number generator and one Dice object and returns an array of
individual die roll values.