"""Realistic event simulation engine for demo/testing.""" import random import math import logging import threading import time from datetime import datetime from typing import Dict, Optional, Callable from models.venue import Venue, Zone from models.queue import QueueStation from models.event import Event, EventPhase logger = logging.getLogger(__name__) def create_demo_venue() -> Venue: """Create a realistic cricket stadium layout (based on a typical Indian cricket ground).""" # Center coordinates (representative sports venue in Mumbai) center_lat = 18.9388 center_lng = 72.8255 zones = [ # Stands Zone(id="north_stand", name="North Stand", zone_type="stand", capacity=8000, lat=center_lat + 0.0015, lng=center_lng, amenities=["seating", "shade"]), Zone(id="south_stand", name="South Stand", zone_type="stand", capacity=8000, lat=center_lat - 0.0015, lng=center_lng, amenities=["seating", "shade"]), Zone(id="east_pavilion", name="East Pavilion", zone_type="stand", capacity=6000, lat=center_lat, lng=center_lng + 0.0015, amenities=["seating", "premium"]), Zone(id="west_pavilion", name="West Pavilion", zone_type="stand", capacity=6000, lat=center_lat, lng=center_lng - 0.0015, amenities=["seating", "premium"]), Zone(id="main_pavilion", name="Main Pavilion (VIP)", zone_type="stand", capacity=3000, lat=center_lat + 0.001, lng=center_lng + 0.001, amenities=["seating", "vip", "lounge"]), # Concourses Zone(id="north_concourse", name="North Concourse", zone_type="concourse", capacity=4000, lat=center_lat + 0.002, lng=center_lng, amenities=["walking"]), Zone(id="south_concourse", name="South Concourse", zone_type="concourse", capacity=4000, lat=center_lat - 0.002, lng=center_lng, amenities=["walking"]), Zone(id="east_concourse", name="East Concourse", zone_type="concourse", capacity=3000, lat=center_lat, lng=center_lng + 0.002, amenities=["walking"]), Zone(id="west_concourse", name="West Concourse", zone_type="concourse", capacity=3000, lat=center_lat, lng=center_lng - 0.002, amenities=["walking"]), # Gates Zone(id="gate_1", name="Gate 1 (Main)", zone_type="gate", capacity=2000, lat=center_lat + 0.0025, lng=center_lng - 0.001, amenities=["entry", "security"]), Zone(id="gate_3", name="Gate 3 (North)", zone_type="gate", capacity=1500, lat=center_lat + 0.0025, lng=center_lng + 0.001, amenities=["entry", "security"]), Zone(id="gate_5", name="Gate 5 (West)", zone_type="gate", capacity=1500, lat=center_lat, lng=center_lng - 0.0025, amenities=["entry", "security"]), Zone(id="gate_7", name="Gate 7 (South)", zone_type="gate", capacity=1500, lat=center_lat - 0.0025, lng=center_lng, amenities=["entry", "security"]), # Food Courts Zone(id="food_north", name="North Food Court", zone_type="food_court", capacity=1500, lat=center_lat + 0.0018, lng=center_lng + 0.0005, amenities=["food", "beverages"]), Zone(id="food_south", name="South Food Court", zone_type="food_court", capacity=1500, lat=center_lat - 0.0018, lng=center_lng - 0.0005, amenities=["food", "beverages"]), Zone(id="food_west", name="West Food Stalls", zone_type="food_court", capacity=800, lat=center_lat - 0.0005, lng=center_lng - 0.0018, amenities=["food"]), # Restrooms Zone(id="restroom_n1", name="Restrooms N1", zone_type="restroom", capacity=200, lat=center_lat + 0.0017, lng=center_lng - 0.0008, amenities=["restroom", "accessible"]), Zone(id="restroom_s1", name="Restrooms S1", zone_type="restroom", capacity=200, lat=center_lat - 0.0017, lng=center_lng + 0.0008, amenities=["restroom", "accessible"]), Zone(id="restroom_e1", name="Restrooms E1", zone_type="restroom", capacity=150, lat=center_lat + 0.0005, lng=center_lng + 0.0017, amenities=["restroom"]), Zone(id="restroom_w1", name="Restrooms W1", zone_type="restroom", capacity=150, lat=center_lat - 0.0005, lng=center_lng - 0.0017, amenities=["restroom"]), # Parking Zone(id="parking_a", name="Parking Lot A", zone_type="parking", capacity=3000, lat=center_lat + 0.003, lng=center_lng, amenities=["parking"]), Zone(id="parking_b", name="Parking Lot B", zone_type="parking", capacity=2000, lat=center_lat, lng=center_lng - 0.003, amenities=["parking"]), ] venue = Venue( id="wankhede", name="Wankhede Stadium", city="Mumbai", total_capacity=50000, zones=zones, center_lat=center_lat, center_lng=center_lng, map_zoom=16, ) return venue def create_demo_queue_stations() -> list: """Create realistic queue stations for the demo venue.""" stations = [ # Food stalls QueueStation(id="f_n1", name="Chaat Counter", category="food", zone_id="food_north", avg_service_time_sec=60, lat=18.9406, lng=72.8260), QueueStation(id="f_n2", name="Biryani House", category="food", zone_id="food_north", avg_service_time_sec=90, lat=18.9406, lng=72.8258), QueueStation(id="f_n3", name="Pizza & Burgers", category="food", zone_id="food_north", avg_service_time_sec=75, lat=18.9406, lng=72.8262), QueueStation(id="f_s1", name="South Indian Delights", category="food", zone_id="food_south", avg_service_time_sec=70, lat=18.9370, lng=72.8250), QueueStation(id="f_s2", name="Beverages & Ice Cream", category="food", zone_id="food_south", avg_service_time_sec=45, lat=18.9370, lng=72.8252), QueueStation(id="f_w1", name="Quick Bites", category="food", zone_id="food_west", avg_service_time_sec=40, lat=18.9385, lng=72.8237), # Merchandise QueueStation(id="m_1", name="Team Jersey Shop", category="merch", zone_id="north_concourse", avg_service_time_sec=120, lat=18.9408, lng=72.8255), QueueStation(id="m_2", name="Souvenir Stand", category="merch", zone_id="east_concourse", avg_service_time_sec=90, lat=18.9388, lng=72.8275), # Restrooms (physical queues) QueueStation(id="r_n1", name="Restrooms N1", category="restroom", zone_id="restroom_n1", avg_service_time_sec=120, lat=18.9405, lng=72.8247), QueueStation(id="r_s1", name="Restrooms S1", category="restroom", zone_id="restroom_s1", avg_service_time_sec=120, lat=18.9371, lng=72.8263), QueueStation(id="r_e1", name="Restrooms E1", category="restroom", zone_id="restroom_e1", avg_service_time_sec=110, lat=18.9393, lng=72.8272), QueueStation(id="r_w1", name="Restrooms W1", category="restroom", zone_id="restroom_w1", avg_service_time_sec=110, lat=18.9383, lng=72.8238), # Entry/Exit gates QueueStation(id="g_1", name="Gate 1 Entry", category="entry", zone_id="gate_1", avg_service_time_sec=15, lat=18.9413, lng=72.8245), QueueStation(id="g_3", name="Gate 3 Entry", category="entry", zone_id="gate_3", avg_service_time_sec=15, lat=18.9413, lng=72.8265), QueueStation(id="g_5", name="Gate 5 Entry", category="entry", zone_id="gate_5", avg_service_time_sec=15, lat=18.9388, lng=72.8230), QueueStation(id="g_7", name="Gate 7 Entry", category="entry", zone_id="gate_7", avg_service_time_sec=15, lat=18.9363, lng=72.8255), ] return stations def create_demo_event() -> Event: """Create a demo cricket match event.""" return Event( id="ipl_2026_01", name="IPL 2026 — Mumbai Indians vs Chennai Super Kings", sport="Cricket", venue_id="wankhede", date=datetime.now(), home_team="Mumbai Indians", away_team="Chennai Super Kings", current_phase="first_half", expected_attendance=33000, actual_attendance=0, weather="clear", temperature_c=31.0, ) # ─── Phase-based simulation profiles ───────────────────────────────────────── PHASE_PROFILES = { "pre_event": { "stand_fill": 0.0, "concourse_fill": 0.05, "gate_fill": 0.1, "food_fill": 0.05, "restroom_fill": 0.05, "parking_fill": 0.3, "queue_food": (0, 3), "queue_restroom": (0, 2), "queue_entry": (0, 5), }, "gates_open": { "stand_fill": 0.1, "concourse_fill": 0.3, "gate_fill": 0.7, "food_fill": 0.15, "restroom_fill": 0.1, "parking_fill": 0.6, "queue_food": (2, 8), "queue_restroom": (1, 4), "queue_entry": (10, 40), }, "filling": { "stand_fill": 0.4, "concourse_fill": 0.5, "gate_fill": 0.8, "food_fill": 0.3, "restroom_fill": 0.2, "parking_fill": 0.8, "queue_food": (5, 15), "queue_restroom": (3, 8), "queue_entry": (20, 60), }, "first_half": { "stand_fill": 0.8, "concourse_fill": 0.15, "gate_fill": 0.1, "food_fill": 0.1, "restroom_fill": 0.1, "parking_fill": 0.9, "queue_food": (2, 8), "queue_restroom": (1, 5), "queue_entry": (0, 5), }, "halftime": { "stand_fill": 0.5, "concourse_fill": 0.7, "gate_fill": 0.15, "food_fill": 0.9, "restroom_fill": 0.8, "parking_fill": 0.9, "queue_food": (15, 40), "queue_restroom": (10, 25), "queue_entry": (0, 3), }, "second_half": { "stand_fill": 0.75, "concourse_fill": 0.1, "gate_fill": 0.1, "food_fill": 0.1, "restroom_fill": 0.1, "parking_fill": 0.9, "queue_food": (2, 8), "queue_restroom": (1, 5), "queue_entry": (0, 2), }, "event_end": { "stand_fill": 0.3, "concourse_fill": 0.6, "gate_fill": 0.8, "food_fill": 0.05, "restroom_fill": 0.3, "parking_fill": 0.9, "queue_food": (0, 3), "queue_restroom": (3, 10), "queue_entry": (0, 0), }, "exiting": { "stand_fill": 0.1, "concourse_fill": 0.4, "gate_fill": 0.9, "food_fill": 0.02, "restroom_fill": 0.2, "parking_fill": 0.7, "queue_food": (0, 2), "queue_restroom": (1, 5), "queue_entry": (0, 0), }, "post_event": { "stand_fill": 0.0, "concourse_fill": 0.02, "gate_fill": 0.05, "food_fill": 0.0, "restroom_fill": 0.02, "parking_fill": 0.2, "queue_food": (0, 0), "queue_restroom": (0, 1), "queue_entry": (0, 0), }, } # Map zone types to profile keys ZONE_TYPE_MAP = { "stand": "stand_fill", "concourse": "concourse_fill", "gate": "gate_fill", "food_court": "food_fill", "restroom": "restroom_fill", "parking": "parking_fill", } class Simulator: """Simulates realistic crowd and queue dynamics.""" def __init__(self, venue: Venue, queue_service, crowd_service, notification_service, event: Event): self.venue = venue self.queue_service = queue_service self.crowd_service = crowd_service self.notification_service = notification_service self.event = event self._running = False self._thread: Optional[threading.Thread] = None self._speed = 1.0 # 1x = real-time, 2x = double speed, etc. self._tick_count = 0 self._update_callback: Optional[Callable] = None @property def is_running(self) -> bool: return self._running def set_update_callback(self, callback: Callable): """Set a callback to fire after each simulation tick.""" self._update_callback = callback def set_phase(self, phase: str): """Manually set the event phase.""" if phase in PHASE_PROFILES: self.event.current_phase = phase logger.info(f"🎬 Simulation phase changed to: {phase}") def set_speed(self, speed: float): """Set simulation speed (1.0 = real-time).""" self._speed = max(0.1, min(10.0, speed)) def start(self): """Start the simulation loop.""" if self._running: return self._running = True self._thread = threading.Thread(target=self._run_loop, daemon=True) self._thread.start() logger.info("▶️ Simulation started") def stop(self): """Stop the simulation loop.""" self._running = False if self._thread: self._thread.join(timeout=5) logger.info("⏹️ Simulation stopped") def tick(self): """Execute one simulation tick — update all zones and queues.""" phase = self.event.current_phase profile = PHASE_PROFILES.get(phase, PHASE_PROFILES["pre_event"]) self._tick_count += 1 # Update zone crowd counts for zone in self.venue.zones: fill_key = ZONE_TYPE_MAP.get(zone.zone_type, "concourse_fill") target_fill = profile.get(fill_key, 0.5) # Add randomness (+/- 15%) jitter = random.uniform(-0.15, 0.15) target_count = int(zone.capacity * max(0, min(1, target_fill + jitter))) # Smooth transition (don't jump instantly) diff = target_count - zone.current_count step = max(1, abs(diff) // 5) if diff > 0: new_count = zone.current_count + min(step, diff) elif diff < 0: new_count = zone.current_count - min(step, abs(diff)) else: new_count = zone.current_count self.crowd_service.update_zone_count(self.venue, zone.id, new_count) # Update queue lengths for station_id, station in self.queue_service._stations.items(): cat = station.category queue_range = profile.get(f"queue_{cat}", (0, 5)) target = random.randint(queue_range[0], queue_range[1]) # Smooth transition diff = target - station.current_length step = max(1, abs(diff) // 3) if diff > 0: new_len = station.current_length + min(step, diff) elif diff < 0: new_len = station.current_length - min(step, abs(diff)) else: new_len = station.current_length self.queue_service.update_queue_length(station_id, max(0, new_len)) # Update attendance count self.event.actual_attendance = self.venue.total_current # Periodic alerts based on conditions if self._tick_count % 10 == 0: self._check_and_alert() # Fire update callback if self._update_callback: try: self._update_callback() except Exception as e: logger.error(f"Simulator callback error: {e}") def _check_and_alert(self): """Generate contextual alerts based on current conditions.""" for zone in self.venue.zones: if zone.occupancy_rate > 0.9: self.notification_service.create_alert( title=f"High Density: {zone.name}", message=f"{zone.name} is at {round(zone.occupancy_rate * 100)}% capacity. Consider redirecting foot traffic.", severity="warning", zone_id=zone.id, target_role="operator", ) def _run_loop(self): """Main simulation loop running in a background thread.""" while self._running: try: self.tick() time.sleep(2.0 / self._speed) except Exception as e: logger.error(f"Simulation error: {e}") time.sleep(1) def get_status(self) -> Dict: """Get current simulation status.""" return { "running": self._running, "speed": self._speed, "tick_count": self._tick_count, "phase": self.event.current_phase, "phase_label": self.event.phase.label, "phase_description": self.event.phase.description, "attendance": self.event.actual_attendance, "venue_occupancy": round(self.venue.overall_occupancy * 100, 1), }