Skip to content

Commit

Permalink
Merge pull request #2 from dalemyers/percentile
Browse files Browse the repository at this point in the history
Add support for percentile dice (Nd%)
  • Loading branch information
zhudotexe committed Feb 27, 2021
2 parents a833fba + cad0d5b commit 445589a
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 23 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ This is the grammar supported by the dice parser, roughly ordered in how tightly
### Numbers
These are the atoms used at the base of the syntax tree.

| Name | Syntax | Description | Examples |
|---------|-------------------|-------------------|--------------------|
| literal | `INT`, `DECIMAL` | A literal number. | `1`, `0.5`, `3.14` |
| dice | `INT? "d" INT` | A set of die. | `d20`, `3d6` |
| set | `"(" (num ("," num)* ","?)? ")"` | A set of expressions. | `()`, `(2,)`, `(1, 3+3, 1d20)` |
| Name | Syntax | Description | Examples |
|---------|-----------------------------------|-----------------------|--------------------------------|
| literal | `INT`, `DECIMAL` | A literal number. | `1`, `0.5`, `3.14` |
| dice | `INT? "d" (INT \| "%")` | A set of die. | `d20`, `3d6` |
| set | `"(" (num ("," num)* ","?)? ")"` | A set of expressions. | `()`, `(2,)`, `(1, 3+3, 1d20)` |

Note that `(3d6)` is equivalent to `3d6`, but `(3d6,)` is the set containing the one element `3d6`.

Expand Down
18 changes: 14 additions & 4 deletions d20/diceast.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,16 @@ def dice_op(self, opsel):
return SetOperator.new(*opsel)

def diceexpr(self, dice):
if len(dice) == 1:
return Dice(1, *dice)
return Dice(*dice)
reduced_dice = []
for token in dice:
if isinstance(token, Token):
reduced_dice.append(token)
else:
reduced_dice.append(token.children[0])

if len(reduced_dice) == 1:
return Dice(1, *reduced_dice)
return Dice(*reduced_dice)

def selector(self, sel):
return SetSelector(*sel)
Expand Down Expand Up @@ -420,7 +427,10 @@ def __init__(self, num, size):
"""
super().__init__()
self.num = int(num)
self.size = int(size)
if size.value == "%":
self.size = size.value
else:
self.size = int(size)

@property
def children(self):
Expand Down
9 changes: 6 additions & 3 deletions d20/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ class Dice(Set):
def __init__(self, num, size, values, operations=None, context=None, **kwargs):
"""
:type num: int
:type size: int
:type size: int|str
:type values: list of Die
:type operations: list[SetOperator]
:type context: dice.RollContext
Expand Down Expand Up @@ -390,11 +390,14 @@ def children(self):
return []

def _add_roll(self):
if self.size < 1:
if self.size != '%' and self.size < 1:
raise errors.RollValueError("Cannot roll a 0-sided die.")
if self._context:
self._context.count_roll()
n = Literal(random.randrange(self.size) + 1) # 200ns faster than randint(1, self._size)
if self.size == '%':
n = Literal(random.randrange(0, 100, 10))
else:
n = Literal(random.randrange(self.size) + 1) # 200ns faster than randint(1, self._size)
self.values.append(n)

def reroll(self):
Expand Down
4 changes: 3 additions & 1 deletion d20/grammar.lark
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ comma: ","
dice_op: (DICE_OPERATOR | SET_OPERATOR) selector
DICE_OPERATOR: "rr" | "ro" | "ra" | "e" | "mi" | "ma"

diceexpr: INTEGER? "d" INTEGER
diceexpr: INTEGER? "d" DICE_VALUE

DICE_VALUE: INTEGER | "%"

selector: [SELTYPE] INTEGER

Expand Down
38 changes: 28 additions & 10 deletions tests/test_dice.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from d20 import *

STANDARD_EXPRESSIONS = [
'1d20', '1+1', '4d6kh3', '(1)', '(1,)', '((1d6))', '4*(3d8kh2+9[fire]+(9d2e2+3[cold])/2)',
'1d20', '1d%', '1+1', '4d6kh3', '(1)', '(1,)', '((1d6))', '4*(3d8kh2+9[fire]+(9d2e2+3[cold])/2)',
'(1d4, 2+2, 3d6kl1)kh1', '((10d6kh5)kl2)kh1'
]

Expand All @@ -29,11 +29,14 @@ def test_roll_types():


def test_sane_totals():
assert 1 <= r("1d20") <= 20
assert 3 <= r("4d6kh3") <= 18
assert 1 <= r("(((1d6)))") <= 6
assert 4 <= r("(1d4, 2+2, 3d6kl1)kh1") <= 6
assert 1 <= r("((10d6kh5)kl2)kh1") <= 6
for _ in range(1000):
assert 1 <= r("1d20") <= 20
assert 0 <= r("1d%") <= 90
assert 0 <= r("1d%") % 10 <= 9
assert 3 <= r("4d6kh3") <= 18
assert 1 <= r("(((1d6)))") <= 6
assert 4 <= r("(1d4, 2+2, 3d6kl1)kh1") <= 6
assert 1 <= r("((10d6kh5)kl2)kh1") <= 6


def test_pemdas():
Expand Down Expand Up @@ -84,10 +87,15 @@ def test_literal():


def test_dice():
assert r("0d6") == 0
assert 1 <= r("d6") <= 6
assert 1 <= r("1d6") <= 6
assert 2 <= r("2d6") <= 12
for _ in range(1000):
assert r("0d6") == 0
assert 1 <= r("d6") <= 6
assert 1 <= r("1d6") <= 6
assert 2 <= r("2d6") <= 12
assert r("0d%") == 0
assert 0 <= r("d%") <= 90
assert 0 <= r("1d%") <= 90
assert 0 <= r("2d%") <= 180


def test_set():
Expand All @@ -112,6 +120,16 @@ def test_binop():
assert r("13 % 2") == 1


def test_binop_dice():
for _ in range(1000):
assert 3 <= r("2 + 1d6") <= 8
assert 2 <= r("2 * 1d6") <= 12
assert r("60 / 1d6") in [60, 30, 20, 15, 12, 10]
assert r("60 // 1d6") in [60, 30, 20, 15, 12, 10]
assert r("1d100 % 10") <= 10
assert r("1d% % 10") <= 10


def test_div_zero():
with pytest.raises(RollValueError):
r("10 / 0")
Expand Down

0 comments on commit 445589a

Please sign in to comment.