rewrite dice module
This commit is contained in:
parent
a7b0de90bf
commit
d36d5770f0
258
modules/dice.py
258
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<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)
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue
Block a user