import { type Range, type DiceFilter, type DiceFunctor, type DiceReducer, type Roll, type DiceExpression, type FilterDirection } from './dice-expression'; import type { Expression, Faces } from './program'; import { type DiceResultMapped, type DieResult, type DieResultFilter, type RollResult } from './roll-result'; export interface RollerOptions { maxExplodeIterations: number; maxRerollIterations: number; maxEmphasisIterations: number; /** * Optional sink charged one unit per explode/reroll/compound iteration. The * Evaluator wires this to its evaluation-wide `maxTotalIterations` budget so * a bounded-but-huge modifier count (e.g. `1d6 explode 5000000 times on 1 or * more`, or `1000d6 explode 100000 times …` whose per-die chains multiply * into 1e8 rolls) is bounded by the same budget as `repeat`/comprehension/ * `fold` rather than running synchronously to completion. Throwing from this * callback aborts the roll. Left undefined for standalone Roller use, where * only the `always`-iteration caps apply. */ tick?: (n: number) => void; } /** Maximum number of dice that can be rolled by a single n-dice expression. */ export declare const MAX_DICE_COUNT = 10000; /** * Single source of truth for the per-expression dice-count cap. Throws when * `count` exceeds {@link MAX_DICE_COUNT}. Every count-bearing roll path (and * the structured-dice path in the Evaluator) routes through this so the bound * cannot be enforced inconsistently. */ export declare function assertDiceCount(count: number): void; /** * Thrown when an unbounded (`explode`/`compound` ... `always`) expansion cannot * converge because every attainable face triggers another roll (e.g. a d1 that * explodes, or `explode always on >= 1`). Such an expression has no finite * distribution, so truncating it at the iteration cap and returning a number * would be a silently wrong answer. */ export declare class DivergentExplosionError extends Error { constructor(message: string); } /** * Wrap an injected {@link Roll} so every draw is validated to be an integer * in `[1, sides]`. A custom RNG (or a tampered replay log) returning `0`, a * value `> sides`, or a float would otherwise silently index `faces[-1]` / * out-of-range faces and corrupt every downstream mechanic with * `undefined`/`NaN`; this turns that class of bug into an immediate, localized * failure. */ export declare function validateRoll(fn: Roll): Roll; export declare class Roller { private readonly variables?; private readonly diceRegistry?; private readonly listVariables?; /** * Callback to evaluate an arbitrary program Expression to an integer. * Provided by the Evaluator so the Roller can resolve modifier-arg * expressions like `drop lowest ($dice - 1)` against the current scope. * When unset, the Roller falls back to a minimal built-in resolver that * handles only literals and bare `$variable` references. */ private readonly evaluateExpression?; /** * Decide whether `r` (a die's face value) matches `range`, using only * literal range values. Throws if the range contains a `$variable` or * arithmetic expression — static-analysis callers (DiceStats, * `DE.alwaysInRange`, etc.) must substitute non-literal range values * upstream before calling. * * Use the instance method {@link Roller#matchRangeWithScope} when * evaluating against a live scope. */ static matchRange(r: number, range: Range): boolean; /** * Instance variant of {@link Roller.matchRange} that resolves * `$variable` references and arithmetic from this Roller's scope and * `evaluateExpression` callback. Used by the dice evaluation paths * inside Roller; external callers should use the static when working * with pure-literal ranges, or substitute upstream. */ matchRangeWithScope(r: number, range: Range): boolean; private static resolveOnMax; private static settledDie; /** * Build the predicate that decides which rolls to keep for a given filter. * * `value` is the *resolved* count (already substituted from the scope if * the filter was parameterised with a `$variable`). Static helper, so the * caller is responsible for resolution. */ static filterf(type: 'drop' | 'keep', dir: FilterDirection, value: number): (res: number, length: number) => boolean; readonly options: RollerOptions; private readonly dieRoll; constructor(dieRoll: Roll, options?: Partial, variables?: Record | undefined, diceRegistry?: Map | undefined, listVariables?: Record | undefined, /** * Callback to evaluate an arbitrary program Expression to an integer. * Provided by the Evaluator so the Roller can resolve modifier-arg * expressions like `drop lowest ($dice - 1)` against the current scope. * When unset, the Roller falls back to a minimal built-in resolver that * handles only literals and bare `$variable` references. */ evaluateExpression?: ((expr: Expression) => number) | undefined); /** * Resolve a modifier-arg Expression to an integer count. * * Literals and bare `$variable` references resolve from `this.variables` * directly; everything else delegates to `evaluateExpression` (the * Evaluator-provided callback). If no callback is set and the expression * isn't a literal or bare variable, throws — DiceStats and other * static-analysis callers don't supply the callback and instead substitute * upstream. */ resolveExpressionArg(expr: Expression, role: 'count' | 'range value'): number; private resolveListBinding; /** * Wrap a bound numeric list into RollResult[] so the filter pipeline can * operate on it. Each value becomes a synthetic `number-literal` result * carrying the value itself. The dice ranks are computed off the bound * numbers — identical semantics to applying the filter directly to the * RHS list at bind site. */ private rollResultsFromList; private materializeFilterable; /** * Roll a `DiceListWithMap` / `DiceListWithMapHomogeneous` head and run * its functor pass. Returns the per-original-die totals as * `RollResult[]` (each entry is the post-functor value for one * original die — sum of explosions for `explode`, last reroll for * `reroll`, accumulated total for `compound`, the kept die for * `emphasis`), along with the captured mapping detail for trace * consumers. */ private rollMapeableWithTrace; /** * Apply each filter in sequence over `rolls`. Each step takes the * kept rolls from the previous step and filters them again. Discard * markers from prior steps are preserved so the final * `DieResultFilter[]` reflects all elimination rounds. */ private applyFilterChain; /** * Collapse a single mapped die into its post-functor total. * * - `normal`: the die itself. * - `rerolled`: the last roll (the reroll-explode replacement chain * means only the final entry survives). * - `exploded`: the sum across all rolls in the explode chain * (original + every triggered explosion), so each original die * contributes one filterable entry rather than the chain * competing internally for filter slots. * - `compounded`: the accumulated total carried by `Compounded.total`. */ private dieTotalForMapped; private rollDiceReduce; /** * Roll a dice expression. Accepts either: * - a unified `Expression` (the post-lift AST emitted by the parser / * consumed by the Evaluator), or * - a legacy `DiceExpression` (for callers that construct dice trees by * hand using the dice-AST constructors). * * Both shapes are handled by the same dispatch — they overlap on * Die/NDice/CustomDie/DiceReduce/StructuredDiceRoll. Where the program * AST and the dice AST diverge (arithmetic and variable references) we * recognize both spellings: `'binary-expr'`/`'binary-op'`, * `'number-literal'`/`'literal'`, `'unary-expr'`/`'unary-op'`, * `'variable-ref'`/`'dice-variable-ref'`. Result trees always use the * program-aligned tags (`'binary-expr-result'`, * `'number-literal-result'`, `'unary-expr-result'`). */ roll(expr: Expression | DiceExpression): RollResult; private resolveNDiceParam; private rollNDice; mapRolls(rolls: DieResult[], functor: DiceFunctor): DiceResultMapped[]; compoundRoll(roll: DieResult, times: number, range: Range, unbounded?: boolean): DiceResultMapped; emphasisRoll(roll: DieResult, furthestFrom: number | 'average', tieBreaker: 'low' | 'high' | 'reroll'): DiceResultMapped; explodeRoll(roll: DieResult, times: number, range: Range, unbounded?: boolean): DiceResultMapped; /** * An unbounded (`always`) explode/compound is divergent — it has no finite * distribution — exactly when *every* attainable face of the die triggers * another roll (e.g. a d1, or `explode always on >= 1`). Detect that * deterministically from the trigger range rather than from the outcome of a * particular run, so a degenerate RNG that happens to keep rolling the * trigger value (common in forced-roll tests) is NOT misread as divergence. * Truncating a genuinely divergent expansion at the cap and returning a * number would be a silently wrong answer, so we throw instead. */ private assertNotDivergent; rerollRoll(roll: DieResult, times: number, range: Range, unbounded?: boolean): DiceResultMapped; rollRange(roll: DieResult, times: number, range: Range): DieResult[]; keepMappedRolls(rolls: DiceResultMapped[]): DieResult[]; filterRolls(rolls: RollResult[], filter: DiceFilter, resolvedValue?: number): DieResultFilter[]; keepFilteredRolls(rolls: DieResultFilter[]): RollResult[]; reduceRolls(rolls: RollResult[], reducer: DiceReducer): number; reduceResults(results: number[], reducer: DiceReducer): number; getRollResults(rolls: RollResult[]): number[]; }