use crate::components::{Orientation, Rider, RiderPhase};
use crate::config::{
BuildingConfig, ElevatorConfig, GroupConfig, LineConfig, PassengerSpawnConfig, SimConfig,
SimulationParams,
};
use crate::dispatch::destination::{ASSIGNED_CAR_EXT_NAME, AssignedCar, DestinationDispatch};
use crate::sim::Simulation;
use crate::stop::{StopConfig, StopId};
fn single_car_config() -> SimConfig {
SimConfig {
building: BuildingConfig {
name: "DCS Test".into(),
stops: vec![
StopConfig {
id: StopId(0),
name: "G".into(),
position: 0.0,
},
StopConfig {
id: StopId(1),
name: "F2".into(),
position: 4.0,
},
StopConfig {
id: StopId(2),
name: "F3".into(),
position: 8.0,
},
],
lines: None,
groups: None,
},
elevators: vec![ElevatorConfig {
id: 0,
name: "Solo".into(),
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
starting_stop: StopId(0),
door_open_ticks: 10,
door_transition_ticks: 5,
restricted_stops: Vec::new(),
#[cfg(feature = "energy")]
energy_profile: None,
service_mode: None,
inspection_speed_factor: 0.25,
}],
simulation: SimulationParams {
ticks_per_second: 60.0,
},
passenger_spawning: PassengerSpawnConfig {
mean_interval_ticks: 120,
weight_range: (50.0, 100.0),
},
}
}
fn two_cars_same_group_config() -> SimConfig {
SimConfig {
building: BuildingConfig {
name: "DCS Two Car".into(),
stops: vec![
StopConfig {
id: StopId(0),
name: "G".into(),
position: 0.0,
},
StopConfig {
id: StopId(1),
name: "F2".into(),
position: 4.0,
},
StopConfig {
id: StopId(2),
name: "F3".into(),
position: 8.0,
},
StopConfig {
id: StopId(3),
name: "F4".into(),
position: 12.0,
},
],
lines: Some(vec![LineConfig {
id: 1,
name: "Main".into(),
serves: vec![StopId(0), StopId(1), StopId(2), StopId(3)],
elevators: vec![
ElevatorConfig {
id: 1,
name: "A".into(),
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
starting_stop: StopId(0),
door_open_ticks: 10,
door_transition_ticks: 5,
restricted_stops: Vec::new(),
#[cfg(feature = "energy")]
energy_profile: None,
service_mode: None,
inspection_speed_factor: 0.25,
},
ElevatorConfig {
id: 2,
name: "B".into(),
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
starting_stop: StopId(3),
door_open_ticks: 10,
door_transition_ticks: 5,
restricted_stops: Vec::new(),
#[cfg(feature = "energy")]
energy_profile: None,
service_mode: None,
inspection_speed_factor: 0.25,
},
],
orientation: Orientation::Vertical,
position: None,
min_position: None,
max_position: None,
max_cars: None,
}]),
groups: Some(vec![GroupConfig {
id: 0,
name: "Main".into(),
lines: vec![1],
dispatch: crate::dispatch::BuiltinStrategy::Destination,
reposition: None,
}]),
},
elevators: vec![],
simulation: SimulationParams {
ticks_per_second: 60.0,
},
passenger_spawning: PassengerSpawnConfig {
mean_interval_ticks: 120,
weight_range: (50.0, 100.0),
},
}
}
#[test]
fn sticky_assignment_persists_across_ticks() {
let mut sim = Simulation::new(&single_car_config(), DestinationDispatch::new()).unwrap();
sim.world_mut()
.register_ext::<AssignedCar>(ASSIGNED_CAR_EXT_NAME);
let rid = sim
.spawn_rider_by_stop_id(StopId(0), StopId(2), 75.0)
.unwrap();
sim.step();
let first = sim.world().get_ext::<AssignedCar>(rid);
assert!(first.is_some(), "rider should be assigned after first tick");
for _ in 0..500 {
sim.step();
if sim
.world()
.rider(rid)
.is_some_and(|r| r.phase() == RiderPhase::Arrived)
{
break;
}
let cur = sim.world().get_ext::<AssignedCar>(rid);
assert_eq!(cur, first, "assignment must be sticky");
}
}
#[test]
fn loading_respects_assignment_other_car_skips() {
let mut sim = Simulation::new(
&two_cars_same_group_config(),
DestinationDispatch::new(),
)
.unwrap();
sim.world_mut()
.register_ext::<AssignedCar>(ASSIGNED_CAR_EXT_NAME);
let elevs: Vec<_> = sim
.world()
.iter_elevators()
.map(|(eid, _, _)| eid)
.collect();
assert_eq!(elevs.len(), 2);
let car_a = elevs
.iter()
.copied()
.find(|&e| {
sim.world()
.position(e)
.is_some_and(|p| p.value.abs() < 1e-9)
})
.unwrap();
let car_b = elevs.iter().copied().find(|e| *e != car_a).unwrap();
let rid = sim
.spawn_rider_by_stop_id(StopId(1), StopId(2), 75.0)
.unwrap();
sim.world_mut()
.insert_ext(rid, AssignedCar(car_b), ASSIGNED_CAR_EXT_NAME);
let f2 = sim.stop_entity(StopId(1)).unwrap();
let f3 = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(car_b, f2).unwrap();
sim.push_destination(car_b, f3).unwrap();
for _ in 0..2000 {
sim.step();
if sim
.world()
.rider(rid)
.is_some_and(|r| r.phase() == RiderPhase::Arrived)
{
break;
}
if let Some(rider) = sim.world().rider(rid) {
match rider.phase() {
RiderPhase::Boarding(e) | RiderPhase::Riding(e) | RiderPhase::Exiting(e) => {
assert_eq!(e, car_b, "rider must only board its assigned car");
}
_ => {}
}
}
}
assert!(
sim.world()
.rider(rid)
.is_some_and(|r| r.phase() == RiderPhase::Arrived),
"rider should eventually arrive via assigned car"
);
}
#[test]
fn unassigned_manual_board_riders_still_work() {
let mut sim = Simulation::new(&single_car_config(), DestinationDispatch::new()).unwrap();
sim.world_mut()
.register_ext::<AssignedCar>(ASSIGNED_CAR_EXT_NAME);
let routed = sim
.spawn_rider_by_stop_id(StopId(0), StopId(2), 75.0)
.unwrap();
sim.step();
assert!(
sim.world().get_ext::<AssignedCar>(routed).is_some(),
"routed rider should be assigned"
);
for _ in 0..2000 {
sim.step();
if sim
.world()
.rider(routed)
.is_some_and(|r| r.phase() == RiderPhase::Arrived)
{
break;
}
}
assert!(
sim.world()
.rider(routed)
.is_some_and(|r| r.phase() == RiderPhase::Arrived)
);
}
#[test]
fn closer_car_is_preferred_when_matching_direction() {
let mut sim =
Simulation::new(&two_cars_same_group_config(), DestinationDispatch::new()).unwrap();
sim.world_mut()
.register_ext::<AssignedCar>(ASSIGNED_CAR_EXT_NAME);
let elevs: Vec<_> = sim
.world()
.iter_elevators()
.map(|(eid, _, _)| eid)
.collect();
let car_a = elevs
.iter()
.copied()
.find(|&e| sim.world().position(e).map_or(0.0, |p| p.value) < 1.0)
.unwrap();
let rid = sim
.spawn_rider_by_stop_id(StopId(1), StopId(2), 75.0)
.unwrap();
sim.step();
let assigned = sim
.world()
.get_ext::<AssignedCar>(rid)
.expect("rider should be assigned");
assert_eq!(assigned.0, car_a, "closer car should be preferred");
}
#[test]
fn up_peak_scenario_delivers_all_riders() {
let mut sim =
Simulation::new(&two_cars_same_group_config(), DestinationDispatch::new()).unwrap();
sim.world_mut()
.register_ext::<AssignedCar>(ASSIGNED_CAR_EXT_NAME);
let mut riders = Vec::new();
for i in 0..20 {
let dest = StopId(1 + (i % 3));
let rid = sim
.spawn_rider_by_stop_id(StopId(0), dest, 75.0)
.expect("spawn");
riders.push(rid);
}
for _ in 0..20_000 {
sim.step();
let done = riders.iter().all(|&rid| {
sim.world()
.rider(rid)
.is_some_and(|r| r.phase() == RiderPhase::Arrived)
});
if done {
break;
}
}
for &rid in &riders {
let phase = sim.world().rider(rid).map(Rider::phase);
assert_eq!(
phase,
Some(RiderPhase::Arrived),
"rider {rid:?} not delivered"
);
}
assert_eq!(sim.metrics().total_delivered(), 20);
}