Use PLY to parse dice strings
This commit is contained in:
		
							parent
							
								
									1bc0dd0b2a
								
							
						
					
					
						commit
						4f56e40ca7
					
				| @ -22,6 +22,9 @@ from extlib import irclib | |||||||
| 
 | 
 | ||||||
| from Module import Module | from Module import Module | ||||||
| 
 | 
 | ||||||
|  | import diceply | ||||||
|  | import ply.yacc as yacc | ||||||
|  | 
 | ||||||
| # Rolls dice, for RPGs mostly | # Rolls dice, for RPGs mostly | ||||||
| 
 | 
 | ||||||
| class Dice(Module): | class Dice(Module): | ||||||
| @ -30,69 +33,9 @@ class Dice(Module): | |||||||
|         whats = what.split(' ') |         whats = what.split(' ') | ||||||
| 
 | 
 | ||||||
|         if whats[0] == 'roll': |         if whats[0] == 'roll': | ||||||
|             rollitrs = re.split(';\s*', ' '.join(whats[1:])) |             dicestr = ' '.join(whats[1:]) | ||||||
|             reply = "" |             yacc.parse(dicestr) | ||||||
|             for count, roll in enumerate(rollitrs): |             reply = diceply.get_result() | ||||||
|                 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 += "; " |  | ||||||
|             if reply is not "": |             if reply is not "": | ||||||
|                 return self.reply(connection, replypath, nick + ': ' + reply) |                 return self.reply(connection, replypath, nick + ': ' + reply) | ||||||
|         if whats[0] == 'ctech': |         if whats[0] == 'ctech': | ||||||
|  | |||||||
							
								
								
									
										246
									
								
								modules/diceply.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										246
									
								
								modules/diceply.py
									
									
									
									
									
										Executable 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 | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user