use super::dispatch_tests::{spawn_elevator, test_group, test_world};
use crate::components::{ElevatorPhase, Route, Weight};
use crate::dispatch::etd::EtdDispatch;
use crate::dispatch::nearest_car::NearestCarDispatch;
use crate::dispatch::{self, DispatchDecision, DispatchManifest, DispatchStrategy, RiderInfo};
#[test]
fn etd_skips_pickup_whose_riders_are_all_direction_filtered() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 8.0);
{
let car = world.elevator_mut(elev).unwrap();
car.going_up = false;
car.going_down = true;
car.phase = ElevatorPhase::MovingToStop(stops[0]);
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[3], stops[0], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
let up_waiter = world.spawn();
manifest
.waiting_at_stop
.entry(stops[2])
.or_default()
.push(RiderInfo {
id: up_waiter,
destination: Some(stops[3]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
manifest
.riding_to_stop
.entry(stops[0])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[0]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
etd.pre_dispatch(&group, &manifest, &mut world);
let decisions = dispatch::assign(&mut etd, &[(elev, 8.0)], &group, &manifest, &world).decisions;
assert_eq!(
decisions[0].1,
DispatchDecision::GoToStop(stops[0]),
"car must continue to the aboard rider's destination, not self-assign to a \
waypoint whose only waiting rider is direction-filtered"
);
}
#[test]
fn nearest_car_skips_pickup_whose_riders_are_all_direction_filtered() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 8.0);
{
let car = world.elevator_mut(elev).unwrap();
car.going_up = false;
car.going_down = true;
car.phase = ElevatorPhase::MovingToStop(stops[0]);
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[3], stops[0], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
let up_waiter = world.spawn();
manifest
.waiting_at_stop
.entry(stops[2])
.or_default()
.push(RiderInfo {
id: up_waiter,
destination: Some(stops[3]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
manifest
.riding_to_stop
.entry(stops[0])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[0]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut nc = NearestCarDispatch::new();
nc.pre_dispatch(&group, &manifest, &mut world);
let decisions = dispatch::assign(&mut nc, &[(elev, 8.0)], &group, &manifest, &world).decisions;
assert_eq!(
decisions[0].1,
DispatchDecision::GoToStop(stops[0]),
"NearestCar must honor the direction filter too"
);
}
#[test]
fn pickup_of_matching_direction_rider_still_passes() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 8.0);
{
let car = world.elevator_mut(elev).unwrap();
car.going_up = false;
car.going_down = true;
car.phase = ElevatorPhase::MovingToStop(stops[0]);
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[3], stops[0], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
let down_waiter = world.spawn();
manifest
.waiting_at_stop
.entry(stops[2])
.or_default()
.push(RiderInfo {
id: down_waiter,
destination: Some(stops[1]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
manifest
.riding_to_stop
.entry(stops[0])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[0]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
etd.pre_dispatch(&group, &manifest, &mut world);
let decisions = dispatch::assign(&mut etd, &[(elev, 8.0)], &group, &manifest, &world).decisions;
assert!(
matches!(decisions[0].1, DispatchDecision::GoToStop(_)),
"a same-direction waiter must not be spuriously filtered; got {:?}",
decisions[0].1
);
}
#[test]
fn etd_skips_self_pair_when_only_demand_is_another_cars_riding() {
let (mut world, stops) = test_world();
let car_a = spawn_elevator(&mut world, 8.0);
let car_b = spawn_elevator(&mut world, 0.0);
{
let b = world.elevator_mut(car_b).unwrap();
b.going_up = true;
b.going_down = false;
b.phase = ElevatorPhase::MovingToStop(stops[2]);
}
let riding = world.spawn();
world.elevator_mut(car_b).unwrap().riders.push(riding);
world.set_route(
riding,
Route::direct(stops[0], stops[2], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![car_a, car_b]);
let mut manifest = DispatchManifest::default();
manifest
.riding_to_stop
.entry(stops[2])
.or_default()
.push(RiderInfo {
id: riding,
destination: Some(stops[2]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
etd.pre_dispatch(&group, &manifest, &mut world);
let decisions =
dispatch::assign(&mut etd, &[(car_a, 8.0)], &group, &manifest, &world).decisions;
assert_eq!(
decisions[0].1,
DispatchDecision::Idle,
"idle car must not self-assign to a stop whose only demand is \
another car's riding_to_stop — otherwise doors cycle at that \
stop while the other car delivers"
);
}
#[test]
fn etd_approves_self_pair_for_riderless_hall_call() {
use crate::components::{CallDirection, HallCall};
let (mut world, stops) = test_world();
let car = spawn_elevator(&mut world, 8.0);
let group = test_group(&stops, vec![car]);
let mut manifest = DispatchManifest::default();
let mut call = HallCall::new(stops[2], CallDirection::Up, 0);
call.pending_riders.clear();
manifest
.hall_calls_at_stop
.entry(stops[2])
.or_default()
.push(call);
let mut etd = EtdDispatch::new();
etd.pre_dispatch(&group, &manifest, &mut world);
let decisions = dispatch::assign(&mut etd, &[(car, 8.0)], &group, &manifest, &world).decisions;
assert_eq!(
decisions[0].1,
DispatchDecision::GoToStop(stops[2]),
"rider-less hall call is a valid reason to open doors — the \
fix must not filter it out along with cross-car riding demand"
);
}
#[test]
fn nearest_car_skips_self_pair_when_only_demand_is_another_cars_riding() {
let (mut world, stops) = test_world();
let car_a = spawn_elevator(&mut world, 8.0);
let car_b = spawn_elevator(&mut world, 0.0);
{
let b = world.elevator_mut(car_b).unwrap();
b.going_up = true;
b.going_down = false;
b.phase = ElevatorPhase::MovingToStop(stops[2]);
}
let riding = world.spawn();
world.elevator_mut(car_b).unwrap().riders.push(riding);
world.set_route(
riding,
Route::direct(stops[0], stops[2], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![car_a, car_b]);
let mut manifest = DispatchManifest::default();
manifest
.riding_to_stop
.entry(stops[2])
.or_default()
.push(RiderInfo {
id: riding,
destination: Some(stops[2]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut nc = NearestCarDispatch::new();
nc.pre_dispatch(&group, &manifest, &mut world);
let decisions = dispatch::assign(&mut nc, &[(car_a, 8.0)], &group, &manifest, &world).decisions;
assert_eq!(
decisions[0].1,
DispatchDecision::Idle,
"NearestCar must honor the cross-car stall fix — pair_is_useful is \
shared, so regressions here would surface in both strategies"
);
}