Skip to content

Commit

Permalink
Use visitor pattern for dice sides.
Browse files Browse the repository at this point in the history
  • Loading branch information
christhekeele committed Mar 3, 2023
1 parent b05713a commit 7e6aa89
Show file tree
Hide file tree
Showing 18 changed files with 293 additions and 134 deletions.
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ Dice expressions let you describe rolling multiple dice of different sizes, and
| :---: | ---: | :--- | ---: | :--- | :--- |
| ***Roll*** | `dS` | Roll a die with `S` sides. | `d20` | Roll one `d20`. | Equivalent to ***Roll Many***: `1dS`. Equivalent to ***Range Die***: `d{1..20}`. |
| ***Addition*** | `dS + C` | Roll a die with `S` sides, and add `C` to the result. | `d20 + 1` | Roll one `d20` and add `1` to the result. | |
| ***Subtraction*** | `dS - C` | Roll a die with `S` sides, and subtract `C` from the result. | `d20 - 2` | Roll 1 `d20` and subtract `2` from the result. | |
| ***Roll Many*** | `NdS` | Roll `N` dice with `S` sides and add the results. | `3d6` | Roll 3 `d6` and add the results. | |
| ***Roll Different*** | `NdS + MdT + C` | Roll `N` dice with `S` sides, `M` dice with `T` sides, and add all together plus `C`. | `3d6 + d4 + 2` | Roll 3 `d6`, one `d4`, and add `2` to the results. | |
| ***Subtraction*** | `dS - C` | Roll a die with `S` sides, and subtract `C` from the result. | `d20 - 2` | Roll one `d20` and subtract `2` from the result. | |
| ***Roll Many*** | `NdS` | Roll `N` dice with `S` sides and add the results. | `3d6` | Roll `3` `d6` and add the results. | |
| ***Roll Different*** | `NdS + MdT + C` | Roll `N` dice with `S` sides, `M` dice with `T` sides, and add all together plus `C`. | `3d6 + d4 + 2` | Roll `3` `d6`, one `d4`, and add `2` to the results. | |

### Roll Modifiers

Expand Down Expand Up @@ -77,13 +77,7 @@ By default, all dice are counted. This can be further modified to:

| Operation | Expression | Interpretation | Example | Meaning | Notes |
| :---: | ---: | :--- | ---: | :--- | :--- |
| ***Drop*** | `NdSD` | Roll `N` dice with `S` sides, and drop a random die. | `2d20D` | Roll 2 `d20` and drop one at random. | Equivalent to ***Drop Random***: `NdSD1R`. |
| ***Drop Many*** | `NdSDM` | Roll `N` dice with `S` sides, and drop `M` random dice. | `3d20D2` | Roll 3 `d20` and drop `2` at random. | Equivalent to ***Drop Random***: `NdSDMR.` |
| ***Drop Random*** | `NdSDMR` | Roll `N` dice with `S` sides, and drop `M` random dice. | `3d20D2R` | Roll 3 `d20` and drop `2` at random. | Equivalent to ***Drop Many***: `NdSKM`. |
| ***Drop Highest*** | `NdSDH` | Roll `N` dice with `S` sides, and drop the highest result. | `2d20DH` | Roll 2 `d20` and drop the highest result. | |
| ***Drop Many Highest*** | `NdSDMH` | Roll `N` dice with `S` sides, and drop the highest `M` results. | `3d20D2H` | Roll 3 `d20` and drop the `2` highest. |
| ***Drop Lowest*** | `NdSDL` | Roll `N` dice with `S` sides, and drop the lowest result. | `2d20DL` | Roll 2 `d20` and drop the lowest result. | |
| ***Drop Many Lowest*** | `NdSDML` | Roll `N` dice with `S` sides, and drop the lowest `M` results. | `3d20D2L` | Roll 3 `d20` and drop the `2` lowest. | |
| ***TBD*** |

#### Explode

Expand All @@ -109,7 +103,11 @@ By default, re-rolls do not trigger explosions. This can be further modified by

***Advantage*** (`dS+`) lets you treat a roll as if you had rolled it twice, and taken the better roll. Conversely, ***Disadvantage*** (`dS-`) takes the worse roll.

> **NOTE** that this works differently from just rolling twice as many dice with ***Keep High***/***Keep Low***, when rolling more than one die with **Advantage**/**Disadvantage**! `KH`/`KL` will remove the worst/best rolls from the entire pool, whereas **Advantage**/**Disadvantage** treats each roll as a pool of two before proceeding, allowing more good/poor rolls to contribute to the total. In practice, this means that `NdS+` will have more variance and average lower results than `2NdSKHN`, and `NdS-` will have more variance and average higher results than `2NdSKLN`.
> **NOTE** that this works differently from just rolling twice as many dice with ***Keep High***/***Keep Low***, when rolling more than one die with **Advantage**/**Disadvantage**!
>
> `KH`/`KL` will remove the worst/best rolls from the entire pool, whereas **Advantage**/**Disadvantage** treats each roll as a pool of two before proceeding, allowing more good/poor rolls to contribute to the total.
>
> In practice, this means that `NdS+` will have more variance and average lower results than `2NdSKHN`, and `NdS-` will have more variance and average higher results than `2NdSKLN`.
| Operation | Expression | Interpretation | Example | Meaning | Notes |
| :---: | ---: | :--- | ---: | :--- | :--- |
Expand Down
61 changes: 7 additions & 54 deletions lib/dice/die.ex
Original file line number Diff line number Diff line change
@@ -1,82 +1,35 @@
defmodule Dice.Die do
defstruct [:faces]
defstruct [:sides]

@symbol "d"
@compile {:inline, symbol: 0}
def symbol, do: @symbol

@range_symbol ".."
@compile {:inline, range_symbol: 0}
def range_symbol, do: @range_symbol

import Dice.Parser.Builder

defparser do
dice_value_descriptor = non_negative_integer_literal()

dice_range_descriptor =
ignore(left_bracket_literal())
|> concat(unwrap_and_tag(integer_literal(), :first))
|> concat(ignore(string(@range_symbol)))
|> concat(unwrap_and_tag(integer_literal(), :last))
|> concat(right_bracket_literal())
|> post_traverse({__MODULE__, :dice_range_constructor, []})

dice_set_descriptor =
ignore(left_brace_literal())
|> concat(unwrap_and_tag(integer_literal(), :number))
|> repeat(concat(ignore(comma_literal()), unwrap_and_tag(integer_literal(), :number)))
|> concat(ignore(right_brace_literal()))
|> post_traverse({__MODULE__, :dice_set_constructor, []})

numeric_dice_descriptor = [
dice_value_descriptor,
dice_range_descriptor,
dice_set_descriptor
]

concat(ignore(string(@symbol)), unwrap_and_tag(choice(numeric_dice_descriptor), :faces))
ignore(string(@symbol))
|> concat(unwrap_and_tag(parsec({Dice.Die.Sides.Parser, :combinator}), :sides))
|> post_traverse({__MODULE__, :from_parse, []})
end

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

die = %__MODULE__{faces: faces}
die = %__MODULE__{sides: sides}

{unparsed, [die], context}
end

def dice_range_constructor(unparsed, parsed, context, _line, _offset) do
first = Keyword.fetch!(parsed, :first)
last = Keyword.fetch!(parsed, :last)

{unparsed, [Range.new(first, last)], context}
end

def dice_set_constructor(unparsed, parsed, context, _line, _offset) do
numbers = Keyword.get_values(parsed, :number) |> :lists.reverse()

{unparsed, [numbers], context}
end

defimpl Dice.Expression.Evaluate do
def evaluate(%Dice.Die{} = die) do
case die.faces do
number when is_integer(number) and number > 0 -> :rand.uniform(number)
%Range{} = range -> Enum.random(range)
faces when is_list(faces) -> Enum.random(faces)
end
Dice.Die.Sides.random(die.sides)
end
end

defimpl String.Chars do
def to_string(%Dice.Die{} = die) do
Dice.Die.symbol() <> case die.faces do
number when is_integer(number) -> Integer.to_string(number)
%Range{} = range -> Enum.join([Integer.to_string(range.first), Dice.Die.range_symbol(), Integer.to_string(range.last)])
faces when is_list(faces) -> Enum.join(faces |> Enum.map(&Integer.to_string/1), ", ")
end
Dice.Die.symbol() <> Kernel.to_string(die.sides)
end
end
end
16 changes: 16 additions & 0 deletions lib/dice/die/sides.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defprotocol Dice.Die.Sides do
def random(faces)
def min(faces)
def max(faces)
def count(faces)

import Dice.Parser.Builder

defparser do
choice([
parsec({Dice.Die.Sides.Faces.Parser, :combinator}),
parsec({Dice.Die.Sides.Range.Parser, :combinator}),
parsec({Dice.Die.Sides.List.Parser, :combinator})
])
end
end
49 changes: 49 additions & 0 deletions lib/dice/die/sides/list.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Dice.Die.Sides.List do
defstruct [:options]

import Dice.Parser.Builder

defparser do
ignore(left_brace_literal())
|> concat(unwrap_and_tag(integer_literal(), :number))
|> repeat(concat(ignore(comma_literal()), unwrap_and_tag(integer_literal(), :number)))
|> concat(ignore(right_brace_literal()))
|> post_traverse({__MODULE__, :from_parse, []})
end

def from_parse(unparsed, parsed, context, _line, _offset) do
options = Keyword.get_values(parsed, :number) |> :lists.reverse()

list = __MODULE__.new(options)

{unparsed, [list], context}
end

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

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

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

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

def count(%Dice.Die.Sides.List{} = list) do
list.options |> length
end
end

defimpl String.Chars do
def to_string(%Dice.Die.Sides.List{} = list) do
Enum.join(list.options |> Enum.map(&Integer.to_string/1), ", ")
end
end
end
63 changes: 63 additions & 0 deletions lib/dice/die/sides/range.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
defmodule Dice.Die.Sides.Range do
defstruct [:first, :last]

@symbol ".."
@compile {:inline, symbol: 0}
def symbol, do: @symbol

import Dice.Parser.Builder

defparser do
ignore(left_bracket_literal())
|> concat(unwrap_and_tag(integer_literal(), :first))
|> concat(ignore(string(@symbol)))
|> concat(unwrap_and_tag(integer_literal(), :last))
|> concat(right_bracket_literal())
|> post_traverse({__MODULE__, :from_parse, []})
end

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

range = __MODULE__.new(first, last)

{unparsed, [range], context}
end

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 Dice.Die.Sides do
def random(%Dice.Die.Sides.Range{} = range) do
Range.new(range.first, range.last) |> Enum.random()
end

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

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

def count(%Dice.Die.Sides.Range{} = range) do
range.first - range.last + 1
end
end

defimpl String.Chars do
def to_string(%Dice.Die.Sides.Range{} = range) do
Enum.join([
Integer.to_string(range.first),
Dice.Die.Sides.Range.symbol(),
Integer.to_string(range.last)
])
end
end
end
46 changes: 46 additions & 0 deletions lib/dice/die/sides/sides.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Dice.Die.Sides.Faces do
defstruct [:number]

import Dice.Parser.Builder

defparser do
unwrap_and_tag(non_negative_integer_literal(), :number)
|> post_traverse({__MODULE__, :from_parse, []})
end

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

sides = __MODULE__.new(number)

{unparsed, [sides], context}
end

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

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

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

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

def count(%Dice.Die.Sides.Faces{} = sides) do
sides.number
end
end

defimpl String.Chars do
def to_string(%Dice.Die.Sides.Faces{} = sides) do
Integer.to_string(sides.number)
end
end
end
5 changes: 2 additions & 3 deletions lib/dice/expression.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ defmodule Dice.Expression do
import Dice.Parser.Builder

defparser do

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

optional(whitespace_literal())
Expand Down
11 changes: 0 additions & 11 deletions lib/dice/expression/operation.ex

This file was deleted.

3 changes: 1 addition & 2 deletions lib/dice/expression/term.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ defmodule Dice.Expression.Term do
defparser do
choice([
parsec({Dice.Pool.Parser, :combinator}),
parsec({Dice.Constant.Parser, :combinator}),
parsec({Dice.Constant.Parser, :combinator})
])
end

end
10 changes: 10 additions & 0 deletions lib/dice/operators.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Dice.Operators do
import Dice.Parser.Builder

defparser do
choice([
parsec({Dice.Operators.Addition.Parser, :combinator}),
parsec({Dice.Operators.Subtraction.Parser, :combinator})
])
end
end
22 changes: 13 additions & 9 deletions lib/dice/operators/addition.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Dice.Operator.Addition do
defmodule Dice.Operators.Addition do
defstruct [:left, :right]

@symbol "+"
Expand Down Expand Up @@ -29,18 +29,22 @@ defmodule Dice.Operator.Addition do
end

defimpl Dice.Expression.Evaluate do
def evaluate(%Dice.Operator.Addition{} = addition) do
Dice.Expression.Evaluate.evaluate(addition.left) + Dice.Expression.Evaluate.evaluate(addition.right)
def evaluate(%Dice.Operators.Addition{} = addition) do
Dice.Expression.Evaluate.evaluate(addition.left) +
Dice.Expression.Evaluate.evaluate(addition.right)
end
end

defimpl String.Chars do
def to_string(%Dice.Operator.Addition{} = addition) do
Enum.join([
Kernel.to_string(addition.left),
Dice.Operator.Addition.symbol(),
Kernel.to_string(addition.right)
], " ")
def to_string(%Dice.Operators.Addition{} = addition) do
Enum.join(
[
Kernel.to_string(addition.left),
Dice.Operators.Addition.symbol(),
Kernel.to_string(addition.right)
],
" "
)
end
end
end
Loading

0 comments on commit 7e6aa89

Please sign in to comment.