elevator_core/dispatch/nearest_car.rs
1//! Nearest-car dispatch — assigns each call to the closest idle elevator.
2
3use super::{DispatchStrategy, RankContext, pair_is_useful};
4
5/// Scores `(car, stop)` by absolute distance between the car and the stop.
6///
7/// Paired with the Hungarian assignment in the dispatch system, this
8/// yields the globally minimum-total-distance matching across the group
9/// — no two cars can be sent to the same hall call.
10///
11/// Two guards are applied on top of the raw distance, both via the
12/// shared [`pair_is_useful`] predicate:
13///
14/// 1. The `(car, stop)` pair must be serviceable — at least one aboard
15/// rider can exit, or at least one waiting rider can fit. A full car
16/// at a pickup stop it cannot serve otherwise self-assigns at zero
17/// cost (doors cycle open → reject → close forever).
18/// 2. A car carrying riders refuses pickups that would pull it backward
19/// (off the path to every aboard rider's destination). Without this,
20/// a stream of closer-destination boarders can indefinitely preempt
21/// a farther aboard rider's delivery — the reported "never reaches
22/// the passenger's desired stop" loop.
23pub struct NearestCarDispatch;
24
25impl NearestCarDispatch {
26 /// Create a new `NearestCarDispatch`.
27 #[must_use]
28 pub const fn new() -> Self {
29 Self
30 }
31}
32
33impl Default for NearestCarDispatch {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl DispatchStrategy for NearestCarDispatch {
40 fn rank(&mut self, ctx: &RankContext<'_>) -> Option<f64> {
41 if !pair_is_useful(ctx) {
42 return None;
43 }
44 Some((ctx.car_position - ctx.stop_position).abs())
45 }
46
47 fn builtin_id(&self) -> Option<super::BuiltinStrategy> {
48 Some(super::BuiltinStrategy::NearestCar)
49 }
50}