Skip to content

Rolling

tilttek/dice

Roller::roll(Pool, Randomizer, RoundingMode) rolls a pool with the randomness it is given:

use Random\Engine\Secure;
use Random\Randomizer;
use Tilttek\Dice\Parser\DiceParser;
use Tilttek\Dice\Rolling\Roller;

$result = new Roller()->roll(new DiceParser()->parse('4d6kh3'), new Randomizer(new Secure()));

Use a secure engine in production. A seeded engine — Mt19937, Xoshiro256StarStar — gives the same dice for the same seed, which is what tests, verifiable and replayable rolls rely on.

Before any die is rolled, the pool is checked against the limits, its custom dice are found (CannotRoll names an unknown one), and custom dice whose faces carry no value are refused inside arithmetic — in an operation, a function, a count or sides rolled first, the limit of an explosion, a group, the count of a list or of repetitions, the subject of a branch. A branch or repeat() gives such dice as they are, the labels of their faces.

What a roll keeps

A PoolResult holds one ExpressionResult per instruction, with its output() — a number, a text or a list — and its total() — null for custom dice whose faces carry no value, a text, a list, or dice given to a merge —, the tree of results that gave it — ConstantResult, OperationResult, NegationResult, FunctionResult, DiceResult, TextResult, ListResult, BranchResult and ReferenceResult (instructions and results) —, and the rounding it was rolled with. NodeResult::sets() lists the sets of dice of a result, in the order they were rolled or listed.

A DiceResult is a set of dice rolled, a list of values, the dice before or those of instructions judged again, or the totals of a group. It keeps every RolledDie, in the order they fell or the order a sort gave:

PropertyMeaning
valueThe die's final value; for a custom die, the face's value, or 0
rollsThe rolls the die is made of: one, or several for a die that compounds its explosions; natural() is their sum, the die as it fell
faceThe Face of a custom die
origininitial, reroll or explosion (DieOrigin)
keptWhether a filter or a selection kept the die
rerolledWhether a reroll replaced it — it no longer counts
explodedWhether it made one more die roll
successWhether it matched the condition of successes, when the expression counts them die by die
failureWhether it took a success away
criticalIts critical mark, critical-success or critical-failure (Mark), for display
lookIts look token, for display

counts() says whether a die counts — kept and not rerolled —; DiceResult::kept() gives the dice that count and tally() counts the faces of custom dice by identifier. DiceResult::groups lists the sets of occurrences or the groups that reach a sum, by their values; DiceResult::parts holds what was rolled before the dice — count, sides, explosion-limit —, or the expressions of a group. DiceResult::notices says what the roll could not do exactly: greedy-groups (Notice) when groups that reach a sum were found greedily.

PoolResult::toArray() writes the whole roll as plain arrays, which a journal keeps and a verification compares; equals() compares two results.

Rerolls, explosions and selections

A repeated reroll rerolls a die until it no longer matches; a reroll once does not reroll the die that replaces it, but that die may explode. A unique die compares itself with the dice of the set that count, those rolled before it. Explosions repeat until a die does not trigger one or its limit is reached; a rolled limit below 0 is 0. Dice all the same roll again, as many as the set rolled, while the dice of the last batch that count all show the same value.

Every die that a repeated operator rolls is counted across the whole pool: explosions of every kind, and the dice of dice all the same, against the limit of explosions; rerolls, repeated, once or unique, against the limit of rerolls; the dice whose count is rolled, the values of lists and the items drawn from text lists, against the limit of dice; the repetitions of repeat(), against the limit of repetitions; the texts of sentences, against the limit of their characters.

A selection chooses among the dice that count, the rerolled ones left out; of equal dice, the first rolled is chosen first. The dice before, @, are the dice that counted in the set rolled or listed just before in the same expression, each as a new die with its value, face and rolls; @ with no set before it in the expression raises CannotRoll.

Groups that reach a sum are the most the dice can form, up to 12 dice: each group is one from which no die can leave with the group still reaching the sum, and of the ways to form the most, the search keeps the first it finds, from the highest die. Beyond 12 dice, one greedy pass finds them, and the result says so with the notice greedy-groups: a die that reaches the sum alone is a group; otherwise the highest die left starts a group, which the lowest die that completes it closes, or the highest die left grows. Each group lists its dice from the highest, the groups by their highest die. Occurrences list the widest sets first and, of equal width, the highest.

Instructions and branches

The instructions are rolled in order; each sees the results of those before it, never their @. The dice of an instruction taken again, bound or merged, are new dice with the value, face and rolls of the dice that counted; their critical marks fall on the lowest and highest of their values. Only the branch that holds is rolled; the cases of a switch are judged in order, and an exclusive switch stops at the first that holds.

Count and sides rolled first

A count or sides rolled first are rolled before the dice, their results kept in the parts. The dice are then checked as if written: a count below 1, sides below 1, or modifiers the dice cannot take raise CannotRoll; sides beyond the limit raise LimitExceeded.

Arithmetic and rounding

Arithmetic is on integers. A division rounds as the RoundingMode passed to the roll says — NegativeInfinity by default, so -7/2 is -4 —; a game rounds up for one side and down for the other by passing the mode of the one who rolls. floor(), ceil() and round() say how the divisions inside them round, whatever the roll says; round() rounds halves away from zero. abs(), min(), max() and repeat() keep the rounding of the roll. Every mode of PHP's RoundingMode is supported, computed without floating point.

A power with a negative exponent is one divided by the power, rounded the same way: 2^-1 is 0 rounded down, 1 rounded up. A remainder has the sign of the divisor. A division or a remainder by zero, zero to a negative power, or a result beyond the range of an integer raises CannotRoll.