osrlib.core.dice
Dice expression parsing and rolling.
The grammar is NdS with an optional +M/-M modifier and an optional ×K
multiplier (x and * accepted as ASCII aliases): 3d6, 1d6+1, 1d4-1, 2d6×10.
N defaults to 1, d% is an alias for d100, and die sizes are the closed set
{2, 3, 4, 6, 8, 10, 12, 20, 100}. Parsing is case-insensitive, surrounding whitespace
is stripped, internal whitespace is rejected, and component order is fixed: dice, then
modifier, then multiplier. Numerals are canonical ASCII digits — no Unicode digits, no
leading zeros — with the dice count in 1–999, the modifier magnitude at most 999999,
and the multiplier in 1–999999. Anything else raises
ContentValidationError.
Evaluation order is (sum of dice + M) × K — the modifier applies before the
multiplier, which is not ordinary arithmetic precedence: 2d6+1×10 means
(2d6 + 1) × 10, the B/X treasure-roll convention, not 2d6 + 10.
Results are not clamped: 1d4-1 can total 0 and 1d4-2 can total −1. Minimum-1-damage
is combat's rule, not the dice module's.
Every roll draws from an explicitly passed RngStream;
there is no default stream and no module-level RNG.
ALLOWED_SIDES
module-attribute
ALLOWED_SIDES = frozenset({2, 3, 4, 6, 8, 10, 12, 20, 100})
The closed set of legal die sizes.
DiceExpression
Bases: BaseModel
A parsed dice expression: count dice of sides sides, +modifier, ×multiplier.
RollResult
Bases: BaseModel
The outcome of rolling a dice expression.
Individual die results are kept — not just the total — because events want to show
the rolls. total is (sum(rolls) + modifier) × multiplier.
parse
parse(expression: str) -> DiceExpression
Parse a dice expression string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expression
|
str
|
A dice expression such as |
required |
Returns:
| Type | Description |
|---|---|
DiceExpression
|
The parsed, frozen expression model. |
Raises:
| Type | Description |
|---|---|
ContentValidationError
|
If the expression doesn't match the grammar: unknown die size, zero dice, zero multiplier, non-canonical numerals, internal whitespace, or components out of the fixed dice-modifier-multiplier order. |
TypeError
|
If |
roll
roll(expression: str | DiceExpression, stream: RngStream) -> RollResult
Roll a dice expression, drawing from the given stream.
Dice roll left to right, one die of size S drawing randbelow(S) + 1 — this
mapping is part of the determinism contract.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expression
|
str | DiceExpression
|
The expression to roll, as a string (parsed first) or an
already-parsed |
required |
stream
|
RngStream
|
The RNG stream to draw from. |
required |
Returns:
| Type | Description |
|---|---|
RollResult
|
The roll outcome, including each individual die result. |
Raises:
| Type | Description |
|---|---|
ContentValidationError
|
If a string expression doesn't match the grammar. |
TypeError
|
If |
Examples:
from osrlib.core.dice import roll
from osrlib.core.rng import RngStreams
stream = RngStreams(master_seed=42).get("treasure")
# A plain 3d6: three dice, summed.
scores = roll("3d6", stream)
assert scores.rolls == (4, 2, 3)
assert scores.total == 9
# Modifier then multiplier: (2d6 + 1) × 10, evaluated in that order.
gold = roll("2d6+1×10", stream)
assert gold.total == (sum(gold.rolls) + 1) * 10