Skip to content

Commit f52761a

Browse files
committed
Seperated AST creation and evaluation
To many breaking or major changes for a full changelog! Plenty of cleanup is still needed.
1 parent 36a8b5a commit f52761a

File tree

8 files changed

+171
-126
lines changed

8 files changed

+171
-126
lines changed

dice/__init__.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,43 @@
22

33
from __future__ import absolute_import, print_function, unicode_literals
44

5-
import sys
65
import argparse
76

87
import dice.grammar
98
import dice.utilities
109

11-
__all__ = ['roll', 'main', 'Dice', 'Roll', 'Bag', 'ParseException']
10+
__all__ = ['parse', 'roll', 'main', 'ParseException']
1211
__author__ = "Sam Clements <[email protected]>"
1312
__version__ = "0.2.0"
1413

1514
from pyparsing import ParseException
16-
from dice.elements import Dice, Roll, Bag
1715

1816
parser = argparse.ArgumentParser()
1917
parser.add_argument(
2018
'-v', '--version', action='version',
2119
version='dice v{0} by {1}'.format(__version__, __author__))
22-
parser.add_argument(
23-
'-s', '--single', action='store_true', dest='single',
24-
help="if a single element is returned, extract it from the list")
2520
parser.add_argument(
2621
'expression',
2722
help="the expression to parse and roll")
2823

29-
def roll(string, single=False):
30-
result = dice.grammar.notation.parseString(string, parseAll=True)
31-
return dice.utilities.single(result) if single else result
24+
def _(obj):
25+
print(obj)
26+
return obj
27+
28+
def parse(string, grammar):
29+
"""Returns an AST parsed from an expression"""
30+
return _(grammar.parseString(string, parseAll=True))
31+
32+
def evaluate(string, grammar):
33+
"""Parse and then evaluate a string with a grammar"""
34+
return _([element.evaluate() for element in parse(string, grammar)])
3235

33-
def main(*argv):
34-
args = parser.parse_args(sys.argv if len(argv) == 0 else argv)
36+
def roll(string):
37+
"""Parses and evaluates an expression"""
38+
return evaluate(string, dice.grammar.expression)
3539

36-
return roll(args.expression, single=args.single)
40+
def main(argv=None):
41+
args = parser.parse_args(argv)
42+
result = roll(args.expression)
43+
print("Result:", dice.utilities.single(result))
44+
return result

dice/elements.py

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,61 @@
11
"""Objects used in the evaluation of the parse tree"""
22

3-
from __future__ import absolute_import, unicode_literals, division
3+
from __future__ import absolute_import, print_function, unicode_literals
44

55
import random
6-
import copy
7-
8-
import six
96

107
from dice.utilities import classname
118

12-
class Integer(int):
9+
10+
class Element(object):
11+
def evaluate(self, verbose=False):
12+
"""Evaluate the current object - a no-op by default"""
13+
return self
14+
15+
def evaluate_object(self, obj, cls=None):
16+
"""Evaluates Elements, and optionally coerces objects to a class"""
17+
if isinstance(obj, Element):
18+
obj = obj.evaluate()
19+
if cls is not None:
20+
obj = cls(obj)
21+
return obj
22+
23+
def print_evaluation(self, result):
24+
"""Prints an explanation of an evaluation"""
25+
print("Evaluating:", str(self), "->", str(result))
26+
27+
28+
class Integer(int, Element):
1329
"""A wrapper around the int class"""
1430

1531
@classmethod
1632
def parse(cls, string, location, tokens):
1733
return cls(tokens[0])
1834

19-
class Roll(list):
35+
36+
class Roll(list, Element):
2037
"""A result from rolling a group of dice"""
2138

2239
@staticmethod
2340
def roll(amount, sides):
2441
return [random.randint(1, sides) for i in range(amount)]
2542

26-
def __init__(self, dice):
27-
super(Roll, self).__init__(self.roll(dice.amount, dice.sides))
28-
self.sides = dice.sides
43+
def __init__(self, amount, sides):
44+
super(Roll, self).__init__(self.roll(amount, sides))
45+
self.sides = sides
2946

3047
def __repr__(self):
31-
return "{0}({1}, sides={0})".format(
48+
return "{0}({1}, sides={2})".format(
3249
classname(self), str(self), self.sides)
3350

3451
def __str__(self):
35-
return ', '.join(self)
52+
return ', '.join(map(str, self))
3653

3754
def __int__(self):
3855
return sum(self)
3956

40-
class Dice(object):
57+
58+
class Dice(Element):
4159
"""A group of dice, all with the same number of sides"""
4260

4361
@classmethod
@@ -58,43 +76,27 @@ def from_string(cls, string):
5876
return cls(int(amount), int(sides))
5977

6078
def __init__(self, amount, sides):
61-
self.amount, self.sides = int(amount), int(sides)
79+
self.amount = amount
80+
self.sides = sides
81+
self.result = None
6282

6383
def __repr__(self):
64-
return "Dice('{0}d{1}')".format(self.amount, self.sides)
84+
return "Dice({0!r}, {1!r})".format(self.amount, self.sides)
6585

6686
def __str__(self):
67-
return "{0}d{1}".format(self.amount, self.sides)
68-
69-
def __int__(self):
70-
# TODO: Remove this when dice are evaluated
71-
return int(self.roll())
72-
73-
def roll(self, cls=Roll):
74-
return cls(self)
87+
return "{0!s}d{1!s}".format(self.amount, self.sides)
7588

76-
class Bag(list):
77-
"""A collection of dice objects"""
89+
def evaluate(self, verbose=False):
90+
self.amount = self.evaluate_object(self.amount, Integer)
91+
self.sides = self.evaluate_object(self.sides, Integer)
7892

79-
@staticmethod
80-
def dice_from_object(obj, cls=Dice):
81-
if isinstance(obj, cls):
82-
return copy.copy(obj)
83-
elif isinstance(obj, six.string_types):
84-
return cls.from_string(obj)
85-
elif isinstance(obj, (tuple, list)):
86-
return cls.from_iterable(obj)
87-
raise TypeError("Dice object cannot be created from " + repr(obj))
88-
89-
def __init__(self, *dice_list):
90-
for d in dice_list:
91-
self.append(self.dice_from_object(d))
93+
if self.result is None:
94+
self.result = Roll(self.amount, self.sides)
9295

93-
def __repr__(self):
94-
return "Bag({0})".format(','.join(map(repr, self)))
96+
if verbose:
97+
self.print_evaluation(self.result)
9598

96-
def __str__(self):
97-
return ', '.join(map(str, self))
99+
return self.result
98100

99101
def roll(self):
100-
return [d.roll() for d in self]
102+
return self.evaluate(verbose=False)

dice/grammar.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,13 @@
66
module for more information.
77
"""
88

9-
from __future__ import absolute_import, unicode_literals, division
10-
from __future__ import print_function
11-
12-
from functools import wraps
13-
from operator import add, sub, mul, floordiv as div
9+
from __future__ import absolute_import, print_function, unicode_literals
1410

1511
from pyparsing import (Forward, Literal, OneOrMore, StringStart, StringEnd,
16-
Suppress, Word, delimitedList, nums, opAssoc)
12+
Suppress, Word, nums, opAssoc)
1713

1814
from dice.elements import Integer, Dice
15+
from dice.operators import Mul, Div, Sub, Add
1916
from dice.utilities import patch_pyparsing
2017

2118
# Set PyParsing options
@@ -78,27 +75,19 @@ def parse_operator(expr, arity, association, action=None):
7875
expression <<= last
7976
return expression
8077

81-
8278
# An integer value
83-
integer = Word(nums).setParseAction(Integer.parse).setName("integer")
84-
85-
def integer_operation(func):
86-
@wraps(func)
87-
def operate(string, location, tokens):
88-
return func(*map(int, tokens))
89-
return operate
79+
integer = Word(nums)
80+
integer.setParseAction(Integer.parse)
81+
integer.setName("integer")
9082

9183
# An expression in dice notation
92-
expression = operatorPrecedence(integer, [
84+
expression = StringStart() + operatorPrecedence(integer, [
9385
(Literal('d').suppress(), 2, opAssoc.LEFT, Dice.parse_binary),
9486
(Literal('d').suppress(), 1, opAssoc.RIGHT, Dice.parse_unary),
9587

96-
(Literal('/').suppress(), 2, opAssoc.LEFT, integer_operation(div)),
97-
(Literal('*').suppress(), 2, opAssoc.LEFT, integer_operation(mul)),
98-
(Literal('-').suppress(), 2, opAssoc.LEFT, integer_operation(sub)),
99-
(Literal('+').suppress(), 2, opAssoc.LEFT, integer_operation(add)),
100-
]).setName("expression")
101-
102-
# Multiple expressions can be separated with delimiters
103-
notation = StringStart() + delimitedList(expression, ';') + StringEnd()
104-
notation.setName("notation")
88+
(Literal('/').suppress(), 2, opAssoc.LEFT, Div.parse),
89+
(Literal('*').suppress(), 2, opAssoc.LEFT, Mul.parse),
90+
(Literal('-').suppress(), 2, opAssoc.LEFT, Sub.parse),
91+
(Literal('+').suppress(), 2, opAssoc.LEFT, Add.parse),
92+
]) + StringEnd()
93+
expression.setName("expression")

dice/operators.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Operator elements"""
2+
3+
from __future__ import absolute_import, print_function, unicode_literals
4+
5+
import operator
6+
7+
from dice.elements import Element
8+
from dice.utilities import classname
9+
10+
class Operator(Element):
11+
@classmethod
12+
def parse(cls, string, location, tokens):
13+
return cls(operands=tokens)
14+
15+
def __init__(self, operands):
16+
self.operands = operands
17+
18+
def __repr__(self):
19+
return "{0}({1})".format(
20+
classname(self),
21+
', '.join(map(repr, self.operands)))
22+
23+
def evaluate(self):
24+
raise NotImplementedError(
25+
"Operator subclass has no evaluate()")
26+
27+
def evaluate_operands(self):
28+
self.operands = map(self.evaluate_object, self.operands)
29+
return self.operands
30+
31+
32+
class FunctionOperator(Operator):
33+
@property
34+
def function(self):
35+
raise NotImplementedError(
36+
"FunctionOperator subclass has no function")
37+
38+
def evaluate(self):
39+
return self.function(*self.evaluate_operands())
40+
41+
42+
class Div(FunctionOperator):
43+
function = operator.floordiv
44+
45+
46+
class Mul(FunctionOperator):
47+
function = operator.mul
48+
49+
50+
class Sub(FunctionOperator):
51+
function = operator.sub
52+
53+
54+
class Add(FunctionOperator):
55+
function = operator.add

dice/tests/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from __future__ import absolute_import, print_function
44

5+
from dice import evaluate
6+
57
def single(iterable):
68
return iterable[0] if len(iterable) == 1 else list(iterable)
79

810
def parse(grammar, string):
911
"""Parses a string with a grammar, returning and printing the result"""
10-
return single(grammar.parseString(string, parseAll=True))
12+
return single(evaluate(string, grammar))

dice/tests/test_elements.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
from __future__ import absolute_import
22

3-
import re
4-
53
import py.test
64

7-
from dice.elements import Integer, Roll, Dice, Bag
5+
from dice.elements import Integer, Roll, Dice
86

97
@py.test.fixture
108
def dice_constructors():
119
return Dice(2, 6), "2d6", (2, 6)
1210

13-
@py.test.fixture
14-
def bag():
15-
return Bag("1d2", "2d4", "4d6", "6d8", "8d10")
16-
1711
def test_integer():
1812
assert isinstance(Integer(1), int)
1913

@@ -28,17 +22,21 @@ def test_dice_from_string():
2822
d = Dice.from_string("2d6")
2923
assert d.amount == 2 and d.sides == 6
3024

31-
def test_dice_from_object(dice_constructors):
32-
for obj in dice_constructors:
33-
d = Bag.dice_from_object(obj)
34-
assert d.amount == 2 and d.sides == 6
35-
36-
def test_dice_from_object_exception():
37-
with py.test.raises(TypeError):
38-
Bag.dice_from_object(NotImplemented)
39-
40-
def test_bag_length(dice_constructors):
41-
assert len(Bag(*dice_constructors)) == len(dice_constructors)
42-
43-
def test_bag_str(bag):
44-
assert re.match("[d,\d]*", str(bag))
25+
#@py.test.fixture
26+
#def bag():
27+
# return Bag("1d2", "2d4", "4d6", "6d8", "8d10")
28+
#
29+
#def test_dice_from_object(dice_constructors):
30+
# for obj in dice_constructors:
31+
# d = Bag.dice_from_object(obj)
32+
# assert d.amount == 2 and d.sides == 6
33+
#
34+
#def test_dice_from_object_exception():
35+
# with py.test.raises(TypeError):
36+
# Bag.dice_from_object(NotImplemented)
37+
#
38+
#def test_bag_length(dice_constructors):
39+
# assert len(Bag(*dice_constructors)) == len(dice_constructors)
40+
#
41+
#def test_bag_str(bag):
42+
# assert re.match("[d,\d]*", str(bag))

0 commit comments

Comments
 (0)