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
49/// Decide whether assigning `ctx.car` to `ctx.stop` is on the path to
50/// any aboard rider's destination. Combined with
51/// [`pair_can_do_work`](super::pair_can_do_work), this keeps a car
52/// carrying riders from being pulled backward by closer pickups.
53fn pair_is_useful(ctx: &RankContext<'_>) -> bool {
54    if !pair_can_do_work(ctx) {
55        return false;
56    }
57
58    let Some(car) = ctx.world.elevator(ctx.car) else {
59        return false;
60    };
61    // Exiting an aboard rider is always on-the-way for that rider.
62    let can_exit_here = car
63        .riders()
64        .iter()
65        .any(|&rid| ctx.world.route(rid).and_then(Route::current_destination) == Some(ctx.stop));
66    if can_exit_here || car.riders().is_empty() {
67        return true;
68    }
69
70    // Route-less aboard riders (game-managed manual riders) don't
71    // publish a destination, so there's no committed path to protect.
72    // Any pickup is trivially on-the-way — fall back to the raw
73    // servability check. Otherwise we'd refuse every pickup the moment
74    // the car carried its first manually-managed passenger.
75    let has_routed_rider = car.riders().iter().any(|&rid| {
76        ctx.world
77            .route(rid)
78            .and_then(Route::current_destination)
79            .is_some()
80    });
81    if !has_routed_rider {
82        return true;
83    }
84
85    // Pickups allowed only on the path to an aboard rider's destination.
86    // Candidate at the car's position (to_cand = 0) trivially qualifies —
87    // useful for same-floor boards.
88    let to_cand = ctx.stop_position - ctx.car_position;
89    car.riders().iter().any(|&rid| {
90        let Some(dest) = ctx.world.route(rid).and_then(Route::current_destination) else {
91            return false;
92        };
93        let Some(dest_pos) = ctx.world.stop_position(dest) else {
94            return false;
95        };
96        let to_dest = dest_pos - ctx.car_position;
97        to_dest * to_cand >= 0.0 && to_cand.abs() <= to_dest.abs()
98    })
99}