use super::Simulation;
use crate::components::RiderPhaseKind;
use crate::components::rider_state::InternalRiderPhase;
use crate::entity::EntityId;
use crate::error::SimError;
use crate::rider_index::RiderIndex;
use crate::world::World;
impl Simulation {
pub(crate) fn transition_rider(
&mut self,
id: EntityId,
new_state: InternalRiderPhase,
) -> Result<(), SimError> {
transition_rider(
&mut self.world,
&mut self.rider_index,
self.tick,
id,
new_state,
)
}
}
pub(crate) fn transition_rider(
world: &mut World,
rider_index: &mut RiderIndex,
tick: u64,
id: EntityId,
new_state: InternalRiderPhase,
) -> Result<(), SimError> {
let r = world.rider_mut(id).ok_or(SimError::EntityNotFound(id))?;
let old_phase = r.phase;
let old_stop = r.current_stop;
let was_aboard = old_phase.is_aboard();
let from_kind = old_phase.kind();
let to_kind = new_state.kind();
if !is_legal_transition(from_kind, to_kind) {
return Err(SimError::IllegalTransition {
rider: id,
from: from_kind,
to: to_kind,
});
}
let now_aboard = matches!(
to_kind,
RiderPhaseKind::Boarding | RiderPhaseKind::Riding | RiderPhaseKind::Exiting
);
r.phase = new_state.as_phase();
r.current_stop = new_state.at_stop();
if !was_aboard && now_aboard {
r.board_tick = Some(tick);
} else if was_aboard && !now_aboard {
r.board_tick = None;
}
let old_bucket = indexed_bucket(from_kind).zip(old_stop);
let new_bucket = indexed_bucket(to_kind).zip(new_state.at_stop());
if old_bucket != new_bucket {
if let Some((bucket, stop)) = old_bucket {
bucket.remove_from(rider_index, stop, id);
}
if let Some((bucket, stop)) = new_bucket {
bucket.insert_into(rider_index, stop, id);
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IndexBucket {
Waiting,
Resident,
Abandoned,
}
impl IndexBucket {
fn insert_into(self, idx: &mut RiderIndex, stop: EntityId, rider: EntityId) {
match self {
Self::Waiting => idx.insert_waiting(stop, rider),
Self::Resident => idx.insert_resident(stop, rider),
Self::Abandoned => idx.insert_abandoned(stop, rider),
}
}
fn remove_from(self, idx: &mut RiderIndex, stop: EntityId, rider: EntityId) {
match self {
Self::Waiting => idx.remove_waiting(stop, rider),
Self::Resident => idx.remove_resident(stop, rider),
Self::Abandoned => idx.remove_abandoned(stop, rider),
}
}
}
const fn indexed_bucket(kind: RiderPhaseKind) -> Option<IndexBucket> {
match kind {
RiderPhaseKind::Waiting => Some(IndexBucket::Waiting),
RiderPhaseKind::Resident => Some(IndexBucket::Resident),
RiderPhaseKind::Abandoned => Some(IndexBucket::Abandoned),
_ => None,
}
}
const fn is_legal_transition(from: RiderPhaseKind, to: RiderPhaseKind) -> bool {
use RiderPhaseKind::{
Abandoned, Arrived, Boarding, Exiting, Resident, Riding, Waiting, Walking,
};
!matches!(
(from, to),
(Resident | Arrived | Abandoned | Walking, Boarding)
| (Resident | Arrived | Abandoned | Walking | Waiting, Riding)
| (
Resident | Arrived | Abandoned | Walking | Waiting | Boarding,
Exiting
)
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::RiderPhaseKind::*;
#[test]
fn legality_rejects_resident_to_aboard() {
assert!(!is_legal_transition(Resident, Boarding));
assert!(!is_legal_transition(Resident, Riding));
assert!(!is_legal_transition(Resident, Exiting));
}
#[test]
fn legality_rejects_terminal_to_aboard() {
for from in [Arrived, Abandoned] {
for to in [Boarding, Riding, Exiting] {
assert!(
!is_legal_transition(from, to),
"{from:?} -> {to:?} should be illegal"
);
}
}
}
#[test]
fn legality_rejects_walking_to_aboard() {
assert!(!is_legal_transition(Walking, Boarding));
assert!(!is_legal_transition(Walking, Riding));
assert!(!is_legal_transition(Walking, Exiting));
}
#[test]
fn legality_rejects_skipping_boarding_or_riding() {
assert!(!is_legal_transition(Waiting, Riding));
assert!(!is_legal_transition(Waiting, Exiting));
assert!(!is_legal_transition(Boarding, Exiting));
}
#[test]
fn legality_allows_rescue_back_to_waiting() {
assert!(is_legal_transition(Boarding, Waiting));
assert!(is_legal_transition(Riding, Waiting));
assert!(is_legal_transition(Exiting, Waiting));
}
#[test]
fn legality_allows_settle_paths() {
assert!(is_legal_transition(Arrived, Resident));
assert!(is_legal_transition(Abandoned, Resident));
assert!(is_legal_transition(Resident, Waiting));
}
#[test]
fn legality_allows_forward_progress() {
assert!(is_legal_transition(Waiting, Boarding));
assert!(is_legal_transition(Boarding, Riding));
assert!(is_legal_transition(Riding, Exiting));
assert!(is_legal_transition(Exiting, Arrived));
}
#[test]
fn legality_allows_patience_abandonment() {
assert!(is_legal_transition(Waiting, Abandoned));
}
#[test]
fn indexed_bucket_covers_only_three_phases() {
assert!(indexed_bucket(Waiting).is_some());
assert!(indexed_bucket(Resident).is_some());
assert!(indexed_bucket(Abandoned).is_some());
assert!(indexed_bucket(Boarding).is_none());
assert!(indexed_bucket(Riding).is_none());
assert!(indexed_bucket(Exiting).is_none());
assert!(indexed_bucket(Arrived).is_none());
assert!(indexed_bucket(Walking).is_none());
}
}