Skip to content

Commit

Permalink
Metaprogram parser modules.
Browse files Browse the repository at this point in the history
  • Loading branch information
christhekeele committed Mar 3, 2023
1 parent e56e47c commit 154d056
Show file tree
Hide file tree
Showing 24 changed files with 446 additions and 426 deletions.
2 changes: 1 addition & 1 deletion .iex.exs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import Dice.Expression
import Dice
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Dice

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

### Basics

Expand Down Expand Up @@ -65,6 +65,26 @@ By default, random dice are dropped. This can be further modified to:
| ***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. | |

#### Count

***Count*** (`C`) lets you change a dice pools output from being a sum of dice rolls, to the count of rolled dice that meet a certain criteria.

By default, all dice are counted. This can be further modified to:

- Count rolls higher than a number (***Count Greater Than***: `C>`)
- Count rolls lower than a number (***Count Lower Than***: `C<`)
- critical success, failure, explosions?

| 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. | |

#### Explode

***Explode*** (`!`) lets you roll extra dice into a pool before adding them together, based on previous rolls.
Expand Down
71 changes: 4 additions & 67 deletions lib/dice.ex
Original file line number Diff line number Diff line change
@@ -1,72 +1,9 @@
defmodule Dice do
defstruct [:faces]

def roll(%__MODULE__{} = dice) do
case dice.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
end

def to_string(%__MODULE__{} = dice) do
case dice.faces do
number when is_integer(number) -> "d#{Integer.to_string(number)}"
%Range{} = range -> "d[#{range.first}..#{range.last}]"
faces when is_list(faces) -> "d{#{Enum.join(faces, ", ")}}"
end
def expression(string) when is_binary(string) do
string |> Dice.Expression.new()
end

def combinator do
import NimbleParsec
import Dice.Expression.Literals

dice_value_descriptor = non_negative_integer_literal()

dice_range_descriptor =
ignore(left_bracket_literal())
|> concat(unwrap_and_tag(integer_literal(), :first))
|> concat(ignore(range_literal()))
|> 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(dice_literal()), unwrap_and_tag(choice(numeric_dice_descriptor), :faces))
|> post_traverse({__MODULE__, :combinator_constructor, []})
def roll(string) when is_binary(string) do
string |> expression |> Dice.Expression.evaluate()
end

def combinator_constructor(rest, args, context, _line, _offset) do
faces = Keyword.fetch!(args, :faces)

numeric_dice = %__MODULE__{faces: faces}

{rest, [numeric_dice], context}
end

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

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

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

{rest, [numbers], context}
end

end
29 changes: 29 additions & 0 deletions lib/dice/constant.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule Dice.Constant do
defstruct [:value]

import Dice.Parser.Builder

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

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

constant = %__MODULE__{
value: value
}

{unparsed, [constant], context}
end

def evaluate(%__MODULE__{} = constant) do
constant.value
end

def to_string(%__MODULE__{} = constant) do
constant.value |> Integer.to_string()
end
end
70 changes: 70 additions & 0 deletions lib/dice/die.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
defmodule Dice.Die do
defstruct [:faces]

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(range_literal()))
|> 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(dice_literal()), unwrap_and_tag(choice(numeric_dice_descriptor), :faces))
|> post_traverse({__MODULE__, :from_parse, []})
end

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

die = %__MODULE__{faces: faces}

{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

def roll(%__MODULE__{} = dice) do
case dice.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
end

def to_string(%__MODULE__{} = dice) do
case dice.faces do
number when is_integer(number) -> "d#{Integer.to_string(number)}"
%Range{} = range -> "d[#{range.first}..#{range.last}]"
faces when is_list(faces) -> "d{#{Enum.join(faces, ", ")}}"
end
end
end
Empty file added lib/dice/die/roll.ex
Empty file.
101 changes: 46 additions & 55 deletions lib/dice/expression.ex
Original file line number Diff line number Diff line change
@@ -1,82 +1,73 @@
defmodule Dice.Expression do
defstruct [terms: []]
defstruct terms: []

def new(expression) when is_binary(expression) do
expression |> parse
import Dice.Parser.Builder

defparser do
optional(whitespace_literal())
|> concat(unwrap_and_tag(Dice.Term.Parser.maybe_signed_term_combinator(), :term))
|> concat(
repeat(
concat(
optional(whitespace_literal()),
unwrap_and_tag(Dice.Term.Parser.signed_term_combinator(), :term)
)
)
)
|> optional(whitespace_literal())
|> post_traverse({__MODULE__, :from_parse, []})
end

def roll(expression) when is_binary(expression) do
expression |> new |> evaluate
def from_parse(unparsed, parsed, context, _line, _offset) do
terms = :lists.reverse(Keyword.get_values(parsed, :term))

expression = %__MODULE__{
terms: terms
}

{unparsed, [expression], context}
end

def generate do
{__MODULE__.Parser, :parse}
|> NimbleParsec.parsec
|> NimbleParsec.generate
|> parse
def new(expression) when is_binary(expression) do
expression |> parse
end

def evaluate(%__MODULE__{} = expression) do
expression.terms
|> Enum.map(&Dice.Expression.Term.evaluate(&1))
|> Enum.sum
|> Enum.map(&Dice.Term.evaluate(&1))
|> Enum.sum()
end

def to_string(%__MODULE__{} = expression) do
expression.terms
|> Enum.map(&Dice.Expression.Term.to_string(&1))
|> Enum.join
|> Enum.map(&Dice.Term.to_string(&1))
|> Enum.join()
|> String.trim_leading("+ ")
|> String.replace_leading("- ", "-")
end

def parse(input) when is_binary(input) do
{:ok, [expression: expression], _unparsed, _context, _line, _offset} = __MODULE__.Parser.parse(input)

expression
end

def combinator do
import NimbleParsec
import Dice.Expression.Literals

optional(whitespace_literal())
|> concat(unwrap_and_tag(Dice.Expression.Term.maybe_signed_term_combinator(), :term))
|> concat(repeat(concat(optional(whitespace_literal()), unwrap_and_tag(Dice.Expression.Term.signed_term_combinator(), :term))))
|> optional(whitespace_literal())
|> post_traverse({__MODULE__, :combinator_constructor, []})
end

def combinator_constructor(rest, args, context, _line, _offset) do
terms = :lists.reverse(Keyword.get_values(args, :term))

expression = %Dice.Expression{
terms: terms
}

{rest, [expression], context}
end

defimpl Inspect do
import Inspect.Algebra

def inspect(expression, _opts) do
[first_term | rest] = expression.terms

concat(List.flatten([
"Dice.Expression.new(\"",
break(""),
first_term
|> Dice.Expression.Term.to_string
|> String.trim_leading("+ ")
|> String.replace_leading("- ", "-"),
break(),
rest
|> Enum.map(&Dice.Expression.Term.to_string(&1))
|> Enum.intersperse(break()),
break(""),
"\")"
]))
concat(
List.flatten([
"#{inspect(__MODULE__)}.new(\"",
break(""),
first_term
|> Dice.Term.to_string()
|> String.trim_leading("+ ")
|> String.replace_leading("- ", "-"),
break(),
rest
|> Enum.map(&Dice.Term.to_string(&1))
|> Enum.intersperse(break()),
break(""),
"\")"
])
)
end
end
end
29 changes: 0 additions & 29 deletions lib/dice/expression/constant.ex

This file was deleted.

7 changes: 0 additions & 7 deletions lib/dice/expression/parser.ex

This file was deleted.

Loading

0 comments on commit 154d056

Please sign in to comment.