Skip to main content

elevator_core/dispatch/
nearest_car.rs

1//! Nearest-car dispatch — assigns each call to the closest idle elevator.
2
3use smallvec::SmallVec;
4
5use crate::entity::EntityId;
6use crate::world::World;
7
8use super::{DispatchDecision, DispatchManifest, DispatchStrategy, ElevatorGroup};
9
10/// Assigns each call to the nearest idle elevator.
11///
12/// For multi-elevator groups, this overrides `decide_all()` to coordinate
13/// across the entire group — preventing two elevators from responding to
14/// the same call.
15pub struct NearestCarDispatch;
16
17impl NearestCarDispatch {
18    /// Create a new `NearestCarDispatch`.
19    #[must_use]
20    pub const fn new() -> Self {
21        Self
22    }
23}
24
25impl Default for NearestCarDispatch {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl DispatchStrategy for NearestCarDispatch {
32    fn decide(
33        &mut self,
34        _elevator: EntityId,
35        _elevator_position: f64,
36        _group: &ElevatorGroup,
37        _manifest: &DispatchManifest,
38        _world: &World,
39    ) -> DispatchDecision {
40        // Not used — decide_all() handles everything.
41        DispatchDecision::Idle
42    }
43
44    fn decide_all(
45        &mut self,
46        elevators: &[(EntityId, f64)],
47        group: &ElevatorGroup,
48        manifest: &DispatchManifest,
49        world: &World,
50    ) -> Vec<(EntityId, DispatchDecision)> {
51        // Collect stops that need service (have demand or rider destinations).
52        let mut pending_stops: SmallVec<[(EntityId, f64); 16]> = SmallVec::new();
53        for &stop_eid in group.stop_entities() {
54            if manifest.has_demand(stop_eid)
55                && let Some(pos) = world.stop_position(stop_eid)
56            {
57                pending_stops.push((stop_eid, pos));
58            }
59        }
60
61        if pending_stops.is_empty() {
62            return elevators
63                .iter()
64                .map(|(eid, _)| (*eid, DispatchDecision::Idle))
65                .collect();
66        }
67
68        let mut results: Vec<(EntityId, DispatchDecision)> = Vec::new();
69        let mut assigned_stops: SmallVec<[EntityId; 16]> = SmallVec::new();
70        let mut assigned_elevators: SmallVec<[EntityId; 16]> = SmallVec::new();
71
72        // Greedy assignment: for each unassigned stop, find nearest unassigned elevator.
73        // Sort stops by total demand (highest first) for priority.
74        pending_stops.sort_by(|a, b| {
75            let demand_a = manifest.waiting_count_at(a.0);
76            let demand_b = manifest.waiting_count_at(b.0);
77            demand_b.cmp(&demand_a)
78        });
79
80        for (stop_eid, stop_pos) in &pending_stops {
81            if assigned_stops.contains(stop_eid) {
82                continue;
83            }
84
85            // Find nearest unassigned elevator.
86            let nearest = elevators
87                .iter()
88                .filter(|(eid, _)| !assigned_elevators.contains(eid))
89                .min_by(|a, b| {
90                    let dist_a = (a.1 - stop_pos).abs();
91                    let dist_b = (b.1 - stop_pos).abs();
92                    dist_a.total_cmp(&dist_b)
93                });
94
95            if let Some((elev_eid, _)) = nearest {
96                results.push((*elev_eid, DispatchDecision::GoToStop(*stop_eid)));
97                assigned_elevators.push(*elev_eid);
98                assigned_stops.push(*stop_eid);
99            }
100        }
101
102        // Remaining unassigned elevators get Idle.
103        for (eid, _) in elevators {
104            if !assigned_elevators.contains(eid) {
105                results.push((*eid, DispatchDecision::Idle));
106            }
107        }
108
109        results
110    }
111}