Design: the notation field (option 2a), the three dice pickers (1h–1j), the parse-error state (9c) and the saved-roll screens (1n–1p, editor 1r, import 9f–9g) are in the clickable design (design/).
dInfinity accepts the tabletop notation most players already know, e.g.
3d6 + 1d20 - 4. This document defines exactly what is accepted and how it
is evaluated.
| Input | Meaning |
|---|---|
d20 |
one twenty-sided die |
3d6 |
three six-sided dice, summed |
3d6 + 1d20 - 4 |
sum of both groups minus 4 |
2d20kh1 |
roll two d20, keep the highest (advantage) |
2d20kl1 |
keep the lowest (disadvantage) |
4d6dl1 |
roll four d6, drop the lowest (stat generation) |
d100 or d% |
percentile: two d10 as tens and units (d% is an alias for d100) |
8d6! |
exploding d6: each 6 rolls an additional d6 |
2 * (1d8 + 3) |
arithmetic and grouping |
1d6 + 1d4 [Fire] |
trailing label, ignored for math, shown in breakdown |
brass:1d20 |
use the d20 from the installed dice set with id brass |
formula := expr label?
expr := term (("+" | "-") term)*
term := factor (("*" | "/") factor)*
factor := ("-")? atom
atom := dice | integer | "(" expr ")"
dice := (setref ":")? count? "d" sides modifier*
count := integer ; default 1, max 1000 (see Limits)
sides := integer | "%" | "F" ; "%" is an alias for 100 (as 2d10), "F" = fudge/fate die
setref := identifier ; installed dice set id
modifier := "kh" integer ; keep highest n
| "kl" integer ; keep lowest n
| "dh" integer ; drop highest n
| "dl" integer ; drop lowest n
| "!" ; explode on max face
| "r" integer ; reroll once on value <= n
| "min" integer ; clamp each die to at least n
label := "[" text "]"
integer := [0-9]+
identifier:= [a-z][a-z0-9_-]*
Whitespace is ignored between tokens. Notation is case-insensitive except for set ids.
To keep the simulation and the probability graph tractable:
| Limit | Value | Behaviour when exceeded |
|---|---|---|
| Dice per formula (parse) | 1,000 | Parse error, shown inline. This bound exists so the outcome graph stays cheap. |
| Dice per roll | table capacity (docs/tables.md), hard cap 100 |
Roll button disabled with the reason; the graph still works |
| Sides per die | must exist in a set | Parse error naming the missing die |
| Explosion depth | 20 | Further explosions ignored, noted in breakdown |
| Nested parentheses | 8 | Parse error |
| Result magnitude | fits in 64-bit | Overflow is a parse-time error via the PMF bound |
A formula like 500d6 can be typed and graphed but cannot be rolled: the
dice would not fit on the table with room to move, and a physics engine with
that many bodies crammed in a box produces garbage rather than a roll. The
capacity check happens before any body is created and the UI explains it
(“500 dice don’t fit on the table; up to 72 do”).
dice node to concrete Die definitions: setref if given,
otherwise the default set from Settings, falling back to the built-in set
per die when the default set lacks that one (so a set with no d12 still
rolls 1d20 + 1d12). The breakdown names the set each die came from and
says when it fell back.RollResult with the total and a per-die breakdown including
dropped dice (shown struck through). The result screen shows the total
large, then each group’s subtotal and the individual dice, then the
modifiers — the user never has to add anything up.Modifiers take effect in this order whatever order they were written in, so
4d6r1dl1 and 4d6dl1r1 mean the same thing:
r n — a die showing n or less is thrown once more. Once: the
replacement stands however low it is. Both dice stay in the breakdown, the
first struck through.! — a die showing its highest face throws another of the same die.
The new die joins that die’s chain rather than the group at large, so
2d6!kh1 keeps the better of two chains, which is what a player means by
it. A chain stops after the explosion depth limit, and the die that would
have exploded again is marked in the breakdown.min n — a die below n counts as n, per die. The face it actually
landed on is still what the breakdown shows; only its contribution changes.kh / kl / dh / dl — whole chains are kept or dropped, ranked by
what each chain came to together. A percentile pair counts as one unit, so
2d%kh1 keeps the better of two 1–100 results.A group may carry each modifier at most once, and may keep or drop, not
both: 4d6dl1dl1 and 4d6kh1dl1 are refused rather than quietly meaning
something. Keeping or dropping more dice than the group rolls is refused too.
Everything that can be known without rolling is checked while the formula is still text, so a roll in front of a player never fails halfway through:
kh/kl/dh/dl count the group cannot satisfy;! on a die whose every face is its highest, which would never stop;1d6 / 1dF is refused, 1d6 / 1d4
is not;Division rounds down by default (7/2 = 3), which is what most game
rules say. The default is a setting, and the result sheet offers Down /
Nearest / Up for the throw in front of you — the total is recomputed from the
same dice, which stay as they landed. Nearest rounds .5 up and anything
below .5 down.
The per-throw override is not remembered: the next roll uses the setting again. The outcome graph always uses the setting, since it is computed before the throw exists.
The roll screen also has a dice picker: tap a d6 three times and a d20
once and you have 3d6 + 1d20. Internally this is the same RollPlan, so
the outcome graph, the breakdown and statistics work identically for picked
dice and typed formulas. A picked roll can be turned into a saved roll with
one tap.
d% is an alias for d100 everywhere: in typed formulas, saved rolls,
imported collections and the breakdown (which always prints d100).
Both are always resolved to a tens d10 (faces 00–90) and a units
d10 (0–9) from the same set, marked as a pair. Result = tens + units, with
00+0 = 100. If the set has a d100-tens die it is used; otherwise the normal
d10 is used with its face values multiplied by ten in the breakdown. Sets can
also define a true 100-face die but it is never chosen by d100 implicitly.
d2 uses the set’s coin if present, otherwise a d6 with face values
1,2,1,2,1,2. Which one was used is visible in the breakdown.
A saved roll is a named formula with an icon, living in a group:
SavedRollGroup {
id, name ("Curse of Strahd" / "Thorin"), icon,
parentId? // one level of nesting: game → character
sortOrder
}
SavedRoll {
id, groupId,
name ("Fireball"), icon (from icon pack or emoji),
formula ("8d6 [Fire]"),
colour tag,
favourite flag,
createdAt, lastUsedAt, useCount
}
D&D / Thorin, Pathfinder / Ezren); deeper trees are not worth the UI.docs/statistics.md).Saved rolls travel as a collection: a JSON file describing groups and rolls.
{
"format": 1,
"name": "Thorin, level 5 fighter",
"groups": [
{ "id": "thorin", "name": "Thorin", "icon": "⚔️", "parent": null }
],
"rolls": [
{ "group": "thorin", "name": "Longsword", "icon": "🗡️", "formula": "1d20 + 7 [Attack]" },
{ "group": "thorin", "name": "Longsword damage", "icon": "💥", "formula": "1d8 + 4 [Slashing]" }
]
}
docs/dice-sets.md). A community can keep a
repo of “stat blocks for monster manual X” this way.Parse errors point at the offending character range and are shown inline under the formula field, e.g.
3d6 + 1d7 - 4
^^ no d7 in set "builtin"
Suggestions are offered when the intent is obvious (d7 → nearest available,
3 d 6 → 3d6).