use std::collections::HashSet;
use crate::components::*;
use crate::dispatch::etd::EtdDispatch;
use crate::dispatch::look::LookDispatch;
use crate::dispatch::nearest_car::NearestCarDispatch;
use crate::dispatch::scan::ScanDispatch;
use crate::dispatch::{
self, DispatchDecision, DispatchManifest, DispatchStrategy, ElevatorGroup, RiderInfo,
};
use crate::door::DoorState;
use crate::ids::GroupId;
use crate::world::World;
fn decide_one(
strategy: &mut dyn DispatchStrategy,
car: crate::entity::EntityId,
pos: f64,
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &mut World,
) -> DispatchDecision {
strategy.pre_dispatch(group, manifest, world);
let result = dispatch::assign(strategy, &[(car, pos)], group, manifest, world);
result.decisions[0].1.clone()
}
fn decide_all(
strategy: &mut dyn DispatchStrategy,
cars: &[(crate::entity::EntityId, f64)],
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &mut World,
) -> Vec<(crate::entity::EntityId, DispatchDecision)> {
strategy.pre_dispatch(group, manifest, world);
dispatch::assign(strategy, cars, group, manifest, world).decisions
}
fn test_world() -> (World, Vec<crate::entity::EntityId>) {
let mut world = World::new();
let stops: Vec<_> = [
("Ground", 0.0),
("Floor 2", 4.0),
("Floor 3", 8.0),
("Roof", 12.0),
]
.iter()
.map(|(name, pos)| {
let eid = world.spawn();
world.set_stop(
eid,
Stop {
name: (*name).into(),
position: *pos,
},
);
eid
})
.collect();
(world, stops)
}
fn test_group(
stop_entities: &[crate::entity::EntityId],
elevator_entities: Vec<crate::entity::EntityId>,
) -> ElevatorGroup {
use crate::dispatch::LineInfo;
ElevatorGroup::new(
GroupId(0),
"Default".into(),
vec![LineInfo::new(
crate::entity::EntityId::default(),
elevator_entities,
stop_entities.to_vec(),
)],
)
}
fn spawn_elevator(world: &mut World, position: f64) -> crate::entity::EntityId {
let eid = world.spawn();
world.set_position(eid, Position { value: position });
world.set_velocity(eid, Velocity { value: 0.0 });
world.set_elevator(
eid,
Elevator {
phase: ElevatorPhase::Idle,
door: DoorState::Closed,
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
current_load: 0.0,
riders: vec![],
target_stop: None,
door_transition_ticks: 15,
door_open_ticks: 60,
line: crate::entity::EntityId::default(),
repositioning: false,
restricted_stops: HashSet::new(),
inspection_speed_factor: 0.25,
going_up: true,
going_down: true,
move_count: 0,
door_command_queue: Vec::new(),
manual_target_velocity: None,
},
);
eid
}
fn add_demand(
manifest: &mut DispatchManifest,
world: &mut World,
stop: crate::entity::EntityId,
weight: f64,
) {
let dummy = world.spawn();
manifest
.waiting_at_stop
.entry(stop)
.or_default()
.push(RiderInfo {
id: dummy,
destination: None,
weight,
wait_ticks: 0,
});
}
fn add_rider_dest(
manifest: &mut DispatchManifest,
world: &mut World,
stop: crate::entity::EntityId,
) {
let dummy = world.spawn();
manifest
.riding_to_stop
.entry(stop)
.or_default()
.push(RiderInfo {
id: dummy,
destination: Some(stop),
weight: 70.0,
wait_ticks: 0,
});
}
#[test]
fn scan_no_requests_returns_idle() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
let group = test_group(&stops, vec![elev]);
let manifest = DispatchManifest::default();
let mut scan = ScanDispatch::new();
let decision = decide_one(&mut scan, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::Idle);
}
#[test]
fn scan_goes_to_nearest_in_direction() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
add_demand(&mut manifest, &mut world, stops[3], 80.0);
let mut scan = ScanDispatch::new();
let decision = decide_one(&mut scan, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[1]));
}
#[test]
fn scan_reverses_when_nothing_ahead() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 8.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[0], 70.0);
add_demand(&mut manifest, &mut world, stops[1], 80.0);
let mut scan = ScanDispatch::new();
let decision = decide_one(&mut scan, elev, 8.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[1]));
}
#[test]
fn scan_serves_rider_destination() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_rider_dest(&mut manifest, &mut world, stops[2]);
let mut scan = ScanDispatch::new();
let decision = decide_one(&mut scan, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn scan_prefers_current_direction() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 4.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[0], 70.0);
add_demand(&mut manifest, &mut world, stops[2], 80.0);
let mut scan = ScanDispatch::new();
let decision = decide_one(&mut scan, elev, 4.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn look_no_requests_returns_idle() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
let group = test_group(&stops, vec![elev]);
let manifest = DispatchManifest::default();
let mut look = LookDispatch::new();
let decision = decide_one(&mut look, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::Idle);
}
#[test]
fn look_reverses_at_last_request() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
let mut look = LookDispatch::new();
let decision = decide_one(&mut look, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[1]));
}
#[test]
fn nearest_car_assigns_closest_elevator() {
let (mut world, stops) = test_world();
let elev_a = spawn_elevator(&mut world, 0.0); let elev_b = spawn_elevator(&mut world, 12.0); let group = test_group(&stops, vec![elev_a, elev_b]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
let mut nc = NearestCarDispatch::new();
let elevators = vec![(elev_a, 0.0), (elev_b, 12.0)];
let decisions = decide_all(&mut nc, &elevators, &group, &manifest, &mut world);
let a_decision = decisions.iter().find(|(e, _)| *e == elev_a).unwrap();
assert_eq!(a_decision.1, DispatchDecision::GoToStop(stops[1]));
let b_decision = decisions.iter().find(|(e, _)| *e == elev_b).unwrap();
assert_eq!(b_decision.1, DispatchDecision::Idle);
}
#[test]
fn nearest_car_multiple_stops() {
let (mut world, stops) = test_world();
let elev_a = spawn_elevator(&mut world, 0.0);
let elev_b = spawn_elevator(&mut world, 12.0);
let group = test_group(&stops, vec![elev_a, elev_b]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[0], 70.0);
add_demand(&mut manifest, &mut world, stops[0], 70.0);
add_demand(&mut manifest, &mut world, stops[3], 70.0);
let mut nc = NearestCarDispatch::new();
let elevators = vec![(elev_a, 0.0), (elev_b, 12.0)];
let decisions = decide_all(&mut nc, &elevators, &group, &manifest, &mut world);
let a_dec = decisions.iter().find(|(e, _)| *e == elev_a).unwrap();
assert_eq!(a_dec.1, DispatchDecision::GoToStop(stops[0]));
let b_dec = decisions.iter().find(|(e, _)| *e == elev_b).unwrap();
assert_eq!(b_dec.1, DispatchDecision::GoToStop(stops[3]));
}
#[test]
fn etd_prefers_idle_elevator() {
let (mut world, stops) = test_world();
let elev_a = spawn_elevator(&mut world, 4.0);
let elev_b = spawn_elevator(&mut world, 4.0);
world.elevator_mut(elev_b).unwrap().riders = vec![world.spawn(), world.spawn()];
let group = test_group(&stops, vec![elev_a, elev_b]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
let elevators = vec![(elev_a, 4.0), (elev_b, 4.0)];
let decisions = decide_all(&mut etd, &elevators, &group, &manifest, &mut world);
let a_dec = decisions.iter().find(|(e, _)| *e == elev_a).unwrap();
assert_eq!(a_dec.1, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn etd_closer_elevator_wins() {
let (mut world, stops) = test_world();
let elev_a = spawn_elevator(&mut world, 0.0);
let elev_b = spawn_elevator(&mut world, 8.0);
let group = test_group(&stops, vec![elev_a, elev_b]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
let elevators = vec![(elev_a, 0.0), (elev_b, 8.0)];
let decisions = decide_all(&mut etd, &elevators, &group, &manifest, &mut world);
let b_dec = decisions.iter().find(|(e, _)| *e == elev_b).unwrap();
assert_eq!(b_dec.1, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn scan_at_exact_stop_skips_current_position() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 4.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut scan = ScanDispatch::new();
let decision = decide_one(&mut scan, elev, 4.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn scan_reversal_picks_nearest_behind() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 12.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[0], 70.0);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut scan = ScanDispatch::new();
let decision = decide_one(&mut scan, elev, 12.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn scan_notify_removed_cleans_state() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 12.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[0], 70.0);
add_demand(&mut manifest, &mut world, stops[1], 70.0);
let mut scan = ScanDispatch::new();
let d1 = decide_one(&mut scan, elev, 12.0, &group, &manifest, &mut world);
assert_eq!(d1, DispatchDecision::GoToStop(stops[1]));
scan.notify_removed(elev);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let d2 = decide_one(&mut scan, elev, 4.0, &group, &manifest, &mut world);
assert_eq!(d2, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn look_notify_removed_cleans_state() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 12.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[0], 70.0);
let mut look = LookDispatch::new();
decide_one(&mut look, elev, 12.0, &group, &manifest, &mut world);
look.notify_removed(elev);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let decision = decide_one(&mut look, elev, 4.0, &group, &manifest, &mut world);
assert_eq!(decision, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn look_down_direction_partitions_correctly() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 8.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[0], 70.0);
add_demand(&mut manifest, &mut world, stops[1], 70.0);
let mut look = LookDispatch::new();
let d1 = decide_one(&mut look, elev, 8.0, &group, &manifest, &mut world);
assert_eq!(d1, DispatchDecision::GoToStop(stops[1]));
let d2 = decide_one(&mut look, elev, 8.0, &group, &manifest, &mut world);
assert_eq!(d2, DispatchDecision::GoToStop(stops[1]));
}
#[test]
fn scan_down_direction_serves_below() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 8.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
add_demand(&mut manifest, &mut world, stops[3], 70.0);
let mut scan = ScanDispatch::new();
let d1 = decide_one(&mut scan, elev, 8.0, &group, &manifest, &mut world);
assert_eq!(d1, DispatchDecision::GoToStop(stops[3]));
manifest.waiting_at_stop.remove(&stops[3]);
let d2 = decide_one(&mut scan, elev, 12.0, &group, &manifest, &mut world);
assert_eq!(d2, DispatchDecision::GoToStop(stops[1]));
add_demand(&mut manifest, &mut world, stops[0], 70.0);
let d3 = decide_one(&mut scan, elev, 8.0, &group, &manifest, &mut world);
assert_eq!(d3, DispatchDecision::GoToStop(stops[1]));
}
#[test]
fn nearest_car_distance_calculation() {
let (mut world, stops) = test_world();
let elev_a = spawn_elevator(&mut world, 3.0);
let elev_b = spawn_elevator(&mut world, 5.0);
let group = test_group(&stops, vec![elev_a, elev_b]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
let mut nc = NearestCarDispatch::new();
let elevators = vec![(elev_a, 3.0), (elev_b, 5.0)];
let decisions = decide_all(&mut nc, &elevators, &group, &manifest, &mut world);
let a_dec = decisions.iter().find(|(e, _)| *e == elev_a).unwrap();
assert_eq!(a_dec.1, DispatchDecision::GoToStop(stops[1]));
}
struct AlwaysIdleDispatch;
impl DispatchStrategy for AlwaysIdleDispatch {
fn rank(
&mut self,
_car: crate::entity::EntityId,
_car_position: f64,
_stop: crate::entity::EntityId,
_stop_position: f64,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &World,
) -> Option<f64> {
None
}
}
#[test]
fn custom_dispatch_strategy() {
use crate::builder::SimulationBuilder;
use crate::stop::StopId;
let mut sim = SimulationBuilder::demo()
.dispatch(AlwaysIdleDispatch)
.build()
.unwrap();
sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
for _ in 0..100 {
sim.step();
}
let elevators: Vec<_> = sim.world().iter_elevators().collect();
assert!(!elevators.is_empty());
assert!(
(elevators[0].1.value - 0.0).abs() < f64::EPSILON,
"elevator should not have moved with AlwaysIdle dispatch"
);
}
#[test]
fn nearest_car_ignores_zero_demand() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[3], 70.0);
let mut nc = NearestCarDispatch::new();
let elevators = vec![(elev, 0.0)];
let decisions = decide_all(&mut nc, &elevators, &group, &manifest, &mut world);
let dec = decisions.iter().find(|(e, _)| *e == elev).unwrap();
assert_eq!(dec.1, DispatchDecision::GoToStop(stops[3]));
}
#[test]
fn assign_handles_large_group_without_overflow() {
let mut world = World::new();
let stop_count = 64;
let car_count = 32;
let stops: Vec<_> = (0..stop_count)
.map(|i| {
let eid = world.spawn();
world.set_stop(
eid,
Stop {
name: format!("S{i}"),
position: i as f64 * 4.0,
},
);
eid
})
.collect();
let cars: Vec<_> = (0..car_count)
.map(|i| spawn_elevator(&mut world, (i as f64) * 4.0))
.collect();
let group = test_group(&stops, cars.clone());
let mut manifest = DispatchManifest::default();
for &s in &stops {
add_demand(&mut manifest, &mut world, s, 70.0);
}
let positions: Vec<_> = cars
.iter()
.map(|&c| (c, world.position(c).unwrap().value))
.collect();
let mut nc = NearestCarDispatch::new();
let decisions = decide_all(&mut nc, &positions, &group, &manifest, &mut world);
let assigned_stops: HashSet<_> = decisions
.iter()
.filter_map(|(_, d)| match d {
DispatchDecision::GoToStop(s) => Some(*s),
DispatchDecision::Idle => None,
})
.collect();
assert_eq!(assigned_stops.len(), car_count);
}
#[test]
fn etd_pre_dispatch_caches_pending_positions() {
use crate::dispatch::DispatchStrategy;
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
etd.pre_dispatch(&group, &manifest, &mut world);
let first = dispatch::assign(&mut etd, &[(elev, 0.0)], &group, &manifest, &world).decisions;
assert!(matches!(first[0].1, DispatchDecision::GoToStop(_)));
let mut manifest2 = DispatchManifest::default();
add_demand(&mut manifest2, &mut world, stops[3], 80.0);
etd.pre_dispatch(&group, &manifest2, &mut world);
let second = dispatch::assign(&mut etd, &[(elev, 0.0)], &group, &manifest2, &world).decisions;
assert_eq!(second[0].1, DispatchDecision::GoToStop(stops[3]));
}
#[test]
fn strategy_rank_is_order_independent_when_state_lives_in_prepare_car() {
use crate::dispatch::DispatchStrategy;
use std::collections::HashMap;
#[derive(Default)]
struct IdleBoost {
idle: HashMap<crate::entity::EntityId, f64>,
tick: u64,
}
impl DispatchStrategy for IdleBoost {
fn pre_dispatch(&mut self, _g: &ElevatorGroup, _m: &DispatchManifest, _w: &mut World) {
self.tick = self.tick.saturating_add(1);
}
fn prepare_car(
&mut self,
car: crate::entity::EntityId,
_pos: f64,
_g: &ElevatorGroup,
_m: &DispatchManifest,
_w: &World,
) {
self.idle.insert(car, self.tick as f64);
}
fn rank(
&mut self,
car: crate::entity::EntityId,
car_pos: f64,
_s: crate::entity::EntityId,
stop_pos: f64,
_g: &ElevatorGroup,
_m: &DispatchManifest,
_w: &World,
) -> Option<f64> {
let boost = self.idle.get(&car).copied().unwrap_or(0.0);
Some(
0.001f64
.mul_add(-boost, (car_pos - stop_pos).abs())
.max(0.0),
)
}
}
let (mut world, stops) = test_world();
let elev_a = spawn_elevator(&mut world, 0.0);
let elev_b = spawn_elevator(&mut world, 12.0);
let group = test_group(&stops, vec![elev_a, elev_b]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut strat = IdleBoost::default();
let first = decide_all(
&mut strat,
&[(elev_a, 0.0), (elev_b, 12.0)],
&group,
&manifest,
&mut world,
);
let second = decide_all(
&mut strat,
&[(elev_b, 12.0), (elev_a, 0.0)],
&group,
&manifest,
&mut world,
);
let pair = |v: Vec<(crate::entity::EntityId, DispatchDecision)>| {
let mut map: HashMap<_, _> = v.into_iter().collect();
(map.remove(&elev_a).unwrap(), map.remove(&elev_b).unwrap())
};
assert_eq!(pair(first), pair(second));
}