parent
7ce7fd8f8b
commit
362ae14448
@ -0,0 +1,516 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
import parsy
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: functions
|
||||||
|
# TODO: dice matching
|
||||||
|
# FIXME: handle infinite loops
|
||||||
|
|
||||||
|
|
||||||
|
def num(x):
|
||||||
|
if int(x) == x:
|
||||||
|
return int(x)
|
||||||
|
return float(x)
|
||||||
|
|
||||||
|
|
||||||
|
class Group(object):
|
||||||
|
def __init__(self, items):
|
||||||
|
self.items = items
|
||||||
|
self.keep = None
|
||||||
|
self.drop = None
|
||||||
|
self.succ = None
|
||||||
|
self.fail = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<group {0} keep:{1} drop:{2} succ:{3} fail:{4}>'.format(
|
||||||
|
' '.join([repr(x) for x in self.items]), self.keep, self.drop, self.succ, self.fail)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
kept = self._kept()
|
||||||
|
return '**{{** {0} **}}**'.format(' + '.join(
|
||||||
|
[str(x) if i in kept else '~~*{0}*~~'.format(x) for i, x in enumerate(self.items)]))
|
||||||
|
|
||||||
|
# TODO: handle single-sub-rolls - len(self.items) == 1
|
||||||
|
|
||||||
|
def _calculated(self):
|
||||||
|
result = []
|
||||||
|
for item in self.items:
|
||||||
|
try:
|
||||||
|
result.append(item.calc())
|
||||||
|
except AttributeError:
|
||||||
|
result.append(num(item))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _kept(self):
|
||||||
|
calculated = self._calculated()
|
||||||
|
result = sorted(enumerate(calculated), key=lambda x: (x[1], len(calculated) - x[0]))
|
||||||
|
if self.keep:
|
||||||
|
if self.keep[1]:
|
||||||
|
result = result[:round(self.keep[0])]
|
||||||
|
else:
|
||||||
|
result = result[-round(self.keep[0]):]
|
||||||
|
if self.drop:
|
||||||
|
if self.drop[1]:
|
||||||
|
result = result[:-round(self.drop[0])]
|
||||||
|
else:
|
||||||
|
result = result[round(self.drop[0]):]
|
||||||
|
return list(list(zip(*sorted(result)))[0])
|
||||||
|
|
||||||
|
def filtered(self):
|
||||||
|
calculated = self._calculated()
|
||||||
|
kept = self._kept()
|
||||||
|
return [x for i, x in enumerate(calculated) if i in kept]
|
||||||
|
|
||||||
|
def calc(self):
|
||||||
|
filtered = self.filtered()
|
||||||
|
if self.succ:
|
||||||
|
result = len([x for x in filtered if self.succ(x)])
|
||||||
|
if self.fail:
|
||||||
|
result -= len([x for x in filtered if self.fail(x)])
|
||||||
|
return result
|
||||||
|
return sum(filtered)
|
||||||
|
|
||||||
|
|
||||||
|
class Operation(object):
|
||||||
|
def __init__(self, op, func, left, right):
|
||||||
|
self.op = op
|
||||||
|
self.func = func
|
||||||
|
self.left = left
|
||||||
|
self.right = right
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{0} {1} {2}>'.format(self.op, repr(self.left), repr(self.right))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# FIXME: drop unneeded parentheses
|
||||||
|
return '( {0} {1} {2} )'.format(str(self.left), self.op.replace('*', '\\*'), str(self.right))
|
||||||
|
|
||||||
|
def calc(self):
|
||||||
|
try:
|
||||||
|
left = self.left.calc()
|
||||||
|
except AttributeError:
|
||||||
|
left = num(self.left)
|
||||||
|
try:
|
||||||
|
right = self.right.calc()
|
||||||
|
except AttributeError:
|
||||||
|
right = num(self.right)
|
||||||
|
return self.func(left, right)
|
||||||
|
|
||||||
|
|
||||||
|
class Roll(object):
|
||||||
|
def __init__(self, result):
|
||||||
|
self.result = result
|
||||||
|
self.label = None
|
||||||
|
self.keep = None
|
||||||
|
self.drop = None
|
||||||
|
self.succ = None
|
||||||
|
self.fail = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<roll {0} label:{1} keep:{2} drop:{3} succ:{4} fail:{5}>'.format(
|
||||||
|
self.result, self.label, self.keep, self.drop, self.succ, self.fail)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
kept = self._kept()
|
||||||
|
return '**(** {0} **)**'.format(' + '.join(
|
||||||
|
[str(x) if i in kept else '~~*{0}*~~'.format(x) for i, x in enumerate(self.result)]))
|
||||||
|
|
||||||
|
def _kept(self):
|
||||||
|
result = sorted(enumerate(self.result), key=lambda x: (x[1], len(self.result) - x[0]))
|
||||||
|
if self.keep:
|
||||||
|
if self.keep[1]:
|
||||||
|
result = result[:round(self.keep[0])]
|
||||||
|
else:
|
||||||
|
result = result[-round(self.keep[0]):]
|
||||||
|
if self.drop:
|
||||||
|
if self.drop[1]:
|
||||||
|
result = result[:-round(self.drop[0])]
|
||||||
|
else:
|
||||||
|
result = result[round(self.drop[0]):]
|
||||||
|
return list(list(zip(*sorted(result)))[0])
|
||||||
|
|
||||||
|
def filtered(self):
|
||||||
|
kept = self._kept()
|
||||||
|
return [x for i, x in enumerate(self.result) if i in kept]
|
||||||
|
|
||||||
|
def calc(self):
|
||||||
|
filtered = self.filtered()
|
||||||
|
if self.succ:
|
||||||
|
result = len([x for x in filtered if self.succ(x)])
|
||||||
|
if self.fail:
|
||||||
|
result -= len([x for x in filtered if self.fail(x)])
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return sum(filtered)
|
||||||
|
|
||||||
|
|
||||||
|
class Parser(object):
|
||||||
|
@classmethod
|
||||||
|
def tokenize(cls, formula):
|
||||||
|
whitespace = parsy.regex(r'\s*')
|
||||||
|
number = parsy.regex(r'(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?').map(float)
|
||||||
|
expression = (whitespace >> (
|
||||||
|
(parsy.regex(r'\*{2}|[()*/%+-]') | number | parsy.regex(r'\[.*?\]') |
|
||||||
|
parsy.regex(r'\!{2}|\!p|mt|ro|k[hl]|d[hl]|s[ad]|[{}dFfmkdrs!,<>=]')
|
||||||
|
) << whitespace)).many()
|
||||||
|
return expression.parse(formula)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, tokens):
|
||||||
|
@parsy.generate
|
||||||
|
def group_failures():
|
||||||
|
result = yield group_successes
|
||||||
|
yield parsy.match_item('f')
|
||||||
|
comparison.operator_required = False
|
||||||
|
condition = yield comparison
|
||||||
|
result.fail = condition
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def group_successes():
|
||||||
|
result = yield group_drop
|
||||||
|
comparison.operator_required = True
|
||||||
|
condition = yield comparison | parsy.success('')
|
||||||
|
if not condition:
|
||||||
|
return result
|
||||||
|
result.succ = condition
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def group_drop():
|
||||||
|
result = yield group_keep
|
||||||
|
modifier = yield parsy.test_item(lambda x: x in ['d', 'dh', 'dl'], 'd[h|l]') | parsy.success('')
|
||||||
|
if not modifier:
|
||||||
|
return result
|
||||||
|
count = yield number
|
||||||
|
result.drop = (count, modifier == 'dh')
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def group_keep():
|
||||||
|
result = yield group
|
||||||
|
modifier = yield parsy.test_item(lambda x: x in ['k', 'kh', 'kl'], 'k[h|l]') | parsy.success('')
|
||||||
|
if not modifier:
|
||||||
|
return result
|
||||||
|
count = yield number
|
||||||
|
result.keep = (count, modifier == 'kl')
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def group():
|
||||||
|
yield parsy.match_item('{')
|
||||||
|
result = yield group_simple
|
||||||
|
result = Group([result])
|
||||||
|
while True:
|
||||||
|
end = yield parsy.match_item('}') | parsy.success('')
|
||||||
|
if end:
|
||||||
|
break
|
||||||
|
yield parsy.match_item(',')
|
||||||
|
other = yield group_simple
|
||||||
|
result.items.append(other)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def expression_additive():
|
||||||
|
result = yield expression_multiplicative
|
||||||
|
sign = parsy.match_item('+') | parsy.match_item('-')
|
||||||
|
while True:
|
||||||
|
operation = yield sign | parsy.success('')
|
||||||
|
if not operation:
|
||||||
|
break
|
||||||
|
operand = yield expression_multiplicative
|
||||||
|
if operation == '+':
|
||||||
|
result = Operation(operation, lambda x, y: x + y, result, operand)
|
||||||
|
elif operation == '-':
|
||||||
|
result = Operation(operation, lambda x, y: x - y, result, operand)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def expression_multiplicative():
|
||||||
|
result = yield expression_exponential
|
||||||
|
operator = parsy.match_item('*') | parsy.match_item('/') | parsy.match_item('%')
|
||||||
|
while True:
|
||||||
|
operation = yield operator | parsy.success('')
|
||||||
|
if not operation:
|
||||||
|
break
|
||||||
|
operand = yield expression_exponential
|
||||||
|
if operation == '*':
|
||||||
|
result = Operation(operation, lambda x, y: x * y, result, operand)
|
||||||
|
elif operation == '/':
|
||||||
|
result = Operation(operation, lambda x, y: x / y, result, operand)
|
||||||
|
elif operation == '%':
|
||||||
|
result = Operation(operation, lambda x, y: x % y, result, operand)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def expression_exponential():
|
||||||
|
result = yield expression_simple
|
||||||
|
operator = parsy.match_item('**')
|
||||||
|
while True:
|
||||||
|
operation = yield operator | parsy.success('')
|
||||||
|
if not operation:
|
||||||
|
break
|
||||||
|
operand = yield expression_simple
|
||||||
|
if operation == '**':
|
||||||
|
result = Operation(operation, lambda x, y: x ** y, result, operand)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def roll():
|
||||||
|
result = yield failures | successes
|
||||||
|
label = parsy.test_item(lambda x: x.startswith('[') and x.endswith(']'), '[LABEL]')
|
||||||
|
label = yield label | parsy.success(None)
|
||||||
|
if label:
|
||||||
|
label = label[1:-1]
|
||||||
|
result.label = label
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def failures():
|
||||||
|
result = yield successes
|
||||||
|
yield parsy.match_item('f')
|
||||||
|
comparison.operator_required = False
|
||||||
|
condition = yield comparison
|
||||||
|
result.fail = condition
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def successes():
|
||||||
|
result = yield drop
|
||||||
|
comparison.operator_required = True
|
||||||
|
condition = yield comparison | parsy.success('')
|
||||||
|
if not condition:
|
||||||
|
return result
|
||||||
|
result.succ = condition
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def drop():
|
||||||
|
result = yield keep
|
||||||
|
modifier = yield parsy.test_item(lambda x: x in ['d', 'dh', 'dl'], 'd[h|l]') | parsy.success('')
|
||||||
|
if not modifier:
|
||||||
|
return result
|
||||||
|
count = yield number
|
||||||
|
result.drop = (count, modifier == 'dh')
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def keep():
|
||||||
|
result = yield modifiers
|
||||||
|
modifier = yield parsy.test_item(lambda x: x in ['k', 'kh', 'kl'], 'k[h|l]') | parsy.success('')
|
||||||
|
if not modifier:
|
||||||
|
return result
|
||||||
|
count = yield number
|
||||||
|
result.keep = (count, modifier == 'kl')
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def modifiers():
|
||||||
|
result, dice = yield basic_roll
|
||||||
|
modifications = {}
|
||||||
|
while True:
|
||||||
|
prio, modify = yield exploding | compounding | penetrating | reroll | sort | parsy.success((None, None))
|
||||||
|
if not prio:
|
||||||
|
break
|
||||||
|
if not prio in modifications:
|
||||||
|
modifications[prio] = []
|
||||||
|
modifications[prio].append(modify)
|
||||||
|
for prio, m in sorted(modifications.items()):
|
||||||
|
if prio == 4: # reroll
|
||||||
|
changed = True
|
||||||
|
while changed:
|
||||||
|
for modify in m:
|
||||||
|
new_result, dice = modify(result, dice)
|
||||||
|
changed = new_result != result
|
||||||
|
result = new_result
|
||||||
|
if changed:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for modify in m:
|
||||||
|
result, dice = modify(result, dice)
|
||||||
|
return Roll(result)
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def sort():
|
||||||
|
modifier = yield parsy.test_item(lambda x: x in ['s', 'sa', 'sd'], 's[a|d]')
|
||||||
|
def modify(result, dice):
|
||||||
|
result = sorted(result, reverse=(modifier == 'sd'))
|
||||||
|
return result, dice
|
||||||
|
return 5, modify
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def reroll():
|
||||||
|
modifier = yield parsy.test_item(lambda x: x in ['r', 'ro'], 'r[o]')
|
||||||
|
comparison.operator_required = False
|
||||||
|
condition = yield comparison
|
||||||
|
def modify(result, dice):
|
||||||
|
result = result[:]
|
||||||
|
i = 0
|
||||||
|
while i < len(result):
|
||||||
|
while condition(result[i], dice[0]):
|
||||||
|
result[i] = random.choice(dice)
|
||||||
|
if modifier == 'ro':
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
return result, dice
|
||||||
|
return 4, modify
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def penetrating():
|
||||||
|
yield parsy.match_item('!p')
|
||||||
|
comparison.operator_required = False
|
||||||
|
condition = yield comparison
|
||||||
|
def modify(result, dice):
|
||||||
|
result = result[:]
|
||||||
|
i = 0
|
||||||
|
while i < len(result):
|
||||||
|
sub = [result[i]]
|
||||||
|
while condition(sub[-1], dice[-1]):
|
||||||
|
sub.append(random.choice(dice))
|
||||||
|
result[i+1 : i+1] = [x - 1 for x in sub][1:]
|
||||||
|
i += len(sub)
|
||||||
|
return result, dice
|
||||||
|
return 3, modify
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def compounding():
|
||||||
|
yield parsy.match_item('!!')
|
||||||
|
comparison.operator_required = False
|
||||||
|
condition = yield comparison
|
||||||
|
def modify(result, dice):
|
||||||
|
result = result[:]
|
||||||
|
i = 0
|
||||||
|
while i < len(result):
|
||||||
|
sub = [result[i]]
|
||||||
|
while condition(sub[-1], dice[-1]):
|
||||||
|
sub.append(random.choice(dice))
|
||||||
|
result[i] = sum(sub)
|
||||||
|
i += 1
|
||||||
|
return result, dice
|
||||||
|
return 2, modify
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def exploding():
|
||||||
|
yield parsy.match_item('!')
|
||||||
|
comparison.operator_required = False
|
||||||
|
condition = yield comparison
|
||||||
|
def modify(result, dice):
|
||||||
|
result = result[:]
|
||||||
|
i = 0
|
||||||
|
while i < len(result):
|
||||||
|
sub = [result[i]]
|
||||||
|
while condition(sub[-1], dice[-1]):
|
||||||
|
sub.append(random.choice(dice))
|
||||||
|
result[i: i+1] = sub
|
||||||
|
i += len(sub)
|
||||||
|
return result, dice
|
||||||
|
return 1, modify
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def comparison():
|
||||||
|
operator = parsy.match_item('<') | parsy.match_item('>') | parsy.match_item('=')
|
||||||
|
if comparison.operator_required:
|
||||||
|
operation = yield operator
|
||||||
|
else:
|
||||||
|
operation = yield operator | parsy.success('')
|
||||||
|
if operation:
|
||||||
|
operand = yield number
|
||||||
|
else:
|
||||||
|
operand = yield number | parsy.success('')
|
||||||
|
def condition(value, default=None):
|
||||||
|
if operation == '<':
|
||||||
|
return value <= operand
|
||||||
|
elif operation == '>':
|
||||||
|
return value >= operand
|
||||||
|
elif operation == '=':
|
||||||
|
return value == operand
|
||||||
|
elif operand:
|
||||||
|
return value == operand
|
||||||
|
else:
|
||||||
|
return value == default
|
||||||
|
return condition
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def basic_roll():
|
||||||
|
count = yield computed_simple
|
||||||
|
yield parsy.match_item('d')
|
||||||
|
sides = yield computed_simple | parsy.match_item('F')
|
||||||
|
dice = [-1, 0, 1] if sides == 'F' else [x + 1 for x in range(round(sides))]
|
||||||
|
result = [random.choice(dice) for _ in range(round(count))]
|
||||||
|
return result, dice
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def computed_additive():
|
||||||
|
result = yield computed_multiplicative
|
||||||
|
sign = parsy.match_item('+') | parsy.match_item('-')
|
||||||
|
while True:
|
||||||
|
operation = yield sign | parsy.success('')
|
||||||
|
if not operation:
|
||||||
|
break
|
||||||
|
operand = yield computed_multiplicative
|
||||||
|
if operation == '+':
|
||||||
|
result += operand
|
||||||
|
elif operation == '-':
|
||||||
|
result -= operand
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def computed_multiplicative():
|
||||||
|
result = yield computed_exponential
|
||||||
|
operator = parsy.match_item('*') | parsy.match_item('/') | parsy.match_item('%')
|
||||||
|
while True:
|
||||||
|
operation = yield operator | parsy.success('')
|
||||||
|
if not operation:
|
||||||
|
break
|
||||||
|
operand = yield computed_exponential
|
||||||
|
if operation == '*':
|
||||||
|
result *= operand
|
||||||
|
elif operation == '/':
|
||||||
|
result /= operand
|
||||||
|
elif operation == '%':
|
||||||
|
result %= operand
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def computed_exponential():
|
||||||
|
result = yield computed_simple
|
||||||
|
operator = parsy.match_item('**')
|
||||||
|
while True:
|
||||||
|
operation = yield operator | parsy.success('')
|
||||||
|
if not operation:
|
||||||
|
break
|
||||||
|
operand = yield computed_simple
|
||||||
|
if operation == '**':
|
||||||
|
result **= operand
|
||||||
|
return result
|
||||||
|
|
||||||
|
@parsy.generate
|
||||||
|
def number():
|
||||||
|
sign = yield parsy.match_item('+') | parsy.match_item('-') | parsy.success('+')
|
||||||
|
value = yield parsy.test_item(lambda x: isinstance(x, float), 'number')
|
||||||
|
return num(value if sign == '+' else -value)
|
||||||
|
|
||||||
|
computed_simple = (parsy.match_item('(') >> computed_additive << parsy.match_item(')')) | number
|
||||||
|
expression_simple = roll | (parsy.match_item('(') >> expression_additive << parsy.match_item(')')) | number
|
||||||
|
group_simple = group_failures | group_successes | expression_additive
|
||||||
|
return group_simple.parse(tokens)
|
||||||
|
|
||||||
|
|
||||||
|
class Roll20Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Roll20(object):
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, formula):
|
||||||
|
try:
|
||||||
|
tokens = Parser.tokenize(formula)
|
||||||
|
result = Parser.parse(tokens)
|
||||||
|
except parsy.ParseError as e:
|
||||||
|
raise Roll20Error(str(e))
|
||||||
|
try:
|
||||||
|
calculated = result.calc()
|
||||||
|
except AttributeError:
|
||||||
|
calculated = num(result)
|
||||||
|
return '{0} = __**{1}**__'.format(str(result), calculated)
|
Loading…
Reference in new issue