|
@@ -8,7 +8,10 @@ import readline
|
|
import operator
|
|
import operator
|
|
from numbers import Number
|
|
from numbers import Number
|
|
from random import SystemRandom
|
|
from random import SystemRandom
|
|
-from pyparsing import Regex, oneOf, Optional, Group, Combine, Literal, CaselessLiteral, ZeroOrMore, StringStart, StringEnd, opAssoc, infixNotation, ParseException, Empty, pyparsing_common, ParseResults, White, Suppress
|
|
|
|
|
|
+from pyparsing import ParserElement, Token, Regex, oneOf, Optional, Group, Combine, Literal, CaselessLiteral, ZeroOrMore, StringStart, StringEnd, opAssoc, infixNotation, ParseException, Empty, pyparsing_common, ParseResults, White, Suppress
|
|
|
|
+
|
|
|
|
+from typing import Union, List, Any, Tuple, Sequence, Dict, Callable, Set, TextIO
|
|
|
|
+from typing import Optional as OptionalType
|
|
|
|
|
|
try:
|
|
try:
|
|
import colorama
|
|
import colorama
|
|
@@ -16,7 +19,7 @@ try:
|
|
from colors import color
|
|
from colors import color
|
|
except ImportError:
|
|
except ImportError:
|
|
# Fall back to no color
|
|
# Fall back to no color
|
|
- def color(s, *args, **kwargs):
|
|
|
|
|
|
+ def color(s: str, *args, **kwargs):
|
|
'''Fake color function that does nothing.
|
|
'''Fake color function that does nothing.
|
|
|
|
|
|
Used when the colors module cannot be imported.'''
|
|
Used when the colors module cannot be imported.'''
|
|
@@ -37,56 +40,75 @@ for handler in logger.handlers:
|
|
sysrand = SystemRandom()
|
|
sysrand = SystemRandom()
|
|
randint = sysrand.randint
|
|
randint = sysrand.randint
|
|
|
|
|
|
|
|
+def int_limit_converter(x: Any) -> OptionalType[int]:
|
|
|
|
+ if x is None:
|
|
|
|
+ return None
|
|
|
|
+ else:
|
|
|
|
+ return int(x)
|
|
|
|
+
|
|
@attr.s
|
|
@attr.s
|
|
class IntegerValidator(object):
|
|
class IntegerValidator(object):
|
|
- min_val = attr.ib(default='-inf', convert=float)
|
|
|
|
- max_val = attr.ib(default='+inf', convert=float)
|
|
|
|
- handle_float = attr.ib(default='exception')
|
|
|
|
|
|
+ min_val: OptionalType[int] = attr.ib(default = None, converter = int_limit_converter)
|
|
|
|
+ max_val: OptionalType[int] = attr.ib(default = None, converter = int_limit_converter)
|
|
|
|
+ handle_float: str = attr.ib(default = 'exception')
|
|
@handle_float.validator
|
|
@handle_float.validator
|
|
def validate_handle_float(self, attribute, value):
|
|
def validate_handle_float(self, attribute, value):
|
|
assert value in ('exception', 'truncate', 'round')
|
|
assert value in ('exception', 'truncate', 'round')
|
|
|
|
+ value_name: str = attr.ib(default = "value")
|
|
|
|
|
|
- def __call__(self, value):
|
|
|
|
|
|
+ def __call__(self, value: Any) -> int:
|
|
|
|
+ xf: float
|
|
|
|
+ x: int
|
|
try:
|
|
try:
|
|
xf = float(value)
|
|
xf = float(value)
|
|
except ValueError:
|
|
except ValueError:
|
|
- raise ValueError('{} does not look like a number'.format(value))
|
|
|
|
|
|
+ raise ValueError('{} {} does not look like a number'.format(self.value_name, value))
|
|
if not xf.is_integer():
|
|
if not xf.is_integer():
|
|
if self.handle_float == 'exception':
|
|
if self.handle_float == 'exception':
|
|
- raise ValueError('{} is not an integer'.format(value))
|
|
|
|
|
|
+ raise ValueError('{} {} is not an integer'.format(self.value_name, value))
|
|
elif self.handle_float == 'truncate':
|
|
elif self.handle_float == 'truncate':
|
|
x = int(xf)
|
|
x = int(xf)
|
|
else:
|
|
else:
|
|
x = round(xf)
|
|
x = round(xf)
|
|
else:
|
|
else:
|
|
x = int(xf)
|
|
x = int(xf)
|
|
- if self.min_val is not None:
|
|
|
|
- assert x >= self.min_val
|
|
|
|
- if self.max_val is not None:
|
|
|
|
- assert x <= self.max_val
|
|
|
|
|
|
+ if self.min_val is not None and x < self.min_val:
|
|
|
|
+ raise ValueError('{} {} is too small; must be at least {}'.format(self.value_name, value, self.min_val))
|
|
|
|
+ if self.max_val is not None and x > self.max_val:
|
|
|
|
+ raise ValueError('{} {} is too large; must be at most {}'.format(self.value_name, value, self.max_val))
|
|
return x
|
|
return x
|
|
|
|
|
|
-def normalize_die_type(x):
|
|
|
|
- if x in ('F', 'F.1', 'F.2'):
|
|
|
|
- return x
|
|
|
|
|
|
+die_face_num_validator = IntegerValidator(
|
|
|
|
+ min_val = 2, handle_float = 'exception',
|
|
|
|
+ value_name = 'die type',
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+DieFaceType = Union[int, str]
|
|
|
|
+def is_fate_face(x: DieFaceType) -> bool:
|
|
|
|
+ if isinstance(x, int):
|
|
|
|
+ return False
|
|
|
|
+ else:
|
|
|
|
+ x = str(x).upper()
|
|
|
|
+ return x in ('F', 'F.1', 'F.2')
|
|
|
|
+
|
|
|
|
+def normalize_die_type(x: DieFaceType) -> DieFaceType:
|
|
|
|
+ if is_fate_face(x):
|
|
|
|
+ return str(x).upper()
|
|
elif x == '%':
|
|
elif x == '%':
|
|
return 100
|
|
return 100
|
|
else:
|
|
else:
|
|
- try:
|
|
|
|
- return IntegerValidator(min_val=2, handle_float='exception')(x)
|
|
|
|
- except Exception:
|
|
|
|
- raise ValueError('Invalid die type: d{}'.format(x))
|
|
|
|
-
|
|
|
|
-def normalize_dice_count(x):
|
|
|
|
- xf = float(x)
|
|
|
|
- x = int(x)
|
|
|
|
- if not xf.is_integer():
|
|
|
|
- raise ValueError('dice count must be an integer, not {}'.format(xf))
|
|
|
|
- if x < 1:
|
|
|
|
- raise ValueError("dice count must be positive; {} is invalid".format(x))
|
|
|
|
- return x
|
|
|
|
-
|
|
|
|
-def ImplicitToken(x):
|
|
|
|
|
|
+ return die_face_num_validator(x)
|
|
|
|
+
|
|
|
|
+dice_count_validator = IntegerValidator(
|
|
|
|
+ min_val = 1, handle_float = 'exception',
|
|
|
|
+ value_name = 'dice count'
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+# Just a named function wrapper for dice_count_validator
|
|
|
|
+def normalize_dice_count(x: Any) -> int:
|
|
|
|
+ return dice_count_validator(x)
|
|
|
|
+
|
|
|
|
+def ImplicitToken(x) -> ParserElement:
|
|
'''Like pyparsing.Empty, but yields one or more tokens instead of nothing.'''
|
|
'''Like pyparsing.Empty, but yields one or more tokens instead of nothing.'''
|
|
return Empty().setParseAction(lambda toks: x)
|
|
return Empty().setParseAction(lambda toks: x)
|
|
|
|
|
|
@@ -97,21 +119,22 @@ def ImplicitToken(x):
|
|
|
|
|
|
# https://stackoverflow.com/a/46583691/125921
|
|
# https://stackoverflow.com/a/46583691/125921
|
|
|
|
|
|
-var_name = pyparsing_common.identifier.copy().setResultsName('varname')
|
|
|
|
-real_num = pyparsing_common.fnumber.copy()
|
|
|
|
-positive_int = pyparsing_common.integer.copy().setParseAction(lambda toks: [ IntegerValidator(min_val=1)(toks[0]) ])
|
|
|
|
|
|
+var_name: ParserElement = pyparsing_common.identifier.copy().setResultsName('varname')
|
|
|
|
+real_num: ParserElement = pyparsing_common.fnumber.copy()
|
|
|
|
+positive_int: ParserElement = pyparsing_common.integer.copy().setParseAction(lambda toks: [ IntegerValidator(min_val=1)(toks[0]) ])
|
|
|
|
|
|
-drop_type = oneOf('K k X x -H -L')
|
|
|
|
-drop_spec = Group(drop_type.setResultsName('type') +
|
|
|
|
- (positive_int | ImplicitToken(1)).setResultsName('count')
|
|
|
|
|
|
+drop_type: ParserElement = oneOf('K k X x -H -L')
|
|
|
|
+drop_spec: ParserElement = Group(
|
|
|
|
+ drop_type.setResultsName('type') +
|
|
|
|
+ (positive_int | ImplicitToken(1)).setResultsName('count')
|
|
).setResultsName('drop')
|
|
).setResultsName('drop')
|
|
|
|
|
|
-pos_int_implicit_one = (positive_int | ImplicitToken(1))
|
|
|
|
|
|
+pos_int_implicit_one: ParserElement = (positive_int | ImplicitToken(1))
|
|
|
|
|
|
-comparator_type = oneOf('<= < >= > ≤ ≥ =')
|
|
|
|
|
|
+comparator_type: ParserElement = oneOf('<= < >= > ≤ ≥ =')
|
|
|
|
|
|
-reroll_type = Combine(oneOf('R r') ^ ( oneOf('! !!') + Optional('p')))
|
|
|
|
-reroll_spec = Group(
|
|
|
|
|
|
+reroll_type: ParserElement = Combine(oneOf('R r') ^ ( oneOf('! !!') + Optional('p')))
|
|
|
|
+reroll_spec: ParserElement = Group(
|
|
reroll_type.setResultsName('type') +
|
|
reroll_type.setResultsName('type') +
|
|
Optional(
|
|
Optional(
|
|
(comparator_type | ImplicitToken('=')).setResultsName('operator') + \
|
|
(comparator_type | ImplicitToken('=')).setResultsName('operator') + \
|
|
@@ -119,7 +142,7 @@ reroll_spec = Group(
|
|
)
|
|
)
|
|
).setResultsName('reroll')
|
|
).setResultsName('reroll')
|
|
|
|
|
|
-count_spec = Group(
|
|
|
|
|
|
+count_spec: ParserElement = Group(
|
|
Group(
|
|
Group(
|
|
comparator_type.setResultsName('operator') + \
|
|
comparator_type.setResultsName('operator') + \
|
|
positive_int.setResultsName('value')
|
|
positive_int.setResultsName('value')
|
|
@@ -133,7 +156,7 @@ count_spec = Group(
|
|
)
|
|
)
|
|
).setResultsName('count_successes')
|
|
).setResultsName('count_successes')
|
|
|
|
|
|
-roll_spec = Group(
|
|
|
|
|
|
+roll_spec: ParserElement = Group(
|
|
(positive_int | ImplicitToken(1)).setResultsName('dice_count') +
|
|
(positive_int | ImplicitToken(1)).setResultsName('dice_count') +
|
|
CaselessLiteral('d') +
|
|
CaselessLiteral('d') +
|
|
(positive_int | oneOf('% F F.1 F.2')).setResultsName('die_type') +
|
|
(positive_int | oneOf('% F F.1 F.2')).setResultsName('die_type') +
|
|
@@ -141,7 +164,7 @@ roll_spec = Group(
|
|
Optional(count_spec)
|
|
Optional(count_spec)
|
|
).setResultsName('roll')
|
|
).setResultsName('roll')
|
|
|
|
|
|
-expr_parser = infixNotation(
|
|
|
|
|
|
+expr_parser: ParserElement = infixNotation(
|
|
baseExpr=(roll_spec ^ positive_int ^ real_num ^ var_name),
|
|
baseExpr=(roll_spec ^ positive_int ^ real_num ^ var_name),
|
|
opList=[
|
|
opList=[
|
|
(oneOf('** ^').setResultsName('operator', True), 2, opAssoc.RIGHT),
|
|
(oneOf('** ^').setResultsName('operator', True), 2, opAssoc.RIGHT),
|
|
@@ -150,9 +173,9 @@ expr_parser = infixNotation(
|
|
]
|
|
]
|
|
).setResultsName('expr')
|
|
).setResultsName('expr')
|
|
|
|
|
|
-assignment_parser = var_name + Literal('=').setResultsName('assignment') + expr_parser
|
|
|
|
|
|
+assignment_parser: ParserElement = var_name + Literal('=').setResultsName('assignment') + expr_parser
|
|
|
|
|
|
-def roll_die(sides=6):
|
|
|
|
|
|
+def roll_die(sides: DieFaceType = 6) -> int:
|
|
'''Roll a single die.
|
|
'''Roll a single die.
|
|
|
|
|
|
Supports any valid integer number of sides as well as 'F' for a fate
|
|
Supports any valid integer number of sides as well as 'F' for a fate
|
|
@@ -182,15 +205,17 @@ class DieRolled(int):
|
|
exploded, or to indicate a critical hit/miss.
|
|
exploded, or to indicate a critical hit/miss.
|
|
|
|
|
|
'''
|
|
'''
|
|
- def __new__(cls, value, formatter='{}'):
|
|
|
|
- newval = super(DieRolled, cls).__new__(cls, value)
|
|
|
|
|
|
+ formatter: str
|
|
|
|
+
|
|
|
|
+ def __new__(cls: type, value: int, formatter: str = '{}') -> 'DieRolled':
|
|
|
|
+ newval = super(DieRolled, cls).__new__(cls, value) # type: ignore
|
|
newval.formatter = formatter
|
|
newval.formatter = formatter
|
|
return newval
|
|
return newval
|
|
|
|
|
|
- def __str__(self):
|
|
|
|
|
|
+ def __str__(self) -> str:
|
|
return self.formatter.format(super().__str__())
|
|
return self.formatter.format(super().__str__())
|
|
|
|
|
|
- def __repr__(self):
|
|
|
|
|
|
+ def __repr__(self) -> str:
|
|
if self.formatter != '{}':
|
|
if self.formatter != '{}':
|
|
return 'DieRolled(value={value!r}, formatter={formatter!r})'.format(
|
|
return 'DieRolled(value={value!r}, formatter={formatter!r})'.format(
|
|
value=int(self),
|
|
value=int(self),
|
|
@@ -199,12 +224,16 @@ class DieRolled(int):
|
|
else:
|
|
else:
|
|
return 'DieRolled({value!r})'.format(value=int(self))
|
|
return 'DieRolled({value!r})'.format(value=int(self))
|
|
|
|
|
|
-def validate_dice_roll_list(instance, attribute, value):
|
|
|
|
|
|
+def normalize_dice_roll_list(value: List[Any]) -> List[int]:
|
|
|
|
+ result = []
|
|
for x in value:
|
|
for x in value:
|
|
- # Not using positive_int here because 0 is a valid roll for
|
|
|
|
- # penetrating dice, and -1 and 0 are valid for fate dice
|
|
|
|
- pyparsing_common.signed_integer.parseString(str(x))
|
|
|
|
-def format_dice_roll_list(rolls, always_list=False):
|
|
|
|
|
|
+ if isinstance(x, int):
|
|
|
|
+ result.append(x)
|
|
|
|
+ else:
|
|
|
|
+ result.append(int(x))
|
|
|
|
+ return result
|
|
|
|
+
|
|
|
|
+def format_dice_roll_list(rolls: List[int], always_list: bool = False) -> str:
|
|
if len(rolls) == 0:
|
|
if len(rolls) == 0:
|
|
raise ValueError('Need at least one die rolled')
|
|
raise ValueError('Need at least one die rolled')
|
|
elif len(rolls) == 1 and not always_list:
|
|
elif len(rolls) == 1 and not always_list:
|
|
@@ -212,30 +241,33 @@ def format_dice_roll_list(rolls, always_list=False):
|
|
else:
|
|
else:
|
|
return '[' + color(" ".join(map(str, rolls)), DETAIL_COLOR) + ']'
|
|
return '[' + color(" ".join(map(str, rolls)), DETAIL_COLOR) + ']'
|
|
|
|
|
|
|
|
+def int_or_none(x: OptionalType[Any]) -> OptionalType[int]:
|
|
|
|
+ if x is None:
|
|
|
|
+ return None
|
|
|
|
+ else:
|
|
|
|
+ return int(x)
|
|
|
|
+
|
|
@attr.s
|
|
@attr.s
|
|
class DiceRolled(object):
|
|
class DiceRolled(object):
|
|
'''Class representing the result of rolling one or more similar dice.'''
|
|
'''Class representing the result of rolling one or more similar dice.'''
|
|
- dice_results = attr.ib(convert=list, validator = validate_dice_roll_list)
|
|
|
|
- dropped_results = attr.ib(default=attr.Factory(list), convert=list,
|
|
|
|
- validator = validate_dice_roll_list)
|
|
|
|
- roll_desc = attr.ib(default='', convert=str)
|
|
|
|
- success_count = attr.ib(default=None)
|
|
|
|
- @success_count.validator
|
|
|
|
- def validate_success_count(self, attribute, value):
|
|
|
|
- if value is not None:
|
|
|
|
- self.success_count = int(value)
|
|
|
|
-
|
|
|
|
- def __attrs_post_init__(self):
|
|
|
|
- if len(self.dice_results) < 1:
|
|
|
|
|
|
+ dice_results: List[int] = attr.ib(converter = normalize_dice_roll_list)
|
|
|
|
+ @dice_results.validator
|
|
|
|
+ def validate_dice_results(self, attribute, value):
|
|
|
|
+ if len(value) == 0:
|
|
raise ValueError('Need at least one non-dropped roll')
|
|
raise ValueError('Need at least one non-dropped roll')
|
|
|
|
+ dropped_results: List[int] = attr.ib(
|
|
|
|
+ default = attr.Factory(list),
|
|
|
|
+ converter = normalize_dice_roll_list)
|
|
|
|
+ roll_desc: str = attr.ib(default = '', converter = str)
|
|
|
|
+ success_count: OptionalType[int] = attr.ib(default = None, converter = int_or_none)
|
|
|
|
|
|
- def total(self):
|
|
|
|
|
|
+ def total(self) -> int:
|
|
if self.success_count is not None:
|
|
if self.success_count is not None:
|
|
return int(self.success_count)
|
|
return int(self.success_count)
|
|
else:
|
|
else:
|
|
return sum(self.dice_results)
|
|
return sum(self.dice_results)
|
|
|
|
|
|
- def __str__(self):
|
|
|
|
|
|
+ def __str__(self) -> str:
|
|
if self.roll_desc:
|
|
if self.roll_desc:
|
|
prefix = '{roll} rolled'.format(roll=color(self.roll_desc, EXPR_COLOR))
|
|
prefix = '{roll} rolled'.format(roll=color(self.roll_desc, EXPR_COLOR))
|
|
else:
|
|
else:
|
|
@@ -257,10 +289,14 @@ class DiceRolled(object):
|
|
tot=tot,
|
|
tot=tot,
|
|
)
|
|
)
|
|
|
|
|
|
- def __int__(self):
|
|
|
|
|
|
+ def __int__(self) -> int:
|
|
return self.total()
|
|
return self.total()
|
|
|
|
|
|
|
|
+ def __float__(self) -> float:
|
|
|
|
+ return float(self.total())
|
|
|
|
+
|
|
def validate_by_parser(parser):
|
|
def validate_by_parser(parser):
|
|
|
|
+ '''Return a validator that validates anything parser can parse.'''
|
|
def private_validator(instance, attribute, value):
|
|
def private_validator(instance, attribute, value):
|
|
parser.parseString(str(value), True)
|
|
parser.parseString(str(value), True)
|
|
return private_validator
|
|
return private_validator
|
|
@@ -276,13 +312,15 @@ class Comparator(object):
|
|
'≥': operator.ge,
|
|
'≥': operator.ge,
|
|
'=': operator.eq,
|
|
'=': operator.eq,
|
|
}
|
|
}
|
|
- operator = attr.ib(convert=str, validator=validate_by_parser(comparator_type))
|
|
|
|
- value = attr.ib(convert=int, validator=validate_by_parser(positive_int))
|
|
|
|
|
|
+ operator: str = attr.ib(converter = str,
|
|
|
|
+ validator = validate_by_parser(comparator_type))
|
|
|
|
+ value: int = attr.ib(converter = int,
|
|
|
|
+ validator = validate_by_parser(positive_int))
|
|
|
|
|
|
- def __str__(self):
|
|
|
|
|
|
+ def __str__(self) -> str:
|
|
return '{op}{val}'.format(op=self.operator, val=self.value)
|
|
return '{op}{val}'.format(op=self.operator, val=self.value)
|
|
|
|
|
|
- def compare(self, x):
|
|
|
|
|
|
+ def compare(self, x) -> bool:
|
|
'''Return True if x satisfies the comparator.
|
|
'''Return True if x satisfies the comparator.
|
|
|
|
|
|
In other words, x is placed on the left-hand side of the
|
|
In other words, x is placed on the left-hand side of the
|
|
@@ -294,31 +332,33 @@ class Comparator(object):
|
|
|
|
|
|
@attr.s
|
|
@attr.s
|
|
class RerollSpec(object):
|
|
class RerollSpec(object):
|
|
- type = attr.ib(convert=str, validator=validate_by_parser(reroll_type))
|
|
|
|
- operator = attr.ib(default=None)
|
|
|
|
- value = attr.ib(default=None)
|
|
|
|
|
|
+ # Yes, it has to be called type
|
|
|
|
+ type: str = attr.ib(converter = str, validator=validate_by_parser(reroll_type))
|
|
|
|
+ operator: OptionalType[str] = attr.ib(default = None)
|
|
|
|
+ value: OptionalType[int] = attr.ib(default = None)
|
|
|
|
|
|
def __attrs_post_init__(self):
|
|
def __attrs_post_init__(self):
|
|
if (self.operator is None) != (self.value is None):
|
|
if (self.operator is None) != (self.value is None):
|
|
raise ValueError('Operator and value must be provided together')
|
|
raise ValueError('Operator and value must be provided together')
|
|
|
|
|
|
- def __str__(self):
|
|
|
|
|
|
+ def __str__(self) -> str:
|
|
result = self.type
|
|
result = self.type
|
|
if self.operator is not None:
|
|
if self.operator is not None:
|
|
result += self.operator + str(self.value)
|
|
result += self.operator + str(self.value)
|
|
return result
|
|
return result
|
|
|
|
|
|
- def roll_die(self, sides):
|
|
|
|
|
|
+ def roll_die(self, sides: DieFaceType) -> List[int]:
|
|
'''Roll a single die, following specified re-rolling rules.
|
|
'''Roll a single die, following specified re-rolling rules.
|
|
|
|
|
|
Returns a list of rolls, since some types of re-rolling
|
|
Returns a list of rolls, since some types of re-rolling
|
|
collect the result of multiple die rolls.
|
|
collect the result of multiple die rolls.
|
|
|
|
|
|
'''
|
|
'''
|
|
- if sides == 'F':
|
|
|
|
|
|
+ if is_fate_face(sides):
|
|
raise ValueError("Re-rolling/exploding is incompatible with Fate dice")
|
|
raise ValueError("Re-rolling/exploding is incompatible with Fate dice")
|
|
sides = int(sides)
|
|
sides = int(sides)
|
|
|
|
|
|
|
|
+ cmpr: Comparator
|
|
if self.value is None:
|
|
if self.value is None:
|
|
if self.type in ('R', 'r'):
|
|
if self.type in ('R', 'r'):
|
|
cmpr = Comparator('=', 1)
|
|
cmpr = Comparator('=', 1)
|
|
@@ -341,7 +381,7 @@ class RerollSpec(object):
|
|
return [ roll ]
|
|
return [ roll ]
|
|
elif self.type in ['!', '!!', '!p', '!!p']:
|
|
elif self.type in ['!', '!!', '!p', '!!p']:
|
|
# Explode/penetrate/compound
|
|
# Explode/penetrate/compound
|
|
- all_rolls = [ roll_die(sides) ]
|
|
|
|
|
|
+ all_rolls: List[int] = [ roll_die(sides) ]
|
|
while cmpr.compare(all_rolls[-1]):
|
|
while cmpr.compare(all_rolls[-1]):
|
|
all_rolls.append(roll_die(sides))
|
|
all_rolls.append(roll_die(sides))
|
|
# If we never re-rolled, no need to do anything special
|
|
# If we never re-rolled, no need to do anything special
|
|
@@ -359,25 +399,28 @@ class RerollSpec(object):
|
|
for i in range(0, len(all_rolls)-1):
|
|
for i in range(0, len(all_rolls)-1):
|
|
all_rolls[i] = DieRolled(all_rolls[i], '{}' + self.type)
|
|
all_rolls[i] = DieRolled(all_rolls[i], '{}' + self.type)
|
|
return all_rolls
|
|
return all_rolls
|
|
|
|
+ else:
|
|
|
|
+ raise Exception('Unknown reroll type: {}'.format(self.type))
|
|
|
|
|
|
@attr.s
|
|
@attr.s
|
|
class DropSpec(object):
|
|
class DropSpec(object):
|
|
- type = attr.ib(convert=str, validator=validate_by_parser(drop_type))
|
|
|
|
- count = attr.ib(default=1, convert=int, validator=validate_by_parser(positive_int))
|
|
|
|
|
|
+ # Yes, it has to be called type
|
|
|
|
+ type: str = attr.ib(converter = str, validator=validate_by_parser(drop_type))
|
|
|
|
+ count: int = attr.ib(default = 1, converter = int, validator=validate_by_parser(positive_int))
|
|
|
|
|
|
- def __str__(self):
|
|
|
|
|
|
+ def __str__(self) -> str:
|
|
if self.count > 1:
|
|
if self.count > 1:
|
|
return self.type + str(self.count)
|
|
return self.type + str(self.count)
|
|
else:
|
|
else:
|
|
return self.type
|
|
return self.type
|
|
|
|
|
|
- def drop_rolls(self, rolls):
|
|
|
|
|
|
+ def drop_rolls(self, rolls: List[int]) -> Tuple[List[int], List[int]]:
|
|
'''Drop the appripriate rolls from a list of rolls.
|
|
'''Drop the appripriate rolls from a list of rolls.
|
|
|
|
|
|
Returns a 2-tuple of roll lists. The first list is the kept
|
|
Returns a 2-tuple of roll lists. The first list is the kept
|
|
rolls, and the second list is the dropped rolls.
|
|
rolls, and the second list is the dropped rolls.
|
|
|
|
|
|
- The order of the rolls is not preserved.
|
|
|
|
|
|
+ The order of the rolls is not preserved. (TODO FIX THIS)
|
|
|
|
|
|
'''
|
|
'''
|
|
if not isinstance(rolls, list):
|
|
if not isinstance(rolls, list):
|
|
@@ -403,20 +446,20 @@ class DropSpec(object):
|
|
|
|
|
|
@attr.s
|
|
@attr.s
|
|
class DiceRoller(object):
|
|
class DiceRoller(object):
|
|
- die_type = attr.ib(convert = normalize_die_type)
|
|
|
|
- dice_count = attr.ib(default=1, convert=normalize_dice_count)
|
|
|
|
- reroll_spec = attr.ib(default=None)
|
|
|
|
|
|
+ die_type: DieFaceType = attr.ib(converter = normalize_die_type)
|
|
|
|
+ dice_count: int = attr.ib(default = 1, converter = normalize_dice_count)
|
|
|
|
+ reroll_spec: OptionalType[RerollSpec] = attr.ib(default = None)
|
|
@reroll_spec.validator
|
|
@reroll_spec.validator
|
|
def validate_reroll_spec(self, attribute, value):
|
|
def validate_reroll_spec(self, attribute, value):
|
|
if value is not None:
|
|
if value is not None:
|
|
assert isinstance(value, RerollSpec)
|
|
assert isinstance(value, RerollSpec)
|
|
- drop_spec = attr.ib(default=None)
|
|
|
|
|
|
+ drop_spec: OptionalType[DropSpec] = attr.ib(default = None)
|
|
@drop_spec.validator
|
|
@drop_spec.validator
|
|
def validate_drop_spec(self, attribute, value):
|
|
def validate_drop_spec(self, attribute, value):
|
|
if value is not None:
|
|
if value is not None:
|
|
assert isinstance(value, DropSpec)
|
|
assert isinstance(value, DropSpec)
|
|
- success_comparator = attr.ib(default=None)
|
|
|
|
- failure_comparator = attr.ib(default=None)
|
|
|
|
|
|
+ success_comparator: OptionalType[Comparator] = attr.ib(default = None)
|
|
|
|
+ failure_comparator: OptionalType[Comparator] = attr.ib(default = None)
|
|
@success_comparator.validator
|
|
@success_comparator.validator
|
|
@failure_comparator.validator
|
|
@failure_comparator.validator
|
|
def validate_comparator(self, attribute, value):
|
|
def validate_comparator(self, attribute, value):
|
|
@@ -429,7 +472,7 @@ class DiceRoller(object):
|
|
if self.success_comparator is None and self.failure_comparator is not None:
|
|
if self.success_comparator is None and self.failure_comparator is not None:
|
|
raise ValueError('Cannot use a failure condition without a success condition')
|
|
raise ValueError('Cannot use a failure condition without a success condition')
|
|
|
|
|
|
- def __str__(self):
|
|
|
|
|
|
+ def __str__(self) -> str:
|
|
return '{count}d{type}{reroll}{drop}{success}{fail}'.format(
|
|
return '{count}d{type}{reroll}{drop}{success}{fail}'.format(
|
|
count = self.dice_count if self.dice_count > 1 else '',
|
|
count = self.dice_count if self.dice_count > 1 else '',
|
|
type = self.die_type,
|
|
type = self.die_type,
|
|
@@ -439,7 +482,7 @@ class DiceRoller(object):
|
|
fail = ('f' + str(self.failure_comparator)) if self.failure_comparator else '',
|
|
fail = ('f' + str(self.failure_comparator)) if self.failure_comparator else '',
|
|
)
|
|
)
|
|
|
|
|
|
- def roll(self):
|
|
|
|
|
|
+ def roll(self) -> DiceRolled:
|
|
'''Roll dice according to specifications. Returns a DiceRolled object.'''
|
|
'''Roll dice according to specifications. Returns a DiceRolled object.'''
|
|
all_rolls = []
|
|
all_rolls = []
|
|
if self.reroll_spec:
|
|
if self.reroll_spec:
|
|
@@ -452,6 +495,7 @@ class DiceRoller(object):
|
|
(dice_results, dropped_results) = self.drop_spec.drop_rolls(all_rolls)
|
|
(dice_results, dropped_results) = self.drop_spec.drop_rolls(all_rolls)
|
|
else:
|
|
else:
|
|
(dice_results, dropped_results) = (all_rolls, [])
|
|
(dice_results, dropped_results) = (all_rolls, [])
|
|
|
|
+ success_count: OptionalType[int]
|
|
if self.success_comparator is not None:
|
|
if self.success_comparator is not None:
|
|
success_count = 0
|
|
success_count = 0
|
|
for roll in dice_results:
|
|
for roll in dice_results:
|
|
@@ -470,15 +514,15 @@ class DiceRoller(object):
|
|
success_count=success_count,
|
|
success_count=success_count,
|
|
)
|
|
)
|
|
|
|
|
|
-def make_dice_roller(expr):
|
|
|
|
|
|
+def make_dice_roller(expr: Union[str,ParseResults]) -> DiceRoller:
|
|
if isinstance(expr, str):
|
|
if isinstance(expr, str):
|
|
- expr = roll_spec.parseString(expr, True)['expr']
|
|
|
|
|
|
+ expr = roll_spec.parseString(expr, True)['roll']
|
|
assert expr.getName() == 'roll'
|
|
assert expr.getName() == 'roll'
|
|
expr = expr.asDict()
|
|
expr = expr.asDict()
|
|
|
|
|
|
dtype = normalize_die_type(expr['die_type'])
|
|
dtype = normalize_die_type(expr['die_type'])
|
|
dcount = normalize_dice_count(expr['dice_count'])
|
|
dcount = normalize_dice_count(expr['dice_count'])
|
|
- constructor_args = {
|
|
|
|
|
|
+ constructor_args: Dict[str, Any] = {
|
|
'die_type': dtype,
|
|
'die_type': dtype,
|
|
'dice_count': dcount,
|
|
'dice_count': dcount,
|
|
'reroll_spec': None,
|
|
'reroll_spec': None,
|
|
@@ -551,7 +595,7 @@ def make_dice_roller(expr):
|
|
|
|
|
|
# r = parse_roll('x + 1 - 2 * y * 4d4 + 2d20K1>=20f<=5')[0]
|
|
# r = parse_roll('x + 1 - 2 * y * 4d4 + 2d20K1>=20f<=5')[0]
|
|
|
|
|
|
-op_dict = {
|
|
|
|
|
|
+op_dict: Dict[str, Callable] = {
|
|
'+': operator.add,
|
|
'+': operator.add,
|
|
'-': operator.sub,
|
|
'-': operator.sub,
|
|
'*': operator.mul,
|
|
'*': operator.mul,
|
|
@@ -562,18 +606,24 @@ op_dict = {
|
|
'^': operator.pow,
|
|
'^': operator.pow,
|
|
}
|
|
}
|
|
|
|
|
|
-def normalize_expr(expr):
|
|
|
|
|
|
+NumericType = Union[float,int]
|
|
|
|
+ExprType = Union[NumericType, str, ParseResults]
|
|
|
|
+
|
|
|
|
+def normalize_expr(expr: ExprType) -> ParseResults:
|
|
if isinstance(expr, str):
|
|
if isinstance(expr, str):
|
|
return expr_parser.parseString(expr)['expr']
|
|
return expr_parser.parseString(expr)['expr']
|
|
- try:
|
|
|
|
- if 'expr' in expr:
|
|
|
|
- return expr['expr']
|
|
|
|
- except TypeError:
|
|
|
|
- pass
|
|
|
|
- return expr
|
|
|
|
-
|
|
|
|
-def _eval_expr_internal(expr, env={}, print_rolls=True, recursed_vars=set()):
|
|
|
|
- if isinstance(expr, Number):
|
|
|
|
|
|
+ elif isinstance(expr, Number):
|
|
|
|
+ return expr
|
|
|
|
+ else:
|
|
|
|
+ assert isinstance(expr, ParseResults)
|
|
|
|
+ return expr['expr']
|
|
|
|
+
|
|
|
|
+def _eval_expr_internal(
|
|
|
|
+ expr: ExprType,
|
|
|
|
+ env: Dict[str, str] = {},
|
|
|
|
+ print_rolls: bool = True,
|
|
|
|
+ recursed_vars: Set[str] = set()) -> NumericType:
|
|
|
|
+ if isinstance(expr, float) or isinstance(expr, int):
|
|
# Numeric literal
|
|
# Numeric literal
|
|
return expr
|
|
return expr
|
|
elif isinstance(expr, str):
|
|
elif isinstance(expr, str):
|
|
@@ -587,37 +637,43 @@ def _eval_expr_internal(expr, env={}, print_rolls=True, recursed_vars=set()):
|
|
recursed_vars = recursed_vars.union([expr]))
|
|
recursed_vars = recursed_vars.union([expr]))
|
|
else:
|
|
else:
|
|
raise ValueError('Expression referenced undefined variable {!r}'.format(expr))
|
|
raise ValueError('Expression referenced undefined variable {!r}'.format(expr))
|
|
- elif 'operator' in expr:
|
|
|
|
- # Compound expression
|
|
|
|
- operands = expr[::2]
|
|
|
|
- operators = expr[1::2]
|
|
|
|
- assert len(operands) == len(operators) + 1
|
|
|
|
- values = [ _eval_expr_internal(x, env, print_rolls, recursed_vars)
|
|
|
|
- for x in operands ]
|
|
|
|
- result = values[0]
|
|
|
|
- for (op, nextval) in zip(operators, values[1:]):
|
|
|
|
- opfun = op_dict[op]
|
|
|
|
- result = opfun(result, nextval)
|
|
|
|
- # Corece integral floats to ints
|
|
|
|
- if isinstance(result, float) and result.is_integer():
|
|
|
|
- result = int(result)
|
|
|
|
- return result
|
|
|
|
else:
|
|
else:
|
|
- # roll specification
|
|
|
|
- roller = make_dice_roller(expr)
|
|
|
|
- result = roller.roll()
|
|
|
|
- if print_rolls:
|
|
|
|
- print(result)
|
|
|
|
- return int(result)
|
|
|
|
-
|
|
|
|
-def eval_expr(expr, env={}, print_rolls=True):
|
|
|
|
|
|
+ assert isinstance(expr, ParseResults)
|
|
|
|
+ if 'operator' in expr:
|
|
|
|
+ # Compound expression
|
|
|
|
+ operands = expr[::2]
|
|
|
|
+ operators = expr[1::2]
|
|
|
|
+ assert len(operands) == len(operators) + 1
|
|
|
|
+ values = [ _eval_expr_internal(x, env, print_rolls, recursed_vars)
|
|
|
|
+ for x in operands ]
|
|
|
|
+ result = values[0]
|
|
|
|
+ for (op, nextval) in zip(operators, values[1:]):
|
|
|
|
+ opfun = op_dict[op]
|
|
|
|
+ result = opfun(result, nextval)
|
|
|
|
+ # https://github.com/python/mypy/issues/6060
|
|
|
|
+ if isinstance(result, float) and result.is_integer(): # type: ignore
|
|
|
|
+ # Corece integral floats to ints
|
|
|
|
+ result = int(result)
|
|
|
|
+ return result
|
|
|
|
+ else:
|
|
|
|
+ # roll specification
|
|
|
|
+ roller = make_dice_roller(expr)
|
|
|
|
+ rolled = roller.roll()
|
|
|
|
+ if print_rolls:
|
|
|
|
+ print(rolled)
|
|
|
|
+ return int(rolled)
|
|
|
|
+
|
|
|
|
+def eval_expr(expr: ExprType,
|
|
|
|
+ env: Dict[str,str] = {},
|
|
|
|
+ print_rolls: bool = True) -> NumericType:
|
|
expr = normalize_expr(expr)
|
|
expr = normalize_expr(expr)
|
|
return _eval_expr_internal(expr, env, print_rolls)
|
|
return _eval_expr_internal(expr, env, print_rolls)
|
|
|
|
|
|
-def _expr_as_str_internal(expr, env={}, recursed_vars = set()):
|
|
|
|
- if isinstance(expr, Number):
|
|
|
|
- # Numeric literal
|
|
|
|
- return str(expr)
|
|
|
|
|
|
+def _expr_as_str_internal(expr: ExprType,
|
|
|
|
+ env: Dict[str,str] = {},
|
|
|
|
+ recursed_vars: Set[str] = set()) -> str:
|
|
|
|
+ if isinstance(expr, float) or isinstance(expr, int):
|
|
|
|
+ return '{:g}'.format(expr)
|
|
elif isinstance(expr, str):
|
|
elif isinstance(expr, str):
|
|
# variable name
|
|
# variable name
|
|
if expr in recursed_vars:
|
|
if expr in recursed_vars:
|
|
@@ -626,41 +682,47 @@ def _expr_as_str_internal(expr, env={}, recursed_vars = set()):
|
|
var_value = env[expr]
|
|
var_value = env[expr]
|
|
parsed = normalize_expr(var_value)
|
|
parsed = normalize_expr(var_value)
|
|
return _expr_as_str_internal(parsed, env, recursed_vars = recursed_vars.union([expr]))
|
|
return _expr_as_str_internal(parsed, env, recursed_vars = recursed_vars.union([expr]))
|
|
|
|
+ # Not a variable name, just a string
|
|
else:
|
|
else:
|
|
return expr
|
|
return expr
|
|
- elif 'operator' in expr:
|
|
|
|
- # Compound expression
|
|
|
|
- operands = expr[::2]
|
|
|
|
- operators = expr[1::2]
|
|
|
|
- assert len(operands) == len(operators) + 1
|
|
|
|
- values = [ _expr_as_str_internal(x, env, recursed_vars)
|
|
|
|
- for x in operands ]
|
|
|
|
- result = str(values[0])
|
|
|
|
- for (op, nextval) in zip(operators, values[1:]):
|
|
|
|
- result += ' {} {}'.format(op, nextval)
|
|
|
|
- return '(' + result + ')'
|
|
|
|
else:
|
|
else:
|
|
- # roll specification
|
|
|
|
- return str(make_dice_roller(expr))
|
|
|
|
|
|
+ assert isinstance(expr, ParseResults)
|
|
|
|
+ if 'operator' in expr:
|
|
|
|
+ # Compound expression
|
|
|
|
+ operands = expr[::2]
|
|
|
|
+ operators = expr[1::2]
|
|
|
|
+ assert len(operands) == len(operators) + 1
|
|
|
|
+ values = [ _expr_as_str_internal(x, env, recursed_vars)
|
|
|
|
+ for x in operands ]
|
|
|
|
+ result = str(values[0])
|
|
|
|
+ for (op, nextval) in zip(operators, values[1:]):
|
|
|
|
+ result += ' {} {}'.format(op, nextval)
|
|
|
|
+ return '(' + result + ')'
|
|
|
|
+ else:
|
|
|
|
+ # roll specification
|
|
|
|
+ return str(make_dice_roller(expr))
|
|
|
|
|
|
-def expr_as_str(expr, env={}):
|
|
|
|
|
|
+def expr_as_str(expr: ExprType, env: Dict[str,str] = {}) -> str:
|
|
expr = normalize_expr(expr)
|
|
expr = normalize_expr(expr)
|
|
expr = _expr_as_str_internal(expr, env)
|
|
expr = _expr_as_str_internal(expr, env)
|
|
if expr.startswith('(') and expr.endswith(')'):
|
|
if expr.startswith('(') and expr.endswith(')'):
|
|
expr = expr[1:-1]
|
|
expr = expr[1:-1]
|
|
return expr
|
|
return expr
|
|
|
|
|
|
-def read_roll(handle=sys.stdin):
|
|
|
|
- return input("Enter roll> ")
|
|
|
|
|
|
+def read_roll(handle: TextIO = sys.stdin) -> str:
|
|
|
|
+ if handle == sys.stdin:
|
|
|
|
+ return input("Enter roll> ")
|
|
|
|
+ else:
|
|
|
|
+ return handle.readline()[:-1]
|
|
|
|
|
|
-special_command_parser = (
|
|
|
|
|
|
+special_command_parser: ParserElement = (
|
|
oneOf('h help ?').setResultsName('help') |
|
|
oneOf('h help ?').setResultsName('help') |
|
|
oneOf('q quit exit').setResultsName('quit') |
|
|
oneOf('q quit exit').setResultsName('quit') |
|
|
oneOf('v vars').setResultsName('vars') |
|
|
oneOf('v vars').setResultsName('vars') |
|
|
(oneOf('d del delete').setResultsName('delete').leaveWhitespace() + Suppress(White()) + var_name)
|
|
(oneOf('d del delete').setResultsName('delete').leaveWhitespace() + Suppress(White()) + var_name)
|
|
)
|
|
)
|
|
|
|
|
|
-def var_name_allowed(vname):
|
|
|
|
|
|
+def var_name_allowed(vname: str) -> bool:
|
|
'''Disallow variable names like 'help' and 'quit'.'''
|
|
'''Disallow variable names like 'help' and 'quit'.'''
|
|
parsers = [ special_command_parser, roll_spec ]
|
|
parsers = [ special_command_parser, roll_spec ]
|
|
for parser in [ special_command_parser, roll_spec ]:
|
|
for parser in [ special_command_parser, roll_spec ]:
|
|
@@ -672,9 +734,12 @@ def var_name_allowed(vname):
|
|
# If the variable name didn't parse as anything else, it's valid
|
|
# If the variable name didn't parse as anything else, it's valid
|
|
return True
|
|
return True
|
|
|
|
|
|
-line_parser = (special_command_parser ^ (assignment_parser | expr_parser))
|
|
|
|
|
|
+line_parser: ParserElement = (
|
|
|
|
+ special_command_parser ^
|
|
|
|
+ (assignment_parser | expr_parser)
|
|
|
|
+)
|
|
|
|
|
|
-def print_interactive_help():
|
|
|
|
|
|
+def print_interactive_help() -> None:
|
|
print('\n' + '''
|
|
print('\n' + '''
|
|
|
|
|
|
To make a roll, type in the roll in dice notation, e.g. '4d4 + 4'.
|
|
To make a roll, type in the roll in dice notation, e.g. '4d4 + 4'.
|
|
@@ -703,7 +768,7 @@ Special commands:
|
|
|
|
|
|
'''.strip() + '\n', file=sys.stdout)
|
|
'''.strip() + '\n', file=sys.stdout)
|
|
|
|
|
|
-def print_vars(env):
|
|
|
|
|
|
+def print_vars(env: Dict[str,str]) -> None:
|
|
if len(env):
|
|
if len(env):
|
|
print('Currently defined variables:')
|
|
print('Currently defined variables:')
|
|
for k in sorted(env.keys()):
|
|
for k in sorted(env.keys()):
|
|
@@ -728,7 +793,7 @@ if __name__ == '__main__':
|
|
raise exc
|
|
raise exc
|
|
sys.exit(1)
|
|
sys.exit(1)
|
|
else:
|
|
else:
|
|
- env = {}
|
|
|
|
|
|
+ env: Dict[str, str] = {}
|
|
while True:
|
|
while True:
|
|
try:
|
|
try:
|
|
expr_string = read_roll()
|
|
expr_string = read_roll()
|