Skip to main content

elevator_core/dispatch/
mod.rs

1//! Pluggable dispatch strategies for assigning elevators to stops.
2//!
3//! # Example: custom dispatch strategy
4//!
5//! ```rust
6//! use elevator_core::prelude::*;
7//! use elevator_core::dispatch::{
8//!     DispatchDecision, DispatchManifest, ElevatorGroup,
9//! };
10//!
11//! struct AlwaysFirstStop;
12//!
13//! impl DispatchStrategy for AlwaysFirstStop {
14//!     fn decide(
15//!         &mut self,
16//!         _elevator: EntityId,
17//!         _elevator_position: f64,
18//!         group: &ElevatorGroup,
19//!         _manifest: &DispatchManifest,
20//!         _world: &elevator_core::world::World,
21//!     ) -> DispatchDecision {
22//!         group.stop_entities().first()
23//!             .map(|&stop| DispatchDecision::GoToStop(stop))
24//!             .unwrap_or(DispatchDecision::Idle)
25//!     }
26//! }
27//!
28//! let sim = SimulationBuilder::demo()
29//!     .dispatch(AlwaysFirstStop)
30//!     .build()
31//!     .unwrap();
32//! ```
33
34/// Estimated Time to Destination dispatch algorithm.
35pub mod etd;
36/// LOOK dispatch algorithm.
37pub mod look;
38/// Nearest-car dispatch algorithm.
39pub mod nearest_car;
40/// Built-in repositioning strategies.
41pub mod reposition;
42/// SCAN dispatch algorithm.
43pub mod scan;
44
45pub use etd::EtdDispatch;
46pub use look::LookDispatch;
47pub use nearest_car::NearestCarDispatch;
48pub use scan::ScanDispatch;
49
50use serde::{Deserialize, Serialize};
51
52use crate::entity::EntityId;
53use crate::ids::GroupId;
54use crate::world::World;
55use std::collections::BTreeMap;
56
57/// Metadata about a single rider, available to dispatch strategies.
58#[derive(Debug, Clone)]
59#[non_exhaustive]
60pub struct RiderInfo {
61    /// Rider entity ID.
62    pub id: EntityId,
63    /// Rider's destination stop entity (from route).
64    pub destination: Option<EntityId>,
65    /// Rider weight.
66    pub weight: f64,
67    /// Ticks this rider has been waiting (0 if riding).
68    pub wait_ticks: u64,
69}
70
71/// Full demand picture for dispatch decisions.
72///
73/// Contains per-rider metadata grouped by stop, enabling entity-aware
74/// dispatch strategies (priority, weight-aware, VIP-first, etc.).
75///
76/// Uses `BTreeMap` for deterministic iteration order.
77#[derive(Debug, Clone, Default)]
78pub struct DispatchManifest {
79    /// Riders waiting at each stop, with full per-rider metadata.
80    pub waiting_at_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
81    /// Riders currently aboard elevators, grouped by their destination stop.
82    pub riding_to_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
83    /// Number of residents at each stop (read-only hint for dispatch strategies).
84    pub resident_count_at_stop: BTreeMap<EntityId, usize>,
85}
86
87impl DispatchManifest {
88    /// Number of riders waiting at a stop.
89    #[must_use]
90    pub fn waiting_count_at(&self, stop: EntityId) -> usize {
91        self.waiting_at_stop.get(&stop).map_or(0, Vec::len)
92    }
93
94    /// Total weight of riders waiting at a stop.
95    #[must_use]
96    pub fn total_weight_at(&self, stop: EntityId) -> f64 {
97        self.waiting_at_stop
98            .get(&stop)
99            .map_or(0.0, |riders| riders.iter().map(|r| r.weight).sum())
100    }
101
102    /// Number of riders heading to a stop (aboard elevators).
103    #[must_use]
104    pub fn riding_count_to(&self, stop: EntityId) -> usize {
105        self.riding_to_stop.get(&stop).map_or(0, Vec::len)
106    }
107
108    /// Whether a stop has any demand (waiting riders or riders heading there).
109    #[must_use]
110    pub fn has_demand(&self, stop: EntityId) -> bool {
111        self.waiting_count_at(stop) > 0 || self.riding_count_to(stop) > 0
112    }
113
114    /// Number of residents at a stop (read-only hint, not active demand).
115    #[must_use]
116    pub fn resident_count_at(&self, stop: EntityId) -> usize {
117        self.resident_count_at_stop.get(&stop).copied().unwrap_or(0)
118    }
119}
120
121/// Serializable identifier for built-in dispatch strategies.
122///
123/// Used in snapshots and config files to restore the correct strategy
124/// without requiring the game to manually re-wire dispatch. Custom strategies
125/// are represented by the `Custom(String)` variant.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[non_exhaustive]
128pub enum BuiltinStrategy {
129    /// SCAN (elevator) algorithm — sweeps end-to-end.
130    Scan,
131    /// LOOK algorithm — reverses at last request.
132    Look,
133    /// Nearest-car — assigns closest idle elevator.
134    NearestCar,
135    /// Estimated Time to Destination — minimizes total cost.
136    Etd,
137    /// Custom strategy identified by name. The game must provide a factory.
138    Custom(String),
139}
140
141impl std::fmt::Display for BuiltinStrategy {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::Scan => write!(f, "Scan"),
145            Self::Look => write!(f, "Look"),
146            Self::NearestCar => write!(f, "NearestCar"),
147            Self::Etd => write!(f, "Etd"),
148            Self::Custom(name) => write!(f, "Custom({name})"),
149        }
150    }
151}
152
153impl BuiltinStrategy {
154    /// Instantiate the dispatch strategy for this variant.
155    ///
156    /// Returns `None` for `Custom` — the game must provide those via
157    /// a factory function.
158    #[must_use]
159    pub fn instantiate(&self) -> Option<Box<dyn DispatchStrategy>> {
160        match self {
161            Self::Scan => Some(Box::new(scan::ScanDispatch::new())),
162            Self::Look => Some(Box::new(look::LookDispatch::new())),
163            Self::NearestCar => Some(Box::new(nearest_car::NearestCarDispatch::new())),
164            Self::Etd => Some(Box::new(etd::EtdDispatch::new())),
165            Self::Custom(_) => None,
166        }
167    }
168}
169
170/// Decision returned by a dispatch strategy.
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[non_exhaustive]
173pub enum DispatchDecision {
174    /// Go to the specified stop entity.
175    GoToStop(EntityId),
176    /// Remain idle.
177    Idle,
178}
179
180/// Per-line relationship data within an [`ElevatorGroup`].
181///
182/// This is a denormalized cache maintained by [`Simulation`](crate::sim::Simulation).
183/// The source of truth for intrinsic line properties is the
184/// [`Line`](crate::components::Line) component in World.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct LineInfo {
187    /// Line entity ID.
188    entity: EntityId,
189    /// Elevator entities on this line.
190    elevators: Vec<EntityId>,
191    /// Stop entities served by this line.
192    serves: Vec<EntityId>,
193}
194
195impl LineInfo {
196    /// Create a new `LineInfo`.
197    #[must_use]
198    pub const fn new(entity: EntityId, elevators: Vec<EntityId>, serves: Vec<EntityId>) -> Self {
199        Self {
200            entity,
201            elevators,
202            serves,
203        }
204    }
205
206    /// Line entity ID.
207    #[must_use]
208    pub const fn entity(&self) -> EntityId {
209        self.entity
210    }
211
212    /// Elevator entities on this line.
213    #[must_use]
214    pub fn elevators(&self) -> &[EntityId] {
215        &self.elevators
216    }
217
218    /// Stop entities served by this line.
219    #[must_use]
220    pub fn serves(&self) -> &[EntityId] {
221        &self.serves
222    }
223
224    /// Set the line entity ID (used during snapshot restore).
225    pub(crate) const fn set_entity(&mut self, entity: EntityId) {
226        self.entity = entity;
227    }
228
229    /// Mutable access to elevator entities on this line.
230    pub(crate) const fn elevators_mut(&mut self) -> &mut Vec<EntityId> {
231        &mut self.elevators
232    }
233
234    /// Mutable access to stop entities served by this line.
235    pub(crate) const fn serves_mut(&mut self) -> &mut Vec<EntityId> {
236        &mut self.serves
237    }
238}
239
240/// Runtime elevator group: a set of lines sharing a dispatch strategy.
241///
242/// A group is the logical dispatch unit. It contains one or more
243/// [`LineInfo`] entries, each representing a physical path with its
244/// elevators and served stops.
245///
246/// The flat `elevator_entities` and `stop_entities` fields are derived
247/// caches (union of all lines' elevators/stops), rebuilt automatically
248/// via [`rebuild_caches()`](Self::rebuild_caches).
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ElevatorGroup {
251    /// Unique group identifier.
252    id: GroupId,
253    /// Human-readable group name.
254    name: String,
255    /// Lines belonging to this group.
256    lines: Vec<LineInfo>,
257    /// Derived flat cache — rebuilt by `rebuild_caches()`.
258    elevator_entities: Vec<EntityId>,
259    /// Derived flat cache — rebuilt by `rebuild_caches()`.
260    stop_entities: Vec<EntityId>,
261}
262
263impl ElevatorGroup {
264    /// Create a new group with the given lines. Caches are built automatically.
265    #[must_use]
266    pub fn new(id: GroupId, name: String, lines: Vec<LineInfo>) -> Self {
267        let mut group = Self {
268            id,
269            name,
270            lines,
271            elevator_entities: Vec::new(),
272            stop_entities: Vec::new(),
273        };
274        group.rebuild_caches();
275        group
276    }
277
278    /// Unique group identifier.
279    #[must_use]
280    pub const fn id(&self) -> GroupId {
281        self.id
282    }
283
284    /// Human-readable group name.
285    #[must_use]
286    pub fn name(&self) -> &str {
287        &self.name
288    }
289
290    /// Lines belonging to this group.
291    #[must_use]
292    pub fn lines(&self) -> &[LineInfo] {
293        &self.lines
294    }
295
296    /// Mutable access to lines (call [`rebuild_caches()`](Self::rebuild_caches) after mutating).
297    pub const fn lines_mut(&mut self) -> &mut Vec<LineInfo> {
298        &mut self.lines
299    }
300
301    /// Elevator entities belonging to this group (derived from lines).
302    #[must_use]
303    pub fn elevator_entities(&self) -> &[EntityId] {
304        &self.elevator_entities
305    }
306
307    /// Stop entities served by this group (derived from lines, deduplicated).
308    #[must_use]
309    pub fn stop_entities(&self) -> &[EntityId] {
310        &self.stop_entities
311    }
312
313    /// Push a stop entity directly into the group's stop cache.
314    ///
315    /// Use when a stop belongs to the group for dispatch purposes but is
316    /// not (yet) assigned to any line. Call `add_stop_to_line` later to
317    /// wire it into the topology graph.
318    pub(crate) fn push_stop(&mut self, stop: EntityId) {
319        if !self.stop_entities.contains(&stop) {
320            self.stop_entities.push(stop);
321        }
322    }
323
324    /// Push an elevator entity directly into the group's elevator cache
325    /// (in addition to the line it belongs to).
326    pub(crate) fn push_elevator(&mut self, elevator: EntityId) {
327        if !self.elevator_entities.contains(&elevator) {
328            self.elevator_entities.push(elevator);
329        }
330    }
331
332    /// Rebuild derived caches from lines. Call after mutating lines.
333    pub fn rebuild_caches(&mut self) {
334        self.elevator_entities = self
335            .lines
336            .iter()
337            .flat_map(|li| li.elevators.iter().copied())
338            .collect();
339        let mut stops: Vec<EntityId> = self
340            .lines
341            .iter()
342            .flat_map(|li| li.serves.iter().copied())
343            .collect();
344        stops.sort_unstable();
345        stops.dedup();
346        self.stop_entities = stops;
347    }
348}
349
350/// Pluggable dispatch algorithm.
351///
352/// Receives a manifest with per-rider metadata grouped by stop.
353/// Convenience methods provide aggregate counts; implementations
354/// can also iterate individual riders for priority/weight-aware dispatch.
355pub trait DispatchStrategy: Send + Sync {
356    /// Decide for a single elevator.
357    fn decide(
358        &mut self,
359        elevator: EntityId,
360        elevator_position: f64,
361        group: &ElevatorGroup,
362        manifest: &DispatchManifest,
363        world: &World,
364    ) -> DispatchDecision;
365
366    /// Decide for all idle elevators in a group.
367    /// Default: calls `decide()` per elevator.
368    fn decide_all(
369        &mut self,
370        elevators: &[(EntityId, f64)], // (entity, position)
371        group: &ElevatorGroup,
372        manifest: &DispatchManifest,
373        world: &World,
374    ) -> Vec<(EntityId, DispatchDecision)> {
375        elevators
376            .iter()
377            .map(|(eid, pos)| (*eid, self.decide(*eid, *pos, group, manifest, world)))
378            .collect()
379    }
380
381    /// Notify the strategy that an elevator has been removed.
382    ///
383    /// Implementations with per-elevator state (e.g., direction tracking)
384    /// should clean up here to prevent unbounded memory growth. Default: no-op.
385    fn notify_removed(&mut self, _elevator: EntityId) {}
386}
387
388/// Pluggable strategy for repositioning idle elevators.
389///
390/// After the dispatch phase, elevators that remain idle (no pending
391/// assignments) are candidates for repositioning. The strategy decides
392/// where each idle elevator should move to improve coverage and reduce
393/// expected response times.
394///
395/// Implementations receive the set of idle elevator positions and the
396/// group's stop positions, then return a target stop for each elevator
397/// (or `None` to leave it in place).
398pub trait RepositionStrategy: Send + Sync {
399    /// Decide where to reposition idle elevators.
400    ///
401    /// Returns a vec of `(elevator_entity, target_stop_entity)` pairs.
402    /// Elevators not in the returned vec remain idle.
403    fn reposition(
404        &mut self,
405        idle_elevators: &[(EntityId, f64)],
406        stop_positions: &[(EntityId, f64)],
407        group: &ElevatorGroup,
408        world: &World,
409    ) -> Vec<(EntityId, EntityId)>;
410}
411
412/// Serializable identifier for built-in repositioning strategies.
413///
414/// Used in config and snapshots to restore the correct strategy.
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
416#[non_exhaustive]
417pub enum BuiltinReposition {
418    /// Distribute idle elevators evenly across stops.
419    SpreadEvenly,
420    /// Return idle elevators to a configured home stop.
421    ReturnToLobby,
422    /// Position near stops with historically high demand.
423    DemandWeighted,
424    /// Keep idle elevators where they are (no-op).
425    NearestIdle,
426    /// Custom strategy identified by name.
427    Custom(String),
428}
429
430impl std::fmt::Display for BuiltinReposition {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        match self {
433            Self::SpreadEvenly => write!(f, "SpreadEvenly"),
434            Self::ReturnToLobby => write!(f, "ReturnToLobby"),
435            Self::DemandWeighted => write!(f, "DemandWeighted"),
436            Self::NearestIdle => write!(f, "NearestIdle"),
437            Self::Custom(name) => write!(f, "Custom({name})"),
438        }
439    }
440}
441
442impl BuiltinReposition {
443    /// Instantiate the reposition strategy for this variant.
444    ///
445    /// Returns `None` for `Custom` — the game must provide those via
446    /// a factory function. `ReturnToLobby` uses stop index 0 as default.
447    #[must_use]
448    pub fn instantiate(&self) -> Option<Box<dyn RepositionStrategy>> {
449        match self {
450            Self::SpreadEvenly => Some(Box::new(reposition::SpreadEvenly)),
451            Self::ReturnToLobby => Some(Box::new(reposition::ReturnToLobby::new())),
452            Self::DemandWeighted => Some(Box::new(reposition::DemandWeighted)),
453            Self::NearestIdle => Some(Box::new(reposition::NearestIdle)),
454            Self::Custom(_) => None,
455        }
456    }
457}