The Monty Hall game#

Suppose you’re on a game show, and you’re given the choice of three doors: Behind one door is a car; behind the others, goats.

In search of a new car, the player picks a door, say 1. The game host then opens one of the other doors, say 3, to reveal a goat and offers to let the player pick door 2 instead of door 1.

image

Goal: Implement this game as a web application.

If you want to look at the code at home, here are step-by-step implementations: UiO-IN3110/UiO-IN3110.github.io

import random
from functools import partial

from pydantic import BaseModel, Field


class MontyHallGame(BaseModel):
    winning: int = Field(default_factory=partial(random.randint, 1, 3))
    first_choice: int = None
    opened: int = None
    second_choice: int = None
    has_won: int = None

    def choose(self, choice: int):
        """The first step: Make a choice"""
        self.first_choice = choice

    def reveal(self):
        """The second step: host reveals a door that definitely has a goat."""
        choices = {1, 2, 3}
        # don't open the winning door
        choices.discard(self.winning)
        # don't open the door they've already chosen
        choices.discard(self.first_choice)
        # open a random remaining door.
        # there is either 1 or 2 choices left
        self.opened = random.choice(list(choices))
        return self.opened

    def choose_again(self, switch: bool):
        """Guest can either switch to the other door, or stay with their first choice"""
        if switch:
            choices = {1, 2, 3}
            choices.discard(self.first_choice)
            choices.discard(self.opened)
            self.second_choice = choices.pop()
        else:
            self.second_choice = self.first_choice
        self.has_won = self.second_choice == self.winning
        return self.second_choice

    def play(self):
        """Play one full round"""
        first_choice = int(input("Pick a door (1, 2, 3): "))
        self.choose(first_choice)
        self.reveal()
        available = {1, 2, 3}
        available.discard(first_choice)
        available.discard(self.opened)
        remaining = available.pop()
        ans = input(
            f"I've opened door number {self.opened}. Would you like to switch (y/N)?"
        )
        second_choice = self.choose_again(switch=ans.lower().startswith("y"))

        print(f"You picked door number {self.second_choice}")
        if self.second_choice == self.winning:
            print("You win!")
        else:
            print(f"Sorry, it was door number {self.winning} :(")
game = MontyHallGame()
game.play()
You picked door number 1
You win!

Now we are going to simulate a number of games, recording our win rate for the switch strategy and the stay strategy.

import random

import ipywidgets
from IPython.display import display
from tqdm.notebook import tqdm


def autoplay(samples):
    switch_progress = tqdm(desc="switch", total=samples, mininterval=0.01)
    stay_progress = tqdm(desc="stay", total=samples, mininterval=0.01)

    for i in range(samples):
        for switch, progress in zip((True, False), (switch_progress, stay_progress)):
            first_choice = random.randint(1, 3)
            game = MontyHallGame()
            game.choose(first_choice)
            game.reveal()
            game.choose_again(switch=switch)
            if game.has_won:
                progress.update(1)
    switch_progress.close()
    stay_progress.close()


autoplay(100_000)

Tasks:

  1. create a ‘welcome’ page

  2. serve a form for “pick a door”

  3. handle form submission for picking a door

  4. select a door to open

  5. serve a form to make final pick

  6. serve a result

Initial FastAPI boilerplate:

from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/", response_class=HTMLResponse)
def welcome():
    ...

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, debug=True)