1use std::cell::{Cell, RefCell};
4use std::ops::RangeInclusive;
5use std::rc::Rc;
6
7use gwr_components::queue::Queue;
8use gwr_engine::engine::Engine;
9use gwr_engine::events::once::Once;
10use gwr_engine::events::repeated::Repeated;
11use gwr_engine::sim_error;
12use gwr_engine::time::clock::Clock;
13use gwr_engine::traits::Event;
14use gwr_engine::types::{SimError, SimResult};
15use gwr_track::entity::Entity;
16use gwr_track::tracker::dev_null_tracker;
17use gwr_track::{Tracker, info, warn};
18
19use crate::config::RestaurantConfig;
20use crate::customer::{Customer, CustomerOutcome, CustomerPlan, generate_demand, spawn_arrivals};
21use crate::menu::{order_name, order_value};
22use crate::recording::{
23 CustomerPhase, CustomerStateCounts, RecordedSimulation, SimulationSnapshot, TimelineEvent,
24 TimelinePoint,
25};
26use crate::staff::{Staffing, spawn_kitchen_worker, spawn_till_worker};
27use crate::time_of_day::TimeOfDay;
28
29#[derive(Default, Debug, Clone)]
30pub struct Metrics {
31 pub arrivals: usize,
32 pub balked: usize,
33 pub gave_up_queue: usize,
34 pub entered_queue: usize,
35 pub till_started: usize,
36 pub orders_started: usize,
37 pub orders_served: usize,
38 pub orders_abandoned: usize,
39 pub revenue: f64,
40 pub ingredient_cost: f64,
41 pub till_wait_ticks_total: u64,
42 pub kitchen_wait_ticks_total: u64,
43 pub visit_ticks_total: u64,
44 pub max_till_queue_len: usize,
45 pub max_kitchen_queue_len: usize,
46}
47
48struct Recorder {
49 timeline: RefCell<Vec<TimelinePoint>>,
50 events: RefCell<Vec<TimelineEvent>>,
51 customer_phases: RefCell<Vec<CustomerPhase>>,
52}
53
54impl Recorder {
55 fn new(num_customers: usize) -> Self {
56 Self {
57 timeline: RefCell::new(Vec::new()),
58 events: RefCell::new(Vec::new()),
59 customer_phases: RefCell::new(vec![CustomerPhase::Planned; num_customers]),
60 }
61 }
62
63 fn set_customer_phase(&self, customer_id: usize, phase: CustomerPhase) {
64 if let Some(slot) = self.customer_phases.borrow_mut().get_mut(customer_id) {
65 *slot = phase;
66 }
67 }
68
69 fn snapshot_counts(&self) -> CustomerStateCounts {
70 let mut counts = CustomerStateCounts::default();
71 for phase in self.customer_phases.borrow().iter().copied() {
72 match phase {
73 CustomerPhase::Planned => counts.planned += 1,
74 CustomerPhase::Balked => counts.balked += 1,
75 CustomerPhase::WaitingTill => counts.waiting_till += 1,
76 CustomerPhase::AtTill => counts.at_till += 1,
77 CustomerPhase::WaitingKitchen => counts.waiting_kitchen += 1,
78 CustomerPhase::PreparingFood => counts.preparing_food += 1,
79 CustomerPhase::CollectingFood => counts.collecting_food += 1,
80 CustomerPhase::Served => counts.served += 1,
81 CustomerPhase::GaveUpQueue => counts.gave_up_queue += 1,
82 CustomerPhase::Abandoned => counts.abandoned += 1,
83 }
84 }
85 counts
86 }
87
88 fn record_snapshot(&self, tick: u64, restaurant: &Restaurant, message: String) {
89 let snapshot = SimulationSnapshot {
90 metrics: restaurant.metrics.borrow().clone(),
91 till_queue: restaurant.till_queue.values(),
92 kitchen_queue: restaurant.kitchen_queue.values(),
93 active_till_workers: restaurant.active_till_workers.get(),
94 active_kitchen_workers: restaurant.active_kitchen_workers.get(),
95 arrivals_complete: restaurant.arrivals_complete.get(),
96 closed: restaurant.closed_seen.get(),
97 customer_counts: self.snapshot_counts(),
98 };
99 self.timeline
100 .borrow_mut()
101 .push(TimelinePoint { tick, snapshot });
102 self.events
103 .borrow_mut()
104 .push(TimelineEvent { tick, message });
105 }
106
107 fn finish(
108 &self,
109 staffing: Staffing,
110 opening_time: TimeOfDay,
111 day_ticks: u64,
112 summary: RunSummary,
113 demand_size: usize,
114 ) -> RecordedSimulation {
115 RecordedSimulation {
116 staffing,
117 opening_time,
118 day_ticks,
119 summary,
120 demand_size,
121 timeline: self.timeline.borrow().clone(),
122 events: self.events.borrow().clone(),
123 }
124 }
125}
126
127#[derive(Clone)]
128struct ScenarioEntities {
129 restaurant: Rc<Entity>,
130 till_workers: Vec<Rc<Entity>>,
131 kitchen_workers: Vec<Rc<Entity>>,
132}
133
134fn build_entities(engine: &Engine, staffing: Staffing) -> ScenarioEntities {
135 let restaurant = Rc::new(Entity::new(engine.top(), "restaurant"));
136 let staff = Rc::new(Entity::new(&restaurant, "staff"));
137 let till_workers = (0..staffing.till)
138 .map(|index| Rc::new(Entity::new(&staff, &format!("till_{index}"))))
139 .collect();
140 let kitchen_workers = (0..staffing.kitchen)
141 .map(|index| Rc::new(Entity::new(&staff, &format!("kitchen_{index}"))))
142 .collect();
143
144 ScenarioEntities {
145 restaurant,
146 till_workers,
147 kitchen_workers,
148 }
149}
150
151pub(crate) struct Restaurant {
152 clock: Clock,
153 pub(crate) till_queue: Queue<usize>,
154 pub(crate) kitchen_queue: Queue<usize>,
155 pub(crate) closed: Once<()>,
156 pub(crate) arrivals_complete: Cell<bool>,
157 pub(crate) closed_seen: Cell<bool>,
158 pub(crate) active_till_workers: Cell<usize>,
159 pub(crate) active_kitchen_workers: Cell<usize>,
160 pub(crate) metrics: RefCell<Metrics>,
161 entity: Rc<Entity>,
162 customers: RefCell<Vec<Option<Rc<Customer>>>>,
163 till_worker_entities: Vec<Rc<Entity>>,
164 kitchen_worker_entities: Vec<Rc<Entity>>,
165 recorder: Option<Rc<Recorder>>,
166}
167
168impl Restaurant {
169 #[must_use]
170 fn new(
171 clock: Clock,
172 entities: ScenarioEntities,
173 num_customers: usize,
174 kitchen_queue_capacity: usize,
175 recorder: Option<Rc<Recorder>>,
176 ) -> Self {
177 let entity = entities.restaurant;
178 let till_queue =
179 Queue::new(&entity, "till_queue", None).expect("queue config should be valid");
180 let kitchen_queue = Queue::new(&entity, "kitchen_queue", Some(kitchen_queue_capacity))
181 .expect("queue config should be valid");
182
183 Self {
184 clock,
185 till_queue,
186 kitchen_queue,
187 closed: Once::default(),
188 arrivals_complete: Cell::new(false),
189 closed_seen: Cell::new(false),
190 active_till_workers: Cell::new(0),
191 active_kitchen_workers: Cell::new(0),
192 metrics: RefCell::new(Metrics::default()),
193 entity,
194 customers: RefCell::new(vec![None; num_customers]),
195 till_worker_entities: entities.till_workers,
196 kitchen_worker_entities: entities.kitchen_workers,
197 recorder,
198 }
199 }
200
201 #[must_use]
202 pub(crate) fn till_queue_changed(&self) -> Repeated<()> {
203 self.till_queue.changed_event()
204 }
205
206 #[must_use]
207 pub(crate) fn kitchen_queue_changed(&self) -> Repeated<()> {
208 self.kitchen_queue.changed_event()
209 }
210
211 pub(crate) fn customer(&self, customer_id: usize) -> Rc<Customer> {
212 self.customers.borrow()[customer_id]
213 .clone()
214 .expect("customer should be registered before use")
215 }
216
217 fn entity(&self) -> &Rc<Entity> {
218 &self.entity
219 }
220
221 #[must_use]
222 pub(crate) fn tick_now(&self) -> u64 {
223 self.clock.tick_now().tick()
224 }
225
226 fn till_worker_entity(&self, worker_id: usize) -> &Rc<Entity> {
227 &self.till_worker_entities[worker_id]
228 }
229
230 fn kitchen_worker_entity(&self, worker_id: usize) -> &Rc<Entity> {
231 &self.kitchen_worker_entities[worker_id]
232 }
233
234 fn set_customer_phase(&self, customer_id: usize, phase: CustomerPhase) {
235 if let Some(recorder) = &self.recorder {
236 recorder.set_customer_phase(customer_id, phase);
237 }
238 }
239
240 pub(crate) fn record_snapshot(&self, message: String) {
241 if let Some(recorder) = &self.recorder {
242 recorder.record_snapshot(self.tick_now(), self, message);
243 }
244 }
245
246 pub(crate) fn customer_arrived(
247 &self,
248 customer: Rc<Customer>,
249 queue_len: usize,
250 join_probability: f64,
251 ) -> usize {
252 let customer_id = customer.id();
253 self.customers.borrow_mut()[customer_id] = Some(customer);
254 self.metrics.borrow_mut().arrivals += 1;
255
256 let message = format!(
257 "customer {customer_id} arrived, till queue len {queue_len}, join probability {join_probability:.2}",
258 );
259 self.record_snapshot(message.clone());
260 info!(self.entity; "{message}");
261 customer_id
262 }
263
264 pub(crate) fn customer_balked(&self, customer_id: usize) {
265 self.set_customer_phase(customer_id, CustomerPhase::Balked);
266 self.metrics.borrow_mut().balked += 1;
267
268 let message = format!("customer {customer_id} balked at the queue");
269 self.record_snapshot(message.clone());
270 info!(self.entity(); "{message}");
271 }
272
273 pub(crate) fn customer_gave_up_in_queue(&self, customer_id: usize) {
274 self.set_customer_phase(customer_id, CustomerPhase::GaveUpQueue);
275 self.metrics.borrow_mut().gave_up_queue += 1;
276
277 let message = format!("customer {customer_id} gave up waiting in the till queue");
278 self.record_snapshot(message.clone());
279 info!(self.entity(); "{message}");
280 }
281
282 pub(crate) fn customer_abandoned_after_order(&self, customer_id: usize) {
283 self.set_customer_phase(customer_id, CustomerPhase::Abandoned);
284 let customer = self.customer(customer_id);
285 let refund = customer
286 .payment_done_tick()
287 .map(|_| order_value(customer.order_index()).0);
288 let mut metrics = self.metrics.borrow_mut();
289 metrics.orders_abandoned += 1;
290 if let Some(refund) = refund {
291 metrics.revenue -= refund;
292 }
293 drop(metrics);
294
295 let message = if let Some(refund) = refund {
296 format!("customer {customer_id} abandoned after ordering and was refunded {refund:.2}")
297 } else {
298 format!("customer {customer_id} abandoned after ordering")
299 };
300 self.record_snapshot(message.clone());
301 warn!(self.entity(); "{message}");
302 }
303
304 pub(crate) fn customer_served(&self, customer_id: usize, joined_queue_tick: Option<u64>) {
305 self.set_customer_phase(customer_id, CustomerPhase::Served);
306 let tick = self.tick_now();
307 if let Some(joined_tick) = joined_queue_tick {
308 self.metrics.borrow_mut().visit_ticks_total += tick - joined_tick;
309 }
310
311 let message = format!("customer {customer_id} served");
312 self.record_snapshot(message.clone());
313 info!(self.entity(); "{message}");
314 }
315
316 #[must_use]
317 pub(crate) fn queue_len(&self) -> usize {
318 self.till_queue.len()
319 }
320
321 pub(crate) async fn enqueue_till(&self, customer_id: usize) -> SimResult {
322 self.set_customer_phase(customer_id, CustomerPhase::WaitingTill);
323 self.till_queue.push(customer_id).await?;
324 let len = self.till_queue.len();
325
326 let mut metrics = self.metrics.borrow_mut();
327 metrics.entered_queue += 1;
328 metrics.max_till_queue_len = metrics.max_till_queue_len.max(len);
329 drop(metrics);
330
331 let message = format!("customer {customer_id} joined the till queue");
332 self.record_snapshot(message.clone());
333 info!(self.entity(); "{message}");
334 Ok(())
335 }
336
337 #[must_use]
338 pub(crate) fn pop_till(&self) -> Option<usize> {
339 self.till_queue.pop_front()
340 }
341
342 #[must_use]
343 pub(crate) fn remove_till(&self, customer_id: usize) -> bool {
344 self.till_queue
345 .remove_where(|queued_id| *queued_id == customer_id)
346 .is_some()
347 }
348
349 pub(crate) async fn enqueue_kitchen(&self, customer_id: usize) -> SimResult {
350 self.set_customer_phase(customer_id, CustomerPhase::WaitingKitchen);
351 while self.kitchen_queue.is_full() {
352 self.kitchen_queue_changed().listen().await;
353 }
354
355 self.kitchen_queue.push(customer_id).await?;
356 let len = self.kitchen_queue.len();
357
358 let mut metrics = self.metrics.borrow_mut();
359 metrics.max_kitchen_queue_len = metrics.max_kitchen_queue_len.max(len);
360 drop(metrics);
361
362 let message = format!("customer {customer_id} entered the kitchen queue");
363 self.record_snapshot(message.clone());
364 info!(self.entity(); "{message}");
365 Ok(())
366 }
367
368 #[must_use]
369 pub(crate) fn pop_kitchen(&self) -> Option<usize> {
370 self.kitchen_queue.pop_front()
371 }
372
373 pub(crate) fn begin_till_service(
374 &self,
375 customer_id: usize,
376 joined_tick: Option<u64>,
377 worker_id: usize,
378 order_index: usize,
379 ) {
380 let tick = self.tick_now();
381 self.set_customer_phase(customer_id, CustomerPhase::AtTill);
382 self.active_till_workers
383 .set(self.active_till_workers.get() + 1);
384 let mut metrics = self.metrics.borrow_mut();
385 metrics.till_started += 1;
386 if let Some(joined_tick) = joined_tick {
387 metrics.till_wait_ticks_total += tick - joined_tick;
388 }
389 drop(metrics);
390
391 let customer_message = format!("customer {customer_id} reached the till");
392 info!(self.entity(); "{customer_message}");
393 self.record_snapshot(customer_message);
394 info!(
395 self.till_worker_entity(worker_id);
396 "started serving customer {customer_id} ({})",
397 order_name(order_index)
398 );
399 }
400
401 pub(crate) fn finish_till_service(&self, customer_id: usize, _worker_id: usize) {
402 self.active_till_workers
403 .set(self.active_till_workers.get().saturating_sub(1));
404 self.record_snapshot(format!("till work completed for customer {customer_id}"));
405 }
406
407 pub(crate) fn record_order_started(&self, customer_id: usize, worker_id: usize) {
408 let (revenue, _) = order_value(self.customer(customer_id).order_index());
409 let mut metrics = self.metrics.borrow_mut();
410 metrics.orders_started += 1;
411 metrics.revenue += revenue;
412 drop(metrics);
413
414 let message = format!("customer {customer_id} finished paying {revenue:.2}");
415 self.record_snapshot(message.clone());
416 info!(self.till_worker_entity(worker_id); "{message}");
417 }
418
419 pub(crate) fn begin_kitchen_service(
420 &self,
421 customer_id: usize,
422 worker_id: usize,
423 order_index: usize,
424 ) {
425 self.set_customer_phase(customer_id, CustomerPhase::PreparingFood);
426 self.active_kitchen_workers
427 .set(self.active_kitchen_workers.get() + 1);
428 let message = format!("kitchen started order for customer {customer_id}");
429 self.record_snapshot(message.clone());
430 info!(
431 self.kitchen_worker_entity(worker_id);
432 "started preparing customer {} ({})",
433 customer_id,
434 order_name(order_index)
435 );
436 }
437
438 pub(crate) fn finish_kitchen_service(&self, customer_id: usize, _worker_id: usize) {
439 self.active_kitchen_workers
440 .set(self.active_kitchen_workers.get().saturating_sub(1));
441 self.record_snapshot(format!(
442 "kitchen finished service work for customer {customer_id}"
443 ));
444 }
445
446 pub(crate) fn record_order_served(
447 &self,
448 customer_id: usize,
449 worker_id: usize,
450 payment_tick: Option<u64>,
451 ) {
452 let tick = self.tick_now();
453 let (_, ingredient_cost) = order_value(self.customer(customer_id).order_index());
454 let mut metrics = self.metrics.borrow_mut();
455 metrics.orders_served += 1;
456 metrics.ingredient_cost += ingredient_cost;
457 if let Some(payment_tick) = payment_tick {
458 metrics.kitchen_wait_ticks_total += tick - payment_tick;
459 }
460 drop(metrics);
461
462 self.set_customer_phase(customer_id, CustomerPhase::CollectingFood);
463 let message = format!("order ready for customer {customer_id}");
464 self.record_snapshot(message.clone());
465 info!(self.kitchen_worker_entity(worker_id); "{message}");
466 }
467
468 pub(crate) fn customer_collecting_food(&self, customer_id: usize) {
469 let message = format!("order ready for customer {customer_id}");
470 info!(self.entity(); "{message}");
471 }
472
473 pub(crate) fn mark_arrivals_complete(&self) -> SimResult {
474 self.arrivals_complete.set(true);
475 let message = "all arrivals processed".to_string();
476 self.record_snapshot(message.clone());
477 info!(self.entity; "{message}");
478 Ok(())
479 }
480
481 pub(crate) fn mark_closed(&self) -> SimResult {
482 self.closed_seen.set(true);
483 let message = "restaurant closed".to_string();
484 self.record_snapshot(message.clone());
485 warn!(self.entity; "{message}");
486 self.closed.notify()
487 }
488
489 fn record_removed_from_till_at_close(&self, customer_id: usize) {
490 self.set_customer_phase(customer_id, CustomerPhase::Abandoned);
491 let message = format!("customer {customer_id} removed from till queue at close");
492 self.record_snapshot(message.clone());
493 warn!(self.entity(); "{message}");
494 }
495
496 #[must_use]
497 pub(crate) fn can_kitchen_exit(&self) -> bool {
498 self.arrivals_complete.get()
499 && self.till_queue.is_empty()
500 && self.kitchen_queue.is_empty()
501 && self.active_till_workers.get() == 0
502 }
503}
504
505#[derive(Debug, Clone)]
506pub struct RunSummary {
507 pub staffing: Staffing,
508 pub arrivals: usize,
509 pub balked: usize,
510 pub gave_up_queue: usize,
511 pub served: usize,
512 pub abandoned: usize,
513 pub revenue: f64,
514 pub ingredient_cost: f64,
515 pub salary_cost: f64,
516 pub profit: f64,
517 pub avg_till_wait: f64,
518 pub avg_kitchen_wait: f64,
519 pub avg_visit: f64,
520 pub max_till_queue_len: usize,
521 pub max_kitchen_queue_len: usize,
522 pub finish_tick: u64,
523}
524
525impl RunSummary {
526 pub fn print_table_header() {
527 println!(
528 "{:>4} {:>7} {:>7} {:>8} {:>8} {:>10} {:>10} {:>9} {:>9} {:>10}",
529 "Till",
530 "Kitchen",
531 "Served",
532 "Balked",
533 "GaveUp",
534 "Revenue",
535 "Costs",
536 "Profit",
537 "Finish h",
538 "Max Queue"
539 );
540 }
541
542 pub fn print_table_row(&self) {
543 let costs = self.ingredient_cost + self.salary_cost;
544 println!(
545 "{:>4} {:>7} {:>7} {:>8} {:>8} {:>10.2} {:>10.2} {:>9.2} {:>9.2} {:>4}/{:<4}",
546 self.staffing.till,
547 self.staffing.kitchen,
548 self.served,
549 self.balked,
550 self.gave_up_queue,
551 self.revenue,
552 costs,
553 self.profit,
554 self.finish_tick as f64 / 3600.0,
555 self.max_till_queue_len,
556 self.max_kitchen_queue_len
557 );
558 }
559
560 pub fn print_best_summary(&self, day_ticks: u64) {
561 println!("Best configuration: {}", self.staffing);
562 println!(
563 "Served {} of {} arrivals, balked {}, gave up in queue {}, abandoned after ordering {}.",
564 self.served, self.arrivals, self.balked, self.gave_up_queue, self.abandoned
565 );
566 println!(
567 "Revenue {:.2}, ingredient cost {:.2}, salary cost {:.2}, profit {:.2}.",
568 self.revenue, self.ingredient_cost, self.salary_cost, self.profit
569 );
570 println!(
571 "Average waits: till {:.1}s, kitchen {:.1}s, full visit {:.1}s.",
572 self.avg_till_wait, self.avg_kitchen_wait, self.avg_visit
573 );
574 if self.finish_tick > day_ticks {
575 println!(
576 "Work completes {:.1} hours after opening, or {:.1} hours after close.",
577 self.finish_tick as f64 / 3600.0,
578 (self.finish_tick - day_ticks) as f64 / 3600.0
579 );
580 }
581 }
582}
583
584pub struct ScenarioResult {
585 pub summary: RunSummary,
586 pub recording: Option<RecordedSimulation>,
587}
588
589pub fn run_sweep(
590 config: &RestaurantConfig,
591 till_range: RangeInclusive<usize>,
592 kitchen_range: RangeInclusive<usize>,
593 tracker: &Tracker,
594) -> Result<(Vec<CustomerPlan>, Vec<RunSummary>), SimError> {
595 let demand = generate_demand(config);
596 let mut results = Vec::new();
597
598 for till in till_range {
599 for kitchen in kitchen_range.clone() {
600 let staffing = Staffing { till, kitchen };
601 results.push(run_configuration(config, &demand, staffing, false, tracker)?.summary);
602 }
603 }
604
605 results.sort_by(|a, b| b.profit.total_cmp(&a.profit));
606 Ok((demand, results))
607}
608
609pub fn run_recorded_scenario(
610 config: &RestaurantConfig,
611 staffing: Staffing,
612) -> Result<RecordedSimulation, SimError> {
613 let demand = generate_demand(config);
614 let tracker = dev_null_tracker();
615 let result = run_configuration(config, &demand, staffing, true, &tracker)?;
616 result
617 .recording
618 .ok_or_else(|| SimError("expected recorded simulation".to_string()))
619}
620
621pub fn run_configuration(
622 config: &RestaurantConfig,
623 demand: &[CustomerPlan],
624 staffing: Staffing,
625 record_timeline: bool,
626 tracker: &Tracker,
627) -> Result<ScenarioResult, SimError> {
628 let mut engine = Engine::new(tracker);
629
630 let entities = build_entities(&engine, staffing);
631 let clock = engine.clock_hz(1.0);
632 let recorder = record_timeline.then(|| Rc::new(Recorder::new(demand.len())));
633 let restaurant_entity = entities.restaurant.clone();
634 let till_workers = entities.till_workers.clone();
635 let kitchen_workers = entities.kitchen_workers.clone();
636 let restaurant = Rc::new(Restaurant::new(
637 clock.clone(),
638 entities,
639 demand.len(),
640 config.max_kitchen_queue_len,
641 recorder.clone(),
642 ));
643 let demand: Rc<Vec<CustomerPlan>> = Rc::new(demand.to_vec());
644
645 if staffing.kitchen == 0 || staffing.till == 0 {
646 return sim_error!("Invalid configuration with 0 staff on either till or in the kitchen");
647 }
648
649 restaurant.record_snapshot("simulation initialised".to_string());
650 info!(
651 restaurant_entity;
652 "starting scenario with {} customer plans, till staff {}, kitchen staff {}",
653 demand.len(),
654 staffing.till,
655 staffing.kitchen
656 );
657
658 spawn_arrivals(&engine, &clock, *config, restaurant.clone(), demand.clone());
659
660 for (worker_id, _) in till_workers.iter().enumerate() {
661 spawn_till_worker(&engine, &clock, *config, worker_id, restaurant.clone());
662 }
663 for (worker_id, _) in kitchen_workers.iter().enumerate() {
664 spawn_kitchen_worker(&engine, &clock, *config, worker_id, restaurant.clone());
665 }
666
667 spawn_close_kitchen(&engine, &clock, config.day_ticks, restaurant.clone());
668
669 engine.run()?;
670
671 let metrics = restaurant.metrics.borrow().clone();
672 let finish_tick = clock.tick_now().tick();
673 let paid_hours = config.paid_hours() as f64;
674 let salary_cost = paid_hours
675 * (staffing.till as f64 * config.till_salary_per_hour
676 + staffing.kitchen as f64 * config.kitchen_salary_per_hour);
677 let profit = metrics.revenue - metrics.ingredient_cost - salary_cost;
678 let served = metrics.orders_served.max(1) as f64;
679 let till_started = metrics.till_started.max(1) as f64;
680
681 let summary = RunSummary {
682 staffing,
683 arrivals: metrics.arrivals,
684 balked: metrics.balked,
685 gave_up_queue: metrics.gave_up_queue,
686 served: metrics.orders_served,
687 abandoned: metrics.orders_abandoned,
688 revenue: metrics.revenue,
689 ingredient_cost: metrics.ingredient_cost,
690 salary_cost,
691 profit,
692 avg_till_wait: metrics.till_wait_ticks_total as f64 / till_started,
693 avg_kitchen_wait: metrics.kitchen_wait_ticks_total as f64 / served,
694 avg_visit: metrics.visit_ticks_total as f64 / served,
695 max_till_queue_len: metrics.max_till_queue_len,
696 max_kitchen_queue_len: metrics.max_kitchen_queue_len,
697 finish_tick,
698 };
699
700 let recording = recorder.map(|recorder| {
701 recorder.finish(
702 staffing,
703 config.opening_time,
704 config.day_ticks,
705 summary.clone(),
706 demand.len(),
707 )
708 });
709 Ok(ScenarioResult { summary, recording })
710}
711
712fn spawn_close_kitchen(engine: &Engine, clock: &Clock, day_ticks: u64, restaurant: Rc<Restaurant>) {
713 let clock = clock.clone();
714 engine.spawn(async move {
715 clock.wait_ticks(day_ticks).await;
716 restaurant.mark_closed()?;
717 finish_till_queue_at_close(&restaurant);
718
719 Ok(())
720 });
721}
722
723fn finish_till_queue_at_close(restaurant: &Restaurant) {
724 while let Some(customer_id) = restaurant.pop_till() {
725 restaurant.record_removed_from_till_at_close(customer_id);
726 restaurant
727 .customer(customer_id)
728 .notify_outcome(CustomerOutcome::KitchenClosed);
729 }
730}