Skip to content

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.

count class-attribute instance-attribute

count: int = Field(ge=1)

sides instance-attribute

sides: int

modifier class-attribute instance-attribute

modifier: int = 0

multiplier class-attribute instance-attribute

multiplier: int = Field(default=1, ge=1)

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.

rolls instance-attribute

rolls: tuple[int, ...]

modifier instance-attribute

modifier: int

multiplier instance-attribute

multiplier: int

total instance-attribute

total: int

parse

parse(expression: str) -> DiceExpression

Parse a dice expression string.

Parameters:

Name Type Description Default
expression str

A dice expression such as "3d6", "d%", or "2d6+1×10". Case-insensitive; surrounding whitespace is ignored.

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 expression is not a string.

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 DiceExpression.

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 expression is neither a string nor a DiceExpression.

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