use super::dispatch_tests::{
add_demand, decide_all, decide_one, spawn_elevator, test_group, test_world,
};
use crate::components::{ElevatorPhase, Route, Speed, Weight};
use crate::dispatch::etd::EtdDispatch;
use crate::dispatch::{DispatchDecision, DispatchManifest, RiderInfo};
#[test]
fn etd_picks_faster_car_at_equal_distance() {
let (mut world, stops) = test_world();
let elev_slow = spawn_elevator(&mut world, 0.0);
let elev_fast = spawn_elevator(&mut world, 16.0);
world.elevator_mut(elev_fast).unwrap().max_speed = Speed::from(4.0);
let group = test_group(&stops, vec![elev_slow, elev_fast]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
let decisions = decide_all(
&mut etd,
&[(elev_slow, 0.0), (elev_fast, 16.0)],
&group,
&manifest,
&mut world,
);
let fast_dec = decisions.iter().find(|(e, _)| *e == elev_fast).unwrap();
assert_eq!(
fast_dec.1,
DispatchDecision::GoToStop(stops[2]),
"faster car should win at equal distance under correct travel-time formula"
);
}
#[test]
fn etd_zero_max_speed_returns_infinity_cost() {
let (mut world, stops) = test_world();
let elev_normal = spawn_elevator(&mut world, 0.0);
let elev_stuck = spawn_elevator(&mut world, 0.0);
world.elevator_mut(elev_stuck).unwrap().max_speed = Speed::from(0.0);
let group = test_group(&stops, vec![elev_normal, elev_stuck]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
let decisions = decide_all(
&mut etd,
&[(elev_normal, 0.0), (elev_stuck, 0.0)],
&group,
&manifest,
&mut world,
);
let normal_dec = decisions.iter().find(|(e, _)| *e == elev_normal).unwrap();
assert_eq!(
normal_dec.1,
DispatchDecision::GoToStop(stops[2]),
"stuck car (zero max_speed) must not be assigned"
);
}
#[test]
fn etd_intervening_pending_stop_adds_door_cost() {
let (mut world, stops) = test_world();
let elev_clear = spawn_elevator(&mut world, 0.0);
let elev_through = spawn_elevator(&mut world, 16.0);
let group = test_group(&stops, vec![elev_clear, elev_through]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
add_demand(&mut manifest, &mut world, stops[3], 70.0);
let mut etd = EtdDispatch::with_weights(1.0, 1.0, 100.0);
let decisions = decide_all(
&mut etd,
&[(elev_clear, 0.0), (elev_through, 16.0)],
&group,
&manifest,
&mut world,
);
let clear_dec = decisions.iter().find(|(e, _)| *e == elev_clear).unwrap();
assert_eq!(
clear_dec.1,
DispatchDecision::GoToStop(stops[2]),
"clear-route car should win when through-route adds door overhead"
);
}
#[test]
fn etd_door_cost_scales_with_door_ticks() {
let (mut world, stops) = test_world();
let elev_quick_doors = spawn_elevator(&mut world, 16.0);
let elev_slow_doors = spawn_elevator(&mut world, 16.0);
{
let car = world.elevator_mut(elev_quick_doors).unwrap();
car.door_transition_ticks = 0;
car.door_open_ticks = 1;
}
{
let car = world.elevator_mut(elev_slow_doors).unwrap();
car.door_transition_ticks = 100;
car.door_open_ticks = 100;
}
let group = test_group(&stops, vec![elev_quick_doors, elev_slow_doors]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
add_demand(&mut manifest, &mut world, stops[3], 70.0);
let mut etd = EtdDispatch::with_weights(1.0, 1.0, 1.0);
let decisions = decide_all(
&mut etd,
&[(elev_quick_doors, 16.0), (elev_slow_doors, 16.0)],
&group,
&manifest,
&mut world,
);
let quick = decisions
.iter()
.find(|(e, _)| *e == elev_quick_doors)
.unwrap();
assert_eq!(
quick.1,
DispatchDecision::GoToStop(stops[2]),
"quick-door car should win when both share intervening pending stops"
);
}
#[test]
fn etd_detour_for_existing_rider_costs_more() {
let (mut world, stops) = test_world();
let elev_no_riders = spawn_elevator(&mut world, 0.0);
let elev_with_rider = spawn_elevator(&mut world, 0.0);
let rider = world.spawn();
world
.elevator_mut(elev_with_rider)
.unwrap()
.riders
.push(rider);
world.set_route(
rider,
Route::direct(stops[0], stops[3], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev_no_riders, elev_with_rider]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
let mut etd = EtdDispatch::with_weights(1.0, 100.0, 0.5);
let decisions = decide_all(
&mut etd,
&[(elev_no_riders, 0.0), (elev_with_rider, 0.0)],
&group,
&manifest,
&mut world,
);
let no_riders_dec = decisions
.iter()
.find(|(e, _)| *e == elev_no_riders)
.unwrap();
assert_eq!(
no_riders_dec.1,
DispatchDecision::GoToStop(stops[1]),
"rider-free car should win when alternative imposes detour"
);
}
#[test]
fn etd_prefers_car_already_moving_toward_target() {
let (mut world, stops) = test_world();
let elev_idle = spawn_elevator(&mut world, 0.0);
let elev_moving = spawn_elevator(&mut world, 0.0);
world.elevator_mut(elev_moving).unwrap().phase = ElevatorPhase::MovingToStop(stops[3]);
let group = test_group(&stops, vec![elev_idle, elev_moving]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
let decisions = decide_all(
&mut etd,
&[(elev_idle, 0.0), (elev_moving, 0.0)],
&group,
&manifest,
&mut world,
);
let moving_dec = decisions.iter().find(|(e, _)| *e == elev_moving).unwrap();
assert_eq!(
moving_dec.1,
DispatchDecision::GoToStop(stops[2]),
"car already heading toward target should win the direction bonus"
);
}
#[test]
fn etd_idle_phase_gets_modest_bonus_over_repositioning() {
let (mut world, stops) = test_world();
let elev_idle = spawn_elevator(&mut world, 0.0);
let elev_repositioning = spawn_elevator(&mut world, 0.0);
world.elevator_mut(elev_repositioning).unwrap().phase = ElevatorPhase::Loading;
let group = test_group(&stops, vec![elev_idle, elev_repositioning]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
let decisions = decide_all(
&mut etd,
&[(elev_idle, 0.0), (elev_repositioning, 0.0)],
&group,
&manifest,
&mut world,
);
let idle_dec = decisions.iter().find(|(e, _)| *e == elev_idle).unwrap();
assert_eq!(
idle_dec.1,
DispatchDecision::GoToStop(stops[2]),
"idle car should beat a non-idle non-moving car (gets the -0.3·travel_time bonus)"
);
}
#[test]
fn etd_idle_short_trip_does_not_break_assignment() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 7.5);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
let mut etd = EtdDispatch::new();
let decision = decide_one(&mut etd, elev, 7.5, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[2]),
"lone idle elevator with negative raw cost (post-clamp 0) must still be assigned"
);
}
#[test]
fn etd_full_car_at_pickup_stop_prefers_rider_destination() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 4.0); {
let car = world.elevator_mut(elev).unwrap();
car.current_load = car.weight_capacity;
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[0], stops[3], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
manifest
.riding_to_stop
.entry(stops[3])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[3]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
let decision = decide_one(&mut etd, elev, 4.0, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[3]),
"full car at a pickup-only stop must be routed to its rider's destination \
instead of self-assigning to the un-serveable stop"
);
}
#[test]
fn etd_full_car_skips_unreachable_pickup_on_the_way() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
{
let car = world.elevator_mut(elev).unwrap();
car.current_load = car.weight_capacity;
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[0], stops[3], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
manifest
.riding_to_stop
.entry(stops[3])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[3]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
let decision = decide_one(&mut etd, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[3]),
"a full car en route must skip pickup stops it can't serve and \
head straight to the aboard rider's destination"
);
}
#[test]
fn etd_squared_wait_prefers_older_waiting_rider() {
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();
let old_waiter = world.spawn();
manifest
.waiting_at_stop
.entry(stops[0])
.or_default()
.push(RiderInfo {
id: old_waiter,
destination: None,
weight: Weight::from(70.0),
wait_ticks: 1000,
});
let new_waiter = world.spawn();
manifest
.waiting_at_stop
.entry(stops[2])
.or_default()
.push(RiderInfo {
id: new_waiter,
destination: None,
weight: Weight::from(70.0),
wait_ticks: 1,
});
let mut etd = EtdDispatch::new().with_wait_squared_weight(1.0);
let decision = decide_one(&mut etd, elev, 4.0, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[0]),
"positive `wait_squared_weight` must bias ETD toward the stop with an older waiter"
);
}
#[test]
fn etd_squared_wait_does_not_override_travel_time() {
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();
let new_waiter = world.spawn();
manifest
.waiting_at_stop
.entry(stops[1])
.or_default()
.push(RiderInfo {
id: new_waiter,
destination: None,
weight: Weight::from(70.0),
wait_ticks: 5,
});
let older = world.spawn();
manifest
.waiting_at_stop
.entry(stops[3])
.or_default()
.push(RiderInfo {
id: older,
destination: None,
weight: Weight::from(70.0),
wait_ticks: 20,
});
let mut etd = EtdDispatch::new().with_wait_squared_weight(0.001);
let decision = decide_one(&mut etd, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[1]),
"modest wait_squared_weight must not reverse travel-time dominance"
);
}