Skip to main content

elevator_core/components/
elevator.rs

1//! Elevator state and configuration component.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5
6use crate::door::DoorState;
7use crate::entity::EntityId;
8
9/// Direction an elevator's indicator lamps are signalling.
10///
11/// Derived from the pair of `going_up` / `going_down` flags on [`Elevator`].
12/// `Either` corresponds to both lamps lit — the car is idle and will accept
13/// riders heading either way. `Up` / `Down` correspond to an actively
14/// committed direction.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[non_exhaustive]
17pub enum Direction {
18    /// Car will serve upward trips only.
19    Up,
20    /// Car will serve downward trips only.
21    Down,
22    /// Car will serve either direction (idle).
23    Either,
24}
25
26impl std::fmt::Display for Direction {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::Up => write!(f, "Up"),
30            Self::Down => write!(f, "Down"),
31            Self::Either => write!(f, "Either"),
32        }
33    }
34}
35
36/// Operational phase of an elevator.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[non_exhaustive]
39pub enum ElevatorPhase {
40    /// Parked with no pending requests.
41    Idle,
42    /// Travelling toward a specific stop in response to a dispatch
43    /// assignment (carrying or about to pick up riders).
44    MovingToStop(EntityId),
45    /// Travelling toward a stop for repositioning — no rider service
46    /// obligation, will transition directly to [`Idle`] on arrival
47    /// without opening doors. Distinct from [`MovingToStop`] so that
48    /// downstream code (dispatch, UI, metrics) can treat opportunistic
49    /// moves differently from scheduled trips.
50    ///
51    /// [`MovingToStop`]: Self::MovingToStop
52    /// [`Idle`]: Self::Idle
53    Repositioning(EntityId),
54    /// Doors are currently opening.
55    DoorOpening,
56    /// Doors open; riders may board or exit.
57    Loading,
58    /// Doors are currently closing.
59    DoorClosing,
60    /// Stopped at a floor (doors closed, awaiting dispatch).
61    Stopped,
62}
63
64impl ElevatorPhase {
65    /// Whether the elevator is currently travelling (in either a dispatched
66    /// or a repositioning move).
67    #[must_use]
68    pub const fn is_moving(&self) -> bool {
69        matches!(self, Self::MovingToStop(_) | Self::Repositioning(_))
70    }
71
72    /// The target stop of a moving elevator, if any.
73    ///
74    /// Returns `Some(stop)` for both [`MovingToStop`] and [`Repositioning`]
75    /// variants; `None` otherwise.
76    ///
77    /// [`MovingToStop`]: Self::MovingToStop
78    /// [`Repositioning`]: Self::Repositioning
79    #[must_use]
80    pub const fn moving_target(&self) -> Option<EntityId> {
81        match self {
82            Self::MovingToStop(s) | Self::Repositioning(s) => Some(*s),
83            _ => None,
84        }
85    }
86}
87
88impl std::fmt::Display for ElevatorPhase {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            Self::Idle => write!(f, "Idle"),
92            Self::MovingToStop(id) => write!(f, "MovingToStop({id:?})"),
93            Self::Repositioning(id) => write!(f, "Repositioning({id:?})"),
94            Self::DoorOpening => write!(f, "DoorOpening"),
95            Self::Loading => write!(f, "Loading"),
96            Self::DoorClosing => write!(f, "DoorClosing"),
97            Self::Stopped => write!(f, "Stopped"),
98        }
99    }
100}
101
102/// Component for an elevator entity.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct Elevator {
105    /// Current operational phase.
106    pub(crate) phase: ElevatorPhase,
107    /// Door finite-state machine.
108    pub(crate) door: DoorState,
109    /// Maximum travel speed (distance/tick).
110    pub(crate) max_speed: f64,
111    /// Acceleration rate (distance/tick^2).
112    pub(crate) acceleration: f64,
113    /// Deceleration rate (distance/tick^2).
114    pub(crate) deceleration: f64,
115    /// Maximum weight the car can carry.
116    pub(crate) weight_capacity: f64,
117    /// Total weight of riders currently aboard.
118    pub(crate) current_load: f64,
119    /// Entity IDs of riders currently aboard.
120    pub(crate) riders: Vec<EntityId>,
121    /// Stop entity the car is heading toward, if any.
122    pub(crate) target_stop: Option<EntityId>,
123    /// Ticks for a door open/close transition.
124    pub(crate) door_transition_ticks: u32,
125    /// Ticks the door stays fully open.
126    pub(crate) door_open_ticks: u32,
127    /// Line entity this car belongs to.
128    #[serde(alias = "group")]
129    pub(crate) line: EntityId,
130    /// Whether this elevator is currently repositioning (not serving a dispatch).
131    #[serde(default)]
132    pub(crate) repositioning: bool,
133    /// Stop entity IDs this elevator cannot serve (access restriction).
134    #[serde(default)]
135    pub(crate) restricted_stops: HashSet<EntityId>,
136    /// Speed multiplier for Inspection mode (0.0..1.0).
137    #[serde(default = "default_inspection_speed_factor")]
138    pub(crate) inspection_speed_factor: f64,
139    /// Up-direction indicator lamp: whether this car will serve upward trips.
140    ///
141    /// Auto-managed by the dispatch phase: set true when heading up (or idle),
142    /// false while actively committed to a downward trip. Affects boarding:
143    /// a rider whose next leg goes up will not board a car with `going_up=false`.
144    #[serde(default = "default_true")]
145    pub(crate) going_up: bool,
146    /// Down-direction indicator lamp: whether this car will serve downward trips.
147    ///
148    /// Auto-managed by the dispatch phase: set true when heading down (or idle),
149    /// false while actively committed to an upward trip. Affects boarding:
150    /// a rider whose next leg goes down will not board a car with `going_down=false`.
151    #[serde(default = "default_true")]
152    pub(crate) going_down: bool,
153    /// Count of rounded-floor transitions (passing-floors + arrivals).
154    /// Useful as a scoring axis for efficiency — fewer moves per delivery
155    /// means less wasted travel.
156    #[serde(default)]
157    pub(crate) move_count: u64,
158}
159
160/// Default inspection speed factor (25% of normal speed).
161const fn default_inspection_speed_factor() -> f64 {
162    0.25
163}
164
165/// Default value for direction indicator fields (both lamps on = idle/either direction).
166const fn default_true() -> bool {
167    true
168}
169
170impl Elevator {
171    /// Current operational phase.
172    #[must_use]
173    pub const fn phase(&self) -> ElevatorPhase {
174        self.phase
175    }
176
177    /// Door finite-state machine.
178    #[must_use]
179    pub const fn door(&self) -> &DoorState {
180        &self.door
181    }
182
183    /// Maximum travel speed (distance/tick).
184    #[must_use]
185    pub const fn max_speed(&self) -> f64 {
186        self.max_speed
187    }
188
189    /// Acceleration rate (distance/tick^2).
190    #[must_use]
191    pub const fn acceleration(&self) -> f64 {
192        self.acceleration
193    }
194
195    /// Deceleration rate (distance/tick^2).
196    #[must_use]
197    pub const fn deceleration(&self) -> f64 {
198        self.deceleration
199    }
200
201    /// Maximum weight the car can carry.
202    #[must_use]
203    pub const fn weight_capacity(&self) -> f64 {
204        self.weight_capacity
205    }
206
207    /// Total weight of riders currently aboard.
208    #[must_use]
209    pub const fn current_load(&self) -> f64 {
210        self.current_load
211    }
212
213    /// Entity IDs of riders currently aboard.
214    #[must_use]
215    pub fn riders(&self) -> &[EntityId] {
216        &self.riders
217    }
218
219    /// Stop entity the car is heading toward, if any.
220    #[must_use]
221    pub const fn target_stop(&self) -> Option<EntityId> {
222        self.target_stop
223    }
224
225    /// Ticks for a door open/close transition.
226    #[must_use]
227    pub const fn door_transition_ticks(&self) -> u32 {
228        self.door_transition_ticks
229    }
230
231    /// Ticks the door stays fully open.
232    #[must_use]
233    pub const fn door_open_ticks(&self) -> u32 {
234        self.door_open_ticks
235    }
236
237    /// Line entity this car belongs to.
238    #[must_use]
239    pub const fn line(&self) -> EntityId {
240        self.line
241    }
242
243    /// Whether this elevator is currently repositioning (not serving a dispatch).
244    #[must_use]
245    pub const fn repositioning(&self) -> bool {
246        self.repositioning
247    }
248
249    /// Stop entity IDs this elevator cannot serve (access restriction).
250    #[must_use]
251    pub const fn restricted_stops(&self) -> &HashSet<EntityId> {
252        &self.restricted_stops
253    }
254
255    /// Speed multiplier applied during Inspection mode.
256    #[must_use]
257    pub const fn inspection_speed_factor(&self) -> f64 {
258        self.inspection_speed_factor
259    }
260
261    /// Whether this car's up-direction indicator lamp is lit.
262    ///
263    /// A lit up-lamp signals the car will serve upward-travelling riders.
264    /// Both lamps lit means the car is idle and will accept either direction.
265    #[must_use]
266    pub const fn going_up(&self) -> bool {
267        self.going_up
268    }
269
270    /// Whether this car's down-direction indicator lamp is lit.
271    ///
272    /// A lit down-lamp signals the car will serve downward-travelling riders.
273    /// Both lamps lit means the car is idle and will accept either direction.
274    #[must_use]
275    pub const fn going_down(&self) -> bool {
276        self.going_down
277    }
278
279    /// Direction this car is currently committed to, derived from the pair
280    /// of indicator-lamp flags.
281    ///
282    /// - `Direction::Up` — only `going_up` is set
283    /// - `Direction::Down` — only `going_down` is set
284    /// - `Direction::Either` — both lamps lit (car is idle / accepting
285    ///   either direction), or neither is set (treated as `Either` too,
286    ///   though the dispatch phase normally keeps at least one lit)
287    #[must_use]
288    pub const fn direction(&self) -> Direction {
289        match (self.going_up, self.going_down) {
290            (true, false) => Direction::Up,
291            (false, true) => Direction::Down,
292            _ => Direction::Either,
293        }
294    }
295
296    /// Count of rounded-floor transitions this elevator has made
297    /// (both passing-floor crossings and arrivals).
298    #[must_use]
299    pub const fn move_count(&self) -> u64 {
300        self.move_count
301    }
302}