Packages

Add a dice-rolling DSL with sigil support and reusable compiled functions to your application.

Current section

Files

Jump to
ex_dice_roller lib ex_dice_roller.ex
Raw

lib/ex_dice_roller.ex

defmodule ExDiceRoller do
@moduledoc """
Converts strings into dice rolls and returns expected results. Ignores any
spaces, including tabs and newlines, in the provided string.
## Examples
iex> ExDiceRoller.roll("1")
1
iex> ExDiceRoller.roll("1d8")
1
iex> ExDiceRoller.roll("2d20 + 5")
34
iex> ExDiceRoller.roll("2d8 + -5")
0
iex> ExDiceRoller.roll("(1d4)d(6*5) - (2/3+1)")
18
iex> ExDiceRoller.roll("1+2-3*4+5/6*7+8-9")
-4
iex> ExDiceRoller.roll("1+\t2*3d 4")
15
## Order of Precedence
The following table shows order of precendence, from highest to lowest,
of the operators available to ExDiceRoller.
Operator | Associativity
--------------------- | ------------
`d` | left-to-right
`+`, `-` | unary
`*`, `/` | left-to-right
`+`, `-` | left-to-right
### Effects of Parentheses
As in math, parentheses can be used to create sub-expressions.
iex> ExDiceRoller.tokenize("1+3d4*1-2/-3") |> elem(1) |> ExDiceRoller.parse()
{:ok,
{{:operator, '-'},
{{:operator, '+'}, {:digit, '1'},
{{:operator, '*'}, {:roll, {:digit, '3'}, {:digit, '4'}}, {:digit, '1'}}},
{{:operator, '/'}, {:digit, '2'}, {:digit, '-3'}}}}
iex> ExDiceRoller.tokenize("(1+3)d4*1-2/-3") |> elem(1) |> ExDiceRoller.parse()
{:ok,
{{:operator, '-'},
{{:operator, '*'},
{:roll, {{:operator, '+'}, {:digit, '1'}, {:digit, '3'}}, {:digit, '4'}},
{:digit, '1'}}, {{:operator, '/'}, {:digit, '2'}, {:digit, '-3'}}}}
iex> ExDiceRoller.tokenize("1+3d(4*1)-2/-3") |> elem(1) |> ExDiceRoller.parse()
{:ok,
{{:operator, '-'},
{{:operator, '+'}, {:digit, '1'},
{:roll, {:digit, '3'}, {{:operator, '*'}, {:digit, '4'}, {:digit, '1'}}}},
{{:operator, '/'}, {:digit, '2'}, {:digit, '-3'}}}}
iex> ExDiceRoller.tokenize("1+3d4*(1-2)/-3") |> elem(1) |> ExDiceRoller.parse()
{:ok,
{{:operator, '+'}, {:digit, '1'},
{{:operator, '/'},
{{:operator, '*'}, {:roll, {:digit, '3'}, {:digit, '4'}},
{{:operator, '-'}, {:digit, '1'}, {:digit, '2'}}}, {:digit, '-3'}}}}
## Compiled Rolls
Some systems utilize complex dice rolling equations. Repeatedly tokenizing,
parsing, and interpreting complicated dice rolls strings can lead to a
performance hit on an application. To ease the burden, developers can
_compile_ a dice roll string into an anonymous function. This anonymous
function can be cached and reused repeatedly without having to re-parse the
string, nor re-interpret the parsed expression.
iex> {:ok, roll_fun} = ExDiceRoller.compile("2d6+3")
iex> ExDiceRoller.execute(roll_fun)
8
iex> ExDiceRoller.execute(roll_fun)
13
iex> ExDiceRoller.execute(roll_fun)
10
iex> ExDiceRoller.execute(roll_fun)
11
"""
alias ExDiceRoller.Compiler
@type tokens :: [token, ...]
@type token :: {token_type, integer, list}
@type token_type :: :digit | :basic_operator | :complex_operator | :roll | :"(" | :")"
@type expression ::
{:digit, list}
| {{:operator, list}, expression, expression}
| {:roll, expression, expression}
@doc """
Processes a given string as a dice roll and returns the final result. Note
that the final result is a rounded integer.
iex> ExDiceRoller.roll("1d6+15")
18
"""
@spec roll(String.t()) :: integer
def roll(roll_string) do
with {:ok, tokens} <- tokenize(roll_string),
{:ok, parsed_tokens} <- parse(tokens) do
parsed_tokens
|> calculate()
|> round()
else
{:error, _} = err -> err
end
end
@doc """
Converts a roll-based string into tokens using leex. The input definition
file is located at `src/dice_lexer.xrl`. See `t:token_type/0`, `t:token/0`,
and `t:tokens/0` for the possible return values.
iex> ExDiceRoller.tokenize("2d8+3")
{:ok,
[
{:digit, 1, '2'},
{:roll, 1, 'd'},
{:digit, 1, '8'},
{:basic_operator, 1, '+'},
{:digit, 1, '3'}
]}
"""
@spec tokenize(String.t()) :: {:ok, tokens}
def tokenize(roll_string) do
with charlist <- String.to_charlist(roll_string),
{:ok, tokens, _} <- :dice_lexer.string(charlist) do
{:ok, tokens}
else
{:error, {1, :dice_lexer, reason}, 1} ->
{:error, {:tokenizing_failed, reason}}
end
end
@doc """
Converts a series of tokens provided by `tokenize/1` and parses them into
an expression structure. This expression structure is what's used by the
dice rolling functions to calculate rolls. The BNF grammar definition
file is located at `src/dice_parser.yrl`.
iex> {:ok, tokens} = ExDiceRoller.tokenize("2d8 + (1+2)")
{:ok,
[
{:digit, 1, '2'},
{:roll, 1, 'd'},
{:digit, 1, '8'},
{:basic_operator, 1, '+'},
{:"(", 1, '('},
{:digit, 1, '1'},
{:basic_operator, 1, '+'},
{:digit, 1, '2'},
{:")", 1, ')'}
]}
iex> {:ok, _} = ExDiceRoller.parse(tokens)
{:ok,
{{:operator, '+'}, {:roll, {:digit, '2'}, {:digit, '8'}},
{{:operator, '+'}, {:digit, '1'}, {:digit, '2'}}}}
"""
@spec parse(tokens) :: {:ok, expression}
def parse(tokens) do
case :dice_parser.parse(tokens) do
{:ok, _} = resp -> resp
{:error, {_, :dice_parser, reason}} -> {:error, {:token_parsing_failed, reason}}
end
end
@doc """
Takes a given expression from parse and calculates the result.
"""
@spec calculate(expression) :: integer | float
def calculate(expression) do
expression
|> compile()
|> elem(1)
|> execute()
end
@doc """
Compiles a string or `t:expression/0` into an anonymous function.
iex> {:ok, roll_fun} = ExDiceRoller.compile("1d8+2d(5d3+4)/3")
iex> ExDiceRoller.execute(roll_fun)
5.0
"""
@spec compile(String.t() | expression) :: {:ok, Compiler.compiled_function()} | {:error, any}
def compile(roll_string) when is_bitstring(roll_string) do
with {:ok, tokens} <- tokenize(roll_string),
{:ok, parsed_tokens} <- parse(tokens) do
compile(parsed_tokens)
else
{:error, _} = err -> err
end
end
def compile(expression) when is_tuple(expression) do
compiled = Compiler.compile(expression)
case is_function(compiled) do
false -> {:ok, fn -> compiled end}
true -> {:ok, compiled}
end
end
@doc "Executes a function built by `compile/1`."
@spec execute(function) :: integer | float
def execute(compiled) when is_function(compiled) do
compiled.()
end
end