elevator_core/dispatch/
etd.rs1use smallvec::SmallVec;
10
11use crate::components::{ElevatorPhase, Route};
12use crate::entity::EntityId;
13use crate::world::World;
14
15use super::{DispatchDecision, DispatchManifest, DispatchStrategy, ElevatorGroup};
16
17pub struct EtdDispatch {
34 pub wait_weight: f64,
36 pub delay_weight: f64,
38 pub door_weight: f64,
40}
41
42impl EtdDispatch {
43 #[must_use]
47 pub const fn new() -> Self {
48 Self {
49 wait_weight: 1.0,
50 delay_weight: 1.0,
51 door_weight: 0.5,
52 }
53 }
54
55 #[must_use]
59 pub const fn with_delay_weight(delay_weight: f64) -> Self {
60 Self {
61 wait_weight: 1.0,
62 delay_weight,
63 door_weight: 0.5,
64 }
65 }
66
67 #[must_use]
69 pub const fn with_weights(wait_weight: f64, delay_weight: f64, door_weight: f64) -> Self {
70 Self {
71 wait_weight,
72 delay_weight,
73 door_weight,
74 }
75 }
76}
77
78impl Default for EtdDispatch {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl DispatchStrategy for EtdDispatch {
85 fn decide(
86 &mut self,
87 _elevator: EntityId,
88 _elevator_position: f64,
89 _group: &ElevatorGroup,
90 _manifest: &DispatchManifest,
91 _world: &World,
92 ) -> DispatchDecision {
93 DispatchDecision::Idle
95 }
96
97 fn decide_all(
98 &mut self,
99 elevators: &[(EntityId, f64)],
100 group: &ElevatorGroup,
101 manifest: &DispatchManifest,
102 world: &World,
103 ) -> Vec<(EntityId, DispatchDecision)> {
104 let pending_stops: SmallVec<[(EntityId, f64); 16]> = group
106 .stop_entities()
107 .iter()
108 .filter_map(|&stop_eid| {
109 if manifest.has_demand(stop_eid) {
110 world.stop_position(stop_eid).map(|pos| (stop_eid, pos))
111 } else {
112 None
113 }
114 })
115 .collect();
116
117 if pending_stops.is_empty() {
118 return elevators
119 .iter()
120 .map(|(eid, _)| (*eid, DispatchDecision::Idle))
121 .collect();
122 }
123
124 let mut results: Vec<(EntityId, DispatchDecision)> = Vec::new();
125 let mut assigned_elevators: SmallVec<[EntityId; 16]> = SmallVec::new();
126
127 for (stop_eid, stop_pos) in &pending_stops {
129 let mut best_elevator: Option<EntityId> = None;
130 let mut best_cost = f64::INFINITY;
131
132 for &(elev_eid, elev_pos) in elevators {
133 if assigned_elevators.contains(&elev_eid) {
134 continue;
135 }
136
137 let cost = self.compute_cost(
138 elev_eid,
139 elev_pos,
140 *stop_pos,
141 &pending_stops,
142 manifest,
143 world,
144 );
145
146 if cost < best_cost {
147 best_cost = cost;
148 best_elevator = Some(elev_eid);
149 }
150 }
151
152 if let Some(elev_eid) = best_elevator {
153 results.push((elev_eid, DispatchDecision::GoToStop(*stop_eid)));
154 assigned_elevators.push(elev_eid);
155 }
156 }
157
158 for (eid, _) in elevators {
160 if !assigned_elevators.contains(eid) {
161 results.push((*eid, DispatchDecision::Idle));
162 }
163 }
164
165 results
166 }
167}
168
169impl EtdDispatch {
170 fn compute_cost(
175 &self,
176 elev_eid: EntityId,
177 elev_pos: f64,
178 target_pos: f64,
179 pending_stops: &[(EntityId, f64)],
180 _manifest: &DispatchManifest,
181 world: &World,
182 ) -> f64 {
183 let Some(car) = world.elevator(elev_eid) else {
184 return f64::INFINITY;
185 };
186
187 let distance = (elev_pos - target_pos).abs();
189 let travel_time = if car.max_speed > 0.0 {
190 distance / car.max_speed
191 } else {
192 return f64::INFINITY;
193 };
194
195 let door_overhead_per_stop = f64::from(car.door_transition_ticks * 2 + car.door_open_ticks);
197
198 let (lo, hi) = if elev_pos < target_pos {
200 (elev_pos, target_pos)
201 } else {
202 (target_pos, elev_pos)
203 };
204 let intervening_stops = pending_stops
205 .iter()
206 .filter(|(_, pos)| *pos > lo + 1e-9 && *pos < hi - 1e-9)
207 .count() as f64;
208 let door_cost = intervening_stops * door_overhead_per_stop;
209
210 let mut existing_rider_delay = 0.0_f64;
213 for &rider_eid in car.riders() {
214 if let Some(dest) = world.route(rider_eid).and_then(Route::current_destination)
215 && let Some(dest_pos) = world.stop_position(dest)
216 {
217 let direct_dist = (elev_pos - dest_pos).abs();
221 let detour_dist = (elev_pos - target_pos).abs() + (target_pos - dest_pos).abs();
222 let extra = (detour_dist - direct_dist).max(0.0);
223 if car.max_speed > 0.0 {
224 existing_rider_delay += extra / car.max_speed;
225 }
226 }
227 }
228
229 let direction_bonus = match car.phase.moving_target() {
233 Some(current_target) => world.stop_position(current_target).map_or(0.0, |ctp| {
234 let moving_up = ctp > elev_pos;
235 let target_is_ahead = if moving_up {
236 target_pos > elev_pos && target_pos <= ctp
237 } else {
238 target_pos < elev_pos && target_pos >= ctp
239 };
240 if target_is_ahead {
241 -travel_time * 0.5
242 } else {
243 0.0
244 }
245 }),
246 None if car.phase == ElevatorPhase::Idle => -travel_time * 0.3, _ => 0.0,
248 };
249
250 self.wait_weight.mul_add(
251 travel_time,
252 self.delay_weight.mul_add(
253 existing_rider_delay,
254 self.door_weight.mul_add(door_cost, direction_bonus),
255 ),
256 )
257 }
258}