Python Monty Hall Problem Simulation

The Monty Hall Problem is a very (to me at least) counter-intuitive probability mind-experiment which contorts my brain and fascinates me at the same time, I have been mulling it over the last few weeks and wanted to write a little simulator to see if the numbers come out as predicted (if not expected, and indeed they do!). I can just about understand the probabilistic arguments, but I still find it very confusing, as soon as I think that I grok it – my ‘understanding’ disappears into the night! I am used to being this bamboozled when reading about quantum mechanics or something, but I find it fascinating that such an apparently simple problem can be so deceptively deep!

Summary

There are 3 doors, behind one lies a car, while behind the other two are goats. A player chooses a door at random. Monty opens one of the other doors to show that there is a goat behind it. Monty then asks the player if they would like to stick with their original choice of door or switch to the other un-opened door.

If the player sticks with their door then their chance of winning the car should be 1/3. If the player switches door then their chances of winning the car increases to 2/3!!!

If you are having problems understanding the outcome, I find it helps to imagine that there are a million doors rather than 3. After you choose your door (1/1,000,000 chance of hiding the car) Monty opens up 999,998 doors that hide goats to leave one door still closed. Now which door do you think is most likely to hide the car? The one you choose, or the one that Monty avoided opening while he opened all 999,998 other doors?! It seems obvious to me that the other door that Monty left un-opened has a massively higher chance of hiding the car than your original choice! As N reduces to 3 this ‘obviousness’ reduces greatly however! This simulator allows you to experiment with more then 3 doors for this reason.

For more info: https://en.wikipedia.org/wiki/Monty_Hall_problem

This program is an experimental simulator to see what numbers we get when the player decides to stick or switch. It can be found on GitHub.

#! /usr/bin/python

#   Copyright 2019 Kevin Godden
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import random

"""
    Simulates the Monty Hall problem with 3 doors

    The Monty Hall problem is a very (to me at least) counter-intuitive probability mind experiment which
    contorts my brain and fascinates me at the same time. I can just about understand the
    probabilistic arguments, but I still find it confusing.  This program is an experimental
    simulator to see what numbers we get when the player decides to stick or switch.

    If you are having problems understanding the outcome, I find it helps to imagine that there
    are a million doors rather than 3.  After you choose your door (1/1,000,000 chance of hiding the car)
    Monty opens up 999,998 doors that hide goats to leave one door still closed.  Now which door do you think
    is most likely to hide the car? The one you choose, or the one that Monty avoided opening while
    he opened all 999,998 other doors?!  It seems obvious to me that the other door that Monty left un opened
    has a massively higher chance of hiding the car than the original choice!  As N reduces to 3 this
    obviousness reduces greatly however!  This simulation can be run with more than 3 doors to experiment
    with this...

    Summary:
    --------

    There are 3 doors, behind one lies a car, while behind the other two are goats.
    A player chooses a door at random.  Monty then opens one of the other doors to show that
    there is no car behind it.  Monty asks the player if they would like to stick with their
    door or switch to the other un-opened door.

    If the player sticks with their door then their chance of winning the car should be 1/3
    If the player switches door then their chances of winning the car increases to 2/3!!!

    If you want to run the game with more doors to see how the probabilities change
    just change the door_count value below.

    For more info: https://en.wikipedia.org/wiki/Monty_Hall_problem
"""

# Should the player switch their guess or stick
# with their original one?
switch = True

# The number of doors in the game, should be 3 for
# the problem as normally stated.
door_count = 3

# The number of times to run the simulation
game_count = 10000

# Should we print lots of messages?
# If door_count is large change this to True to
# make simulation run faster.
quiet = False

def pick_random_door():
    """
    Pick a door at 'random', doors are 0-indexed so this will
    return a number between 0 and door_count - 1

    :return: the chosen door
    """

    return random.randint(0, door_count - 1)

def pick_monty_doors(car_door, players_guess):
    """
    Pick the doors for Monty to open, of the number
    of doors is 3 then Monty opens up the first door that doesn't
    have a car and that play hasn't already chosen...

    If door_count > 3 then Monty will open up all of the doors
    that hide goats avoiding the door that has been already picked

    :param car_door: The door behind which the car resides
    :param players_guess: The door which the play choose
    :return: A list of the indices of the doors that Monty opens
    """

    monty_doors = []

    for d in range(0, door_count):

        if len(monty_doors) == door_count - 2:
            break

        if d == car_door:
            continue

        if d == players_guess:
            continue

        monty_doors.append(d)

    return monty_doors

def pick_other_door(players_guess, monty_doors):
    """
    The player has chosen to switch their chosen door, so we
    need to pick another one for them.  We pick the door that

    a.) Doesn't match the player's original guess
    b.) Wasn't chosen by Monty

    :param players_guess:  The door that the player originally picked
    :param monty_doors: The doors that Monty has opened (usually just 1)
    :return: The other door's index
    """

    for d in range(0, door_count):

        # Can't switch to the player's original guess
        if d == players_guess:
            continue

        # Don;t pick a door that Monty has opened
        if d in monty_doors:
            continue

        return d

def run_game():
    """
    Run a single game

    :return:  win/loose --> True/False
    """

    # Pick the door with the car
    car_door = pick_random_door()

    if not quiet:
        print("Car is behind door %d" % car_door)

    # Player chooses a door
    players_guess = pick_random_door()

    if not quiet:
        print("Player has guessed door %d" % players_guess)

    # Monty opens up a door that doesn't have a
    # car behind it.
    monty_doors = pick_monty_doors(car_door, players_guess)

    if not quiet:
        print("Monty opens: " + str(monty_doors))

    # Does the player switch doors after Monty
    # opens his??
    if switch:
        players_guess = pick_other_door(players_guess, monty_doors)

        if not quiet:
            print("Player switches to door %d" % players_guess)

    if not quiet:
        if players_guess == car_door:
            print("Player wins!")
        else:
            print("Player looses ;-(")

    return players_guess == car_door

def run_games():
    wins = 0
    game = 0

    while game < game_count:
        game += 1

        # Run a game and see if we win!
        if run_game():
            wins += 1

    print("Games: %d" % game)
    print("Wins: %d" % wins)

    print("------------------------------")

    if wins == 0:
        print("you didn't win any games!")
    else:
        win_rate = wins / game
        print("Win rate: %f" % win_rate)

if __name__ == "__main__":
    run_games()