rewrite dice module

This commit is contained in:
iou1name 2020-01-08 09:27:34 -05:00
parent a7b0de90bf
commit d36d5770f0
2 changed files with 47 additions and 225 deletions

View File

@ -2,242 +2,50 @@
""" """
Dice rolling, the core function of any IRC bot. Dice rolling, the core function of any IRC bot.
""" """
import random
import re import re
import operator import random
import module 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<dice_num>-?\d*)
d
(?P<dice_type>-?\d+)
(v(?P<drop_lowest>-?\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.commands("roll", "dice", "d")
@module.example(".roll 3d1+1", "You roll 3d1+1: (1+1+1)+1 = 4") @module.example(".roll 3d1+1", "You roll 3d1+1: (1+1+1)+1 = 4")
def roll(bot, trigger): 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 X is the number of dice. Y is the number of faces in the dice. Z is
number of lowest dice to be dropped from the result. N is the constant to the constant to be applied to the end result.
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.
""" """
if len(trigger.args) < 2: if len(trigger.args) < 2:
return bot.reply("I'd choose an option, but you didn't give me any.") return bot.reply("Roll what?")
msg = ' '.join(trigger.args[1:]) regex = re.compile(r'(\d+)?d(\d+)([+-]\d+)?')
choices = [msg] reg = re.match(regex, trigger.args[1])
for delim in '|\\/,': if not reg:
choices = msg.split(delim) return bot.reply("Invalid dice notation.")
if len(choices) > 1: num_dice = int(reg.groups()[0]) if reg.groups()[0] else 1
break num_sides = int(reg.groups()[1])
# Use a different delimiter in the output, to prevent ambiguity. modifier = int(reg.groups()[2]) if reg.groups()[2] else 0
for show_delim in ',|/\\':
if show_delim not in msg:
show_delim += ' '
break
pick = random.choice(choices) num_dice = min(num_dice, 100)
msg = f"Your options: {show_delim.join(choices)}. My choice: {pick}" 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) bot.reply(msg)

View File

@ -32,6 +32,20 @@ def rand(bot, trigger):
bot.reply(f"random({low}, {high}) = {number}") 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') @commands('letters')
@example('.letters', 'iloynlle') @example('.letters', 'iloynlle')
@example('.letters 16', 'oaotordbwaauouxk') @example('.letters 16', 'oaotordbwaauouxk')