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