Skip to main content

elevator_core/dispatch/
nearest_car.rs

1//! Nearest-car dispatch — assigns each call to the closest idle elevator.
2
3use crate::components::Route;
4
5use super::{DispatchStrategy, RankContext, pair_can_do_work};
6
7/// Scores `(car, stop)` by absolute distance between the car and the stop.
8///
9/// Paired with the Hungarian assignment in the dispatch system, this
10/// yields the globally minimum-total-distance matching across the group
11/// — no two cars can be sent to the same hall call.
12///
13/// Two guards are applied on top of the raw distance:
14///
15/// 1. The `(car, stop)` pair must be serviceable — at least one aboard
16///    rider can exit, or at least one waiting rider can fit. A full car
17///    at a pickup stop it cannot serve otherwise self-assigns at zero
18///    cost (doors cycle open → reject → close forever).
19/// 2. A car carrying riders refuses pickups that would pull it backward
20///    (off the path to every aboard rider's destination). Without this,
21///    a stream of closer-destination boarders can indefinitely preempt
22///    a farther aboard rider's delivery — the reported "never reaches
23///    the passenger's desired stop" loop.
24pub struct NearestCarDispatch;
25
26impl NearestCarDispatch {
27    /// Create a new `NearestCarDispatch`.
28    #[must_use]
29    pub const fn new() -> Self {
30        Self
31    }
32}
33
34impl Default for NearestCarDispatch {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl DispatchStrategy for NearestCarDispatch {
41    fn rank(&mut self, ctx: &RankContext<'_>) -> Option<f64> {
42        if !pair_is_useful(ctx) {
43            return None;
44        }
45        Some((ctx.car_position - ctx.stop_position).abs())
46    }
47
48    fn builtin_id(&self) -> Option<super::BuiltinStrategy> {
49        Some(super::BuiltinStrategy::NearestCar)
50    }
51}
52
53/// Decide whether assigning `ctx.car` to `ctx.stop` is on the path to
54/// any aboard rider's destination. Combined with
55/// [`pair_can_do_work`](super::pair_can_do_work), this keeps a car
56/// carrying riders from being pulled backward by closer pickups.
57fn pair_is_useful(ctx: &RankContext<'_>) -> bool {
58    if !pair_can_do_work(ctx) {
59        return false;
60    }
61
62    let Some(car) = ctx.world.elevator(ctx.car) else {
63        return false;
64    };
65    // Exiting an aboard rider is always on-the-way for that rider.
66    let can_exit_here = car
67        .riders()
68        .iter()
69        .any(|&rid| ctx.world.route(rid).and_then(Route::current_destination) == Some(ctx.stop));
70    if can_exit_here || car.riders().is_empty() {
71        return true;
72    }
73
74    // Route-less aboard riders (game-managed manual riders) don't
75    // publish a destination, so there's no committed path to protect.
76    // Any pickup is trivially on-the-way — fall back to the raw
77    // servability check. Otherwise we'd refuse every pickup the moment
78    // the car carried its first manually-managed passenger.
79    let has_routed_rider = car.riders().iter().any(|&rid| {
80        ctx.world
81            .route(rid)
82            .and_then(Route::current_destination)
83            .is_some()
84    });
85    if !has_routed_rider {
86        return true;
87    }
88
89    // Pickups allowed only on the path to an aboard rider's destination.
90    // Candidate at the car's position (to_cand = 0) trivially qualifies —
91    // useful for same-floor boards.
92    let to_cand = ctx.stop_position - ctx.car_position;
93    car.riders().iter().any(|&rid| {
94        let Some(dest) = ctx.world.route(rid).and_then(Route::current_destination) else {
95            return false;
96        };
97        let Some(dest_pos) = ctx.world.stop_position(dest) else {
98            return false;
99        };
100        let to_dest = dest_pos - ctx.car_position;
101        to_dest * to_cand >= 0.0 && to_cand.abs() <= to_dest.abs()
102    })
103}