use crate::components::Weight;
use crate::dispatch::DispatchStrategy;
use crate::dispatch::etd::EtdDispatch;
use crate::dispatch::look::LookDispatch;
use crate::dispatch::nearest_car::NearestCarDispatch;
use crate::dispatch::scan::ScanDispatch;
use crate::scenario::{Condition, Scenario, ScenarioRunner, SpawnSchedule};
use crate::stop::StopId;
use super::helpers::{assert_p95_wait_under, multi_floor_config};
type StrategyFactory = Box<dyn Fn() -> Box<dyn DispatchStrategy>>;
#[test]
fn up_peak_8_floor_2_car_delivers_within_budget() {
let config = multi_floor_config(8, 2);
let stops = (0..8).map(StopId).collect::<Vec<_>>();
let mut schedule = SpawnSchedule::new();
for i in 0..20u64 {
let dest = stops[1 + (i as usize % 7)];
schedule = schedule.staggered(stops[0], dest, 1, i * 60, 60, 70.0);
}
let scenario = Scenario {
name: "up-peak 8-floor 2-car".into(),
config,
spawns: schedule.into_spawns(),
conditions: vec![
Condition::AllDeliveredByTick(12_000),
Condition::MaxWaitBelow(6_000),
],
max_ticks: 15_000,
};
let mut runner = ScenarioRunner::new(scenario, EtdDispatch::new()).unwrap();
let result = runner.run_to_completion();
assert!(
result.passed,
"up-peak must satisfy timeout + max-wait: {:#?}",
result.conditions
);
assert_eq!(runner.skipped_spawns(), 0);
}
#[test]
fn down_peak_8_floor_2_car_delivers_within_budget() {
let config = multi_floor_config(8, 2);
let stops = (0..8).map(StopId).collect::<Vec<_>>();
let mut schedule = SpawnSchedule::new();
for i in 0..20u64 {
let origin = stops[1 + (i as usize % 7)];
schedule = schedule.staggered(origin, stops[0], 1, i * 60, 60, 70.0);
}
let scenario = Scenario {
name: "down-peak 8-floor 2-car".into(),
config,
spawns: schedule.into_spawns(),
conditions: vec![
Condition::AllDeliveredByTick(12_000),
Condition::MaxWaitBelow(6_000),
],
max_ticks: 15_000,
};
let mut runner = ScenarioRunner::new(scenario, EtdDispatch::new()).unwrap();
let result = runner.run_to_completion();
assert!(
result.passed,
"down-peak must satisfy timeout + max-wait: {:#?}",
result.conditions
);
assert_eq!(runner.skipped_spawns(), 0);
}
#[test]
fn full_load_cycle_delivers_all_in_bounded_trips() {
let mut config = multi_floor_config(6, 1);
config.elevators[0].weight_capacity = Weight::from(400.0);
let stops = (0..6).map(StopId).collect::<Vec<_>>();
let schedule = SpawnSchedule::new().burst(stops[0], stops[5], 20, 0, 70.0);
let scenario = Scenario {
name: "full-load cycle".into(),
config,
spawns: schedule.into_spawns(),
conditions: vec![
Condition::AllDeliveredByTick(20_000),
Condition::AbandonmentRateBelow(0.01),
],
max_ticks: 25_000,
};
let mut runner = ScenarioRunner::new(scenario, ScanDispatch::new()).unwrap();
let result = runner.run_to_completion();
assert!(
result.passed,
"full-load cycle must deliver all 20 riders without abandonment: {:#?}",
result.conditions
);
assert_eq!(
result.metrics.total_delivered(),
20,
"exactly 20 riders must reach their destination"
);
}
#[test]
fn burst_then_silence_handles_latecomer() {
let config = multi_floor_config(5, 2);
let stops = (0..5).map(StopId).collect::<Vec<_>>();
let schedule = SpawnSchedule::new()
.staggered(stops[0], stops[3], 15, 0, 1, 70.0) .push(crate::scenario::TimedSpawn {
tick: 5_000,
origin: stops[4],
destination: stops[1],
weight: 70.0,
});
let scenario = Scenario {
name: "burst then silence".into(),
config,
spawns: schedule.into_spawns(),
conditions: vec![Condition::AllDeliveredByTick(10_000)],
max_ticks: 12_000,
};
let mut runner = ScenarioRunner::new(scenario, EtdDispatch::new()).unwrap();
let result = runner.run_to_completion();
assert!(
result.passed,
"burst-then-silence must deliver all 16 riders: {:#?}",
result.conditions
);
assert_eq!(result.metrics.total_delivered(), 16);
assert_p95_wait_under(runner.sim(), 3_000);
}
#[test]
fn strategy_matrix_all_builtins_deliver_up_peak() {
let stops = (0..6).map(StopId).collect::<Vec<_>>();
let make_schedule = || {
let mut s = SpawnSchedule::new();
for i in 0..12u64 {
let dest = stops[1 + (i as usize % 5)];
s = s.staggered(stops[0], dest, 1, i * 80, 80, 70.0);
}
s
};
let strategies: Vec<(&str, StrategyFactory)> = vec![
("Scan", Box::new(|| Box::new(ScanDispatch::new()))),
("Look", Box::new(|| Box::new(LookDispatch::new()))),
(
"NearestCar",
Box::new(|| Box::new(NearestCarDispatch::new())),
),
("Etd", Box::new(|| Box::new(EtdDispatch::new()))),
];
for (name, factory) in strategies {
let scenario = Scenario {
name: format!("up-peak via {name}"),
config: multi_floor_config(6, 2),
spawns: make_schedule().into_spawns(),
conditions: vec![Condition::AllDeliveredByTick(15_000)],
max_ticks: 20_000,
};
let mut runner = ScenarioRunner::new(scenario, BoxedStrategy(factory())).unwrap();
let result = runner.run_to_completion();
assert!(
result.passed,
"strategy {name} must deliver all 12 riders: {:#?}",
result.conditions
);
assert_eq!(
result.metrics.total_delivered(),
12,
"{name} must deliver exactly 12 riders"
);
}
}
struct BoxedStrategy(Box<dyn DispatchStrategy>);
impl DispatchStrategy for BoxedStrategy {
fn pre_dispatch(
&mut self,
group: &crate::dispatch::ElevatorGroup,
manifest: &crate::dispatch::DispatchManifest,
world: &mut crate::world::World,
) {
self.0.pre_dispatch(group, manifest, world);
}
fn prepare_car(
&mut self,
car: crate::entity::EntityId,
pos: f64,
group: &crate::dispatch::ElevatorGroup,
manifest: &crate::dispatch::DispatchManifest,
world: &crate::world::World,
) {
self.0.prepare_car(car, pos, group, manifest, world);
}
fn rank(&mut self, ctx: &crate::dispatch::RankContext<'_>) -> Option<f64> {
self.0.rank(ctx)
}
fn fallback(
&mut self,
car: crate::entity::EntityId,
pos: f64,
group: &crate::dispatch::ElevatorGroup,
manifest: &crate::dispatch::DispatchManifest,
world: &crate::world::World,
) -> crate::dispatch::DispatchDecision {
self.0.fallback(car, pos, group, manifest, world)
}
fn notify_removed(&mut self, elevator: crate::entity::EntityId) {
self.0.notify_removed(elevator);
}
}