import random import simpy def _get_prep_time_and_emit_pace(cafe, config): barista_queue = len(cafe.baristas.get_queue) cashier_queues = sum(len(c.queue) for c in cafe.cashiers) total_waiting = barista_queue + cashier_queues pace = "Steady" if total_waiting >= 6: pace = "Peak" elif total_waiting == 0: pace = "Quiet" cafe.emit("pace_change", "system", {"pace": pace}) prep_mode = (config["prep_min"] + config["prep_max"]) / 2.0 prep_time = random.triangular(config["prep_min"], config["prep_max"], prep_mode) if pace == "Peak": prep_time *= 0.75 # 25% reduction Batch Prep return prep_time def handle_reservation_flow(env, cafe, name, config, arrival_time): # VIPs bypass the queue and instantly check-in at the first cashier desk cafe.emit("vip_checkin", name, {"cashier_index": 0}) yield env.timeout(2.0) # 2 seconds to allow the ticket animation to finish # Check if their reserved table is still waiting for them! my_table = next((t for t in cafe.tables if t["status"] == "reserved" and t["reserved_for"] == name), None) if not my_table: # Oh no! They missed the window. They take their pre-made drink and leave immediately. cafe.emit("missed_reservation", name) cafe.emit("served", name) yield env.timeout(1.0) cafe.emit("leave", name) return # Claim the table my_table["status"] = "occupied" my_table["reserved_for"] = None cafe.emit("seated_waiting_order", name, {"is_reservation": True, "table_id": my_table["id"]}) # Drink is already made and waiting for them yield env.timeout(1.0) cafe.emit("served", name) yield env.timeout(random.uniform(config["dwell_min"], config["dwell_max"])) # Leave and free table my_table["status"] = "available" if env.now > config.get("warmup_time", 0): cafe.cycle_times.append(env.now - arrival_time) cafe.completed_customers += 1 def handle_takeout_flow(env, cafe, name, config, arrival_time): cafe.emit("waiting_pickup", name) barista_req = cafe.baristas.get() print(f"[{env.now:.1f}] {name} handle_takeout_flow requesting barista...") if cafe.event_queue is None: barista_idx = yield barista_req print(f"[{env.now:.1f}] {name} got barista (headless)") else: while True: results = yield barista_req | env.timeout(60.0) if barista_req in results: barista_idx = results[barista_req] print(f"[{env.now:.1f}] {name} got barista {barista_idx}") break print(f"[{env.now:.1f}] {name} still waiting for barista...") cafe.emit("prep_waiting_frustration", name) print(f"[{env.now:.1f}] {name} getting prep time...") prep_time = _get_prep_time_and_emit_pace(cafe, config) print(f"[{env.now:.1f}] {name} emitting start_prep...") cafe.emit("start_prep", name, { "barista_index": barista_idx, "duration": prep_time }) if cafe.event_queue is None: yield env.timeout(prep_time) else: elapsed = 0.0 while elapsed < prep_time: chunk = min(60.0, prep_time - elapsed) yield env.timeout(chunk) elapsed += chunk if elapsed < prep_time: cafe.emit("prep_waiting_frustration", name) cafe.baristas.put(barista_idx) # Free the barista cafe.emit("served", name) cafe.emit("leave", name) if env.now > config.get("warmup_time", 0): cafe.cycle_times.append(env.now - arrival_time) cafe.completed_customers += 1 def handle_dine_in_flow(env, cafe, name, config, arrival_time): cafe.emit("waiting_table", name, {"is_reservation": False}) drink_ready_event = env.event() def make_drink(): barista_req = cafe.baristas.get() print(f"[{env.now:.1f}] {name} make_drink requesting barista...") if cafe.event_queue is None: barista_idx = yield barista_req print(f"[{env.now:.1f}] {name} got barista (headless)") else: while True: results = yield barista_req | env.timeout(60.0) if barista_req in results: barista_idx = results[barista_req] print(f"[{env.now:.1f}] {name} got barista {barista_idx}") break print(f"[{env.now:.1f}] {name} still waiting for barista...") cafe.emit("prep_waiting_frustration", name) print(f"[{env.now:.1f}] {name} getting prep time...") prep_time = _get_prep_time_and_emit_pace(cafe, config) print(f"[{env.now:.1f}] {name} emitting start_prep...") cafe.emit("start_prep", name, { "barista_index": barista_idx, "duration": prep_time }) if cafe.event_queue is None: yield env.timeout(prep_time) else: elapsed = 0.0 while elapsed < prep_time: chunk = min(60.0, prep_time - elapsed) yield env.timeout(chunk) elapsed += chunk if elapsed < prep_time: cafe.emit("prep_waiting_frustration", name) cafe.baristas.put(barista_idx) # Free the barista cafe.emit("finish_prep", name) # Tell UI the barista is done drink_ready_event.succeed() # Start making the drink concurrently env.process(make_drink()) # Wait for a table to become available table = None while True: available = [t for t in cafe.tables if t["status"] == "available"] if available: table = random.choice(available) table["status"] = "occupied" break yield env.timeout(1.0) if cafe.event_queue: # Emit frustration periodically if waiting too long if random.random() < 0.1: cafe.emit("prep_waiting_frustration", name) cafe.emit("seated_waiting_order", name, {"is_reservation": False, "table_id": table["id"]}) # Wait for the barista to actually finish making the drink yield drink_ready_event yield env.timeout(2.0) # Time to receive drink cafe.emit("served", name) yield env.timeout(random.uniform(config["dwell_min"], config["dwell_max"])) table["status"] = "available" cafe.emit("leave", name) if env.now > config.get("warmup_time", 0): cafe.cycle_times.append(env.now - arrival_time) cafe.completed_customers += 1 def customer_process(env, cafe, name, config, force_takeout=False, is_reservation=False): cafe.emit("arrive", name) arrival_time = env.now # Spend a moment walking into the shop yield env.timeout(random.uniform(1.0, 2.0)) if is_reservation: yield from handle_reservation_flow(env, cafe, name, config, arrival_time) return base_takeout_prob = config.get("takeout_prob", 0.5) # Check dynamic table capacity to influence takeout prob available_tables = len([t for t in cafe.tables if t["status"] == "available"]) if available_tables > 0: effective_takeout_prob = max(0.1, base_takeout_prob * 0.5) else: effective_takeout_prob = min(0.95, base_takeout_prob * 1.5) is_takeout = force_takeout or (random.random() < effective_takeout_prob) # Find shortest cashier queue shortest_queue_idx = 0 min_len = cafe.cashiers[0].count + len(cafe.cashiers[0].queue) for i in range(1, len(cafe.cashiers)): q_len = cafe.cashiers[i].count + len(cafe.cashiers[i].queue) if q_len < min_len: min_len = q_len shortest_queue_idx = i # Balking logic if min_len >= config.get("balk_threshold", 8): if random.random() < config.get("balk_prob", 0.5): cafe.emit("balking_start", name) yield env.timeout(random.uniform(4.0, 8.0)) cafe.emit("balk_leave", name) if env.now > config.get("warmup_time", 0): cafe.lost_customers += 1 return cafe.emit("queue_cashier", name, {"cashier_index": shortest_queue_idx}) target_cashier = cafe.cashiers[shortest_queue_idx] will_renege = random.random() < config.get("renege_prob", 0.3) max_strikes = random.randint(1, config.get("max_strikes", 3)) if will_renege else float('inf') patience_interval = random.uniform(15.0, 30.0) strikes = 0 with target_cashier.request() as req: if cafe.event_queue is None: if will_renege: total_patience = sum(random.uniform(15.0, 30.0) for _ in range(max_strikes)) results = yield req | env.timeout(total_patience) if req in results: wait_time = env.now - arrival_time if env.now > config.get("warmup_time", 0): cafe.waiting_times.append(wait_time) else: if env.now > config.get("warmup_time", 0): cafe.lost_customers += 1 return else: yield req wait_time = env.now - arrival_time if env.now > config.get("warmup_time", 0): cafe.waiting_times.append(wait_time) else: while True: results = yield req | env.timeout(patience_interval) if req in results: wait_time = env.now - arrival_time if env.now > config.get("warmup_time", 0): cafe.waiting_times.append(wait_time) break position = -1 if req in target_cashier.queue: position = target_cashier.queue.index(req) if position >= 0 and position <= 2: cafe.emit("frustrated_waiting", name) yield req break strikes += 1 if strikes >= max_strikes: cafe.emit("renege_leave", name, {"cashier_index": shortest_queue_idx}) if env.now > config.get("warmup_time", 0): cafe.lost_customers += 1 return else: cafe.emit("patience_warning", name, {"strike": strikes, "max": max_strikes}) decide_time = random.uniform(config["decide_min"], config["decide_max"]) pay_time = random.uniform(config["pay_min"], config["pay_max"]) total_cashier_time = decide_time + pay_time cafe.emit("start_deciding", name, {"cashier_index": shortest_queue_idx, "duration": total_cashier_time}) yield env.timeout(decide_time) cafe.emit("start_paying", name, {"cashier_index": shortest_queue_idx}) yield env.timeout(pay_time) if is_takeout: yield from handle_takeout_flow(env, cafe, name, config, arrival_time) else: yield from handle_dine_in_flow(env, cafe, name, config, arrival_time)