sim_restaurant/
recording.rs

1// Copyright (c) 2026 Graphcore Ltd. All rights reserved.
2
3use crate::sim::{Metrics, RunSummary};
4use crate::staff::Staffing;
5use crate::time_of_day::TimeOfDay;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum CustomerPhase {
9    Planned,
10    Balked,
11    WaitingTill,
12    AtTill,
13    WaitingKitchen,
14    PreparingFood,
15    CollectingFood,
16    Served,
17    GaveUpQueue,
18    Abandoned,
19}
20
21#[derive(Clone, Debug, Default)]
22pub struct CustomerStateCounts {
23    pub planned: usize,
24    pub balked: usize,
25    pub waiting_till: usize,
26    pub at_till: usize,
27    pub waiting_kitchen: usize,
28    pub preparing_food: usize,
29    pub collecting_food: usize,
30    pub served: usize,
31    pub gave_up_queue: usize,
32    pub abandoned: usize,
33}
34
35#[derive(Clone, Debug)]
36pub struct SimulationSnapshot {
37    pub metrics: Metrics,
38    pub till_queue: Vec<usize>,
39    pub kitchen_queue: Vec<usize>,
40    pub active_till_workers: usize,
41    pub active_kitchen_workers: usize,
42    pub arrivals_complete: bool,
43    pub closed: bool,
44    pub customer_counts: CustomerStateCounts,
45}
46
47#[derive(Clone, Debug)]
48pub struct TimelinePoint {
49    pub tick: u64,
50    pub snapshot: SimulationSnapshot,
51}
52
53#[derive(Clone, Debug)]
54pub struct TimelineEvent {
55    pub tick: u64,
56    pub message: String,
57}
58
59#[derive(Clone, Debug)]
60pub struct RecordedSimulation {
61    pub staffing: Staffing,
62    pub opening_time: TimeOfDay,
63    pub day_ticks: u64,
64    pub summary: RunSummary,
65    pub demand_size: usize,
66    pub timeline: Vec<TimelinePoint>,
67    pub events: Vec<TimelineEvent>,
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum PlotStat {
72    TillQueueLen,
73    KitchenQueueLen,
74    ActiveTillWorkers,
75    ActiveKitchenWorkers,
76    Arrivals,
77    Balked,
78    GaveUpQueue,
79    OrdersStarted,
80    OrdersServed,
81    OrdersAbandoned,
82    Revenue,
83    IngredientCost,
84    Profit,
85}
86
87impl PlotStat {
88    pub const ALL: [Self; 13] = [
89        Self::TillQueueLen,
90        Self::KitchenQueueLen,
91        Self::ActiveTillWorkers,
92        Self::ActiveKitchenWorkers,
93        Self::Arrivals,
94        Self::Balked,
95        Self::GaveUpQueue,
96        Self::OrdersStarted,
97        Self::OrdersServed,
98        Self::OrdersAbandoned,
99        Self::Revenue,
100        Self::IngredientCost,
101        Self::Profit,
102    ];
103
104    #[must_use]
105    pub fn label(self) -> &'static str {
106        match self {
107            Self::TillQueueLen => "Till Queue",
108            Self::KitchenQueueLen => "Kitchen Queue",
109            Self::ActiveTillWorkers => "Busy Till Workers",
110            Self::ActiveKitchenWorkers => "Busy Kitchen Workers",
111            Self::Arrivals => "Arrivals",
112            Self::Balked => "Balked",
113            Self::GaveUpQueue => "Gave Up Queue",
114            Self::OrdersStarted => "Orders Started",
115            Self::OrdersServed => "Orders Served",
116            Self::OrdersAbandoned => "Orders Abandoned",
117            Self::Revenue => "Revenue",
118            Self::IngredientCost => "Ingredient Cost",
119            Self::Profit => "Profit",
120        }
121    }
122
123    #[must_use]
124    pub fn supports_windowing(self) -> bool {
125        matches!(
126            self,
127            Self::Arrivals
128                | Self::Balked
129                | Self::GaveUpQueue
130                | Self::OrdersStarted
131                | Self::OrdersServed
132                | Self::OrdersAbandoned
133                | Self::Revenue
134                | Self::IngredientCost
135                | Self::Profit
136        )
137    }
138
139    #[must_use]
140    pub fn title(self, windowed: bool, window_ticks: u64) -> String {
141        if windowed && self.supports_windowing() {
142            format!("{} (Windowed over {} ticks)", self.label(), window_ticks)
143        } else {
144            self.label().to_string()
145        }
146    }
147
148    #[must_use]
149    pub fn base_value(
150        self,
151        snapshot: &SimulationSnapshot,
152        tick: u64,
153        salary_cost: f64,
154        day_ticks: u64,
155    ) -> f64 {
156        match self {
157            Self::TillQueueLen => snapshot.till_queue.len() as f64,
158            Self::KitchenQueueLen => snapshot.kitchen_queue.len() as f64,
159            Self::ActiveTillWorkers => snapshot.active_till_workers as f64,
160            Self::ActiveKitchenWorkers => snapshot.active_kitchen_workers as f64,
161            Self::Arrivals => snapshot.metrics.arrivals as f64,
162            Self::Balked => snapshot.metrics.balked as f64,
163            Self::GaveUpQueue => snapshot.metrics.gave_up_queue as f64,
164            Self::OrdersStarted => snapshot.metrics.orders_started as f64,
165            Self::OrdersServed => snapshot.metrics.orders_served as f64,
166            Self::OrdersAbandoned => snapshot.metrics.orders_abandoned as f64,
167            Self::Revenue => snapshot.metrics.revenue,
168            Self::IngredientCost => snapshot.metrics.ingredient_cost,
169            Self::Profit => {
170                let accrued_salary_cost = if day_ticks == 0 {
171                    0.0
172                } else {
173                    salary_cost * (tick.min(day_ticks) as f64 / day_ticks as f64)
174                };
175                snapshot.metrics.revenue - snapshot.metrics.ingredient_cost - accrued_salary_cost
176            }
177        }
178    }
179
180    #[must_use]
181    pub fn next(self) -> Self {
182        let index = Self::ALL.iter().position(|stat| *stat == self).unwrap_or(0);
183        Self::ALL[(index + 1) % Self::ALL.len()]
184    }
185
186    #[must_use]
187    pub fn previous(self) -> Self {
188        let index = Self::ALL.iter().position(|stat| *stat == self).unwrap_or(0);
189        Self::ALL[(index + Self::ALL.len() - 1) % Self::ALL.len()]
190    }
191}