Yahtzee and Monte Carlo
Using code to find optimal strategies for everyday issues? I got interested in Monte Carlo simulations when I read the book "Fooled by Randomness: The Hidden Role of Chance in Life and in the Markets" by Nassim Taleb.
“Monte Carlo mathematics, I think of it as a happy combination of the two: The Monte Carlo man’s realism without the shallowness, combined with the mathematician’s intuitions without the excessive abstraction. Indeed, this branch of mathematics has immense practical use — it doesn't present the same dryness commonly associated with mathematics. I became addicted to it the minute I became a trader. It shaped my thinking in matters related to randomness. Most of the examples used in this book were created with my Monte Carlo generator, which I introduce in this chapter. Yet it is far more a way of thinking than a computational method. Mathematics is primarily a tool to meditate, rather than to compute.”
Aside from my interest in finding optimal investment cases, I wanted to experiment a bit, and the use case that I picked for my first exercise was the game of Yahtzee. I often play it with my family and wanted to use Monte Carlo simulations to find out how well my default strategy is performing. According to Wikipedia, the optimal average score for Yahtzee is 254.59. I put together a small Python script to represent the game, which helped me to formalize my strategy.
My strategy is not super sophisticated, and I think there are still many areas that could be optimized. However, through Monte Carlo simulations, I was able to calculate that my current simple strategy returns an average of 202.82 points. The worst strategy I could conceive would score 72 points, basically only throwing once and going through the fields from top to bottom.
Conclusion
- My main conclusion here is that Monte Carlo simulations are an amazing tool for evaluating the actual expected return of a strategy you want to evaluate. You don't need any complex statistical formulas and can run the simulations on the simplest of computers. It simplifies things a great deal!
- In fact, it helps you create deterministic simulations that include small probabilistic portions to understand the bigger picture and determine which levers have the greatest effect, without the need to solve sophisticated statistical formulas.
- Monte Carlo simulations assist you in evaluating whether you are close to optimum. This can give you confidence, but it can also help you identify where there's room to grow and where to invest.
- My Yahtzee strategy is somewhat satisfactory, but there are certainly areas for improvement (on average, 50 points to improve).
Simulation Code
I first wrote a class that would represent the game state for one player. This game state is for one game only and it keeps track of the different scores with the correct number of points, but without validation.
from collections import Counter
class Yahtzee:
def __init__(self):
self.upper_scores = {i: None for i in range(1, 7)}
self.lower_scores = {
'three_kind': None, 'four_kind': None, 'full_house': None,
'small_straight': None, 'large_straight': None,
'yahtzee': None, 'chance': None
}
def print_scores(self):
...
def _hand_score(self, hand, key):
...
def get_available_fields(self, hand):
available_fields = []
counts = [hand.count(i) for i in range(1, 7)]
for i in range(1, 7):
if counts[i-1] > 0 and self.upper_scores[i] is None:
available_fields.append((i, 'upper'))
def consecutive(l):
return sorted(l) == list(range(min(l), max(l)+1))
if 5 in counts and self.lower_scores['yahtzee'] is None:
available_fields.append(('yahtzee', 'lower'))
elif sorted(Counter(hand).values()) == [2, 3]:
available_fields.append(('full_house', 'lower'))
elif 4 in counts and self.lower_scores['four_kind'] is None:
available_fields.append(('four_kind', 'lower'))
elif 3 in counts and self.lower_scores['three_kind'] is None:
available_fields.append(('three_kind', 'lower'))
elif consecutive(hand) and len(set(hand)) == 5 and self.lower_scores['large_straight'] is None:
available_fields.append(('large_straight', 'lower'))
elif consecutive(hand) and len(set(hand)) >= 4 and self.lower_scores['small_straight'] is None:
available_fields.append(('small_straight', 'lower'))
elif self.lower_scores['chance'] is None:
available_fields.append(('chance', 'lower'))
return available_fields
def get_empty_fields(self):
...
def eliminate_field(self, key):
...
def set_field(self, key, hand):
field, section = key
if section == 'upper':
self.upper_scores[field] = self._hand_score(hand, key)
elif section == 'lower':
self.lower_scores[field] = self._hand_score(hand, key)
def get_score(self):
us = sum(value for value in self.upper_scores.values() if value is not None and value is not False)
ls = sum(value for value in self.lower_scores.values() if value is not None and value is not False)
us_bonus = 35 if us >= 63 else 0
return us + ls + us_bonus
Based on this class I wrote a small script that would allow me to test various strategies.
import random
from yahtzee import Yahtzee
# Strategies
from stupidStrategy import StrategyStupid
# debug = True
debug = False
def roll_dice(n):
return [random.randint(1, 6) for _ in range(n)]
def simulate_game(strategy, rolls=3, turns=13):
game = Yahtzee()
for turn in range(turns):
if debug: print('----Turn ', turn)
hand = []
for roll in range(rolls):
hand += roll_dice(5 - len(hand))
if debug: print("- roll", roll, hand)
hand = strategy.execute(game, hand, turn, roll)
if debug: game.print_scores()
if debug: print('')
return game.get_score()
def monte_carlo_simulation(strategy, n=100000):
total = 0
for _ in range(n):
total += simulate_game(strategy)
return total / n
print(monte_carlo_simulation(StrategyStupid(), 1))
Example of a very stupid strategy that would on average score only 73pts.
class StrategyStupid:
def __init__(self):
self.name = 'StrategyStupid'
self.description = """
Per Roll: Only roll once
Scoring: Always fills from top to bottom all the fields
"""
def execute(self, game, hand, turn, roll):
# print('thinking...', hand, turn, roll)
if roll == 2:
afields = game.get_available_fields(hand)
if len(afields) > 0:
game.set_field(afields[0], hand)
# print('set:\t\t',afields[0], hand)
else:
efields = game.get_empty_fields()
if len(efields) > 0:
game.eliminate_field(efields[0])
# print('eliminate:\t',efields[0])
return hand