First set of updates from the major "backendification" rewrite #1
| @ -5,9 +5,7 @@ import random | ||||
| 
 | ||||
| from irc.client import NickMask | ||||
| 
 | ||||
| import ply.lex as lex | ||||
| import ply.yacc as yacc | ||||
| 
 | ||||
| from dice.roller import DiceRoller | ||||
| from ircbot.lib import Plugin | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| @ -60,265 +58,17 @@ class Dice(Plugin): | ||||
|         dicestr = match.group(1) | ||||
| 
 | ||||
|         logger.debug(event.recursing) | ||||
|         try: | ||||
|             reply_str = self.roller.do_roll(dicestr) | ||||
|         except AssertionError as aex: | ||||
|             reply_str = f"Could not roll dice: {aex}" | ||||
|         except ValueError: | ||||
|             reply_str = "Unable to parse roll" | ||||
| 
 | ||||
|         if event.recursing: | ||||
|             reply = "{0:s}".format(self.roller.do_roll(dicestr)) | ||||
|             reply = "{0:s}".format(reply_str) | ||||
|         else: | ||||
|             reply = "{0:s}: {1:s}".format(nick, self.roller.do_roll(dicestr)) | ||||
|             reply = "{0:s}: {1:s}".format(nick, reply_str) | ||||
|         return self.bot.reply(event, re.sub(r'(\d+)(.*?\s+)(\(.*?\))', r'\1\214\3', reply)) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class DiceRoller(object): | ||||
| 
 | ||||
|     tokens = ['NUMBER', 'TEXT', 'ROLLSEP'] | ||||
|     literals = ['#', '/', '+', '-', 'd'] | ||||
| 
 | ||||
|     t_TEXT = r'\s+[^;]+' | ||||
|     t_ROLLSEP = r';\s*' | ||||
| 
 | ||||
|     def build(self): | ||||
|         lex.lex(module=self) | ||||
|         yacc.yacc(module=self) | ||||
| 
 | ||||
|     def t_NUMBER(self, t): | ||||
|         r'\d+' | ||||
|         t.value = int(t.value) | ||||
|         return t | ||||
| 
 | ||||
|     def t_error(self, t): | ||||
|         t.lexer.skip(1) | ||||
| 
 | ||||
|     precedence = ( | ||||
|         ('left', 'ROLLSEP'), | ||||
|         ('left', '+', '-'), | ||||
|         ('right', 'd'), | ||||
|         ('left', '#'), | ||||
|         ('left', '/') | ||||
|     ) | ||||
| 
 | ||||
|     output = "" | ||||
| 
 | ||||
|     def roll_dice(self, keep, dice, size): | ||||
|         """Takes the parsed dice string for a single roll (eg 3/4d20) and performs | ||||
|         the actual roll. Returns a string representing the result. | ||||
|         """ | ||||
| 
 | ||||
|         a = list(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 = "[" + ",".join(str(i) for i in a) + "]" | ||||
| 
 | ||||
|         return (total, outstr) | ||||
| 
 | ||||
|     def process_roll(self, trials, mods, comment): | ||||
|         """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). | ||||
|         """ | ||||
| 
 | ||||
|         output = "" | ||||
|         repeat = 1 | ||||
|         if trials != None: | ||||
|             repeat = trials | ||||
|         for i in range(repeat): | ||||
|             mode = 1 | ||||
|             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 | ||||
|                     if size < 1: | ||||
|                         output = "# of sides for die is incorrect: %d" % size | ||||
|                         return output | ||||
|                     if dice < 1: | ||||
|                         output = "# of dice is incorrect: %d" % dice | ||||
|                         return output | ||||
|                     res = self.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 | ||||
|             if repeat == 1: | ||||
|                 if comment != None: | ||||
|                     output = "%d %s (%s)" % (total, comment.strip(), curr_str) | ||||
|                 else: | ||||
|                     output = "%d (%s)" % (total, curr_str) | ||||
|             else: | ||||
|                 output += "%d (%s)" % (total, curr_str) | ||||
|                 if i == repeat - 1: | ||||
|                     if comment != None: | ||||
|                         output += " (%s)" % (comment.strip()) | ||||
|         return output | ||||
| 
 | ||||
|     def p_roll_r(self, p): | ||||
|         # Chain rolls together. | ||||
| 
 | ||||
|         # 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 | ||||
| 
 | ||||
|         'roll : roll ROLLSEP roll' | ||||
|         global output | ||||
|         p[0] = p[1] + "; " + p[3] | ||||
|         output = p[0] | ||||
| 
 | ||||
|     def p_roll(self, p): | ||||
|         # Parse a basic roll string. | ||||
| 
 | ||||
|         'roll : trial modifier comment' | ||||
|         global output | ||||
|         mods = [] | ||||
|         if type(p[2]) == list: | ||||
|             mods = p[2] | ||||
|         else: | ||||
|             mods = [p[2]] | ||||
|         p[0] = self.process_roll(p[1], mods, p[3]) | ||||
|         output = p[0] | ||||
| 
 | ||||
|     def p_roll_no_trials(self, p): | ||||
|         # Parse a roll string without trials. | ||||
| 
 | ||||
|         'roll : modifier comment' | ||||
|         global output | ||||
|         mods = [] | ||||
|         if type(p[1]) == list: | ||||
|             mods = p[1] | ||||
|         else: | ||||
|             mods = [p[1]] | ||||
|         p[0] = self.process_roll(None, mods, p[2]) | ||||
|         output = p[0] | ||||
| 
 | ||||
|     def p_comment(self, p): | ||||
|         # Parse a comment. | ||||
| 
 | ||||
|         '''comment : TEXT | ||||
|                    |''' | ||||
|         if len(p) == 2: | ||||
|             p[0] = p[1] | ||||
|         else: | ||||
|             p[0] = None | ||||
| 
 | ||||
|     def p_modifier(self, p): | ||||
|         # Parse a modifier on a roll string. | ||||
| 
 | ||||
|         '''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]] | ||||
| 
 | ||||
|     def p_die(self, p): | ||||
|         # Return the left side before the "d", and the number of faces. | ||||
| 
 | ||||
|         'modifier : left NUMBER' | ||||
|         p[0] = (p[1], p[2]) | ||||
| 
 | ||||
|     def p_die_num(self, p): | ||||
|         'modifier : NUMBER' | ||||
|         p[0] = p[1] | ||||
| 
 | ||||
|     def p_left(self, p): | ||||
|         # Parse the number of dice we are rolling, and how many we are keeping. | ||||
|         'left : keep dice' | ||||
|         if p[1] == None: | ||||
|             p[0] = [None, p[2]] | ||||
|         else: | ||||
|             p[0] = [p[1], p[2]] | ||||
| 
 | ||||
|     def p_left_all(self, p): | ||||
|         'left : dice' | ||||
|         p[0] = [None, p[1]] | ||||
| 
 | ||||
|     def p_left_e(self, p): | ||||
|         'left :' | ||||
|         p[0] = None | ||||
| 
 | ||||
|     def p_total(self, p): | ||||
|         'trial : NUMBER "#"' | ||||
|         if len(p) > 1: | ||||
|             p[0] = p[1] | ||||
|         else: | ||||
|             p[0] = None | ||||
| 
 | ||||
|     def p_keep(self, p): | ||||
|         'keep : NUMBER "/"' | ||||
|         if p[1] != None: | ||||
|             p[0] = p[1] | ||||
|         else: | ||||
|             p[0] = None | ||||
| 
 | ||||
|     def p_dice(self, p): | ||||
|         'dice : NUMBER "d"' | ||||
|         p[0] = p[1] | ||||
| 
 | ||||
|     def p_dice_one(self, p): | ||||
|         'dice : "d"' | ||||
|         p[0] = 1 | ||||
| 
 | ||||
|     def p_error(self, p): | ||||
|         # Provide the user with something (albeit not much) when the roll can't be parsed. | ||||
|         global output | ||||
|         output = "Unable to parse roll" | ||||
| 
 | ||||
|     def get_result(self): | ||||
|         global output | ||||
|         return output | ||||
| 
 | ||||
|     def do_roll(self, dicestr): | ||||
|         """ | ||||
|         Roll some dice and get the result (with broken out rolls). | ||||
| 
 | ||||
|         Keyword arguments: | ||||
|         dicestr - format: | ||||
|         N#X/YdS+M label | ||||
|             N#: do the following roll N times (optional) | ||||
|             X/: take the top X rolls of the Y times rolled (optional) | ||||
|             Y : roll the die specified Y times (optional, defaults to 1) | ||||
|             dS: roll a S-sided die | ||||
|             +M: add M to the result (-M for subtraction) (optional) | ||||
|         """ | ||||
|         self.build() | ||||
|         yacc.parse(dicestr) | ||||
|         return self.get_result() | ||||
| 
 | ||||
| 
 | ||||
| plugin = Dice | ||||
|  | ||||
							
								
								
									
										263
									
								
								dice/roller.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								dice/roller.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,263 @@ | ||||
| """Dice rollers used by the views, bots, etc.""" | ||||
| import logging | ||||
| import random | ||||
| 
 | ||||
| import ply.lex as lex | ||||
| import ply.yacc as yacc | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class DiceRoller(object): | ||||
| 
 | ||||
|     tokens = ['NUMBER', 'TEXT', 'ROLLSEP'] | ||||
|     literals = ['#', '/', '+', '-', 'd'] | ||||
| 
 | ||||
|     t_TEXT = r'\s+[^;]+' | ||||
|     t_ROLLSEP = r';\s*' | ||||
| 
 | ||||
|     def build(self): | ||||
|         lex.lex(module=self) | ||||
|         yacc.yacc(module=self) | ||||
| 
 | ||||
|     def t_NUMBER(self, t): | ||||
|         r'\d+' | ||||
|         t.value = int(t.value) | ||||
|         return t | ||||
| 
 | ||||
|     def t_error(self, t): | ||||
|         t.lexer.skip(1) | ||||
| 
 | ||||
|     precedence = ( | ||||
|         ('left', 'ROLLSEP'), | ||||
|         ('left', '+', '-'), | ||||
|         ('right', 'd'), | ||||
|         ('left', '#'), | ||||
|         ('left', '/') | ||||
|     ) | ||||
| 
 | ||||
|     output = "" | ||||
| 
 | ||||
|     def roll_dice(self, keep, dice, size): | ||||
|         """Takes the parsed dice string for a single roll (eg 3/4d20) and performs | ||||
|         the actual roll. Returns a string representing the result. | ||||
|         """ | ||||
| 
 | ||||
|         a = list(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 = "[" + ",".join(str(i) for i in a) + "]" | ||||
| 
 | ||||
|         return (total, outstr) | ||||
| 
 | ||||
|     def process_roll(self, trials, mods, comment): | ||||
|         """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). | ||||
|         """ | ||||
|         rolls = mods[0][0][1] if mods[0][0][1] else 1 | ||||
|         assert trials <= 10, "Too many rolls (max: 10)." | ||||
|         assert rolls <= 50, "Too many dice (max: 50) in roll." | ||||
| 
 | ||||
|         output = "" | ||||
|         repeat = 1 | ||||
|         if trials != None: | ||||
|             repeat = trials | ||||
|         for i in range(repeat): | ||||
|             mode = 1 | ||||
|             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 | ||||
|                     if size < 1: | ||||
|                         output = "# of sides for die is incorrect: %d" % size | ||||
|                         return output | ||||
|                     if dice < 1: | ||||
|                         output = "# of dice is incorrect: %d" % dice | ||||
|                         return output | ||||
|                     res = self.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 | ||||
|             if repeat == 1: | ||||
|                 if comment != None: | ||||
|                     output = "%d %s (%s)" % (total, comment.strip(), curr_str) | ||||
|                 else: | ||||
|                     output = "%d (%s)" % (total, curr_str) | ||||
|             else: | ||||
|                 output += "%d (%s)" % (total, curr_str) | ||||
|                 if i == repeat - 1: | ||||
|                     if comment != None: | ||||
|                         output += " (%s)" % (comment.strip()) | ||||
|         return output | ||||
| 
 | ||||
|     def p_roll_r(self, p): | ||||
|         # Chain rolls together. | ||||
| 
 | ||||
|         # 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 | ||||
| 
 | ||||
|         'roll : roll ROLLSEP roll' | ||||
|         global output | ||||
|         p[0] = p[1] + "; " + p[3] | ||||
|         output = p[0] | ||||
| 
 | ||||
|     def p_roll(self, p): | ||||
|         # Parse a basic roll string. | ||||
| 
 | ||||
|         'roll : trial modifier comment' | ||||
|         global output | ||||
|         mods = [] | ||||
|         if type(p[2]) == list: | ||||
|             mods = p[2] | ||||
|         else: | ||||
|             mods = [p[2]] | ||||
|         p[0] = self.process_roll(p[1], mods, p[3]) | ||||
|         output = p[0] | ||||
| 
 | ||||
|     def p_roll_no_trials(self, p): | ||||
|         # Parse a roll string without trials. | ||||
| 
 | ||||
|         'roll : modifier comment' | ||||
|         global output | ||||
|         mods = [] | ||||
|         if type(p[1]) == list: | ||||
|             mods = p[1] | ||||
|         else: | ||||
|             mods = [p[1]] | ||||
|         p[0] = self.process_roll(None, mods, p[2]) | ||||
|         output = p[0] | ||||
| 
 | ||||
|     def p_comment(self, p): | ||||
|         # Parse a comment. | ||||
| 
 | ||||
|         '''comment : TEXT | ||||
|                    |''' | ||||
|         if len(p) == 2: | ||||
|             p[0] = p[1] | ||||
|         else: | ||||
|             p[0] = None | ||||
| 
 | ||||
|     def p_modifier(self, p): | ||||
|         # Parse a modifier on a roll string. | ||||
| 
 | ||||
|         '''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]] | ||||
| 
 | ||||
|     def p_die(self, p): | ||||
|         # Return the left side before the "d", and the number of faces. | ||||
| 
 | ||||
|         'modifier : left NUMBER' | ||||
|         p[0] = (p[1], p[2]) | ||||
| 
 | ||||
|     def p_die_num(self, p): | ||||
|         'modifier : NUMBER' | ||||
|         p[0] = p[1] | ||||
| 
 | ||||
|     def p_left(self, p): | ||||
|         # Parse the number of dice we are rolling, and how many we are keeping. | ||||
|         'left : keep dice' | ||||
|         if p[1] == None: | ||||
|             p[0] = [None, p[2]] | ||||
|         else: | ||||
|             p[0] = [p[1], p[2]] | ||||
| 
 | ||||
|     def p_left_all(self, p): | ||||
|         'left : dice' | ||||
|         p[0] = [None, p[1]] | ||||
| 
 | ||||
|     def p_left_e(self, p): | ||||
|         'left :' | ||||
|         p[0] = None | ||||
| 
 | ||||
|     def p_total(self, p): | ||||
|         'trial : NUMBER "#"' | ||||
|         if len(p) > 1: | ||||
|             p[0] = p[1] | ||||
|         else: | ||||
|             p[0] = None | ||||
| 
 | ||||
|     def p_keep(self, p): | ||||
|         'keep : NUMBER "/"' | ||||
|         if p[1] != None: | ||||
|             p[0] = p[1] | ||||
|         else: | ||||
|             p[0] = None | ||||
| 
 | ||||
|     def p_dice(self, p): | ||||
|         'dice : NUMBER "d"' | ||||
|         p[0] = p[1] | ||||
| 
 | ||||
|     def p_dice_one(self, p): | ||||
|         'dice : "d"' | ||||
|         p[0] = 1 | ||||
| 
 | ||||
|     def p_error(self, p): | ||||
|         """Raise ValueError on unparseable strings.""" | ||||
|         raise ValueError("Error occurred in parser") | ||||
| 
 | ||||
|     def get_result(self): | ||||
|         global output | ||||
|         return output | ||||
| 
 | ||||
|     def do_roll(self, dicestr): | ||||
|         """ | ||||
|         Roll some dice and get the result (with broken out rolls). | ||||
| 
 | ||||
|         Keyword arguments: | ||||
|         dicestr - format: | ||||
|         N#X/YdS+M label | ||||
|             N#: do the following roll N times (optional) | ||||
|             X/: take the top X rolls of the Y times rolled (optional) | ||||
|             Y : roll the die specified Y times (optional, defaults to 1) | ||||
|             dS: roll a S-sided die | ||||
|             +M: add M to the result (-M for subtraction) (optional) | ||||
|         """ | ||||
|         self.build() | ||||
|         yacc.parse(dicestr) | ||||
|         return self.get_result() | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user