Pythonic Dice

Project Description: Dice

Develop two functions that model dice, so they may be used in a D&D simulation.
One should be based on the other to avoid code duplication.
They should be Pythonic, easy to use, and make no assumptions.

The first function will model a single dice of a given size.
Minimum dice size is 1. A 1 sided dice should always return 1.
Maximum dice size is 100. A 100 sided dice should always return in the range 1-100. Output requires a uniform or flat probability distribution within the proper range. The second will model a given number of dice of a given size and returns the sum. The smallest dice needed is 1d1, the biggest is 100d100, anything in-between is also fair game even if it would be geometrically impossible, 1d5 for example.

Project Specification: Dice

Python Function: d(sides)

@guard sides must be an int in range 1-100, inclusive.
@param sides: Dice Size: represents the number of dice sides or faces.
@return: Rolled Value: a number within the range 1 to sides, inclusive with a flat distribution.

Python Function: dice(rolls, sides)

@guard rolls must be an int in range 1-100, inclusive.
@param rolls: Number of iterations of d(sides): represents the number of dice rolls.
@param sides: Dice Size: represents the number of dice sides or faces.
@return: Sum Total of the dice rolled. Matches the probability distribution of real dice over a sufficiently large data set.

First Attempt, down and dirty!

#!/usr/bin/python3
# file name: dice.py
from random import randint


def d(sides):
    return randint(1, sides)


def dice(rolls, sides):
    return sum(d(sides) for _ in range(rolls))

Example Usage in Python Console

>>> from dice import d, dice
>>> d(20)
15
>>> dice(6, 8)
22

The code above is not the most performant way to roll digital dice, but Python is more about beautiful, expressive, concise code than it is about performance. If we need more than a few million dice rolled per second then we should look to C++ or another high performance language. C++ would be a great choice because it’s very fast and we can use it to extend Python, giving us the best of both worlds.

Before we jump down the C++ rabbit hole, there are some bugs, or at least some assumptions that we should address. What happens when someone tries to roll a zero sided dice? The way we’ve used the randint function above doesn’t play nice with zero as an input. How about if they try rolling a negative number of dice? Regardless of what the range function does with negative numbers, what does it mean to roll a negative number of dice? And what exactly is a negatively sided dice? The following code shows one way we might answer these questions.

Second Attempt, better but not perfect

#!/usr/bin/python3
# file name: dice.py
from random import randint


def d(sides):
    assert sides > 0, "Spacetime Error, number of sides must be greater than zero."
    return randint(1, sides)


def dice(rolls, sides):
    assert rolls > 0, "Spacetime Error, number of rolls must be greater than zero."
    return sum(d(sides) for _ in range(rolls))

OMG more bugs! What if someone tries to roll a million billion sided dice a million billion times? And what if someone sends in a floating point number as the number of sides or rolls? Perfectly valid concerns. I don’t believe I’ve seen anything bigger than a d100 in an RPG… It might be subtle, but in the third attempt (below), we manage to solve all three issues of upper bound, lower bound and value type with one check per function. Fail early and loudly.

Third Attempt, here we go!

#!/usr/bin/python3
# file name: dice.py
from random import randint


def d(sides):
    min_sides = 1
    max_sides = 100
    input_error = f"Input Error sides = {sides}, number of sides must be an integer in range {min_sides} - {max_sides}."
    assert sides in range(min_sides, max_sides + 1), input_error
    return randint(1, sides)


def dice(rolls, sides):
    min_rolls = 1
    max_rolls = 100
    input_error = f"Input Error rolls = {rolls}, number of rolls must be an integer in range {min_rolls} - {max_rolls}."
    assert rolls in range(min_rolls, max_rolls + 1), input_error
    return sum(d(sides) for _ in range(rolls))

Feel free to change the maximum limit for rolls and sides, 100 is quite low for Python. As a challenge, think about how you would change the code, such that the number of rolls times the number of sides is always less than the maximum Python integer. I leave it to you to implement that idea. Happy homework! For those of you that take the time to figure it out and play with it – I bet each of you will learn something cool about Python.

Next time we’ll look at dice in the deep-dark world of C++, and then some time in the future we’ll see about turning that mysterious C++ code into a beautiful Python C-extension.

Until then, be creative!