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 int
s 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!
Obviously, if you get nearly all of your showdowns,
you ought to be a profitable participant.