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.
"""
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<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.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)

View File

@ -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')