Skip to content

Movie Theater

Covers:

  • Resources
  • Event operators
  • Shared events

This example 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) 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 type to assemble all the related data (movies, the counter, tickets left, collected data, ...). The counter is a Resource with a capacity of one.

The moviegoer process function 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.

////MovieRenege.kt
import org.kalasim.*
import org.kalasim.misc.roundAny
import kotlin.time.Duration.Companion.minutes

fun main() {

    val RANDOM_SEED = 158
    val TICKETS = 50  // Number of tickets per movie
    val SIM_TIME = 120.minutes  // Simulate until

    data class Movie(val name: String)

    val MOVIES = listOf("Julia Unchained", "Kill Process", "Pulp Implementation").map { Movie(it) }

    createSimulation(randomSeed = RANDOM_SEED) {
        enableComponentLogger()

        // note: it's not really needed to model the theater (because it has no process), but we follow the julia model here
        val theater = object {
            val tickets =
                MOVIES.associateWith { DepletableResource("room ${MOVIES.indexOf(it)}", capacity = TICKETS) }
            val numReneged = MOVIES.associateWith { 0 }.toMutableMap()
            val counter = Resource("counter", capacity = 1)
        }

        class Cineast(val movie: Movie, val numTickets: Int) : Component() {
            override fun process() = sequence {
                request(theater.counter) {
                    request(theater.tickets[movie]!! withQuantity numTickets, failAt = 0.simTime)
                    if(failed) {
                        theater.numReneged.merge(movie, 1, Int::plus)
                    }
                }
            }
        }

        ComponentGenerator(iat = exponential(0.5.minutes)) {
            Cineast(MOVIES.random(), discreteUniform(1, 6).sample())
        }

        run(SIM_TIME)

        MOVIES.forEach { movie ->
            val numLeftQueue = theater.numReneged[movie]!!
            val soldOutSince = theater.tickets[movie]!!.occupancyTimeline.stepFun()
                // find the first time when tickets were sold out
                .first { it.value == 1.0 }.time.toTickTime().value.roundAny(2)

            println("Movie ${movie.name} sold out $soldOutSince minutes after ticket counter opening.")
            println("$numLeftQueue walked away after film was sold out.")
        }

//        // Visualize ticket sales
//        val plotData = theater.tickets.values.flatMap {
//            it.occupancyTimeline.stepFun().map { sf -> Triple(it.name, sf.first, sf.second) }
//        }
//
//        plotData.toDataFrame().plot(x = "second", y = "third")
//            .geomStep().facetWrap("first").title("Theater Occupancy")
//            .xLabel("Time (min)").yLabel("Occupancy")
    }
}

The example also details out how we could now easily plot the occupancy progressions using automatically captured monitoring data.

Adopted from SimJulia example.