""" Dice - roll dice when asked, intended for RPGs Copyright (C) 2010 Brian S. Stephan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import math import re import random import ply.lex as lex import ply.yacc as yacc from Module import Module class Dice(Module): """Roll simple or complex dice strings.""" tokens = ['NUMBER', 'TEXT', 'ROLLSEP'] literals = ['#', '/', '+', '-', 'd'] def __init__(self, irc, config): """Set up XML-RPC methods.""" Module.__init__(self, irc, config) irc.xmlrpc_register_function(self.do_roll, "dice_roll") def build(self): lex.lex(module=self) yacc.yacc(module=self) t_TEXT = r'\s+[^;]+' t_ROLLSEP = 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', '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 = 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() def do(self, connection, event, nick, userhost, what, admin_unlocked): match = re.search('^!roll\s+(.*)$', what) if match: dicestr = match.group(1) reply = nick + ": " + self.do_roll(dicestr) # irc format it return self.irc.reply(event, re.sub(r"(\d+)(.*?\s+)(\(.*?\))", r"\1\214\3", reply)) match = re.search('^!ctech\s+(.*)$', what) if match: rollitrs = re.split(';\s*', match.group(1)) reply = "" for count, roll in enumerate(rollitrs): pattern = '^(\d+)d(?:(\+|\-)(\d+))?(?:\s+(.*))?' regex = re.compile(pattern) matches = regex.search(roll) if matches is not None: dice = int(matches.group(1)) modifier = 0 if matches.group(2) is not None and matches.group(3) is not None: if str(matches.group(2)) == '-': modifier = -1 * int(matches.group(3)) else: modifier = int(matches.group(3)) result = roll + ': ' rolls = [] for d in range(dice): rolls.append(random.randint(1, 10)) rolls.sort() rolls.reverse() # highest single die method method1 = rolls[0] # highest set method method2 = 0 rolling_sum = 0 for i, r in enumerate(rolls): # if next roll is same as current, sum and continue, else see if sum is best so far if i+1 < len(rolls) and rolls[i+1] == r: if rolling_sum == 0: rolling_sum = r rolling_sum += r else: if rolling_sum > method2: method2 = rolling_sum rolling_sum = 0 # check for set in progress (e.g. lots of 1s) if rolling_sum > method2: method2 = rolling_sum # straight method method3 = 0 rolling_sum = 0 count = 0 for i, r in enumerate(rolls): # if next roll is one less as current, sum and continue, else check len and see if sum is best so far if i+1 < len(rolls) and rolls[i+1] == r-1: if rolling_sum == 0: rolling_sum = r count += 1 rolling_sum += r-1 count += 1 else: if count >= 3 and rolling_sum > method3: method3 = rolling_sum rolling_sum = 0 # check for straight in progress (e.g. straight ending in 1) if count >= 3 and rolling_sum > method3: method3 = rolling_sum # get best roll best = max([method1, method2, method3]) # check for critical failure botch = False ones = 0 for r in rolls: if r == 1: ones += 1 if ones >= math.ceil(float(len(rolls))/2): botch = True if botch: result += 'BOTCH' else: result += str(best + modifier) rollres = '' for i,r in enumerate(rolls): rollres += str(r) if i is not len(rolls)-1: rollres += ',' result += ' [' + rollres if modifier != 0: if modifier > 0: result += ' +' + str(modifier) else: result += ' -' + str(modifier * -1) result += ']' reply += result if count is not len(rollitrs)-1: reply += "; " if reply is not "": return self.irc.reply(event, nick + ': ' + reply) # vi:tabstop=4:expandtab:autoindent # kate: indent-mode python;indent-width 4;replace-tabs on;