# 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