Monty Hall simulation with any number of doors After answering Monty hall python simulation I wondered what the outcome were to be if there were more than three doors, say four or seven. And so I decided to modify the problem slightly to adjust for this.

There are \$x\$ amount of doors. Behind one door is a car, the others are goats. You pick a door, and the host reveals a goat in a different door. You're then given the choice to change from the selected door to any door except the open door, or the already selected door.

And so I decided to find out how many times a person is likely to win if they always pick a random door when asked if they want to switch door. And so programmed the following:

import random

def monty_hall(amount, doors=3):
    if doors &lt; 3:
        raise ValueError(f'doors must be greater than three, not {doors}')

    wins = 0
    for _ in range(amount):
        player_choice = random.randrange(doors)
        car_placement = random.randrange(doors)
        other_doors = set(range(doors)) - {player_choice, car_placement}

        shown_door = random.choice(list(other_doors))
        swap_doors = set(range(doors)) - {player_choice, shown_door}
        final_choice = random.choice(list(swap_doors))

        wins += final_choice == car_placement
    return wins

print(monty_hall(1000000, 3))
print(monty_hall(1000000, 4))
print(monty_hall(1000000, 5))


I then decided to optimize the above code, and came up with the following ways to do this:


Change player_choice to doors - 1. The first player choice doesn't have to be random.
Change other_doors and swap_doors to account for (1). Rather than removing an item that will always be in the set, remove it from the range and the second set. And so other_doors becomes: set(range(doors - 1)) - {car_placement}.
Remove player_choice as it's no longer used.
Change shown_door to always be the first door. However if the car is the first door, show the second. shown_door = car_placement == 0.
Remove other_doors as it's no longer used.
Change swap_doors to be a list, rather then a set with an item being removed. This requires the use of an if else to manually split the list if the car is in the first door. And so can become:

if car_placment == 0:
    swap_doors = [0] + list(range(2, doors - 1))
else:
    swap_doors = list(range(1, doors - 1))
final_choice = random.choice(swap_doors)

Rather than making my code WET, as I did in (6), we can instead move the car to the second door if it's in the first, and use the else whatever the case.
Finally we can merge all the code into a sum and a generator comprehension.


This resulted in the following code:

import random

def monty_hall(amount, doors=3):
    if doors &lt; 3:
        raise ValueError(f'doors must be greater than three, not {doors}')

    rand = random.randrange
    return sum(
        max(rand(doors), 1) == rand(1, doors - 1)
        for _ in range(amount)
    )

print(monty_hall(1000000, 3))
print(monty_hall(1000000, 4))
print(monty_hall(1000000, 5))


Are there any improvements that I can make on either the optimized or non-optimized solutions? Also are there any issues with the optimized solution, as my method for optimization weren't exactly scientific. As far as I know it looks good.

Code is only runnable in Python 3.6+
I would have used an helper function in the first version so that your monty_hall function is:

def monty_hall(amount, doors=3):
    if doors &lt; 3:
        raise ValueError(f'doors must be greater than three, not {doors}')

    return sum(player_wins_a_car(doors) for _ in range(amount))


This also applies to the second version but removes some of the speed optimizations you made there; so not sure if it’s worth keeping.

However, in the first version, I would use list-comprehensions to reduce a bit the memory needed. It would also exhibit the actions more like a real player would do. Since creating the set from range will already iterate over each elements, using the list-comp won't be much more costy:

import random

def player_wins_a_car(doors):
    player_choice = random.randrange(doors)
    car_placement = random.randrange(doors)
    shown_door = random.choice([door for door in range(doors)
                                if door not in (player_choice, car_placement)])
    final_choice = random.choice([door for door in range(doors)
                                  if door not in (player_choice, shown_door)])

    return final_choice == car_placement


For the second version, if you were to keep two functions that could lead to more explanatory stuff like:

def player_wins_a_car(doors):
    # Choose a door that hide a car. Car is never behind door 0,
    # if it were behind door 0, consider door 0 and 1 to be
    # swapped so that host always open door 0.
    car_placement = random.randrange(doors) or 1
    # Consider an initial choice of door `doors - 1` and host
    # showing door 0 unconditionaly. Always choosing a random
    # door out of these two.
    final_choice = random.randrange(1, doors-1)

    return car_placement == final_choice


I’m using or here as the only value triggering max would be 0, not sure which one performs better.