Skip to content

Commit

Permalink
Rename to Alea, fully split parser from evaluator.
Browse files Browse the repository at this point in the history
  • Loading branch information
christhekeele committed Mar 3, 2023
1 parent 07943a0 commit f3522d5
Show file tree
Hide file tree
Showing 50 changed files with 651 additions and 469 deletions.
2 changes: 1 addition & 1 deletion .iex.exs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import Dice
import Alea
55 changes: 51 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dice
# Alea

> ***An RPG dice engine and notation language for `Elixir`.***
> ***An RPG dice notation language and dice rolling engine.***
## Dice Notation

Expand All @@ -23,7 +23,50 @@ Dice expressions let you describe rolling multiple dice of different sizes, and

### Roll Modifiers

Roll modifiers let you describe rolling a pool of dice, and doing something with the results before factoring them into the calculation. Multiple modifiers stack, and are applied in order.
Roll modifiers let you describe rolling a pool of dice, and doing something with the results before factoring them into the calculation.

Available modifiers are:

- ***Advantage***: (`+`) lets you treat each die rolled as if you had rolled it twice, and taken the better roll.
- ***Disadvantage*** (`-`): lets you treat each die rolled as if you had rolled it twice, and taken the worse roll.
- ***Maximize*** (`M`) lets you make the highest possible rolls for a given pool.
- ***Minimize*** (`m`) lets you make the lowest possible rolls for a given pool.
- ***Explode*** (`!`): lets you roll extra dice into a pool before adding them together, based on previous rolls.
- ***Keep*** (`K`): lets you only hold on to certain results in a pool before adding them together.
- ***Drop*** (`D`): lets you discard certain results from a pool before adding them together.
- ***Count*** (`C`) lets you change a dice pool's output from being a sum of dice rolls, to the count of rolled dice that meet a certain criteria.

#### Combining Modifiers

You can combine multiple modifiers in any order without restriction, except for ***Count***, which must be last and can only be used at most once.

You can think of the modifiers as belonging these different groups:

- ***Advantage***, ***Disadvantage***, ***Maximize***, and ***Minimize*** let you change *how you roll* the dice in a pool.
- ***Keep*** and ***Drop***, and ***Explode*** let you look at what you've rolled so far and *modify the pool* before you proceed.
- ***Count*** lets you change *how you* ***combine*** results at the end of rolling a pool.

Since this is the order of resolution, this is the conventional order that they are written in expressions.

In practice, it is rare to use more than one modifier from each category.

##### Interactions

***Maximize*** and ***Minimize*** override each other and ***Advantage*** and ***Disadvantage***, so it never makes sense to repeat them or combine with ***Advantage*** and ***Disadvantage***.

***Explode***d dice add new, unrolled dice to the pool. These dice do not keep the ***Advantage***, ***Disadvantage***, ***Maximize***, and ***Minimize*** modifiers of the original pool; you must apply them again after the explosion modifier if you want them to apply to exploded die. For example:

- ***Maximize*** then ***Explode*** will maximize the originally rolled dice, but not the explosions.
- ***Explode*** then ***Maximize*** will only maximize the explosion dice.
- To maximize both in a `d20` roll, you would need to write `d20M!M`.

***Advantage*** and ***Disadvantage*** can be repeated, but use-cases for this are rare. For example, a `d20+-` rolled with advantage, then disadvantage, will:

- roll `2d20` and keep the highest result
- roll another `2d20` and keep the highest result
- then keep the lowest result of the two

***Keep*** and ***Drop*** can be repeated in various orders to filter dice from the pool. Again, use-cases are rare.

#### Keep

Expand Down Expand Up @@ -77,7 +120,11 @@ By default, random dice are dropped. This can be further modified to:
>
> ***Keep Exploding*** (`!!`) will not trigger on 1-sided dice, or dice with a single value on all sides.
>
> ***Keep Exploding At*** (`!N`) will not explode dice where no side is less than `N`. Other safeguards exist to ensure no explosion chain is allowed to run forever.
> ***Keep Exploding At*** (`!N`) will not explode dice where no side is less than `N`.
>
> ***Keep Exploding*** (`!!`) chains cannot be ***Maximize***d (`M`).
>
> Other safeguards exist to ensure no explosion chain is allowed to run forever.
| Operation | Expression | Interpretation | Example | Meaning | Notes |
| :---: | ---: | :--- | ---: | :--- | :--- |
Expand Down
13 changes: 13 additions & 0 deletions lib/alea.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule Alea do
def expression(string) when is_binary(string) do
string |> Alea.Expression.new()
end

def roll(%Alea.Expression{} = expression) do
expression |> Alea.Expression.Evaluate.evaluate()
end

def roll(string) when is_binary(string) do
string |> expression |> roll
end
end
20 changes: 20 additions & 0 deletions lib/alea/dice/pool.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Alea.Dice.Pool do
defstruct dice: [], modifiers: [], rolls: []

def roll_all(%Alea.Dice.Pool{} = pool) do
%Alea.Dice.Pool{
pool
| dice: [],
rolls: (pool.rolls ++ pool.dice) |> Enum.map(&Alea.Die.Roll.roll/1)
}
end

def evaluate(%Alea.Dice.Pool{modifiers: [modifier | modifiers]} = pool) do
pool = %Alea.Dice.Pool{pool | modifiers: modifiers}
Alea.Dice.Pool.Modifier.apply(modifier, pool)
end

def evaluate(%Alea.Dice.Pool{modifiers: []} = pool) do
Alea.Dice.Pool.roll_all(pool)
end
end
3 changes: 3 additions & 0 deletions lib/alea/dice/pool/modifier.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defprotocol Alea.Dice.Pool.Modifier do
def apply(modifier, pool)
end
25 changes: 25 additions & 0 deletions lib/alea/dice/pool/modifier/drop.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Alea.Dice.Pool.Modifier.Drop do
defstruct [:number, mode: :random]

def new(number, mode) do
%__MODULE__{
number: number,
mode: mode
}
end

def rolls(%__MODULE__{} = drop, rolls) do
case drop.mode do
:random -> Enum.take_random(rolls, length(rolls) - drop.number)
:low -> Enum.sort_by(rolls, & &1.result) |> Enum.drop(drop.number)
:high -> Enum.sort_by(rolls, & &1.result) |> Enum.drop(-drop.number)
end
end

defimpl Alea.Dice.Pool.Modifier do
def apply(%Alea.Dice.Pool.Modifier.Drop{} = drop, pool) do
pool = Alea.Dice.Pool.roll_all(pool)
%Alea.Dice.Pool{pool | rolls: Alea.Dice.Pool.Modifier.Drop.rolls(drop, pool.rolls)}
end
end
end
25 changes: 25 additions & 0 deletions lib/alea/dice/pool/modifier/keep.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Alea.Dice.Pool.Modifier.Keep do
defstruct [:number, mode: :random]

def new(number, mode) do
%__MODULE__{
number: number,
mode: mode
}
end

def rolls(%__MODULE__{} = keep, rolls) do
case keep.mode do
:random -> Enum.take_random(rolls, keep.number)
:low -> Enum.sort_by(rolls, & &1.result) |> Enum.take(keep.number)
:high -> Enum.sort_by(rolls, & &1.result) |> Enum.take(-keep.number)
end
end

defimpl Alea.Dice.Pool.Modifier do
def apply(%Alea.Dice.Pool.Modifier.Keep{} = keep, pool) do
pool = Alea.Dice.Pool.roll_all(pool)
%Alea.Dice.Pool{pool | rolls: Alea.Dice.Pool.Modifier.Keep.rolls(keep, pool.rolls)}
end
end
end
5 changes: 5 additions & 0 deletions lib/alea/die/roll.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defprotocol Alea.Die.Roll do
defstruct [:die, :result]

def roll(die)
end
6 changes: 6 additions & 0 deletions lib/alea/die/sides.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
defprotocol Alea.Die.Sides do
def random(faces)
def min(faces)
def max(faces)
def count(faces)
end
25 changes: 25 additions & 0 deletions lib/alea/die/sides/faces.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Alea.Die.Sides.Faces do
defstruct [:number]

def new(number) when is_integer(number) and number > 0 do
%__MODULE__{number: number}
end

defimpl Alea.Die.Sides do
def random(%Alea.Die.Sides.Faces{} = sides) do
:rand.uniform(sides.number)
end

def min(%Alea.Die.Sides.Faces{} = _sides) do
1
end

def max(%Alea.Die.Sides.Faces{} = sides) do
sides.number
end

def count(%Alea.Die.Sides.Faces{} = sides) do
sides.number
end
end
end
25 changes: 25 additions & 0 deletions lib/alea/die/sides/list.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Alea.Die.Sides.List do
defstruct [:options]

def new(options) when is_list(options) do
%__MODULE__{options: Enum.sort(options)}
end

defimpl Alea.Die.Sides do
def random(%Alea.Die.Sides.List{} = list) do
list.options |> Enum.random()
end

def min(%Alea.Die.Sides.List{} = list) do
list.options |> List.first()
end

def max(%Alea.Die.Sides.List{} = list) do
list.options |> List.last()
end

def count(%Alea.Die.Sides.List{} = list) do
list.options |> length
end
end
end
29 changes: 29 additions & 0 deletions lib/alea/die/sides/range.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule Alea.Die.Sides.Range do
defstruct [:first, :last]

def new(first, last) when is_integer(first) and is_integer(last) do
if first < last do
%__MODULE__{first: first, last: last}
else
%__MODULE__{first: last, last: first}
end
end

defimpl Alea.Die.Sides do
def random(%Alea.Die.Sides.Range{} = range) do
Range.new(range.first, range.last) |> Enum.random()
end

def min(%Alea.Die.Sides.Range{} = range) do
range.first
end

def max(%Alea.Die.Sides.Range{} = range) do
range.last
end

def count(%Alea.Die.Sides.Range{} = range) do
range.first - range.last + 1
end
end
end
12 changes: 12 additions & 0 deletions lib/alea/die/types/normal.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Alea.Die.Types.Normal do
defstruct [:sides]

defimpl Alea.Die.Roll do
def roll(%Alea.Die.Types.Normal{} = die) do
%Alea.Die.Roll{
die: die,
result: Alea.Die.Sides.random(die.sides)
}
end
end
end
21 changes: 9 additions & 12 deletions lib/dice/expression.ex → lib/alea/expression.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
defmodule Dice.Expression do
defmodule Alea.Expression do
defstruct [:expression]

import Dice.Parser.Builder
import Alea.Expression.Parser.Builder

defparser do
expression = [
parsec({Dice.Operators.Parser, :combinator}),
parsec({Dice.Expression.Term.Parser, :combinator})
parsec({Alea.Expression.Operators.Parser, :combinator}),
parsec({Alea.Expression.Term.Parser, :combinator})
]

optional(whitespace_literal())
Expand All @@ -15,9 +15,6 @@ defmodule Dice.Expression do
|> post_traverse({__MODULE__, :from_parse, []})
end

# import NimbleParsec
# defparsec :parse, parsec({__MODULE__.Parser, :parse})

def from_parse(unparsed, parsed, context, _line, _offset) do
expression = Keyword.fetch!(parsed, :expression)

Expand All @@ -28,22 +25,22 @@ defmodule Dice.Expression do
{unparsed, [expression], context}
end

defimpl Dice.Expression.Evaluate do
def evaluate(%Dice.Expression{} = expression) do
Dice.Expression.Evaluate.evaluate(expression.expression)
defimpl Alea.Expression.Evaluate do
def evaluate(%Alea.Expression{} = expression) do
Alea.Expression.Evaluate.evaluate(expression.expression)
end
end

defimpl String.Chars do
def to_string(%Dice.Expression{} = expression) do
def to_string(%Alea.Expression{} = expression) do
Kernel.to_string(expression.expression)
end
end

defimpl Inspect do
import Inspect.Algebra

def inspect(%Dice.Expression{} = expression, _opts) do
def inspect(%Alea.Expression{} = expression, _opts) do
concat([
"#{inspect(__impl__(:for))}.new(\"",
break(""),
Expand Down
10 changes: 5 additions & 5 deletions lib/dice/constant.ex → lib/alea/expression/constant.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Dice.Constant do
defmodule Alea.Expression.Constant do
defstruct [:value]

import Dice.Parser.Builder
import Alea.Expression.Parser.Builder

defparser do
non_negative_integer_literal()
Expand All @@ -19,14 +19,14 @@ defmodule Dice.Constant do
{unparsed, [constant], context}
end

defimpl Dice.Expression.Evaluate do
def evaluate(%Dice.Constant{} = constant) do
defimpl Alea.Expression.Evaluate do
def evaluate(%Alea.Expression.Constant{} = constant) do
constant.value
end
end

defimpl String.Chars do
def to_string(%Dice.Constant{} = constant) do
def to_string(%Alea.Expression.Constant{} = constant) do
Kernel.to_string(constant.value)
end
end
Expand Down
Loading

0 comments on commit f3522d5

Please sign in to comment.