Current section
Files
Jump to
Current section
Files
lib/droll.ex
defmodule Droll do
@moduledoc """
Simple implementation of standard dice notation
See [The Wikipedia Page](https://en.wikipedia.org/wiki/Dice_notation) for
more information.
"""
alias Droll.{Result, Formula}
@doc """
Parse a standard dice notation formula
Examples:
iex> Droll.parse("d20")
{:ok, %Droll.Formula{num_dice: 1, num_sides: 20, modifier: 0, operation: :+}}
iex> Droll.parse("4d6")
{:ok, %Droll.Formula{num_dice: 4, num_sides: 6, modifier: 0, operation: :+}}
iex> Droll.parse("1d6+1")
{:ok, %Droll.Formula{num_dice: 1, num_sides: 6, modifier: 1, operation: :+}}
iex> Droll.parse("10d5-2")
{:ok, %Droll.Formula{num_dice: 10, num_sides: 5, modifier: 2, operation: :-}}
iex> Droll.parse("1d10/1")
{:ok, %Droll.Formula{num_dice: 1, num_sides: 10, modifier: 1, operation: :/}}
iex> Droll.parse("1d10x5")
{:ok, %Droll.Formula{num_dice: 1, num_sides: 10, modifier: 5, operation: :x}}
"""
@spec parse(iodata()) :: {:ok, Formula.t()} | {:error, String.t()}
def parse(formula) do
with {:ok, tokens, _} <- :dice_lexer.string(to_charlist(formula)),
{:ok, {num_dice, :d, num_sides, operation, modifier}} <- :dice_parser.parse(tokens) do
{:ok,
%Formula{
num_dice: num_dice,
num_sides: num_sides,
modifier: modifier,
operation: operation
}}
else
{:error, {_, :dice_lexer, {:illegal, chars}}, _} ->
{:error, "could not decode formula. unexpected input: #{inspect(chars)}"}
{:error, {_, :dice_parser, _}} ->
{:error, "could not decode formula"}
{:error, reason} ->
{:error, reason}
e when is_list(e) ->
{:error, "could not decode formula"}
end
end
@doc """
Execute a roll based on a formula. See `Droll.parse/1` for more information
"""
def roll(formula_str) do
with {:ok, formula} <- parse(formula_str) do
{:ok,
%Result{}
|> apply_roll(formula)
|> apply_modifier(formula)
|> total()
|> min()
|> max()
|> avg()}
end
end
@spec apply_roll(Result.t(), Formula.t()) :: Result.t()
defp apply_roll(result, formula) do
rolls =
Enum.map(1..formula.num_dice, fn _ ->
1 + :erlang.floor(:rand.uniform() * formula.num_sides)
end)
%{result | rolls: rolls}
end
@spec total(Result.t()) :: Result.t()
defp total(result) do
Enum.reduce(1..Enum.count(result.rolls), result, fn b, result ->
%{result | total: result.total + Enum.at(result.rolls, b - 1)}
end)
end
@spec min(Result.t()) :: Result.t()
defp min(result), do: %{result | min: Enum.min(result.rolls)}
@spec max(Result.t()) :: Result.t()
defp max(result), do: %{result | min: Enum.max(result.rolls)}
@spec avg(Result.t()) :: Result.t()
defp avg(result), do: %{result | avg: result.total / Enum.count(result.rolls)}
@spec apply_modifier(Result.t(), Formula.t()) :: Result.t()
defp apply_modifier(result, %{modifier: modifier, operation: :+}),
do: %{result | total: result.total + modifier}
defp apply_modifier(result, %{modifier: modifier, operation: :-}),
do: %{result | total: result.total - modifier}
defp apply_modifier(result, %{modifier: modifier, operation: :/}),
do: %{result | total: result.total / modifier}
defp apply_modifier(result, %{modifier: modifier, operation: :x}),
do: %{result | total: result.total * modifier}
end