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::{
DispatchDecision, DispatchManifest, DispatchStrategy, ElevatorGroup, RiderInfo,
};
use crate::door::DoorState;
use crate::ids::GroupId;
use crate::world::World;
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,
},
);
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 = scan.decide(elev, 0.0, &group, &manifest, &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 = scan.decide(elev, 0.0, &group, &manifest, &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 = scan.decide(elev, 8.0, &group, &manifest, &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 = scan.decide(elev, 0.0, &group, &manifest, &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 = scan.decide(elev, 4.0, &group, &manifest, &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 = look.decide(elev, 0.0, &group, &manifest, &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 = look.decide(elev, 0.0, &group, &manifest, &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 = nc.decide_all(&elevators, &group, &manifest, &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 = nc.decide_all(&elevators, &group, &manifest, &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 = etd.decide_all(&elevators, &group, &manifest, &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 = etd.decide_all(&elevators, &group, &manifest, &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 = scan.decide(elev, 4.0, &group, &manifest, &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 = scan.decide(elev, 12.0, &group, &manifest, &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 = scan.decide(elev, 12.0, &group, &manifest, &world);
assert_eq!(d1, DispatchDecision::GoToStop(stops[1]));
scan.notify_removed(elev);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let d2 = scan.decide(elev, 4.0, &group, &manifest, &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();
look.decide(elev, 12.0, &group, &manifest, &world);
look.notify_removed(elev);
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let decision = look.decide(elev, 4.0, &group, &manifest, &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 = look.decide(elev, 8.0, &group, &manifest, &world);
assert_eq!(d1, DispatchDecision::GoToStop(stops[1]));
let d2 = look.decide(elev, 8.0, &group, &manifest, &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 = scan.decide(elev, 8.0, &group, &manifest, &world);
assert_eq!(d1, DispatchDecision::GoToStop(stops[3]));
manifest.waiting_at_stop.remove(&stops[3]);
let d2 = scan.decide(elev, 12.0, &group, &manifest, &world);
assert_eq!(d2, DispatchDecision::GoToStop(stops[1]));
add_demand(&mut manifest, &mut world, stops[0], 70.0);
let d3 = scan.decide(elev, 8.0, &group, &manifest, &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 = nc.decide_all(&elevators, &group, &manifest, &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 decide(
&mut self,
_elevator: crate::entity::EntityId,
_elevator_position: f64,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &World,
) -> DispatchDecision {
DispatchDecision::Idle
}
}
#[test]
fn custom_dispatch_strategy() {
use crate::builder::SimulationBuilder;
use crate::stop::StopId;
let mut sim = SimulationBuilder::new()
.dispatch(AlwaysIdleDispatch)
.build()
.unwrap();
sim.spawn_rider_by_stop_id(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 = nc.decide_all(&elevators, &group, &manifest, &world);
let dec = decisions.iter().find(|(e, _)| *e == elev).unwrap();
assert_eq!(dec.1, DispatchDecision::GoToStop(stops[3]));
}