# Python Dice Optimizer

## A python code created to optimize a dice game called "Pig." This code utilizes loops, dataframes, and graphs to determine what strategy gives the player the greatest chance of winning.

In our family, we enjoy playing a dice game known as Pig. The game utilizes a standard 6-sided die, with a unique twist: instead of the usual one dot, there's a pig illustration.

Ever since I was a child, I knew that there should be a way to optimize this game. After all, the rules are elementary: A player simply rolls the dice as many times as they desire for each turn, each roll accumulates those points onto the total points per turn. The risk is that if the player rolls a pig then they get 0 points for that turn. The first player to reach 100 points wins.

To enhance the game strategy, I devised a practical approach. Instead of relying on intricate mathematical calculations, I chose a more direct method—leveraging the power of computation. By programming a computer to play tens of thousands of games and analyzing the outcomes, I aimed to uncover optimal strategies.

Logic 1 - Stopping at x Number of Rolls

First, I created a model out of some loops with the logic that the player would only roll x number of times then quit, regardless of what the score was. Here is my code for that simulation:

# Import Packages

import random

import pandas as pd

import matplotlib.pyplot as plt

# Set itterations (10000 should remove all meaningful variance)

itterations = 10000

# Create empty dataframe

df = pd.DataFrame()

# Create range of rolls per turn to be tested

test_rolls = tuple(range(1, 16))

# Itterate over the numbers of rolls per turn to be tested

for f in (test_rolls):

# Reset average turns for each rolls per turn we test

average_turns=0

# Itterate over variable itterations (each itteration is a new simulated game)

for i in range (itterations):

# Reset turns and point_total for each game

turns=0

point_total=0

# While loop continues play until the winning score is attained

while point_total < 100:

# Add a turn for each while loop trip and reset the point total for each turn at the start of the turn

turns+=1

point_total_turn=0

# This itteration rolls the dice and runs until a pig is thrown or we reach our desired number of rolls per turn

for t in range(f):

roll=random.randint(1,6)

if roll != 1:

point_total_turn +=roll

else:

point_total_turn=0

break

# If a pig wasn't thrown then we add our total points per turn to the total points accumulated through the game so far

if point_total_turn > 0:

point_total+=point_total_turn

else:

point_total + 0

# Not actually the average turns but rather the total turns for that tested number of rolls

# It needs to be divided by itterations to get the average

average_turns += turns

# Created a dataframe of the average turns and each number of rolls tested

df = pd.concat([df, pd.DataFrame({'Rolls Per Turn': [f], 'Average Turns': [average_turns/itterations]})], ignore_index=True)

# Print dataframe

print(df)

Results

The results were not surprising, once the player went up to 2 rolls per turn, the number of turns that player used to get up to 100 points became somewhat competitive. I determined that 5 rolls per turn is the optimum number of rolls, resulting in an average of around 13.5 turns. Unsurprisingly, both 4 and 6 are also very near the 5 roll mark in terms of average turns, something that we will discuss later when we consider the real-world implementations.

Logic 2 - Stopping at x Points

After Running this model, I thought of another strategy that could potentially be more optimized, rolling the dice until a certain number is reached for each turn. After all, if I were to roll the dice 5 times and rolled 2 every time I would only have 10 points but if I rolled 6 every time I would have 30. Rolling the dice an additional time would not garner the same risk/reward in both scenarios. By going to a certain point threshold and disregarding the number of rolls could help us find that perfect risk/reward location at which to end our turn. Here is the code I used to run that simulation:

# Import packages

import random

import pandas as pd

import matplotlib.pyplot as plt

# Set itterations (10000 should remove all meaningful variance)

itterations = 10000

# Create empty dataframe

df = pd.DataFrame()

# create a tuple of numbers we would like to roll to before quiting our turn, testing each one individually

#numbers_until_quit = tuple(range(1, 71))

# I ran by 5's to get a glance

#numbers_until_quit =(5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95)

# Honed into effective range

numbers_until_quit = tuple(range(10,30))

# Itterate for each number we want to reach before we quit our turn

for f in (numbers_until_quit):

# Reset average turns before testing a new number

average_turns=0

# Each itterations is equal to 1 game

for i in range (itterations):

# Reset turns and total points before starting new game

turns=0

point_total=0

# While loop keeps the game going until we reach the winning score, 100

while point_total <100:

# Add a turn and reset the point total for each turn before starting a new turn

turns+=1

point_total_turn=0

# While loop keeps us rolling until we reach the point total we want to stop at or until we roll a pig

while point_total_turn < f:

roll=random.randint(1,6)

if roll != 1:

point_total_turn +=roll

else:

point_total_turn=0

break

# If we do not roll a pig and we reach our point total threshold then we add those points to our total game points

if point_total_turn > 0:

point_total+=point_total_turn

else:

point_total + 0

# Not actually the average turns but the total turns for each number tested

# Must be divided by total itterations to get average turns

average_turns += turns

# Put our point threshold next to the average turns in a dataframe

df = pd.concat([df, pd.DataFrame({'Points Until End of Turn': [f], 'Average Turns': [average_turns/itterations]})], ignore_index=True)

Results

This model yielded an unexpected outcome. I initially anticipated a uniform parabolic trend, where the number of turns would gradually decrease, reach a minimum, and then ascend again. Contrary to this expectation, the graph exhibited intriguing wave-like patterns. As the points per turn increased, the number of turns decreased until reaching approximately 16 points. Subsequently, the count fluctuated slightly until showing a noticeable increase around 27 points. At 30 points, there was a significant decline in turns, approaching but not quite reaching the optimal point—determined, in this case, to be 19 points per turn, resulting in an average of around 13 turns.

This model gave me an unexpected result. I assumed it would also be some form of a uniform parabola, where the turns would gradually decrease and then gradually increase again. I did not expect to see any wave like patterns as seen in the graph below. As the points per turn increase, the number of turns decreases until roughly 16 points. Then, the number of turns fluctuates up and down silightly until noticably increasing at roughly 27 points. At 30, the number of turns declines significantly where it gets near, but does not reach, the optimal point, which, using this method, happens to be 19. At 19 points per turn the player can average around 13 turns. This method also out performs the first method of stopping at x number of rolls by around .5 turns. This could be significant when playing 1,000 or 10,000 games but would not be noticeable in real life.

At first, when I saw this fluctuation I was concerned about the logic of my code and whether it was doing the optimization correctly. After thinking about it for a day or two I came up with a hypothesis that the fluctuations happen based on the probability of the number of rolls it takes to get to x number of points.

Statistically, rolling 15 points in 5 rolls is nearly as likely as rolling 20 in 5 rolls. To prove this, to you and myself, I plugged it into a dice calculator (omnicalculator.com). Of course the calculator doesn't account for the pig so 1's are allowed in the calculation, the concept is still valid. To roll 15 with 5 rolls has a probability of 0.0837191 and to roll 20 with the same number of rolls is 0.0837191, the exact same... The reason for this is because they both are possible with many combinations of numbers. Once we calculate the probability of rolling 25 with those 5 rolls the probability goes down to 0.0162037 and finally, rolling 30 with those 5 rolls would require us to roll a 6 every time so the probability is 0.0001286008. In short, there are certain single point increasing places along the score axis that result in a higher probability of the roll count going up than at other single point increases. For example, there is an entire 5 point stretch between 15 and 20 that all have the same probabilities with 5 rolls but as soon as we go from 25 (0.0162037) to 26 (0.00900206) we see the probability drop by .00720145. Over 0.7%. In essence, certain point increments along the score axis exhibit higher probabilities of increasing the roll count than others. This non-uniform probability distribution contributes to variations in the average turns required to complete the game, resulting in the observed waves.

As for me and my family, I think we'll continue to trust our gut and other superstitions as to how far to push our luck, it's more fun that way.

References

Sas, Wojciech. “Dice Probability Calculator.” Omni Calculator, Omni Calculator, 6 Nov. 2023, www.omnicalculator.com/statistics/dice.