Pixel-art 3D dice roller. The roll itself is computed authoritatively in
Elixir; the optional LiveView component (DicExWeb.DiceRoller) visualises
the result with Three.js + Rapier physics.
Quick start
iex> DicEx.roll("1d20")
%DicEx.Result{total: 14, ...}
iex> DicEx.roll("3d6 + 2")
%DicEx.Result{total: 13, ...}
iex> DicEx.roll("2d20kh1") # advantage — keep highest
%DicEx.Result{...}
iex> DicEx.roll("4d6dl1") # 4d6, drop lowest (ability score)
%DicEx.Result{...}
iex> DicEx.roll("8d6!") # explode (Sneak Attack / fire)
%DicEx.Result{...}
iex> DicEx.roll("1d20r1") # reroll natural 1s
%DicEx.Result{...}Notation
| Token | Meaning |
|---|---|
NdS | Roll N dice with S sides (d4..d100) |
kh[n] | Keep highest n (advantage) |
kl[n] | Keep lowest n (disadvantage) |
dh[n] | Drop highest n |
dl[n] | Drop lowest n |
! / !p | Explode / explode & penetrate |
r<op>n | Reroll (< <= = >= >), append o for once |
+ / - | Add / subtract modifiers or pools |
Reproducible rolls (tests, replays, anti-cheat)
Seed the default RNG for a reproducible sequence:
DicEx.roll("2d20kh1", seed: 42)Or thread an explicit RNG module/tuple via the lower-level roll/2 second
argument.
A note on :seed
:seed reseeds the calling process's :rand state to produce a
reproducible sequence. The prior state is not restored, so in a long-lived
process (e.g. a LiveView) a later unseeded roll/2 continues the seeded
sequence rather than drawing fresh entropy. Thread :rng explicitly when
you need isolation, or re-seed per request.
Summary
Functions
Pretty-prints a result as a human readable string, e.g. "2d20kh1+5 = 23".
Rolls the given expression and returns a DicEx.Result.
Convenience for a single typed roll — the shape the dragonEx UI emits.
Like roll/2 but returns {:ok, result} or {:error, reason} without
raising. Handy when the expression comes from untrusted input (e.g. a chat
command parsed by an LLM in dragonEx).
Types
Functions
@spec format(DicEx.Result.t()) :: String.t()
Pretty-prints a result as a human readable string, e.g. "2d20kh1+5 = 23".
@spec roll(binary(), [roll_opt()]) :: DicEx.Result.t()
Rolls the given expression and returns a DicEx.Result.
Options
:seed— integer seed for the default RNG (reproducible sequence).:rng— an explicit RNG module (stateless) or{module, state}tuple.
@spec roll_dice(pos_integer(), pos_integer(), keyword()) :: DicEx.Result.t()
Convenience for a single typed roll — the shape the dragonEx UI emits.
DicEx.roll_dice(2, 20) # 2d20
DicEx.roll_dice(1, 20, mod: 5) # 1d20 + 5Options
:mod— integer modifier added to the total (+/-).:advantage— boolean, keeps highest of 2 (implies 2 dice ifcount==1).:disadvantage— boolean, keeps lowest of 2.- Plus any option accepted by
roll/2(:seed,:rng).
@spec roll_e(binary(), [roll_opt()]) :: {:ok, DicEx.Result.t()} | {:error, String.t()}
Like roll/2 but returns {:ok, result} or {:error, reason} without
raising. Handy when the expression comes from untrusted input (e.g. a chat
command parsed by an LLM in dragonEx).