# 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. ```python # 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: ```python # 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.