diff --git a/modules/dice.py b/modules/dice.py index 316f109..a6cca7c 100755 --- a/modules/dice.py +++ b/modules/dice.py @@ -2,242 +2,50 @@ """ Dice rolling, the core function of any IRC bot. """ -import random import re -import operator +import random import module -from tools.calculation import eval_equation - - -class DicePouch: - def __init__(self, num_of_die, type_of_die, addition): - """ - Initialize dice pouch and roll the dice. - - Args: - num_of_die: number of dice in the pouch. - type_of_die: how many faces the dice have. - addition: how much is added to the result of the dice. - """ - self.num = num_of_die - self.type = type_of_die - self.addition = addition - - self.dice = {} - self.dropped = {} - - self.roll_dice() - - def roll_dice(self): - """Roll all the dice in the pouch.""" - self.dice = {} - self.dropped = {} - for __ in range(self.num): - number = random.randint(1, self.type) - count = self.dice.setdefault(number, 0) - self.dice[number] = count + 1 - - def drop_lowest(self, n): - """ - Drop n lowest dice from the result. - - Args: - n: the number of dice to drop. - """ - - sorted_x = sorted(self.dice.items(), key=operator.itemgetter(0)) - - for i, count in sorted_x: - count = self.dice[i] - if n == 0: - break - elif n < count: - self.dice[i] = count - n - self.dropped[i] = n - break - else: - self.dice[i] = 0 - self.dropped[i] = count - n = n - count - - for i, count in self.dropped.items(): - if self.dice[i] == 0: - del self.dice[i] - - def get_simple_string(self): - """Return the values of the dice like (2+2+2[+1+1])+1.""" - dice = self.dice.items() - faces = ("+".join([str(face)] * times) for face, times in dice) - dice_str = "+".join(faces) - - dropped_str = "" - if self.dropped: - dropped = self.dropped.items() - dfaces = ("+".join([str(face)] * times) for face, times in dropped) - dropped_str = "[+%s]" % ("+".join(dfaces),) - - plus_str = "" - if self.addition: - plus_str = "{:+d}".format(self.addition) - - return "(%s%s)%s" % (dice_str, dropped_str, plus_str) - - def get_compressed_string(self): - """Return the values of the dice like (3x2[+2x1])+1.""" - dice = self.dice.items() - faces = ("%dx%d" % (times, face) for face, times in dice) - dice_str = "+".join(faces) - - dropped_str = "" - if self.dropped: - dropped = self.dropped.items() - dfaces = ("%dx%d" % (times, face) for face, times in dropped) - dropped_str = "[+%s]" % ("+".join(dfaces),) - - plus_str = "" - if self.addition: - plus_str = "{:+d}".format(self.addition) - - return "(%s%s)%s" % (dice_str, dropped_str, plus_str) - - def get_sum(self): - """Get the sum of non-dropped dice and the addition.""" - result = self.addition - for face, times in self.dice.items(): - result += face * times - return result - - def get_number_of_faces(self): - """ - Returns sum of different faces for dropped and not dropped dice - - This can be used to estimate, whether the result can be shown in - compressed form in a reasonable amount of space. - """ - return len(self.dice) + len(self.dropped) - - -def _roll_dice(bot, dice_expression): - result = re.search( - r""" - (?P-?\d*) - d - (?P-?\d+) - (v(?P-?\d+))? - $""", - dice_expression, - re.IGNORECASE | re.VERBOSE) - - dice_num = int(result.group('dice_num') or 1) - dice_type = int(result.group('dice_type')) - - # Dice can't have zero or a negative number of sides. - if dice_type <= 0: - bot.reply("I don't have any dice with %d sides. =(" % dice_type) - return None # Signal there was a problem - - # Can't roll a negative number of dice. - if dice_num < 0: - bot.reply("I'd rather not roll a negative amount of dice. =(") - return None # Signal there was a problem - - # Upper limit for dice should be at most a million. Creating a dict with - # more than a million elements already takes a noticeable amount of time - # on a fast computer and ~55kB of memory. - if dice_num > 1000: - bot.reply('I only have 1000 dice. =(') - return None # Signal there was a problem - - dice = DicePouch(dice_num, dice_type, 0) - - if result.group('drop_lowest'): - drop = int(result.group('drop_lowest')) - if drop >= 0: - dice.drop_lowest(drop) - else: - bot.reply("I can't drop the lowest %d dice. =(" % drop) - - return dice @module.commands("roll", "dice", "d") @module.example(".roll 3d1+1", "You roll 3d1+1: (1+1+1)+1 = 4") def roll(bot, trigger): """ - .dice XdY[vZ][+N], rolls dice and reports the result. + .dice XdY[+Z], rolls dice and reports the result. - X is the number of dice. Y is the number of faces in the dice. Z is the - number of lowest dice to be dropped from the result. N is the constant to - be applied to the end result. - """ - # This regexp is only allowed to have one captured group, because having - # more would alter the output of re.findall. - dice_regexp = r"-?\d*[dD]-?\d+(?:[vV]-?\d+)?" - - # Get a list of all dice expressions, evaluate them and then replace the - # expressions in the original string with the results. Replacing is done - # using string formatting, so %-characters must be escaped. - if len(trigger.args) < 2: - return bot.reply("No dice to roll.") - arg_str = ' '.join(trigger.args[1:]) - dice_expressions = re.findall(dice_regexp, arg_str) - arg_str = arg_str.replace("%", "%%") - arg_str = re.sub(dice_regexp, "%s", arg_str) - - f = lambda dice_expr: _roll_dice(bot, dice_expr) - dice = list(map(f, dice_expressions)) - - if None in dice: - # Stop computing roll if there was a problem rolling dice. - return - - def _get_eval_str(dice): - return "(%d)" % (dice.get_sum(),) - - def _get_pretty_str(dice): - if dice.num <= 10: - return dice.get_simple_string() - elif dice.get_number_of_faces() <= 10: - return dice.get_compressed_string() - else: - return "(...)" - - eval_str = arg_str % (tuple(map(_get_eval_str, dice))) - pretty_str = arg_str % (tuple(map(_get_pretty_str, dice))) - - # Showing the actual error will hopefully give a better hint of what is - # wrong with the syntax than a generic error message. - try: - result = eval_equation(eval_str) - except Exception as e: - bot.reply("SyntaxError, eval(%s), %s" % (eval_str, e)) - return - - bot.reply("You roll %s: %s = %d" % ( - ' '.join(trigger.args[1:]), pretty_str, result)) - - -@module.commands("choice", "choose") -@module.example(".choose opt1,opt2,opt3") -def choose(bot, trigger): - """ - .choice option1|option2|option3 - Makes a difficult choice easy. + X is the number of dice. Y is the number of faces in the dice. Z is + the constant to be applied to the end result. """ if len(trigger.args) < 2: - return bot.reply("I'd choose an option, but you didn't give me any.") - msg = ' '.join(trigger.args[1:]) - choices = [msg] - for delim in '|\\/,': - choices = msg.split(delim) - if len(choices) > 1: - break - # Use a different delimiter in the output, to prevent ambiguity. - for show_delim in ',|/\\': - if show_delim not in msg: - show_delim += ' ' - break + return bot.reply("Roll what?") + regex = re.compile(r'(\d+)?d(\d+)([+-]\d+)?') + reg = re.match(regex, trigger.args[1]) + if not reg: + return bot.reply("Invalid dice notation.") + num_dice = int(reg.groups()[0]) if reg.groups()[0] else 1 + num_sides = int(reg.groups()[1]) + modifier = int(reg.groups()[2]) if reg.groups()[2] else 0 - pick = random.choice(choices) - msg = f"Your options: {show_delim.join(choices)}. My choice: {pick}" + num_dice = min(num_dice, 100) + num_sides = min(num_sides, 1000) + + results = [] + for _ in range(num_dice): + result = random.randint(0, num_sides) + 1 + results.append(result) + total = sum(results) + modifier + + msg = f"You roll {num_dice}d{num_sides}" + if modifier: + if modifier > 0: + msg += '+' + msg += str(modifier) + msg += ': ' + msg += '(' + '+'.join([str(a) for a in results]) + ')' + if modifier: + if modifier > 0: + msg += '+' + msg += str(modifier) + msg += " = " + str(total) bot.reply(msg) diff --git a/modules/rand.py b/modules/rand.py index 675ea53..6d920e3 100755 --- a/modules/rand.py +++ b/modules/rand.py @@ -32,6 +32,20 @@ def rand(bot, trigger): bot.reply(f"random({low}, {high}) = {number}") +@commands("choice", "choose") +@example(".choose opt1|opt2|opt3") +def choose(bot, trigger): + """ + .choice option1|option2|option3 - Makes a difficult choice easy. + """ + if len(trigger.args) < 2: + return bot.reply("Choose what?") + choices = trigger.args[1].split('|') + pick = random.choice(choices) + msg = f"Your options: {'|'.join(choices)}. My choice: {pick}" + bot.reply(msg) + + @commands('letters') @example('.letters', 'iloynlle') @example('.letters 16', 'oaotordbwaauouxk')