Use PLY to parse dice strings

This commit is contained in:
kad 2010-10-30 15:04:58 -05:00
parent 1bc0dd0b2a
commit 4f56e40ca7
2 changed files with 252 additions and 63 deletions

View File

@ -22,6 +22,9 @@ from extlib import irclib
from Module import Module
import diceply
import ply.yacc as yacc
# Rolls dice, for RPGs mostly
class Dice(Module):
@ -30,69 +33,9 @@ class Dice(Module):
whats = what.split(' ')
if whats[0] == 'roll':
rollitrs = re.split(';\s*', ' '.join(whats[1:]))
reply = ""
for count, roll in enumerate(rollitrs):
pattern = '^(?:(\d+)#)?(?:(\d+)/)?(\d+)?d(\d+)(?:(\+|\-)(\d+))?(?:\s+(.*))?'
regex = re.compile(pattern)
matches = regex.search(roll)
if matches is not None:
# set variables including defaults for unspecified stuff
faces = int(matches.group(4))
comment = matches.group(7)
if matches.group(1) is None:
times = 1
else:
times = int(matches.group(1))
if matches.group(3) is None:
dice = 1
else:
dice = int(matches.group(3))
if matches.group(2) is None:
top = dice
else:
top = int(matches.group(2))
if matches.group(5) is None or matches.group(6) is None:
modifier = 0
else:
if str(matches.group(5)) == '-':
modifier = -1 * int(matches.group(6))
else:
modifier = int(matches.group(6))
result = roll + ': '
for t in range(times):
ressubstr = ""
rolls = []
for d in range(dice):
rolls.append(str(random.randint(1, faces)))
rolls.sort()
rolls.reverse()
ressubstr = ','.join(rolls[0:top])
sum = 0
for r in rolls[0:top]:
sum += int(r)
sumplus = sum + modifier
result += str(sumplus) + ' [' + ressubstr
if modifier != 0:
if modifier > 0:
result += ' + ' + str(modifier)
else:
result += ' - ' + str(-1 * modifier)
result += ']'
if t != times-1:
result += ', '
reply += result
if count is not len(rollitrs)-1:
reply += "; "
dicestr = ' '.join(whats[1:])
yacc.parse(dicestr)
reply = diceply.get_result()
if reply is not "":
return self.reply(connection, replypath, nick + ': ' + reply)
if whats[0] == 'ctech':

246
modules/diceply.py Executable file
View File

@ -0,0 +1,246 @@
# diceply.py
# Dice string grammar using PLY
# After initial run, parser.out should have the full grammar and the states
# and any shift/reduce or reduce/reduce conflicts.
#
# The current state of the grammar has some shift/reduce conflicts, because
# I don't know LR parsers well enough to prevent them. Also, currently spaces
# in the roll string aren't working if we want comments (for now)
#
# This script can be run standalone if you enable the tests at the bottom
import sys
import random
tokens = ['NUMBER', 'TEXT']
literals = ['#', '/', '+', '-', 'd', ';']
t_TEXT = r'\s+[^;]+'
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
def t_error(t):
t.lexer.skip(1)
import ply.lex as lex
lex.lex()
precedence = (
('left', ';'),
('left', '+', '-'),
('right', 'd'),
('left', '#'),
('left', '/')
)
output = ""
# Takes the parsed dice string for a single roll (eg 3/4d20) and performs
# the actual roll. Returns a string representing the result
def roll_dice(keep, dice, size):
a = range(dice)
for i in range(dice):
a[i] = random.randint(1, size)
if keep != dice:
b = sorted(a, reverse=True)
b = b[0:keep]
else:
b = a
total = sum(b)
outstr = "[";
if len(b) != len(a):
# Bold is \002
outstr += ",".join(str(i) for i in b) + ","
temp = sorted(a, reverse=True)
temp = temp[keep:]
bstr = ",".join(str(i) for i in temp)
outstr += bstr
else:
outstr += ",".join(str(i) for i in a)
outstr += "]"
return (total, outstr)
# Processes rolls coming from the parser. This generates the inputs
# for the roll_dice() command, and returns the full string representing
# the whole current dice string (the part up to a semicolon or end of line)
def process_roll(trials, mods, comment):
output = ""
mode = 1
repeat = 1
if trials != None:
repeat = trials
for i in range(repeat):
total = 0
curr_str = ""
if i > 0:
output += ", "
for m in mods:
keep = 0
dice = 1
res = 0
# if m is a tuple, then it is a die roll
# m[0] = (keep, num dice)
# m[1] = num faces on the die
if type(m) == tuple:
if m[0] != None:
if m[0][0] != None:
keep = m[0][0]
dice = m[0][1]
size = m[1]
if keep > dice or keep == 0:
keep = dice
res = roll_dice(keep, dice, size)
curr_str += "%d%s" % (res[0], res[1])
res = res[0]
elif m == "+":
mode = 1
curr_str += "+"
elif m == "-":
mode = -1
curr_str += "-"
else:
res = m
curr_str += str(m)
total += mode * res
output += "%d (%s)" % (total, curr_str)
if comment != None:
output = "%s: %s" % (comment.strip(), output)
return output
# General idea I had when creating this grammar: A roll string is a chain
# of modifiers, which may be repeated for a certain number of trials. It can
# have a comment that describes the roll
# Multiple roll strings can be chained with semicolon
def p_roll_r(p):
'roll : roll ";" roll'
global output
p[0] = p[1] + "; " + p[3]
output = p[0]
# The basic roll string
def p_roll(p):
'roll : trial modifier comment'
global output
mods = []
if type(p[2]) == list:
mods = p[2]
else:
mods = [p[2]]
p[0] = process_roll(p[1], mods, p[3])
output = p[0]
# Trial is optional so have a rule without it
def p_roll_no_trials(p):
'roll : modifier comment'
global output
mods = []
if type(p[1]) == list:
mods = p[1]
else:
mods = [p[1]]
p[0] = process_roll(None, mods, p[2])
output = p[0]
def p_comment(p):
'''comment : TEXT
|'''
if len(p) == 2:
p[0] = p[1]
else:
p[0] = None
def p_modifier(p):
'''modifier : modifier "+" modifier
| modifier "-" modifier'''
# Use append to prevent nested lists (makes dealing with this easier)
if type(p[1]) == list:
p[1].append(p[2])
p[1].append(p[3])
p[0] = p[1]
elif type(p[3]) == list:
p[3].insert(0, p[2])
p[3].insert(0, p[1])
p[0] = p[3]
else:
p[0] = [p[1], p[2], p[3]]
# Return the left side before the "d", and the number of faces
def p_die(p):
'modifier : left NUMBER'
p[0] = (p[1], p[2])
def p_die_num(p):
'modifier : NUMBER'
p[0] = p[1]
# left is the number of dice we are rolling, and how many we are keeping
def p_left(p):
'left : keep dice'
if p[1] == None:
p[0] = [None, p[2]]
else:
p[0] = [p[1], p[2]]
def p_left1(p):
'left : dice'
p[0] = [None, p[1]]
def p_left_e(p):
'left :'
p[0] = None
def p_total(p):
'trial : NUMBER "#"'
if len(p) > 1:
p[0] = p[1]
else:
p[0] = None
def p_keep(p):
'keep : NUMBER "/"'
if p[1] != None:
p[0] = p[1]
else:
p[0] = None
def p_dice(p):
'dice : NUMBER "d"'
p[0] = p[1]
def p_dice2(p):
'dice : "d"'
p[0] = 1
# Provide the user with something (albeit not much) when the roll can't be parsed
def p_error(p):
print "Unable to parse roll"
print yacc.token()
global output
output = "Unable to parse roll"
def get_result():
global output
return output
import ply.yacc as yacc
yacc.yacc()
# Testing stuff:
# rolls = (
# "5#3/4d20+1+2+d20;3/4d20;4d20",
# "d20+10 attack;d8+6 damage",
# "2#d20+10 twin strike!111;d10+9 damage",
# "d20+d20;d20+d20;d20+d20+d20",
# "error"
# )
# for roll in rolls:
# print "***** trying roll " + roll
# yacc.parse(roll, debug=0)
# print output