use crate::components::{Patience, Preferences, RiderPhase};
use crate::dispatch::scan::ScanDispatch;
use crate::events::Event;
use crate::sim::Simulation;
use crate::stop::StopId;
use crate::tests::helpers::default_config;
#[test]
fn patience_zero_abandons_immediately() {
let config = default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.world_mut().set_patience(
rider.entity(),
Patience {
max_wait_ticks: 0,
waited_ticks: 0,
},
);
sim.step();
let phase = sim.world().rider(rider.entity()).map(|r| r.phase);
assert_eq!(
phase,
Some(RiderPhase::Abandoned),
"rider with max_wait_ticks=0 should abandon immediately"
);
}
#[test]
fn patience_one_abandons_after_one_tick() {
let config = default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.world_mut().set_patience(
rider.entity(),
Patience {
max_wait_ticks: 1,
waited_ticks: 0,
},
);
sim.step();
let phase = sim.world().rider(rider.entity()).map(|r| r.phase);
assert_eq!(
phase,
Some(RiderPhase::Abandoned),
"rider with max_wait_ticks=1 should abandon after 1 tick"
);
}
#[test]
fn patience_zero_and_one_are_equivalent() {
for max_wait in [0, 1] {
let config = default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.world_mut().set_patience(
rider.entity(),
Patience {
max_wait_ticks: max_wait,
waited_ticks: 0,
},
);
sim.step();
let phase = sim.world().rider(rider.entity()).map(|r| r.phase);
assert_eq!(
phase,
Some(RiderPhase::Abandoned),
"rider with max_wait_ticks={max_wait} should abandon after 1 step"
);
}
}
#[test]
fn patience_two_survives_first_tick() {
let config = default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.world_mut().set_patience(
rider.entity(),
Patience {
max_wait_ticks: 2,
waited_ticks: 0,
},
);
sim.step();
let phase = sim.world().rider(rider.entity()).map(|r| r.phase);
assert_eq!(
phase,
Some(RiderPhase::Waiting),
"rider with max_wait_ticks=2 should survive the first tick"
);
sim.step();
let phase = sim.world().rider(rider.entity()).map(|r| r.phase);
assert_eq!(
phase,
Some(RiderPhase::Abandoned),
"rider with max_wait_ticks=2 should abandon after the second tick"
);
}
#[test]
fn patience_max_never_overflows() {
let config = default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let rider = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.world_mut().set_patience(
rider.entity(),
Patience {
max_wait_ticks: u64::MAX,
waited_ticks: u64::MAX - 1,
},
);
sim.step();
let phase = sim.world().rider(rider.entity()).map(|r| r.phase);
assert_eq!(phase, Some(RiderPhase::Abandoned));
}
#[test]
fn preferences_zero_crowding_rejects_any_load() {
let config = default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let r1 = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
let r2 = sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.world_mut().set_preferences(
r2.entity(),
Preferences {
skip_full_elevator: true,
max_crowding_factor: 0.0,
balk_threshold_ticks: None,
abandon_on_full: false,
},
);
for _ in 0..500 {
sim.step();
if let Some(r) = sim.world().rider(r1.entity())
&& matches!(r.phase, RiderPhase::Riding(_) | RiderPhase::Arrived)
{
break;
}
}
let events = sim.drain_events();
let preference_rejections = events.iter().any(|e| {
matches!(
e,
Event::RiderRejected {
rider,
reason,
..
} if *rider == r2.entity() && *reason == crate::error::RejectionReason::PreferenceBased
)
});
if sim
.world()
.rider(r1.entity())
.is_some_and(|r| matches!(r.phase, RiderPhase::Riding(_) | RiderPhase::Arrived))
{
assert!(
preference_rejections,
"rider with max_crowding_factor=0.0 should be rejected when elevator has any load"
);
}
}
#[test]
fn weight_exactly_at_capacity_after_partial_load_boards() {
let config = default_config(); let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let r1 = sim.spawn_rider(StopId(0), StopId(2), 500.0).unwrap();
let r2 = sim.spawn_rider(StopId(0), StopId(2), 300.0).unwrap();
let mut both_boarded = false;
for _ in 0..1000 {
sim.step();
let r1_riding = sim.world().rider(r1.entity()).is_some_and(|r| {
matches!(
r.phase,
RiderPhase::Boarding(_) | RiderPhase::Riding(_) | RiderPhase::Arrived
)
});
let r2_riding = sim.world().rider(r2.entity()).is_some_and(|r| {
matches!(
r.phase,
RiderPhase::Boarding(_) | RiderPhase::Riding(_) | RiderPhase::Arrived
)
});
if r1_riding && r2_riding {
both_boarded = true;
break;
}
}
assert!(
both_boarded,
"rider weighing exactly the remaining capacity (500+300=800) should board"
);
}
#[test]
fn weight_exceeds_capacity_by_epsilon_rejects() {
let config = default_config(); let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let r1 = sim.spawn_rider(StopId(0), StopId(2), 799.5).unwrap();
let r2 = sim.spawn_rider(StopId(0), StopId(2), 0.500001).unwrap();
for _ in 0..100 {
sim.step();
if sim
.world()
.rider(r1.entity())
.is_some_and(|r| matches!(r.phase, RiderPhase::Riding(_)))
{
break;
}
}
assert!(
sim.world()
.rider(r1.entity())
.is_some_and(|r| matches!(r.phase, RiderPhase::Riding(_))),
"r1 should be riding by now"
);
let rejected = sim
.drain_events()
.into_iter()
.any(|e| matches!(e, Event::RiderRejected { rider, .. } if rider == r2.entity()));
assert!(
rejected,
"r2 (0.500001 kg) should be rejected while remaining capacity is 0.5 kg"
);
}
#[test]
fn weight_exactly_at_capacity_boards() {
let config = default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let rider = sim.spawn_rider(StopId(0), StopId(2), 800.0).unwrap();
let mut boarded = false;
for _ in 0..500 {
sim.step();
if let Some(r) = sim.world().rider(rider.entity())
&& matches!(
r.phase,
RiderPhase::Boarding(_) | RiderPhase::Riding(_) | RiderPhase::Arrived
)
{
boarded = true;
break;
}
}
assert!(
boarded,
"rider weighing exactly capacity should be able to board"
);
}