Keeping Score: The Beginnings of a Yahtzee Scorecard

Now that I’ve got a YahtzeeHand class that is able to detect all of the common Yahtzee hands, I want to develop a scorecard that will allow me to keep track of each score in the game. The tricky part about Yahtzee is that once you “score” a hand, you cannot score another hand of that type for the rest of the game. For example, once you use the Full House, if you roll another Full House, you have to either re-roll the whole thing, or count a triple or double of some type.

Because of this, I think I’ll build a smart-ish Scorecard class that can keep track of each type of hand and also suggest the “open” items available to still be scored. I’ll start off with making a dictionary of “scores” and a simple method to evaluate a hand and apply the score to this dictionary. We’ll also use a cool feature of Python, specifically the ability to store an actual function as the value in a dictionary item to calculate the scores. Here is what it looks like:

Yahtzee: The Last Brick in the Foundation

In the previous section, I talked about how our strong foundation was letting me write code very easily as I expanded my implementation. I need one last piece of foundational code in my hand class. For the Three of a Kind, Four of a Kind, and Chance rolls, the score for the rolls is the total of all dice in the hand. It seems like if I implement one more simple method to count the total of the individual dice in the hand, I’ll have a very solid foundation. Because of the way we have modeled the SimpleHand, this is a very simple method to write also (especially if we remember that our actual hand is represented as a list of the individual dice in the hand:

class YahtzeeHand(SimpleHand):
    def get_hand_total_score(self):
        return sum(self.hand)

Yahtzee in Python: Counting Individual Numbers

Now that I’ve got the “special” hands done, the other part I will need to support is the ability to score the “top of the card.” This means that I will need to be able to count (and score) ones, twos, threes, fours, fives, and sixes. The get_counts() and count_of() methods that we build in the previous post to help us with finding full houses will come in handy here. As a reminder, the methods in question look like this:

    def get_counts(self) -> tuple:
        Get the count of each number appearing in the hand.
        e.g. - a hand of [3, 3, 4, 4, 1] will return (1, 0, 2, 2, 0, 0)
        :return: A tuple of integers with the value at each index indicating the number of occurences of number n-1 in
        the hand.
        retval = tuple(self.hand.count(i) for i in range(1, self.num_sides+1))
        return retval

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

Using these two methods, I’ve got two ways to figure out the scores for the individual numbers. Simply, the score for an individual number is:

    def get_score_for_number(self, number):
        return self.count_of(number) * number

Hopefully it’s becoming apparent that taking the time to build some of the methods in the SimpleHand class is starting to pay off! The next step for me is to write one more helper method, update my unit tests and then put all of this together and use it to build a scorecard. Once I’m there, I’ll be able to simulate a full game!

Yahtzee: Finding the Full Houses

I think I’m making good progress on my implementation of a YahtzeeHand to analyze the game. However, I don’t have any logic right now to detect a Full House — three of one number combined with two of another number. This is a bit difficult to implement with the current logic, so I’m going to expand the SimpleHand class to include a new method: get_counts(). This method will return a tuple of size num_sides, where each item will indicate the number of dice in the hand bearing the same number as the index. For example, with the following hand [1, 4, 1, 4, 2], get_counts() would return (2, 1, 0, 2, 0, 0) — which would indicate 2 x ones, 1 x twos, 0 x threes, 2 x fours, and 0 fives and sixes. Having this functionality will really help discover full houses, and will also help a lot down the road when I have to figure out the “upper” part of the Yahtzee scorecard (ones, twos, threes, etc).

It turns out that get_counts() is pretty easy to implement with a comprehension:

Expanding the Yahtzee Simulation: Adding more hands

Now that I’ve got a solid foundation in SimpleHand, it’s relatively easy to expand the implementation of YahtzeeHand. This is a valuable aspect of inheritance and object oriented programming in general. I’m able to add the logic for 3, 4 and 5 (Yahtzee!) of a kind very easily. Here’s what it looks like all together in the YahtzeeHand class:

class YahtzeeHand(SimpleHand):
    def __init__(self, seed: int = 0):
        super(YahtzeeHand, self).__init__(num_sides=6, num_dice=5, seed=seed)

    def is_large_straight(self) -> bool:
        return self.num_sequential() == 5

    def is_small_straight(self) -> bool:
        return self.num_sequential() >= 4

    def is_triple(self) -> bool:
        return self.max_duplicates() >= 3

    def is_four_of_a_kind(self) -> bool:
        return self.max_duplicates() >= 4

    def is_yahtzee(self) -> bool:
        return self.max_duplicates() >= 5

I’ve got enough here now that I can run a quick simulation to answer some basic questions about Yahtzee — specifically how difficult it is to make each type of hand on an initial roll. Because I’ve got the basic Yahtzee class built, the code to run 100,000 rolls and collect specifics flows pretty easily:

def verify_yahtzee_hand(num_rolls: int = 10000):
    h = YahtzeeHand()

    num_special_hands = {
        "LargeStraight": 0,
        "SmallStraight": 0,
        "3ofaKind": 0,
        "4ofaKind": 0,
        "Yahtzee!": 0

    for i in range(num_rolls):
        if h.is_small_straight():
            num_special_hands["SmallStraight"] += 1
        if h.is_large_straight():
            num_special_hands["LargeStraight"] += 1
        if h.is_triple():
            num_special_hands["3ofaKind"] += 1
        if h.is_four_of_a_kind():
            num_special_hands["4ofaKind"] += 1
        if h.is_yahtzee():
            num_special_hands["Yahtzee!"] += 1

    print(f"{num_rolls} rolls:")

    for (name, occurrences) in num_special_hands.items():
        print(f"{name:20} : {occurrences:8d} / {num_rolls} -- {(occurrences / num_rolls):>8.2%}")

When we run the sim over 100,000 rolls, we get this result:

100000 rolls:
LargeStraight        :     3034 / 100000 --    3.03%
SmallStraight        :    15357 / 100000 --   15.36%
3ofaKind             :    21388 / 100000 --   21.39%
4ofaKind             :     2071 / 100000 --    2.07%
Yahtzee!             :       70 / 100000 --    0.07%

This yields some interesting results… namely, a “natural” Yahtzee (Yahtzee in one roll) is pretty tough to get! But also, a small straight (4 consecutive numbers) is relatively easy (about one chance in six). We are missing one major type of hand still: the full house. I’m going to need to build a bit of special logic to handle that, so I’ll do that in the next post!

A Simple Yahtzee Simulation in Python

This is the beginning of what will be a multi-part series on simulating the game Yahtzee in Python. It builds upon some of the previous work I’ve done with the simple dice hand simulator.

Using this simulator, let’s see how we can extend it to model some common dice games. An important thing to realize is that there are two basic ways to understand events that involve chance: theory or simulation. This is very similar to the split between theoretical and experimental physics. Each approach has advantages and disadvantages, however my goal is to practice my Python programming, so using simulation to understand complex problems seems much more appropriate than trying to mathematically model dice games using probability theory.

Yahtzee is a game that I’ve been playing a bit lately and was wondering about some of the probabilities / strategies, so I’ve decided to explore that first.

Using SimpleHand as a base class, we can extend it to build the concept of a YahtzeeHand that is able to count the differing types of hands that appear in the game Yahtzee. We will start with a few of the most basic Yahtzee hands — Small Straight (4 sequential number), Large Straight (5 sequential numbers), and a Full House (a triple and a double in the same hand). Notice how that we can use the foundation we’ve built to make the implementation of this quite easy:

class YahtzeeHand(SimpleHand):
    def __init__(self, seed: int = None):
        super(YahtzeeHand, self).__init__(num_sides=6, num_dice=5, seed=seed)

    def is_large_straight(self) -> bool:
        return self.num_sequential() == 5

    def is_small_straight(self) -> bool:
        return self.num_sequential() >= 4

    def is_full_house(self) -> bool:
        counts = self.get_counts()
        return 3 in counts and 2 in counts

That’s it! Now we’ve got the basics of a Yahtzee hand and can use it to calculate some probabilities. Next time, I think I’ll extend this to include the other types of hands like three of a kind, four of a kind, Yahtzee and more.

Verifying the Dice Simulation – Theory vs. Reality

Now that I’ve built a basic framework for simulating “hands” of dice and I’ve also run enough unit tests to convince me that the “counting” algorithms work, the next thing to do is to make sure that when we roll the dice a lot of times, that the results come close to what probability theory says we should expect.

So, first, let’s figure out what some key probabilities are for a 3 dice hand:

  • P(3 of the same number or “triple”) = 6 / 216 (2.78%)
    [(1,1,1), (2,2,2), (3,3,3), (4,4,4), (5,5,5), (6,6,6)] / 6 * 6 * 6
  • P(2 of the same number or “double) = 90 / 216 (41.67%)
    [(1, 1, not 1), (2, 2, not 2)... (1, not 1, 1), (2, not 2, 2) ... (not 1, 1, 1), (not 2, 2, 2)] / 6 * 6 * 6
  • P(Everything else or “singles”) = 120 / 216 (55.56%)
    (216 - 96) / 216

Now, let’s run the following code over 100,000 hands to see how close we come to these probabilities:

def verify_simple_hand(num_rolls: int = 10000):
    h = SimpleHand(num_sides=6, num_dice=3)
    num_dups = {
        1: 0,
        2: 0,
        3: 0
    for r in range(num_rolls):
        nd = h.max_duplicates()
        num_dups[nd] += 1

    for (duplicates, occurrences) in num_dups.items():
        print(f"{duplicates} duplicates: {occurrences:8d} / {num_rolls} -- {(occurrences / num_rolls):>8.2%}")
1 duplicates:    55600 / 100000 --   55.60%
2 duplicates:    41628 / 100000 --   41.63%
3 duplicates:     2772 / 100000 --    2.77%

Process finished with exit code 0

This looks pretty good to me! Our simulated value for triples is within 6 of our expected number (2778) over 100k rolls. This is well in line with the variability we would expect. If we were being really serious, we probably could compute a p-value here… but this is just for fun… so repeatable results like this lead me to believe that we are correctly simulating the dice rolls and should have a good foundation for our future exploration!

Simple Python Dice Simulator – How to Test Something Random

In the last note, I outlined the basic design of the dice simulator, and also mentioned that I had built some unit tests. Some of you may be asking, “How do you run repeatable tests on something that’s random?” It’s a good question, and one that uses a technique that it’s good to be familiar with — specifically, the concept of a random number generator seed.

Some background: Random number generators on computers aren’t truly random. Instead they use a pseudo random number generation (PRNG) algorithm that appears random, but is actually deterministic (or predictable). The thing to remember is that a PRNG will produce the exact same sequence of numbers if it is initialized with the same seed. In practice, most PRNGs are seeded with the low order bits of the current time, or some other rapidly changing device characteristics… and because of this, they appear to produce random results. However, you can also override this default and initialize them with a given seed instead. If you do this, they will produce the same numbers… every time. This is super helpful for testing, or even for reproducing a simulation run for debugging purposes.

Why Do We Have Low Expectations of Others?

Over the weekend, it struck me that a good deal of the current political discourse (in the United States, at least) is driven by the belief that if “other” people are left to their own devices, they will do “bad” things. Certainly there is precedent for this; history is full of examples of small groups that have used power to oppress, suppress, or otherwise subjugate others. The interesting element for me, is that inherently I think most of us believe that we are better than that… no matter which side of issues we fall on.

So, it brings us to a point where we have multiple people on multiple sides of an issue each believing that they are the ones that would be benevolent, while everyone else would be malevolent. This seems intellectually dishonest to me. It’s not even an argument about being better equipped to solve a particular problem… It’s about intentions. I think the argument reduces to many of us truly believing that WE have good intentions, while other people do not. Why is this the case? Certainly if we were able to see inside the hearts of others, it might make things easier… but we can’t.

In my experience, one of the only things that has consistently shown that it can overcome this type of mistrust is familiarity. The more familiar we are with a person, the easier it is for us to believe that although their actions may be disagreeable to us, their intentions are good… and I think THAT is what we are gradually losing in society today. We are losing familiarity with those who think about things in a different way.

I think that a way out of our current situation is to simply spend (a lot of) time with people we don’t agree with… not to convince them that we are right and they are wrong, but to create the space and time to convince ourselves that their intentions are good… and frankly to give them the time to see that our intentions are good too.

It may be true that “the road to hell is paved with good intentions,” but it’s also true that even before it’s paved, the roadbed is built on a foundation of misunderstanding. That is what I think we should address.

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:

Scroll to Top