Modeling Dice Games in Python: The Beginning

After spending a bit of time with ISBN and UPC parsing, I’ve decided that for the next bit of programming practice, I’ll take a bit of time to work with randomness, simulation, and maybe a bit of decision tree stuff. So, for the next couple of weeks or so, I’m going to spend some time modeling some popular dice games and seeing what I can learn about those games by proceeding “bottom up” (simulating a bunch of rolls) instead of “top down” (figuring out the odds using probability theory).

To do most of this work, I will need a basic model of a dice “hand” — a set of dice that are rolled and have some values. Note: It’s also possible, and perhaps more efficient, to do this using NumPy and making really big arrays of rolls… then processing things that way. However, that wouldn’t be a good mechanism for me to practice basic Python programming, so I’m not doing that!

Instead, I took a bit of time to build a simple class that represented a “hand” of dice. I created an underlying list of ints that represents the individual dice in the hand. I also built a couple of methods in the class that will help later to figure out various types of rolls and allow us to calculate them more easily.

Here is what the code looks like:

import random

class SimpleHand:
    def __init__(self, num_sides: int = 6, num_dice: int = 2, seed: int = None):
        self.rng = random.Random(seed)
        self.num_sides = num_sides
        self.num_dice = num_dice
        self.hand = [self.rng.randint(1, num_sides) for i in range(num_dice)]
        self.sorted_hand = None

    def __repr__(self):
        return str(self.hand)

    def _force_new(self, new_hand_list: list):
        # Sanity check
        if len(new_hand_list) != self.num_dice:
            raise Exception(f"Size of list [{len(new_hand_list)}] != number of dice in hand [{self.num_dice}]")
        for value in new_hand_list:
            if value > self.num_sides or value < 1:
                raise Exception(f"Improper value [{value}] for a hand with [{self.num_sides}] sides")
        self.hand = new_hand_list.copy()
        self.sorted_hand = None

    def roll_selected(self, positions: list):
        for i in positions:
            self.hand[i] = self.rng.randint(1, self.num_sides)
        self.sorted_hand = None

    def roll_all(self):
        self.roll_selected(list(range(self.num_dice)))

    def set_seed(self, new_seed: int = None):
        self.rng.seed(new_seed)

    def get(self) -> tuple:
        return tuple(self.hand)

    def get_sorted(self) -> tuple:
        if not self.sorted_hand:
            self.sorted_hand = sorted(self.hand)
        return tuple(self.sorted_hand)

    def get_counts(self) -> tuple:
        # TODO: Easiest implementation, but not very good performance
        """

        :return: A tuple containing the number of occurrences of each value indexed by die value
        """
        retval = tuple(self.hand.count(i) for i in range(1, self.num_sides+1))
        return retval

    def max_duplicates(self) -> int:
        return max(self.get_counts())

    def count_of(self, num: int) -> int:
        return self.hand.count(num)

    def num_sequential(self) -> int:
        sorted_hand = self.get_sorted()
        last_num = -1
        cur_sequence_length = 1
        max_sequence_length = 0
        for d in sorted_hand:
            if d == last_num + 1:
                cur_sequence_length += 1
            elif d == last_num:
                # Duplicate number case
                pass
            else:
                max_sequence_length = max(cur_sequence_length, max_sequence_length)
                cur_sequence_length = 1
            last_num = d
        return max(max_sequence_length, cur_sequence_length)

I also built some unit tests for this code. One notable piece of this is that you might notice the _force_new() method above. I created that so that I could, from the unit tests, force certain values into the hand in order to test the “analytical” methods like max_duplicates() and num_sequential(). In Python, beginning a method with an underscore indicates that it should be treated as “private” to that class and not used as part of the public interface.

I still need to tweak the usability of this a bit, but I think it will provide a good foundation for exploring a couple of popular dice games. Next up, we’ll try running some basic statistics and see how they line up with our calculated probabilities to make sure our model is working right!

1 thought on “Modeling Dice Games in Python: The Beginning”

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top