diff --git a/modules/Dice.py b/modules/Dice.py index 7674e99..e03927f 100644 --- a/modules/Dice.py +++ b/modules/Dice.py @@ -22,77 +22,234 @@ from extlib import irclib from Module import Module +import ply.lex as lex +import ply.yacc as yacc + # Rolls dice, for RPGs mostly class Dice(Module): + tokens = ['NUMBER', 'TEXT'] + literals = ['#', '/', '+', '-', 'd', ';'] + + def build(self): + lex.lex(module=self) + yacc.yacc(module=self) + + t_TEXT = r'\s+[^;]+' + + 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', ';'), + ('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(self, 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(self, 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 = 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 + 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(self, p): + 'roll : roll ";" roll' + global output + p[0] = p[1] + "; " + p[3] + output = p[0] + + # The basic roll string + def p_roll(self, p): + '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] + + # Trial is optional so have a rule without it + def p_roll_no_trials(self, p): + '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): + '''comment : TEXT + |''' + if len(p) == 2: + p[0] = p[1] + else: + p[0] = None + + def p_modifier(self, 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(self, p): + 'modifier : left NUMBER' + p[0] = (p[1], p[2]) + + def p_die_num(self, 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(self, p): + '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 + + # Provide the user with something (albeit not much) when the roll can't be parsed + def p_error(self, p): + global output + output = "Unable to parse roll" + + def get_result(self): + global output + return output + def do(self, connection, event, nick, userhost, replypath, what, admin_unlocked): 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:]) + self.build() + yacc.parse(dicestr) + reply = self.get_result() if reply is not "": return self.reply(connection, replypath, nick + ': ' + reply) if whats[0] == 'ctech':