Repository: sopel-irc/sopel

Issue:
.dice breaks if you try to roll a die with no sides
Repro:

```
(8:55:43 PM) creftos: .roll 1d0
(8:55:43 PM) Willie: ValueError: empty range for randrange() (1,1, 0) (file "/usr/lib64/python2.7/random.py", line 217, in randrange)
```

Logs:

```
    Traceback (most recent call last):
      File "/home/bdc/workspace/willie/willie/bot.py", line 741, in call
        exit_code = func(willie, trigger)
      File "/home/bdc/workspace/willie/willie/modules/dice.py", line 180, in roll
        dice = list(map(_roll_dice, dice_expressions))
      File "/home/bdc/workspace/willie/willie/modules/dice.py", line 140, in _roll_dice
        dice = DicePouch(dice_num, dice_type, 0)
      File "/home/bdc/workspace/willie/willie/modules/dice.py", line 34, in __init__
        self.roll_dice()
      File "/home/bdc/workspace/willie/willie/modules/dice.py", line 41, in roll_dice
        number = random.randint(1, self.type)
      File "/usr/lib64/python2.7/random.py", line 241, in randint
        return self.randrange(a, b+1)
      File "/usr/lib64/python2.7/random.py", line 217, in randrange
        raise ValueError, "empty range for randrange() (%d,%d, %d)" % (istart, istop, width)
    ValueError: empty range for randrange() (1,1, 0)
```

Proposed Solution:
Needs a catch to make sure user can't do this.

Additional Information:
Willie v. 4.5.1
OS: Fedora release 20 (Heisenbug)
Kernel Version: 3.16.2-201.fc20.x86_64



Discussion:
Should rolling d1s also be disallowed or at least caught and switched out
for a constant?

@firerogue Good question. I don't see any reason why we should disallow d1's since they do give a valid result even if there's only one possibility. d0's cannot give a valid result because there isn't one. (Like dividing by zero.) Also, I suppose it could save a tiny bit of processing power to switch out the processing of a d1 for a constant result, but unless we're planning to scale to thousands (or millions) of rolls per second, I don't think it's something we need to worry about. :)


Resolving patch:
diff --git a/willie/modules/dice.py b/willie/modules/dice.py
--- a/willie/modules/dice.py
+++ b/willie/modules/dice.py
@@ -117,7 +117,7 @@ def get_number_of_faces(self):
         return len(self.dice) + len(self.dropped)
 
 
-def _roll_dice(dice_expression):
+def _roll_dice(bot, dice_expression):
     result = re.search(
         r"""
         (?P<dice_num>\d*)
@@ -131,11 +131,17 @@ def _roll_dice(dice_expression):
     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
+
     # 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:
-        return None
+        bot.reply('I only have 1000 dice. =(')
+        return None # Signal there was a problem
 
     dice = DicePouch(dice_num, dice_type, 0)
 
@@ -171,13 +177,18 @@ def roll(bot, trigger):
     # 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 not trigger.group(2):
+        return bot.reply("No dice to roll.")
     arg_str = trigger.group(2)
     dice_expressions = re.findall(dice_regexp, arg_str)
     arg_str = arg_str.replace("%", "%%")
     arg_str = re.sub(dice_regexp, "%s", arg_str)
-    dice = list(map(_roll_dice, dice_expressions))
+
+    f = lambda dice_expr: _roll_dice  (bot, dice_expr)
+    dice = list(map(f, dice_expressions))
+
     if None in dice:
-        bot.reply("I only have 1000 dice. =(")
+        # Stop computing roll if there was a problem rolling dice.
         return
 
     def _get_eval_str(dice):


Test patch: