Wednesday, May 14, 2025

Understanding Simulator Architecture: Principles, Components, and Best Practices

Motivation 

Simulators are powerful tools that allow us to explore, analyze, and predict the behavior of complex systems without the cost, risk, or impracticality of experimenting with the real thing. Whether simulating the flow of traffic in a city, the spread of disease in a population, or the operation of a supermarket, the architecture of a simulator is shaped by the need to balance realism with efficiency and clarity. The art of simulation lies in deciding what to model and what to leave out, ensuring that the simulator is both useful and manageable.


The Purpose and Scope of Simulation

At its core, a simulator is a software system that mimics the evolution of a real or hypothetical process over time. The purpose of simulation can vary widely: it might be used to test hypotheses, optimize operations, train personnel, or simply to gain insight into how a system behaves under different conditions. The scope of a simulator is defined by the questions it is intended to answer. For example, a supermarket simulator might be designed to help managers understand how the number of open checkout counters affects customer wait times, or to evaluate the impact of introducing self-checkout kiosks.

Because it is rarely practical or necessary to model every detail of the real world, simulators are built on the principle of abstraction. This means that only those aspects of the system that are relevant to the simulation’s objectives are included, while others are deliberately omitted. This selective modeling is not a limitation, but a strength: it allows the simulator to focus computational resources and analytical attention on the factors that matter most.


Main Software Components of a Simulator

The architecture of a simulator is typically organized around several main software components, each with a distinct role. The simulation engine is the heart of the system, responsible for advancing the simulation through time and orchestrating the execution of events. The engine maintains the global state of the simulation and ensures that all components interact in a coherent and consistent manner.


Entity models represent the actors or agents within the system. In a supermarket simulator, these might include customers, cashiers, and possibly other staff such as stockers or managers. Each entity is characterized by a set of properties and behaviors. For example, a customer might have a shopping list, a shopping speed, and a preferred checkout method, while a cashier might have a service rate and a work schedule.


The environment model provides the context in which entities operate. In the supermarket example, the environment might include the layout of the store, the number and type of checkout counters, and the rules governing queue formation and service. The environment defines the constraints and opportunities that shape the behavior of entities.

A crucial component of most simulators is the event scheduler. In event-driven simulations, the scheduler maintains a queue of future events, each associated with a specific time and action. As the simulation progresses, the scheduler processes events in chronological order, updating the state of the system and scheduling new events as needed. This approach allows the simulator to efficiently handle large numbers of entities and interactions, focusing computational effort on moments when the system actually changes.

Finally, the statistics and reporting module collects data during the simulation and generates outputs that help users interpret the results. This might include summary statistics such as average wait times, queue lengths, and resource utilization, as well as more detailed logs or visualizations.


Architectural Patterns and Best Practices

The design of a simulator benefits greatly from the application of established software engineering principles and design patterns. One of the most important patterns in simulation is the event-driven architecture. In this approach, the simulation is structured around a central event queue, and the system state is updated only in response to discrete events. This allows the simulator to efficiently model systems where changes occur at irregular intervals, such as customers arriving at a supermarket or completing their shopping.


Another valuable pattern is the observer pattern, which decouples the core simulation logic from auxiliary functions such as data collection, logging, or visualization. By allowing external modules to subscribe to events or state changes, the simulator can be extended or customized without modifying the core codebase.

The factory pattern is often used to create entities with varying properties. For example, a customer factory might generate customers with different shopping lists, arrival times, and checkout preferences, reflecting the diversity of real-world shoppers.

Best practices in simulator design include maintaining a clear separation of concerns, ensuring that each component has a well-defined responsibility. Modularity is essential, as it allows the simulator to be extended or modified without requiring extensive changes to the codebase. Configurability is also important, enabling users to adjust parameters such as the number of open checkout counters, the rate of customer arrivals, or the distribution of shopping times. Reproducibility should be ensured by controlling random seeds, allowing simulations to be repeated with consistent results. Finally, validation and testing are critical, as they help ensure that the simulator behaves as expected and produces reliable results.


The Supermarket Example: Bringing It All Together

To illustrate these concepts, let us consider a detailed example of a supermarket simulator. The goal of this simulator is to model the flow of customers through a supermarket, from arrival to departure, and to analyze how operational parameters such as the number of open checkout counters and the rate of customer arrivals affect key outcomes like queue lengths and wait times.

In this simulator, customers are represented as agents who arrive at the supermarket according to a specified arrival rate. For instance, the simulator might be configured so that, on average, one customer enters the store every thirty seconds during peak hours. Each customer is assigned a shopping list, which determines how long they will spend in the store before proceeding to the checkout area. The time spent shopping can be modeled as a random variable, perhaps following a normal or exponential distribution, to reflect the natural variability in customer behavior.

Once a customer has finished shopping, they proceed to the checkout area, where they must choose a queue. The simulator can be configured to model different queueing strategies. In one scenario, each checkout counter might have its own separate queue, and customers choose the shortest available line. In another, there might be a single, central queue that feeds into the next available cashier. The number of open checkout counters is a key parameter that can be adjusted to explore its impact on system performance. For example, the simulator might be run with three, five, or ten open counters to see how this affects average wait times and queue lengths.

As customers join queues, the simulator tracks their position and the time they spend waiting. When a customer reaches the front of the queue, they begin the checkout process, which takes a certain amount of time depending on factors such as the number of items in their cart and the efficiency of the cashier. The simulator can model this process in detail or use a simplified approach, depending on the level of abstraction desired. Once the transaction is complete, the customer leaves the supermarket, and the simulator records relevant statistics such as total time spent in the store, time spent waiting in line, and the utilization rate of each cashier.

Throughout the simulation, the system maintains a global state that tracks the status of all customers, cashiers, and queues. An event scheduler manages the timing and sequencing of key events, such as customer arrivals, the completion of shopping, the start and end of checkout, and customer departures. This event-driven approach allows the simulator to efficiently handle large numbers of customers and to accurately model the dynamic interactions between different parts of the system.

The simulator also includes mechanisms for collecting and reporting statistics. For example, it might generate reports showing the average and maximum queue lengths over time, the distribution of customer wait times, and the percentage of time each cashier is busy. These outputs provide valuable insights into the performance of the supermarket under different operating conditions and can be used to inform decisions about staffing, store layout, and customer service policies.


Example Code: A Minimal Supermarket Simulator

To make these ideas more concrete, consider the following minimal example of a supermarket simulator written in Python. This example demonstrates the core components and event-driven architecture described above.


import heapq

import random


class Event:

    def __init__(self, time, action, customer=None):

        self.time = time

        self.action = action

        self.customer = customer

    def __lt__(self, other):

        return self.time < other.time


class Customer:

    def __init__(self, id, arrival_time):

        self.id = id

        self.arrival_time = arrival_time

        self.shopping_time = random.expovariate(1/5)  # average 5 minutes

        self.checkout_time = random.expovariate(1/2)  # average 2 minutes


class SupermarketSimulator:

    def __init__(self, num_cashiers, arrival_rate, simulation_time):

        self.num_cashiers = num_cashiers

        self.arrival_rate = arrival_rate

        self.simulation_time = simulation_time

        self.event_queue = []

        self.current_time = 0

        self.cashier_queues = [[] for _ in range(num_cashiers)]

        self.cashier_busy = [False] * num_cashiers

        self.statistics = {'wait_times': [], 'queue_lengths': []}

        self.customer_id = 0


    def schedule_event(self, event):

        heapq.heappush(self.event_queue, event)


    def run(self):

        # Schedule first customer arrival

        self.schedule_event(Event(0, self.customer_arrival))

        while self.event_queue and self.current_time < self.simulation_time:

            event = heapq.heappop(self.event_queue)

            self.current_time = event.time

            event.action(event.customer)


    def customer_arrival(self, customer=None):

        # Create new customer

        customer = Customer(self.customer_id, self.current_time)

        self.customer_id += 1

        # Schedule end of shopping

        self.schedule_event(Event(self.current_time + customer.shopping_time, self.customer_ready_for_checkout, customer))

        # Schedule next arrival

        next_arrival = self.current_time + random.expovariate(self.arrival_rate)

        if next_arrival < self.simulation_time:

            self.schedule_event(Event(next_arrival, self.customer_arrival))


    def customer_ready_for_checkout(self, customer):

        # Choose the shortest queue

        queue_lengths = [len(q) for q in self.cashier_queues]

        cashier_index = queue_lengths.index(min(queue_lengths))

        self.cashier_queues[cashier_index].append(customer)

        self.statistics['queue_lengths'].append((self.current_time, list(queue_lengths)))

        # If cashier is free, start checkout

        if not self.cashier_busy[cashier_index]:

            self.start_checkout(cashier_index)


    def start_checkout(self, cashier_index):

        if self.cashier_queues[cashier_index]:

            customer = self.cashier_queues[cashier_index].pop(0)

            self.cashier_busy[cashier_index] = True

            wait_time = self.current_time - customer.arrival_time - customer.shopping_time

            self.statistics['wait_times'].append(wait_time)

            # Schedule end of checkout

            self.schedule_event(Event(self.current_time + customer.checkout_time, lambda c=customer: self.end_checkout(cashier_index, c)))


    def end_checkout(self, cashier_index, customer):

        self.cashier_busy[cashier_index] = False

        # Start next checkout if queue is not empty

        if self.cashier_queues[cashier_index]:

            self.start_checkout(cashier_index)


# Example usage:

sim = SupermarketSimulator(num_cashiers=4, arrival_rate=0.2, simulation_time=120)  # 4 cashiers, 1 customer every 5 minutes, 2 hours

sim.run()

print("Average wait time:", sum(sim.statistics['wait_times']) / len(sim.statistics['wait_times']))


This code models a supermarket with a configurable number of cashiers and a specified customer arrival rate. Customers arrive according to a Poisson process, spend a random amount of time shopping, and then join the shortest available queue. The event-driven architecture ensures that the simulation progresses efficiently, processing only those events that change the state of the system.


Selective Modeling: What Is and Isn’t Included

It is important to recognize that the supermarket simulator, like all simulators, is a purposeful abstraction. It does not attempt to model every aspect of the real-world supermarket experience. For instance, the simulator might ignore the exact physical layout of the store, the specific locations of products on the shelves, or the detailed walking paths taken by customers. It might also omit factors such as customer satisfaction, impulse purchases, or the effects of in-store promotions. These omissions are not oversights but deliberate choices, made to keep the simulation focused, efficient, and relevant to the questions being asked.

By concentrating on the flow of customers, the operation of checkout counters, and the dynamics of queues, the simulator provides a powerful tool for exploring how changes in key parameters—such as the number of open cashiers or the rate of customer arrivals—affect the overall performance of the supermarket. At the same time, the modular and extensible architecture of the simulator ensures that additional features can be added as needed, should new questions or requirements arise.


Extending the Simulator

The basic architecture described here can be extended in numerous ways. For example, the environment model could be enhanced to include different types of checkout counters, such as self-service kiosks or express lanes. The customer model could be expanded to include preferences or loyalty behaviors, and the event scheduler could be adapted to handle more complex interactions, such as restocking or staff breaks. By adhering to modular design principles and employing proven software patterns, the simulator remains flexible and maintainable as new features are added.


Conclusion

In summary, the architecture of a simulator is defined by its main software components, including the simulation engine, entity and environment models, event scheduler, and statistics module. By following best practices such as abstraction, modularity, configurability, and reproducibility, and by leveraging design patterns like event-driven architecture, observer, and factory, simulator designers can create powerful tools for exploring complex systems. The supermarket example demonstrates how these principles come together in practice, providing a foundation for both practical analysis and future extension. Through careful design and thoughtful abstraction, simulators enable deeper understanding and better decision-making in a wide range of domains.


Addendeum: Full Code of a Supermarket Simulator

The following code contains a one page HTML implementation of the simulator:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Supermarket Simulator</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 0;
            background-color: #f0f0f0;
            overflow: hidden; /* Prevents scrollbars if content slightly overflows */
        }
        .controls {
            background-color: #fff;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            margin-bottom: 20px;
            display: flex;
            gap: 20px;
            align-items: center;
            flex-wrap: wrap;
        }
        .control-group {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .control-group label {
            margin-bottom: 5px;
            font-size: 0.9em;
        }
        .control-group input[type="range"] {
            width: 150px;
        }
        .control-group span {
            font-size: 0.9em;
            min-width: 20px;
            text-align: center;
        }

        #supermarket {
            width: 800px;
            height: 500px;
            background-color: #e0e0e0;
            border: 2px solid #333;
            position: relative;
            overflow: hidden;
            box-shadow: 0 0 20px rgba(0,0,0,0.2) inset;
            animation: dynamicLighting 20s infinite alternate;
        }

        @keyframes dynamicLighting {
            0% { background: radial-gradient(ellipse at center, #f0f8ff 0%, #d0e0f0 70%, #b0c0d0 100%); }
            50% { background: radial-gradient(ellipse at center, #e6f2ff 0%, #c0d0e0 70%, #a0b0c0 100%); }
            100% { background: radial-gradient(ellipse at center, #f0f8ff 0%, #d0e0f0 70%, #b0c0d0 100%); }
        }

        .person {
            width: 12px;
            height: 12px;
            background-color: #3498db; /* Default/Entering */
            border-radius: 50%;
            position: absolute;
            transition: left 0.4s linear, top 0.4s linear, background-color 0.3s ease;
            box-shadow: 0 0 5px rgba(0,0,0,0.3);
            z-index: 10;
        }
        .person.entering { background-color: #3498db; }
        .person.shopping { background-color: #f1c40f; }
        .person.movingToCashier { background-color: #e74c3c; }
        .person.queuing { background-color: #e67e22; }
        .person.paying { background-color: #2ecc71; }
        .person.leaving { background-color: #95a5a6; }

        .cashier {
            width: 60px;
            height: 30px;
            background-color: #9b59b6; /* Open */
            border: 1px solid #542e71;
            position: absolute;
            bottom: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 0.7em;
            border-radius: 4px;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
            z-index: 5;
            transition: background-color 0.3s ease;
        }
        .cashier.closed {
            background-color: #7f8c8d; /* Closed */
            border-color: #525a5b;
        }

        .entrance, .exit {
            position: absolute;
            width: 50px;
            height: 50px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 0.8em;
            color: white;
            border-radius: 4px;
            z-index: 5;
        }
        .entrance {
            background-color: #27ae60;
            left: 10px;
            top: 225px; /* Will be adjusted by updateDimensions */
        }
        .exit {
            background-color: #c0392b;
            right: 10px;
            top: 225px; /* Will be adjusted by updateDimensions */
        }
        .aisle {
            background-color: #c8c8c8;
            border: 1px solid #a0a0a0;
            position: absolute;
            box-shadow: inset 0 0 8px rgba(0,0,0,0.15);
            z-index: 1;
        }
        .stats {
            margin-top: 10px;
            font-size: 0.9em;
        }
    </style>
</head>
<body>
    <div class="controls">
        <div class="control-group">
            <label for="peopleSlider">People Arrival Rate:</label>
            <div style="display: flex; align-items: center;">
                <input type="range" id="peopleSlider" min="1" max="10" value="3">
                <span id="peopleValue">3</span>
            </div>
        </div>
        <div class="control-group">
            <label for="cashierSlider">Open Cashiers:</label>
            <div style="display: flex; align-items: center;">
                <input type="range" id="cashierSlider" min="0" max="5" value="2">
                <span id="cashierValue">2</span>
            </div>
        </div>
    </div>

    <div id="supermarket">
        <div class="entrance">ENTRANCE</div>
        <div class="exit">EXIT</div>
        <div class="aisle" id="aisle-0"></div>
        <div class="aisle" id="aisle-1"></div>
        <div class="aisle" id="aisle-2"></div>
    </div>
    <div class="stats">
        Total People: <span id="totalPeopleStat">0</span> |
        Shopping: <span id="shoppingPeopleStat">0</span> |
        Queuing: <span id="queuingPeopleStat">0</span> |
        Paying: <span id="payingPeopleStat">0</span>
    </div>

    <script>
        const supermarket = document.getElementById('supermarket');
        const peopleSlider = document.getElementById('peopleSlider');
        const peopleValueSpan = document.getElementById('peopleValue');
        const cashierSlider = document.getElementById('cashierSlider');
        const cashierValueSpan = document.getElementById('cashierValue');

        const totalPeopleStat = document.getElementById('totalPeopleStat');
        const shoppingPeopleStat = document.getElementById('shoppingPeopleStat');
        const queuingPeopleStat = document.getElementById('queuingPeopleStat');
        const payingPeopleStat = document.getElementById('payingPeopleStat');

        let supermarketWidth = 800; 
        let supermarketHeight = 500; 
        
        let entrancePos = { x: 30, y: 250 }; 
        let exitPos = { x: 770, y: 250 };    
        
        const aisleDefinitions = [
            { id: 'aisle-0', x: 180, y: 50, width: 40, height: 380 },
            { id: 'aisle-1', x: 380, y: 50, width: 40, height: 380 },
            { id: 'aisle-2', x: 580, y: 50, width: 40, height: 380 }
        ];

        let people = [];
        let cashiers = [];
        let personIdCounter = 0;
        let arrivalRate = 3; 
        let numOpenCashiers = 2;
        const MAX_CASHIERS = 5;
        let CASHIER_Y_POS; 
        let CASHIER_QUEUE_Y_START;
        const PERSON_IN_QUEUE_SPACING = 18;
        const QUEUE_SWITCH_THRESHOLD = 2; // Switch if new queue is at least this many people shorter

        function updateDimensions() {
            supermarketWidth = supermarket.offsetWidth;
            supermarketHeight = supermarket.offsetHeight;
            entrancePos = { x: 30, y: supermarketHeight / 2 };
            exitPos = { x: supermarketWidth - 30, y: supermarketHeight / 2 };
            CASHIER_Y_POS = supermarketHeight - 45; 
            CASHIER_QUEUE_Y_START = CASHIER_Y_POS - 30;

            const entranceEl = document.querySelector('.entrance');
            const exitEl = document.querySelector('.exit');
            if(entranceEl) entranceEl.style.top = `${(supermarketHeight / 2) - (entranceEl.offsetHeight / 2)}px`;
            if(exitEl) exitEl.style.top = `${(supermarketHeight / 2) - (exitEl.offsetHeight / 2)}px`;
        }


        function updateSliderValues() {
            console.log("updateSliderValues triggered.");
            arrivalRate = parseInt(peopleSlider.value);
            peopleValueSpan.textContent = arrivalRate.toString();
            numOpenCashiers = parseInt(cashierSlider.value);
            cashierValueSpan.textContent = numOpenCashiers.toString();
            console.log(`New arrivalRate: ${arrivalRate}, New numOpenCashiers: ${numOpenCashiers}`);
            updateCashierStatus();
        }

        peopleSlider.addEventListener('input', updateSliderValues);
        cashierSlider.addEventListener('input', updateSliderValues);

        function setupAisles() {
            aisleDefinitions.forEach(aisleData => {
                const aisleEl = document.getElementById(aisleData.id);
                if (aisleEl) {
                    aisleEl.style.left = `${aisleData.x}px`;
                    aisleEl.style.top = `${aisleData.y}px`;
                    aisleEl.style.width = `${aisleData.width}px`;
                    aisleEl.style.height = `${aisleData.height}px`;
                }
            });
        }

        class Person {
            constructor(id) {
                this.id = id;
                this.x = entrancePos.x;
                this.y = entrancePos.y;
                this.element = document.createElement('div');
                this.element.className = 'person';
                this.element.id = `person-${id}`;
                supermarket.appendChild(this.element);
                
                this.state = 'entering'; 
                this.shoppingTime = Math.random() * 7000 + 6000; 
                this.paymentTime = Math.random() * 2500 + 2500; 
                this.targetX = 0;
                this.targetY = 0;
                this.assignedCashier = null;
                this.timeSinceLastQueueCheck = 0;
                this.queueCheckInterval = 2000 + Math.random() * 2000; // Check queue every 2-4 seconds
                
                this.updateVisualState();
                this.updatePosition();
                this.setRandomShoppingTarget(); 
            }

            updateVisualState() {
                const states = ['entering', 'shopping', 'movingToCashier', 'queuing', 'paying', 'leaving'];
                states.forEach(s => this.element.classList.remove(s));
                if (this.state) {
                    this.element.classList.add(this.state);
                }
            }

            setRandomShoppingTarget() {
                const randomAisle = aisleDefinitions[Math.floor(Math.random() * aisleDefinitions.length)];
                this.targetX = randomAisle.x + (randomAisle.width / 2) + (Math.random() * 30 - 15);
                this.targetY = randomAisle.y + Math.random() * randomAisle.height; 
            }

            updatePosition() {
                this.element.style.left = `${this.x - 6}px`;
                this.element.style.top = `${this.y - 6}px`;
            }

            move() {
                const speed = (this.state === 'shopping') ? 1.2 : 2.2; 
                const dx = this.targetX - this.x;
                const dy = this.targetY - this.y;
                const distance = Math.sqrt(dx * dx + dy * dy);

                if (distance < speed * 1.5) { 
                    this.x = this.targetX;
                    this.y = this.targetY;
                    this.handleArrivalAtTarget();
                } else {
                    this.x += (dx / distance) * speed;
                    this.y += (dy / distance) * speed;
                }
                this.updatePosition();
            }

            handleArrivalAtTarget() {
                if (this.state === 'entering') { 
                    this.state = 'shopping';
                    this.updateVisualState();
                    this.setRandomShoppingTarget(); 
                } else if (this.state === 'shopping') {
                    this.shoppingTime -= 100; 
                    if (this.shoppingTime <= 0) {
                        this.state = 'movingToCashier'; 
                        this.updateVisualState();
                        this.assignedCashier = null; 
                    } else {
                        setTimeout(() => {
                            if (this.state === 'shopping') this.setRandomShoppingTarget();
                        }, Math.random() * 1000 + 800);
                    }
                } else if (this.state === 'movingToCashier') {
                    if (this.assignedCashier) { 
                        if (Math.abs(this.y - this.targetY) < 5 && Math.abs(this.x - this.targetX) < 5) {
                           this.state = 'queuing';
                           this.updateVisualState();
                           console.log(`Person ${this.id} arrived at queue for C${this.assignedCashier.id+1}, state changed to 'queuing'.`);
                        }
                    }
                } else if (this.state === 'leaving' && Math.abs(this.x - exitPos.x) < 10 && Math.abs(this.y - exitPos.y) < 10) {
                    this.remove();
                }
            }
            
            assignToCashierQueue(cashier, queuePosition) {
                this.assignedCashier = cashier;
                this.targetX = cashier.x; 
                this.targetY = CASHIER_QUEUE_Y_START - (queuePosition * PERSON_IN_QUEUE_SPACING); 
                this.state = 'movingToCashier'; 
                this.updateVisualState();
                console.log(`Person ${this.id} assigned to Cashier C${cashier.id+1}, queue pos ${queuePosition}. Target: ${this.targetX.toFixed(1)},${this.targetY.toFixed(1)}`);
            }

            startPaying(cashier) {
                this.state = 'paying';
                this.updateVisualState();
                this.targetX = cashier.x; 
                this.targetY = cashier.y + 15; 
                this.paymentTime = Math.random() * 3000 + 4000; 
            }

            remove() {
                if (this.element.parentNode) {
                    this.element.remove();
                }
                people = people.filter(p => p.id !== this.id);
            }

            checkForShorterQueue() {
                if (this.state !== 'queuing' || !this.assignedCashier || this.assignedCashier.currentCustomer === this) {
                    return; // Only check if queuing and not currently being served
                }

                const currentCashier = this.assignedCashier;
                const currentQueueTotalLength = currentCashier.queue.length + (currentCashier.currentCustomer ? 1 : 0);

                let bestAlternativeCashier = null;
                let bestAlternativeQueueLength = currentQueueTotalLength; // Initialize with current length

                for (const cashier of cashiers) {
                    if (cashier.isOpen && cashier !== currentCashier) {
                        const alternativeQueueTotalLength = cashier.queue.length + (cashier.currentCustomer ? 1 : 0);
                        
                        if (alternativeQueueTotalLength < bestAlternativeQueueLength - QUEUE_SWITCH_THRESHOLD) {
                            bestAlternativeQueueLength = alternativeQueueTotalLength;
                            bestAlternativeCashier = cashier;
                        }
                    }
                }

                if (bestAlternativeCashier) {
                    console.log(`Person ${this.id} (at C${currentCashier.id+1}, queue total ${currentQueueTotalLength}) found better queue at C${bestAlternativeCashier.id+1} (total ${bestAlternativeQueueLength}). Switching.`);
                    currentCashier.removeFromQueue(this);
                    this.assignedCashier = null; // Clear before reassigning
                    bestAlternativeCashier.addToQueue(this); // This will set state to movingToCashier and update target
                }
            }
        }

        class Cashier {
            constructor(id, xPos) {
                this.id = id;
                this.x = xPos;
                this.y = CASHIER_Y_POS;
                this.isOpen = false; 
                this.queue = [];
                this.currentCustomer = null;
                this.element = document.createElement('div');
                this.element.className = 'cashier closed'; 
                this.element.id = `cashier-${id}`;
                this.element.textContent = `C${id + 1}`;
                this.element.style.left = `${xPos - 30}px`; // Assuming width 60
                supermarket.appendChild(this.element);
            }

            setOpen(isOpen) {
                console.log(`Cashier C${this.id + 1} setOpen called with: ${isOpen}. Current this.isOpen before change: ${this.isOpen}`);
                const wasOpen = this.isOpen;
                this.isOpen = isOpen;
                this.element.classList.toggle('closed', !this.isOpen);

                if (wasOpen && !this.isOpen) { 
                    console.log(`Cashier C${this.id + 1} is closing. Clearing queue and current customer.`);
                    if (this.currentCustomer) {
                        if (this.currentCustomer.state !== 'leaving') {
                           this.currentCustomer.state = 'movingToCashier';
                           this.currentCustomer.assignedCashier = null;
                           this.currentCustomer.updateVisualState();
                        }
                        this.currentCustomer = null; 
                    }
                    this.queue.forEach(person => {
                        person.state = 'movingToCashier';
                        person.assignedCashier = null;
                        person.updateVisualState();
                    });
                    this.queue = []; 
                }
            }

            addToQueue(person) {
                console.log(`Attempting to add Person ${person.id} to Cashier C${this.id+1}. Cashier open: ${this.isOpen}. Queue length: ${this.queue.length}`);
                if (this.isOpen && !this.queue.includes(person) && this.currentCustomer !== person) {
                    this.queue.push(person);
                    person.assignToCashierQueue(this, this.queue.length - 1); 
                    console.log(`Person ${person.id} added to queue of Cashier C${this.id+1}. New queue length: ${this.queue.length}`);
                } else {
                    console.log(`Failed to add Person ${person.id} to Cashier C${this.id+1}. isOpen: ${this.isOpen}, already in queue: ${this.queue.includes(person)}, is current customer: ${this.currentCustomer === person}`);
                }
            }

            removeFromQueue(person) {
                const index = this.queue.indexOf(person);
                if (index > -1) {
                    this.queue.splice(index, 1);
                    console.log(`Person ${person.id} removed from queue of Cashier C${this.id+1}.`);
                    this.updateQueuePositions(); 
                    return true;
                }
                return false;
            }
            
            updateQueuePositions() { 
                this.queue.forEach((person, index) => {
                    person.targetX = this.x;
                    person.targetY = CASHIER_QUEUE_Y_START - (index * PERSON_IN_QUEUE_SPACING);
                    if (person.state === 'queuing' || person.state === 'movingToCashier') { // Ensure target is updated even if moving
                        // console.log(`Updating target for Person ${person.id} in C${this.id+1}'s queue to pos ${index}`);
                    }
                });
            }

            process() {
                if (!this.isOpen && this.queue.length === 0 && !this.currentCustomer) return;

                if (this.currentCustomer) {
                    if (this.currentCustomer.state === 'paying') { 
                         this.currentCustomer.paymentTime -= 100; 
                        if (this.currentCustomer.paymentTime <= 0) {
                            this.currentCustomer.state = 'leaving';
                            this.currentCustomer.updateVisualState();
                            this.currentCustomer.targetX = exitPos.x;
                            this.currentCustomer.targetY = exitPos.y;
                            this.currentCustomer.assignedCashier = null; 
                            this.currentCustomer = null;
                        }
                    }
                }

                if (!this.currentCustomer && this.queue.length > 0) {
                    const nextPerson = this.queue[0];
                    console.log(`Cashier C${this.id+1} process: Trying to take next person. Queue head: Person ${nextPerson.id}, state: ${nextPerson.state}. Cashier open: ${this.isOpen}`);
                    if (this.isOpen && nextPerson.state === 'queuing') { 
                        this.currentCustomer = this.queue.shift();
                        console.log(`Cashier C${this.id+1} took Person ${this.currentCustomer.id} from queue. State was 'queuing'.`);
                        this.currentCustomer.startPaying(this);
                        this.updateQueuePositions(); 
                    } else if (!this.isOpen) {
                        console.log(`Cashier C${this.id+1} is closed, cannot process person ${nextPerson.id}`);
                    } else if (nextPerson.state !== 'queuing') {
                        console.log(`Cashier C${this.id+1} process: Next person ${nextPerson.id} is not in 'queuing' state (state is ${nextPerson.state}). Not taking.`);
                    }
                }
            }
        }

        function setupCashiers() {
            cashiers = []; 
            const totalCashierZoneWidth = supermarketWidth * 0.7;
            const startX = supermarketWidth * 0.15;
            const cashierSpacing = totalCashierZoneWidth / (MAX_CASHIERS); 
            for (let i = 0; i < MAX_CASHIERS; i++) {
                const xPos = startX + (i * cashierSpacing) + (cashierSpacing / 2);
                cashiers.push(new Cashier(i, xPos));
            }
        }

        function updateCashierStatus() {
            console.log(`updateCashierStatus called. numOpenCashiers: ${numOpenCashiers}, cashiers.length: ${cashiers ? cashiers.length : 'undefined'}`);
            if (!cashiers || cashiers.length === 0) {
                console.error("Cashiers array is not initialized or empty in updateCashierStatus.");
                return;
            }
            cashiers.forEach((cashier, index) => {
                console.log(`Setting cashier C${cashier.id + 1} (index ${index}) open status based on numOpenCashiers (${numOpenCashiers})`);
                cashier.setOpen(index < numOpenCashiers);
            });
        }
        
        function findBestCashierFor(person) {
            let bestCashier = null;
            let minQueueLength = Infinity; 
            let openCashiersFound = 0;

            for (const cashier of cashiers) {
                if (cashier.isOpen) {
                    openCashiersFound++;
                    const currentQueueSize = cashier.queue.length + (cashier.currentCustomer ? 1 : 0);
                    if (currentQueueSize < minQueueLength) {
                        minQueueLength = currentQueueSize;
                        bestCashier = cashier;
                    } else if (currentQueueSize === minQueueLength) {
                        if (bestCashier && Math.hypot(person.x - cashier.x, person.y - cashier.y) < Math.hypot(person.x - bestCashier.x, person.y - bestCashier.y)) {
                            bestCashier = cashier;
                        }
                    }
                }
            }
            return bestCashier;
        }

        function gameLoop() {
            // Person arrival
            if (Math.random() < (arrivalRate * arrivalRate) / 600 ) { 
                if (people.length < 70) { 
                    const newPerson = new Person(personIdCounter++);
                    people.push(newPerson);
                }
            }

            let shoppingCount = 0;
            let queuingCount = 0;
            let payingCount = 0;

            people.forEach(person => {
                // Assign to cashier if needed
                if (person.state === 'movingToCashier' && !person.assignedCashier) {
                    const bestCashier = findBestCashierFor(person);
                    if (bestCashier) {
                        bestCashier.addToQueue(person); 
                    } else {
                        if (!person.waitingForCashierTimeout) {
                            person.waitingForCashierTimeout = setTimeout(() => {
                                if(person.state === 'movingToCashier' && !person.assignedCashier) { 
                                   person.state = 'shopping';
                                   person.updateVisualState();
                                   person.setRandomShoppingTarget();
                                }
                                person.waitingForCashierTimeout = null; 
                           }, 2000 + Math.random() * 2000); 
                        }
                    }
                }
                
                person.move();

                // Check for shorter queues if currently queuing
                if (person.state === 'queuing') {
                    person.timeSinceLastQueueCheck += 16; // Approximate ms per frame
                    if (person.timeSinceLastQueueCheck >= person.queueCheckInterval) {
                        person.checkForShorterQueue();
                        person.timeSinceLastQueueCheck = 0;
                        person.queueCheckInterval = 2000 + Math.random() * 2000; // Randomize next check
                    }
                }

                // Update stats
                if (person.state === 'shopping') shoppingCount++;
                else if (person.state === 'queuing') queuingCount++;
                else if (person.state === 'paying') payingCount++;
            });

            cashiers.forEach(cashier => {
                cashier.process();
            });

            totalPeopleStat.textContent = people.length.toString();
            shoppingPeopleStat.textContent = shoppingCount.toString();
            queuingPeopleStat.textContent = queuingCount.toString();
            payingPeopleStat.textContent = payingCount.toString();

            requestAnimationFrame(gameLoop);
        }

        document.addEventListener('DOMContentLoaded', () => {
            console.log("DOM Content Loaded. Initializing simulation.");
            updateDimensions(); 
            setupAisles();
            setupCashiers(); 
            updateSliderValues(); 
            gameLoop();
        });
    </script>
</body>
</html>
```

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Supermarket Simulator</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 0;
            background-color: #f0f0f0;
            overflow: hidden; /* Prevents scrollbars if content slightly overflows */
        }
        .controls {
            background-color: #fff;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            margin-bottom: 20px;
            display: flex;
            gap: 20px;
            align-items: center;
            flex-wrap: wrap;
        }
        .control-group {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .control-group label {
            margin-bottom: 5px;
            font-size: 0.9em;
        }
        .control-group input[type="range"] {
            width: 150px;
        }
        .control-group span {
            font-size: 0.9em;
            min-width: 20px;
            text-align: center;
        }

        #supermarket {
            width: 800px;
            height: 500px;
            background-color: #e0e0e0;
            border: 2px solid #333;
            position: relative;
            overflow: hidden;
            box-shadow: 0 0 20px rgba(0,0,0,0.2) inset;
            animation: dynamicLighting 20s infinite alternate;
        }

        @keyframes dynamicLighting {
            0% { background: radial-gradient(ellipse at center, #f0f8ff 0%, #d0e0f0 70%, #b0c0d0 100%); }
            50% { background: radial-gradient(ellipse at center, #e6f2ff 0%, #c0d0e0 70%, #a0b0c0 100%); }
            100% { background: radial-gradient(ellipse at center, #f0f8ff 0%, #d0e0f0 70%, #b0c0d0 100%); }
        }

        .person {
            width: 12px;
            height: 12px;
            background-color: #3498db; /* Default/Entering */
            border-radius: 50%;
            position: absolute;
            transition: left 0.4s linear, top 0.4s linear, background-color 0.3s ease;
            box-shadow: 0 0 5px rgba(0,0,0,0.3);
            z-index: 10;
        }
        .person.entering { background-color: #3498db; }
        .person.shopping { background-color: #f1c40f; }
        .person.movingToCashier { background-color: #e74c3c; }
        .person.queuing { background-color: #e67e22; }
        .person.paying { background-color: #2ecc71; }
        .person.leaving { background-color: #95a5a6; }

        .cashier {
            width: 60px;
            height: 30px;
            background-color: #9b59b6; /* Open */
            border: 1px solid #542e71;
            position: absolute;
            bottom: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 0.7em;
            border-radius: 4px;
            box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
            z-index: 5;
            transition: background-color 0.3s ease;
        }
        .cashier.closed {
            background-color: #7f8c8d; /* Closed */
            border-color: #525a5b;
        }

        .entrance, .exit {
            position: absolute;
            width: 50px;
            height: 50px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 0.8em;
            color: white;
            border-radius: 4px;
            z-index: 5;
        }
        .entrance {
            background-color: #27ae60;
            left: 10px;
            top: 225px; /* Will be adjusted by updateDimensions */
        }
        .exit {
            background-color: #c0392b;
            right: 10px;
            top: 225px; /* Will be adjusted by updateDimensions */
        }
        .aisle {
            background-color: #c8c8c8;
            border: 1px solid #a0a0a0;
            position: absolute;
            box-shadow: inset 0 0 8px rgba(0,0,0,0.15);
            z-index: 1;
        }
        .stats {
            margin-top: 10px;
            font-size: 0.9em;
        }
    </style>
</head>
<body>
    <div class="controls">
        <div class="control-group">
            <label for="peopleSlider">People Arrival Rate:</label>
            <div style="display: flex; align-items: center;">
                <input type="range" id="peopleSlider" min="1" max="10" value="3">
                <span id="peopleValue">3</span>
            </div>
        </div>
        <div class="control-group">
            <label for="cashierSlider">Open Cashiers:</label>
            <div style="display: flex; align-items: center;">
                <input type="range" id="cashierSlider" min="0" max="5" value="2">
                <span id="cashierValue">2</span>
            </div>
        </div>
    </div>

    <div id="supermarket">
        <div class="entrance">ENTRANCE</div>
        <div class="exit">EXIT</div>
        <div class="aisle" id="aisle-0"></div>
        <div class="aisle" id="aisle-1"></div>
        <div class="aisle" id="aisle-2"></div>
    </div>
    <div class="stats">
        Total People: <span id="totalPeopleStat">0</span> |
        Shopping: <span id="shoppingPeopleStat">0</span> |
        Queuing: <span id="queuingPeopleStat">0</span> |
        Paying: <span id="payingPeopleStat">0</span>
    </div>

    <script>
        const supermarket = document.getElementById('supermarket');
        const peopleSlider = document.getElementById('peopleSlider');
        const peopleValueSpan = document.getElementById('peopleValue');
        const cashierSlider = document.getElementById('cashierSlider');
        const cashierValueSpan = document.getElementById('cashierValue');

        const totalPeopleStat = document.getElementById('totalPeopleStat');
        const shoppingPeopleStat = document.getElementById('shoppingPeopleStat');
        const queuingPeopleStat = document.getElementById('queuingPeopleStat');
        const payingPeopleStat = document.getElementById('payingPeopleStat');

        let supermarketWidth = 800; 
        let supermarketHeight = 500; 
        
        let entrancePos = { x: 30, y: 250 }; 
        let exitPos = { x: 770, y: 250 };    
        
        const aisleDefinitions = [
            { id: 'aisle-0', x: 180, y: 50, width: 40, height: 380 },
            { id: 'aisle-1', x: 380, y: 50, width: 40, height: 380 },
            { id: 'aisle-2', x: 580, y: 50, width: 40, height: 380 }
        ];

        let people = [];
        let cashiers = [];
        let personIdCounter = 0;
        let arrivalRate = 3; 
        let numOpenCashiers = 2;
        const MAX_CASHIERS = 5;
        let CASHIER_Y_POS; 
        let CASHIER_QUEUE_Y_START;
        const PERSON_IN_QUEUE_SPACING = 18;
        const QUEUE_SWITCH_THRESHOLD = 2; // Switch if new queue is at least this many people shorter

        function updateDimensions() {
            supermarketWidth = supermarket.offsetWidth;
            supermarketHeight = supermarket.offsetHeight;
            entrancePos = { x: 30, y: supermarketHeight / 2 };
            exitPos = { x: supermarketWidth - 30, y: supermarketHeight / 2 };
            CASHIER_Y_POS = supermarketHeight - 45; 
            CASHIER_QUEUE_Y_START = CASHIER_Y_POS - 30;

            const entranceEl = document.querySelector('.entrance');
            const exitEl = document.querySelector('.exit');
            if(entranceEl) entranceEl.style.top = `${(supermarketHeight / 2) - (entranceEl.offsetHeight / 2)}px`;
            if(exitEl) exitEl.style.top = `${(supermarketHeight / 2) - (exitEl.offsetHeight / 2)}px`;
        }


        function updateSliderValues() {
            console.log("updateSliderValues triggered.");
            arrivalRate = parseInt(peopleSlider.value);
            peopleValueSpan.textContent = arrivalRate.toString();
            numOpenCashiers = parseInt(cashierSlider.value);
            cashierValueSpan.textContent = numOpenCashiers.toString();
            console.log(`New arrivalRate: ${arrivalRate}, New numOpenCashiers: ${numOpenCashiers}`);
            updateCashierStatus();
        }

        peopleSlider.addEventListener('input', updateSliderValues);
        cashierSlider.addEventListener('input', updateSliderValues);

        function setupAisles() {
            aisleDefinitions.forEach(aisleData => {
                const aisleEl = document.getElementById(aisleData.id);
                if (aisleEl) {
                    aisleEl.style.left = `${aisleData.x}px`;
                    aisleEl.style.top = `${aisleData.y}px`;
                    aisleEl.style.width = `${aisleData.width}px`;
                    aisleEl.style.height = `${aisleData.height}px`;
                }
            });
        }

        class Person {
            constructor(id) {
                this.id = id;
                this.x = entrancePos.x;
                this.y = entrancePos.y;
                this.element = document.createElement('div');
                this.element.className = 'person';
                this.element.id = `person-${id}`;
                supermarket.appendChild(this.element);
                
                this.state = 'entering'; 
                this.shoppingTime = Math.random() * 7000 + 6000; 
                this.paymentTime = Math.random() * 2500 + 2500; 
                this.targetX = 0;
                this.targetY = 0;
                this.assignedCashier = null;
                this.timeSinceLastQueueCheck = 0;
                this.queueCheckInterval = 2000 + Math.random() * 2000; // Check queue every 2-4 seconds
                
                this.updateVisualState();
                this.updatePosition();
                this.setRandomShoppingTarget(); 
            }

            updateVisualState() {
                const states = ['entering', 'shopping', 'movingToCashier', 'queuing', 'paying', 'leaving'];
                states.forEach(s => this.element.classList.remove(s));
                if (this.state) {
                    this.element.classList.add(this.state);
                }
            }

            setRandomShoppingTarget() {
                const randomAisle = aisleDefinitions[Math.floor(Math.random() * aisleDefinitions.length)];
                this.targetX = randomAisle.x + (randomAisle.width / 2) + (Math.random() * 30 - 15);
                this.targetY = randomAisle.y + Math.random() * randomAisle.height; 
            }

            updatePosition() {
                this.element.style.left = `${this.x - 6}px`;
                this.element.style.top = `${this.y - 6}px`;
            }

            move() {
                const speed = (this.state === 'shopping') ? 1.2 : 2.2; 
                const dx = this.targetX - this.x;
                const dy = this.targetY - this.y;
                const distance = Math.sqrt(dx * dx + dy * dy);

                if (distance < speed * 1.5) { 
                    this.x = this.targetX;
                    this.y = this.targetY;
                    this.handleArrivalAtTarget();
                } else {
                    this.x += (dx / distance) * speed;
                    this.y += (dy / distance) * speed;
                }
                this.updatePosition();
            }

            handleArrivalAtTarget() {
                if (this.state === 'entering') { 
                    this.state = 'shopping';
                    this.updateVisualState();
                    this.setRandomShoppingTarget(); 
                } else if (this.state === 'shopping') {
                    this.shoppingTime -= 100; 
                    if (this.shoppingTime <= 0) {
                        this.state = 'movingToCashier'; 
                        this.updateVisualState();
                        this.assignedCashier = null; 
                    } else {
                        setTimeout(() => {
                            if (this.state === 'shopping') this.setRandomShoppingTarget();
                        }, Math.random() * 1000 + 800);
                    }
                } else if (this.state === 'movingToCashier') {
                    if (this.assignedCashier) { 
                        if (Math.abs(this.y - this.targetY) < 5 && Math.abs(this.x - this.targetX) < 5) {
                           this.state = 'queuing';
                           this.updateVisualState();
                           console.log(`Person ${this.id} arrived at queue for C${this.assignedCashier.id+1}, state changed to 'queuing'.`);
                        }
                    }
                } else if (this.state === 'leaving' && Math.abs(this.x - exitPos.x) < 10 && Math.abs(this.y - exitPos.y) < 10) {
                    this.remove();
                }
            }
            
            assignToCashierQueue(cashier, queuePosition) {
                this.assignedCashier = cashier;
                this.targetX = cashier.x; 
                this.targetY = CASHIER_QUEUE_Y_START - (queuePosition * PERSON_IN_QUEUE_SPACING); 
                this.state = 'movingToCashier'; 
                this.updateVisualState();
                console.log(`Person ${this.id} assigned to Cashier C${cashier.id+1}, queue pos ${queuePosition}. Target: ${this.targetX.toFixed(1)},${this.targetY.toFixed(1)}`);
            }

            startPaying(cashier) {
                this.state = 'paying';
                this.updateVisualState();
                this.targetX = cashier.x; 
                this.targetY = cashier.y + 15; 
                this.paymentTime = Math.random() * 3000 + 4000; 
            }

            remove() {
                if (this.element.parentNode) {
                    this.element.remove();
                }
                people = people.filter(p => p.id !== this.id);
            }

            checkForShorterQueue() {
                if (this.state !== 'queuing' || !this.assignedCashier || this.assignedCashier.currentCustomer === this) {
                    return; // Only check if queuing and not currently being served
                }

                const currentCashier = this.assignedCashier;
                const currentQueueTotalLength = currentCashier.queue.length + (currentCashier.currentCustomer ? 1 : 0);

                let bestAlternativeCashier = null;
                let bestAlternativeQueueLength = currentQueueTotalLength; // Initialize with current length

                for (const cashier of cashiers) {
                    if (cashier.isOpen && cashier !== currentCashier) {
                        const alternativeQueueTotalLength = cashier.queue.length + (cashier.currentCustomer ? 1 : 0);
                        
                        if (alternativeQueueTotalLength < bestAlternativeQueueLength - QUEUE_SWITCH_THRESHOLD) {
                            bestAlternativeQueueLength = alternativeQueueTotalLength;
                            bestAlternativeCashier = cashier;
                        }
                    }
                }

                if (bestAlternativeCashier) {
                    console.log(`Person ${this.id} (at C${currentCashier.id+1}, queue total ${currentQueueTotalLength}) found better queue at C${bestAlternativeCashier.id+1} (total ${bestAlternativeQueueLength}). Switching.`);
                    currentCashier.removeFromQueue(this);
                    this.assignedCashier = null; // Clear before reassigning
                    bestAlternativeCashier.addToQueue(this); // This will set state to movingToCashier and update target
                }
            }
        }

        class Cashier {
            constructor(id, xPos) {
                this.id = id;
                this.x = xPos;
                this.y = CASHIER_Y_POS;
                this.isOpen = false; 
                this.queue = [];
                this.currentCustomer = null;
                this.element = document.createElement('div');
                this.element.className = 'cashier closed'; 
                this.element.id = `cashier-${id}`;
                this.element.textContent = `C${id + 1}`;
                this.element.style.left = `${xPos - 30}px`; // Assuming width 60
                supermarket.appendChild(this.element);
            }

            setOpen(isOpen) {
                console.log(`Cashier C${this.id + 1} setOpen called with: ${isOpen}. Current this.isOpen before change: ${this.isOpen}`);
                const wasOpen = this.isOpen;
                this.isOpen = isOpen;
                this.element.classList.toggle('closed', !this.isOpen);

                if (wasOpen && !this.isOpen) { 
                    console.log(`Cashier C${this.id + 1} is closing. Clearing queue and current customer.`);
                    if (this.currentCustomer) {
                        if (this.currentCustomer.state !== 'leaving') {
                           this.currentCustomer.state = 'movingToCashier';
                           this.currentCustomer.assignedCashier = null;
                           this.currentCustomer.updateVisualState();
                        }
                        this.currentCustomer = null; 
                    }
                    this.queue.forEach(person => {
                        person.state = 'movingToCashier';
                        person.assignedCashier = null;
                        person.updateVisualState();
                    });
                    this.queue = []; 
                }
            }

            addToQueue(person) {
                console.log(`Attempting to add Person ${person.id} to Cashier C${this.id+1}. Cashier open: ${this.isOpen}. Queue length: ${this.queue.length}`);
                if (this.isOpen && !this.queue.includes(person) && this.currentCustomer !== person) {
                    this.queue.push(person);
                    person.assignToCashierQueue(this, this.queue.length - 1); 
                    console.log(`Person ${person.id} added to queue of Cashier C${this.id+1}. New queue length: ${this.queue.length}`);
                } else {
                    console.log(`Failed to add Person ${person.id} to Cashier C${this.id+1}. isOpen: ${this.isOpen}, already in queue: ${this.queue.includes(person)}, is current customer: ${this.currentCustomer === person}`);
                }
            }

            removeFromQueue(person) {
                const index = this.queue.indexOf(person);
                if (index > -1) {
                    this.queue.splice(index, 1);
                    console.log(`Person ${person.id} removed from queue of Cashier C${this.id+1}.`);
                    this.updateQueuePositions(); 
                    return true;
                }
                return false;
            }
            
            updateQueuePositions() { 
                this.queue.forEach((person, index) => {
                    person.targetX = this.x;
                    person.targetY = CASHIER_QUEUE_Y_START - (index * PERSON_IN_QUEUE_SPACING);
                    if (person.state === 'queuing' || person.state === 'movingToCashier') { // Ensure target is updated even if moving
                        // console.log(`Updating target for Person ${person.id} in C${this.id+1}'s queue to pos ${index}`);
                    }
                });
            }

            process() {
                if (!this.isOpen && this.queue.length === 0 && !this.currentCustomer) return;

                if (this.currentCustomer) {
                    if (this.currentCustomer.state === 'paying') { 
                         this.currentCustomer.paymentTime -= 100; 
                        if (this.currentCustomer.paymentTime <= 0) {
                            this.currentCustomer.state = 'leaving';
                            this.currentCustomer.updateVisualState();
                            this.currentCustomer.targetX = exitPos.x;
                            this.currentCustomer.targetY = exitPos.y;
                            this.currentCustomer.assignedCashier = null; 
                            this.currentCustomer = null;
                        }
                    }
                }

                if (!this.currentCustomer && this.queue.length > 0) {
                    const nextPerson = this.queue[0];
                    console.log(`Cashier C${this.id+1} process: Trying to take next person. Queue head: Person ${nextPerson.id}, state: ${nextPerson.state}. Cashier open: ${this.isOpen}`);
                    if (this.isOpen && nextPerson.state === 'queuing') { 
                        this.currentCustomer = this.queue.shift();
                        console.log(`Cashier C${this.id+1} took Person ${this.currentCustomer.id} from queue. State was 'queuing'.`);
                        this.currentCustomer.startPaying(this);
                        this.updateQueuePositions(); 
                    } else if (!this.isOpen) {
                        console.log(`Cashier C${this.id+1} is closed, cannot process person ${nextPerson.id}`);
                    } else if (nextPerson.state !== 'queuing') {
                        console.log(`Cashier C${this.id+1} process: Next person ${nextPerson.id} is not in 'queuing' state (state is ${nextPerson.state}). Not taking.`);
                    }
                }
            }
        }

        function setupCashiers() {
            cashiers = []; 
            const totalCashierZoneWidth = supermarketWidth * 0.7;
            const startX = supermarketWidth * 0.15;
            const cashierSpacing = totalCashierZoneWidth / (MAX_CASHIERS); 
            for (let i = 0; i < MAX_CASHIERS; i++) {
                const xPos = startX + (i * cashierSpacing) + (cashierSpacing / 2);
                cashiers.push(new Cashier(i, xPos));
            }
        }

        function updateCashierStatus() {
            console.log(`updateCashierStatus called. numOpenCashiers: ${numOpenCashiers}, cashiers.length: ${cashiers ? cashiers.length : 'undefined'}`);
            if (!cashiers || cashiers.length === 0) {
                console.error("Cashiers array is not initialized or empty in updateCashierStatus.");
                return;
            }
            cashiers.forEach((cashier, index) => {
                console.log(`Setting cashier C${cashier.id + 1} (index ${index}) open status based on numOpenCashiers (${numOpenCashiers})`);
                cashier.setOpen(index < numOpenCashiers);
            });
        }
        
        function findBestCashierFor(person) {
            let bestCashier = null;
            let minQueueLength = Infinity; 
            let openCashiersFound = 0;

            for (const cashier of cashiers) {
                if (cashier.isOpen) {
                    openCashiersFound++;
                    const currentQueueSize = cashier.queue.length + (cashier.currentCustomer ? 1 : 0);
                    if (currentQueueSize < minQueueLength) {
                        minQueueLength = currentQueueSize;
                        bestCashier = cashier;
                    } else if (currentQueueSize === minQueueLength) {
                        if (bestCashier && Math.hypot(person.x - cashier.x, person.y - cashier.y) < Math.hypot(person.x - bestCashier.x, person.y - bestCashier.y)) {
                            bestCashier = cashier;
                        }
                    }
                }
            }
            return bestCashier;
        }

        function gameLoop() {
            // Person arrival
            if (Math.random() < (arrivalRate * arrivalRate) / 600 ) { 
                if (people.length < 70) { 
                    const newPerson = new Person(personIdCounter++);
                    people.push(newPerson);
                }
            }

            let shoppingCount = 0;
            let queuingCount = 0;
            let payingCount = 0;

            people.forEach(person => {
                // Assign to cashier if needed
                if (person.state === 'movingToCashier' && !person.assignedCashier) {
                    const bestCashier = findBestCashierFor(person);
                    if (bestCashier) {
                        bestCashier.addToQueue(person); 
                    } else {
                        if (!person.waitingForCashierTimeout) {
                            person.waitingForCashierTimeout = setTimeout(() => {
                                if(person.state === 'movingToCashier' && !person.assignedCashier) { 
                                   person.state = 'shopping';
                                   person.updateVisualState();
                                   person.setRandomShoppingTarget();
                                }
                                person.waitingForCashierTimeout = null; 
                           }, 2000 + Math.random() * 2000); 
                        }
                    }
                }
                
                person.move();

                // Check for shorter queues if currently queuing
                if (person.state === 'queuing') {
                    person.timeSinceLastQueueCheck += 16; // Approximate ms per frame
                    if (person.timeSinceLastQueueCheck >= person.queueCheckInterval) {
                        person.checkForShorterQueue();
                        person.timeSinceLastQueueCheck = 0;
                        person.queueCheckInterval = 2000 + Math.random() * 2000; // Randomize next check
                    }
                }

                // Update stats
                if (person.state === 'shopping') shoppingCount++;
                else if (person.state === 'queuing') queuingCount++;
                else if (person.state === 'paying') payingCount++;
            });

            cashiers.forEach(cashier => {
                cashier.process();
            });

            totalPeopleStat.textContent = people.length.toString();
            shoppingPeopleStat.textContent = shoppingCount.toString();
            queuingPeopleStat.textContent = queuingCount.toString();
            payingPeopleStat.textContent = payingCount.toString();

            requestAnimationFrame(gameLoop);
        }

        document.addEventListener('DOMContentLoaded', () => {
            console.log("DOM Content Loaded. Initializing simulation.");
            updateDimensions(); 
            setupAisles();
            setupCashiers(); 
            updateSliderValues(); 
            gameLoop();
        });
    </script>
</body>
</html>

```

No comments: