# Movie Renege

Covers:

- Resources: Resource

- Condition events

- Shared events

This examples models a movie theater with one ticket counter selling tickets for three movies (next show only). People arrive at random times and try to buy a random number (1–6) of tickets for a random movie. When a movie is sold out, all people waiting to buy a ticket for that movie renege (leave the queue).

The movie theater is just a container for all the related data (movies, the counter, tickets left, collected data, …). The counter is a `Resource` with a capacity of one.

The moviegoer process starts waiting until either it’s his turn (it acquires the counter resource) or until the sold out signal is triggered. If the latter is the case it reneges (leaves the queue). If it gets to the counter, it tries to buy some tickets. This might not be successful, e.g. if the process tries to buy 5 tickets but only 3 are left. If less than two tickets are left after the ticket purchase, the sold out signal is triggered.

Moviegoers are generated by the customer arrivals process. It also chooses a movie and the number of tickets for the moviegoer.

    """
    Movie renege example

    Covers:

    - Resources: Resource
    - Condition events
    - Shared events

    Scenario:
      A movie theatre has one ticket counter selling tickets for three
      movies (next show only). When a movie is sold out, all people waiting
      to buy tickets for that movie renege (leave queue).

    """
    from __future__ import annotations

    import random
    from typing import Dict, List, NamedTuple, Optional

    import simpy

    RANDOM_SEED = 42
    TICKETS = 50  # Number of tickets per movie
    SELLOUT_THRESHOLD = 2  # Fewer tickets than this is a sellout
    SIM_TIME = 120  # Simulate until

    def moviegoer(env, movie, num_tickets, theater):
        """A moviegoer tries to by a number of tickets (*num_tickets*) for
        a certain *movie* in a *theater*.

        If the movie becomes sold out, she leaves the theater. If she gets
        to the counter, she tries to buy a number of tickets. If not enough
        tickets are left, she argues with the teller and leaves.

        If at most one ticket is left after the moviegoer bought her
        tickets, the *sold out* event for this movie is triggered causing
        all remaining moviegoers to leave.

        """
        with theater.counter.request() as my_turn:
            # Wait until it's our turn or until the movie is sold out
            result = yield my_turn | theater.sold_out[movie]

            # Check if it's our turn or if movie is sold out
            if my_turn not in result:
                theater.num_renegers[movie] += 1
                return

            # Check if enough tickets left.
            if theater.available[movie] < num_tickets:
                # Moviegoer leaves after some discussion
                yield env.timeout(0.5)
                return

            # Buy tickets
            theater.available[movie] -= num_tickets
            if theater.available[movie] < SELLOUT_THRESHOLD:
                # Trigger the "sold out" event for the movie
                theater.sold_out[movie].succeed()
                theater.when_sold_out[movie] = env.now
                theater.available[movie] = 0
            yield env.timeout(1)

    def customer_arrivals(env, theater):
        """Create new *moviegoers* until the sim time reaches 120."""
        while True:
            yield env.timeout(random.expovariate(1 / 0.5))

            movie = random.choice(theater.movies)
            num_tickets = random.randint(1, 6)
            if theater.available[movie]:
                env.process(moviegoer(env, movie, num_tickets, theater))

    class Theater(NamedTuple):
        counter: simpy.Resource
        movies: List[str]
        available: Dict[str, int]
        sold_out: Dict[str, simpy.Event]
        when_sold_out: Dict[str, Optional[float]]
        num_renegers: Dict[str, int]

    # Setup and start the simulation
    print('Movie renege')
    random.seed(RANDOM_SEED)
    env = simpy.Environment()

    # Create movie theater
    movies = ['Python Unchained', 'Kill Process', 'Pulp Implementation']
    theater = Theater(
        counter=simpy.Resource(env, capacity=1),
        movies=movies,
        available={movie: TICKETS for movie in movies},
        sold_out={movie: env.event() for movie in movies},
        when_sold_out={movie: None for movie in movies},
        num_renegers={movie: 0 for movie in movies},
    )

    # Start process and run
    env.process(customer_arrivals(env, theater))
    env.run(until=SIM_TIME)

    # Analysis/results
    for movie in movies:
        if theater.sold_out[movie]:
            sellout_time = theater.when_sold_out[movie]
            num_renegers = theater.num_renegers[movie]
            print(
                f'Movie "{movie}" sold out {sellout_time:.1f} minutes '
                f'after ticket counter opening.'
            )
            print(f'  Number of people leaving queue when film sold out: {num_renegers}')

The simulation’s output:

    Movie renege
    Movie "Python Unchained" sold out 38.0 minutes after ticket counter opening.
      Number of people leaving queue when film sold out: 16
    Movie "Kill Process" sold out 43.0 minutes after ticket counter opening.
      Number of people leaving queue when film sold out: 5
    Movie "Pulp Implementation" sold out 28.0 minutes after ticket counter opening.
      Number of people leaving queue when film sold out: 5