Packages

Pure-Elixir tabletop RPG game support and rules modeled on the System Reference Document (SRD) 5.2 with opt-in support for some 5.1 rules.

Current section

Files

Jump to
srd_5e lib srd dice expr.ex
Raw

lib/srd/dice/expr.ex

defmodule Srd.Dice.Expr do
@moduledoc """
A parsed, unrolled dice expression. A dice expression can be built
manually or can be parsed from human-readable dice notation like d20, 2d4+1, etc.
"""
@enforce_keys [:count, :sides]
defstruct count: 1, sides: nil, modifier: 0
@type t :: %__MODULE__{count: pos_integer(), sides: pos_integer(), modifier: integer()}
# NdX ±M (optional)
@dice_notation ~r/^(\d*)d(\d+)(?:([+-])(\d+))?$/i
@spec parse(String.t() | t()) :: {:ok, t()} | {:error, {:invalid_dice, String.t()}}
# already parsed → pass through
def parse(%__MODULE__{} = expr), do: {:ok, expr}
def parse(string) when is_binary(string) do
case Regex.run(@dice_notation, String.trim(string), capture: :all_but_first) do
[count, sides, sign, mod] -> build(count, sides, sign, mod, string)
# :re drops trailing non-participating groups, so a modifier-less roll
# comes back as just [count, sides].
[count, sides] -> build(count, sides, "", "", string)
_ -> {:error, {:invalid_dice, string}}
end
end
@spec parse!(String.t() | t()) :: t()
def parse!(input) do
case parse(input) do
{:ok, expr} -> expr
{:error, {:invalid_dice, s}} -> raise ArgumentError, "invalid dice notation: #{inspect(s)}"
end
end
defp build(count, sides, sign, mod, original) do
# "d20" → 1d20
count = if count == "", do: 1, else: String.to_integer(count)
sides = String.to_integer(sides)
modifier =
case mod do
"" -> 0
m -> String.to_integer(m) * if(sign == "-", do: -1, else: 1)
end
if count >= 1 and sides >= 1 do
{:ok, %__MODULE__{count: count, sides: sides, modifier: modifier}}
else
{:error, {:invalid_dice, original}}
end
end
end