123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 |
- #!/usr/bin/env python
- import attr
- import logging
- import re
- import sys
- import readline
- import operator
- from numbers import Number
- 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
- try:
- import colorama
- colorama.init()
- from colors import color
- except ImportError:
- # Fall back to no color
- def color(s, *args, **kwargs):
- '''Fake color function that does nothing.
- Used when the colors module cannot be imported.'''
- return s
- EXPR_COLOR = "green"
- RESULT_COLOR = "red"
- DETAIL_COLOR = "yellow"
- logFormatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
- logger = logging.getLogger(__name__)
- logger.setLevel(logging.INFO)
- logger.handlers = []
- logger.addHandler(logging.StreamHandler())
- for handler in logger.handlers:
- handler.setFormatter(logFormatter)
- sysrand = SystemRandom()
- randint = sysrand.randint
- @attr.s
- 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')
- @handle_float.validator
- def validate_handle_float(self, attribute, value):
- assert value in ('exception', 'truncate', 'round')
- def __call__(self, value):
- try:
- xf = float(value)
- except ValueError:
- raise ValueError('{} does not look like a number'.format(value))
- if not xf.is_integer():
- if self.handle_float == 'exception':
- raise ValueError('{} is not an integer'.format(value))
- elif self.handle_float == 'truncate':
- x = int(xf)
- else:
- x = round(xf)
- else:
- 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
- return x
- def normalize_die_type(x):
- if x in ('F', 'F.1', 'F.2'):
- return x
- elif x == '%':
- return 100
- 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):
- '''Like pyparsing.Empty, but yields one or more tokens instead of nothing.'''
- return Empty().setParseAction(lambda toks: x)
- # TODO: Look at http://infohost.nmt.edu/tcc/help/pubs/pyparsing/web/classes.html#class-ParserElement
- # Implementing the syntax described here: https://www.critdice.com/roll-advanced-dice
- # https://stackoverflow.com/a/23956778/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]) ])
- drop_type = oneOf('K k X x -H -L')
- drop_spec = Group(drop_type.setResultsName('type') +
- (positive_int | ImplicitToken(1)).setResultsName('count')
- ).setResultsName('drop')
- pos_int_implicit_one = (positive_int | ImplicitToken(1))
- comparator_type = oneOf('<= < >= > ≤ ≥ =')
- reroll_type = Combine(oneOf('R r') ^ ( oneOf('! !!') + Optional('p')))
- reroll_spec = Group(
- reroll_type.setResultsName('type') +
- Optional(
- (comparator_type | ImplicitToken('=')).setResultsName('operator') + \
- positive_int.setResultsName('value')
- )
- ).setResultsName('reroll')
- count_spec = Group(
- Group(
- comparator_type.setResultsName('operator') + \
- positive_int.setResultsName('value')
- ).setResultsName('success_condition') +
- Optional(
- Literal('f') +
- Group(
- comparator_type.setResultsName('operator') + \
- positive_int.setResultsName('value')
- ).setResultsName('failure_condition')
- )
- ).setResultsName('count_successes')
- roll_spec = Group(
- (positive_int | ImplicitToken(1)).setResultsName('dice_count') +
- CaselessLiteral('d') +
- (positive_int | oneOf('% F F.1 F.2')).setResultsName('die_type') +
- Optional(reroll_spec ^ drop_spec) +
- Optional(count_spec)
- ).setResultsName('roll')
- expr_parser = infixNotation(
- baseExpr=(roll_spec ^ positive_int ^ real_num ^ var_name),
- opList=[
- (oneOf('** ^').setResultsName('operator', True), 2, opAssoc.RIGHT),
- (oneOf('* / × ÷').setResultsName('operator', True), 2, opAssoc.LEFT),
- (oneOf('+ -').setResultsName('operator', True), 2, opAssoc.LEFT),
- ]
- ).setResultsName('expr')
- assignment_parser = var_name + Literal('=').setResultsName('assignment') + expr_parser
- def roll_die(sides=6):
- '''Roll a single die.
- Supports any valid integer number of sides as well as 'F' for a fate
- die, which can return -1, 0, or 1 with equal probability.
- '''
- if sides in ('F', 'F.2'):
- # Fate die = 1d3-2
- return roll_die(3) - 2
- elif sides == 'F.1':
- d6 = roll_die(6)
- if d6 == 1:
- return -1
- elif d6 == 6:
- return 1
- else:
- return 0
- else:
- return randint(1, int(sides))
- class DieRolled(int):
- '''Subclass of int that allows a string suffix.
- This is meant for recording the result of rolling a die. The
- suffix is purely cosmetic, for the purposes of string conversion.
- It can be used to indicate a die roll that has been re-rolled or
- exploded, or to indicate a critical hit/miss.
- '''
- def __new__(cls, value, formatter='{}'):
- newval = super(DieRolled, cls).__new__(cls, value)
- newval.formatter = formatter
- return newval
- def __str__(self):
- return self.formatter.format(super().__str__())
- def __repr__(self):
- if self.formatter != '{}':
- return 'DieRolled(value={value!r}, formatter={formatter!r})'.format(
- value=int(self),
- formatter=self.formatter,
- )
- else:
- return 'DieRolled({value!r})'.format(value=int(self))
- def validate_dice_roll_list(instance, attribute, 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 len(rolls) == 0:
- raise ValueError('Need at least one die rolled')
- elif len(rolls) == 1 and not always_list:
- return color(str(rolls[0]), DETAIL_COLOR)
- else:
- return '[' + color(" ".join(map(str, rolls)), DETAIL_COLOR) + ']'
- @attr.s
- class DiceRolled(object):
- '''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:
- raise ValueError('Need at least one non-dropped roll')
- def total(self):
- if self.success_count is not None:
- return int(self.success_count)
- else:
- return sum(self.dice_results)
- def __str__(self):
- if self.roll_desc:
- prefix = '{roll} rolled'.format(roll=color(self.roll_desc, EXPR_COLOR))
- else:
- prefix = 'Rolled'
- if self.dropped_results:
- drop = ' (dropped {dropped})'.format(dropped = format_dice_roll_list(self.dropped_results))
- else:
- drop = ''
- if self.success_count is not None:
- tot = ', Total successes: ' + color(str(self.total()), DETAIL_COLOR)
- elif len(self.dice_results) > 1:
- tot = ', Total: ' + color(str(self.total()), DETAIL_COLOR)
- else:
- tot = ''
- return '{prefix}: {results}{drop}{tot}'.format(
- prefix=prefix,
- results=format_dice_roll_list(self.dice_results),
- drop=drop,
- tot=tot,
- )
- def __int__(self):
- return self.total()
- def validate_by_parser(parser):
- def private_validator(instance, attribute, value):
- parser.parseString(str(value), True)
- return private_validator
- @attr.s
- class Comparator(object):
- cmp_dict = {
- '<=': operator.le,
- '<': operator.lt,
- '>=': operator.ge,
- '>': operator.gt,
- '≤': operator.le,
- '≥': operator.ge,
- '=': operator.eq,
- }
- operator = attr.ib(convert=str, validator=validate_by_parser(comparator_type))
- value = attr.ib(convert=int, validator=validate_by_parser(positive_int))
- def __str__(self):
- return '{op}{val}'.format(op=self.operator, val=self.value)
- def compare(self, x):
- '''Return True if x satisfies the comparator.
- In other words, x is placed on the left-hand side of the
- comparison, the Comparator's value is placed on the right hand
- side, and the truth value of the resulting test is returned.
- '''
- return self.cmp_dict[self.operator](x, self.value)
- @attr.s
- class RerollSpec(object):
- type = attr.ib(convert=str, validator=validate_by_parser(reroll_type))
- operator = attr.ib(default=None)
- value = attr.ib(default=None)
- def __attrs_post_init__(self):
- if (self.operator is None) != (self.value is None):
- raise ValueError('Operator and value must be provided together')
- def __str__(self):
- result = self.type
- if self.operator is not None:
- result += self.operator + str(self.value)
- return result
- def roll_die(self, sides):
- '''Roll a single die, following specified re-rolling rules.
- Returns a list of rolls, since some types of re-rolling
- collect the result of multiple die rolls.
- '''
- if sides == 'F':
- raise ValueError("Re-rolling/exploding is incompatible with Fate dice")
- sides = int(sides)
- if self.value is None:
- if self.type in ('R', 'r'):
- cmpr = Comparator('=', 1)
- else:
- cmpr = Comparator('=', sides)
- else:
- cmpr = Comparator(self.operator, self.value)
- if self.type == 'r':
- # Single reroll
- roll = roll_die(sides)
- if cmpr.compare(roll):
- roll = DieRolled(roll_die(sides), '{}' + self.type)
- return [ roll ]
- elif self.type == 'R':
- # Indefinite reroll
- roll = roll_die(sides)
- while cmpr.compare(roll):
- roll = DieRolled(roll_die(sides), '{}' + self.type)
- return [ roll ]
- elif self.type in ['!', '!!', '!p', '!!p']:
- # Explode/penetrate/compound
- all_rolls = [ roll_die(sides) ]
- while cmpr.compare(all_rolls[-1]):
- all_rolls.append(roll_die(sides))
- # If we never re-rolled, no need to do anything special
- if len(all_rolls) == 1:
- return all_rolls
- # For penetration, subtract 1 from all rolls except the first
- if self.type.endswith('p'):
- for i in range(1, len(all_rolls)):
- all_rolls[i] -= 1
- # For compounding, return the sum
- if self.type.startswith('!!'):
- total = sum(all_rolls)
- return [ DieRolled(total, '{}' + self.type) ]
- else:
- for i in range(0, len(all_rolls)-1):
- all_rolls[i] = DieRolled(all_rolls[i], '{}' + self.type)
- return all_rolls
- @attr.s
- 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))
- def __str__(self):
- if self.count > 1:
- return self.type + str(self.count)
- else:
- return self.type
- def drop_rolls(self, rolls):
- '''Drop the appripriate rolls from a list of rolls.
- Returns a 2-tuple of roll lists. The first list is the kept
- rolls, and the second list is the dropped rolls.
- The order of the rolls is not preserved.
- '''
- if not isinstance(rolls, list):
- rolls = list(rolls)
- keeping = self.type in ('K', 'k')
- if keeping:
- num_to_keep = self.count
- else:
- num_to_keep = len(rolls) - self.count
- if num_to_keep == 0:
- raise ValueError('Not enough rolls: would drop all rolls')
- elif num_to_keep == len(rolls):
- raise ValueError('Keeping too many rolls: would not drop any rolls')
- rolls.sort()
- if self.type in ('K', 'X', '-H'):
- rolls.reverse()
- (head, tail) = rolls[:self.count], rolls[self.count:]
- if keeping:
- (kept, dropped) = (head, tail)
- else:
- (kept, dropped) = (tail, head)
- return (kept, dropped)
- @attr.s
- 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)
- @reroll_spec.validator
- def validate_reroll_spec(self, attribute, value):
- if value is not None:
- assert isinstance(value, RerollSpec)
- drop_spec = attr.ib(default=None)
- @drop_spec.validator
- def validate_drop_spec(self, attribute, value):
- if value is not None:
- assert isinstance(value, DropSpec)
- success_comparator = attr.ib(default=None)
- failure_comparator = attr.ib(default=None)
- @success_comparator.validator
- @failure_comparator.validator
- def validate_comparator(self, attribute, value):
- if value is not None:
- assert isinstance(value, Comparator)
- def __attrs_post_init__(self):
- if self.reroll_spec is not None and self.drop_spec is not None:
- raise ValueError('Reroll and drop specs are mutually exclusive')
- if self.success_comparator is None and self.failure_comparator is not None:
- raise ValueError('Cannot use a failure condition without a success condition')
- def __str__(self):
- return '{count}d{type}{reroll}{drop}{success}{fail}'.format(
- count = self.dice_count if self.dice_count > 1 else '',
- type = self.die_type,
- reroll = self.reroll_spec or '',
- drop = self.drop_spec or '',
- success = self.success_comparator or '',
- fail = ('f' + str(self.failure_comparator)) if self.failure_comparator else '',
- )
- def roll(self):
- '''Roll dice according to specifications. Returns a DiceRolled object.'''
- all_rolls = []
- if self.reroll_spec:
- for i in range(self.dice_count):
- all_rolls.extend(self.reroll_spec.roll_die(self.die_type))
- else:
- for i in range(self.dice_count):
- all_rolls.append(roll_die(self.die_type))
- if self.drop_spec:
- (dice_results, dropped_results) = self.drop_spec.drop_rolls(all_rolls)
- else:
- (dice_results, dropped_results) = (all_rolls, [])
- if self.success_comparator is not None:
- success_count = 0
- for roll in dice_results:
- if self.success_comparator.compare(roll):
- success_count += 1
- if self.failure_comparator is not None:
- for roll in dice_results:
- if self.failure_comparator.compare(roll):
- success_count -= 1
- else:
- success_count = None
- return DiceRolled(
- dice_results=dice_results,
- dropped_results=dropped_results,
- roll_desc=str(self),
- success_count=success_count,
- )
- def make_dice_roller(expr):
- if isinstance(expr, str):
- expr = roll_spec.parseString(expr, True)['expr']
- assert expr.getName() == 'roll'
- expr = expr.asDict()
- dtype = normalize_die_type(expr['die_type'])
- dcount = normalize_dice_count(expr['dice_count'])
- constructor_args = {
- 'die_type': dtype,
- 'dice_count': dcount,
- 'reroll_spec': None,
- 'drop_spec': None,
- 'success_comparator': None,
- 'failure_comparator': None,
- }
- rrdict = None
- if 'reroll' in expr:
- rrdict = expr['reroll']
- constructor_args['reroll_spec'] = RerollSpec(**rrdict)
- if 'drop' in expr:
- ddict = expr['drop']
- constructor_args['drop_spec'] = DropSpec(**ddict)
- if 'count_successes' in expr:
- csdict = expr['count_successes']
- constructor_args['success_comparator'] = Comparator(**csdict['success_condition'])
- if 'failure_condition' in csdict:
- constructor_args['failure_comparator'] = Comparator(**csdict['failure_condition'])
- return DiceRoller(**constructor_args)
- # examples = [
- # '1+1',
- # '1 + 1 + x',
- # '3d8',
- # '2e3 * 4d6 + 2',
- # '2d20k',
- # '3d20x2',
- # '4d4rK3',
- # '4d4R4',
- # '4d4R>=3',
- # '4d4r>=3',
- # '4d4!1',
- # '4d4!<3',
- # '4d4!p',
- # '2D20K+10',
- # '2D20k+10',
- # '10d6X4',
- # '4d8r + 6',
- # '20d6R≤2',
- # '6d10!≥8+6',
- # '10d4!p',
- # '20d6≥6',
- # '8d12≥10f≤2',
- # # '4d20R<=2!>=19Xx21>=20f<=5*2+3', # Pretty much every possible feature
- # ]
- # example_results = {}
- # for x in examples:
- # try:
- # example_results[x] = parse_roll(x)
- # except ParseException as ex:
- # example_results[x] = ex
- # example_results
- # rs = RerollSpec('!!p', '=', 6)
- # rs.roll_die(6)
- # ds = DropSpec('K', 2)
- # ds.drop_rolls([1,2,3,4,5])
- # ds = DropSpec('x', 2)
- # ds.drop_rolls([1,2,3,4,5])
- # parse_roll = lambda x: expr_parser.parseString(x)[0]
- # exprstring = 'x + 1 + (2 + (3 + 4))'
- # expr = parse_roll(exprstring)
- # r = parse_roll('x + 1 - 2 * y * 4d4 + 2d20K1>=20f<=5')[0]
- op_dict = {
- '+': operator.add,
- '-': operator.sub,
- '*': operator.mul,
- '×': operator.mul,
- '/': operator.truediv,
- '÷': operator.truediv,
- '**': operator.pow,
- '^': operator.pow,
- }
- def normalize_expr(expr):
- if isinstance(expr, str):
- 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):
- # Numeric literal
- return expr
- elif isinstance(expr, str):
- # variable name
- if expr in recursed_vars:
- raise ValueError('Recursive variable definition detected for {!r}'.format(expr))
- elif expr in env:
- var_value = env[expr]
- parsed = normalize_expr(var_value)
- return _eval_expr_internal(parsed, env, print_rolls,
- recursed_vars = recursed_vars.union([expr]))
- else:
- 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:
- # roll specification
- roller = make_dice_roller(expr)
- result = roller.roll()
- if print_rolls:
- print(result, file=sys.stderr)
- return int(result)
- def eval_expr(expr, env={}, print_rolls=True):
- expr = normalize_expr(expr)
- 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)
- elif isinstance(expr, str):
- # variable name
- if expr in recursed_vars:
- raise ValueError('Recursive variable definition detected for {!r}'.format(expr))
- elif expr in env:
- var_value = env[expr]
- parsed = normalize_expr(var_value)
- return _expr_as_str_internal(parsed, env, recursed_vars = recursed_vars.union([expr]))
- else:
- 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:
- # roll specification
- return str(make_dice_roller(expr))
- def expr_as_str(expr, env={}):
- expr = normalize_expr(expr)
- expr = _expr_as_str_internal(expr, env)
- if expr.startswith('(') and expr.endswith(')'):
- expr = expr[1:-1]
- return expr
- def read_roll(handle=sys.stdin):
- return input("Enter roll> ")
- special_command_parser = (
- oneOf('h help ?').setResultsName('help') |
- oneOf('q quit exit').setResultsName('quit') |
- oneOf('v vars').setResultsName('vars') |
- (oneOf('d del delete').setResultsName('delete').leaveWhitespace() + Suppress(White()) + var_name)
- )
- def var_name_allowed(vname):
- '''Disallow variable names like 'help' and 'quit'.'''
- parsers = [ special_command_parser, roll_spec ]
- for parser in [ special_command_parser, roll_spec ]:
- try:
- parser.parseString(vname, True)
- return False
- except ParseException:
- pass
- # If the variable name didn't parse as anything else, it's valid
- return True
- line_parser = (special_command_parser ^ (assignment_parser | expr_parser))
- def print_interactive_help():
- print('\n' + '''
- To make a roll, type in the roll in dice notation, e.g. '4d4 + 4'.
- Nearly all dice notation forms listed in the following references should be supported:
- - http://rpg.greenimp.co.uk/dice-roller/
- - https://www.critdice.com/roll-advanced-dice
- Expressions can include numeric constants, addition, subtraction,
- multiplication, division, and exponentiation.
- To assign a variable, use 'VAR = VALUE'. For example 'health_potion =
- 2d4+2'. Subsequent roll expressions (and other variables) can refer to
- this variable, whose value will be substituted in to the expression.
- If a variable's value includes any dice rolls, those dice will be
- rolled (and produce a different value) every time the variable is
- used.
- Special commands:
- - To show the values of all currently assigned variables, type 'vars'.
- - To delete a previously defined variable, type 'del VAR'.
- - To show this help text, type 'help'.
- - To quit, type 'quit'.
- '''.strip() + '\n', file=sys.stdout)
- def print_vars(env):
- if len(env):
- logger.info('Currently defined variables:')
- for k in sorted(env.keys()):
- print('{} = {!r}'.format(k, env[k]), file=sys.stderr)
- else:
- logger.info('No vars are currently defined.')
- if __name__ == '__main__':
- expr_string = " ".join(sys.argv[1:])
- if re.search("\\S", expr_string):
- try:
- # Note: using expr_parser instead of line_parser, because
- # on the command line only roll expressions are valid.
- expr = expr_parser.parseString(expr_string, True)
- result = eval_expr(expr)
- logger.info('Total roll for {expr}: {result}'.format(
- expr=color(expr_as_str(expr), EXPR_COLOR),
- result=color(result, RESULT_COLOR),
- ))
- except Exception as exc:
- logger.error("Error while rolling: %s", repr(exc))
- raise exc
- sys.exit(1)
- else:
- env = {}
- while True:
- try:
- expr_string = read_roll()
- if not re.search("\\S", expr_string):
- continue
- parsed = line_parser.parseString(expr_string, True)
- if 'help' in parsed:
- print_interactive_help()
- elif 'quit' in parsed:
- logger.info('Quitting.')
- break
- elif 'vars' in parsed:
- print_vars(env)
- elif 'delete' in parsed:
- vname = parsed['varname']
- if vname in env:
- logger.info('Deleting saved value for "{var}".'.format(var=color(vname, RESULT_COLOR)))
- del env[vname]
- else:
- logger.error('Variable "{var}" is not defined.'.format(var=color(vname, RESULT_COLOR)))
- elif re.search("\\S", expr_string):
- if 'assignment' in parsed:
- # We have an assignment operation
- vname = parsed['varname']
- if var_name_allowed(vname):
- env[vname] = expr_as_str(parsed['expr'])
- logger.info('Saving "{var}" as "{expr}"'.format(
- var=color(vname, RESULT_COLOR),
- expr=color(env[vname], EXPR_COLOR),
- ))
- else:
- logger.error('You cannot use {!r} as a variable name.'.format(vname))
- else:
- # Just an expression to evaluate
- result = eval_expr(parsed['expr'], env)
- logger.info('Total roll for {expr}: {result}'.format(
- expr=color(expr_as_str(parsed, env), EXPR_COLOR),
- result=color(result, RESULT_COLOR),
- ))
- print('', file=sys.stderr)
- except KeyboardInterrupt:
- print('', file=sys.stderr)
- except EOFError:
- logger.info('Quitting.')
- break
- except Exception as exc:
- logger.error('Error while evaluating {expr!r}: {ex!r}'.format(
- expr=expr_string, ex=exc,
- ))
|