Skip to main content

rustsim_mobility/
mode_state.rs

1//! Mode-transition state machine for travellers.
2//!
3//! A traveller progresses through their [`TripPlan`] one [`Leg`] at a
4//! time. This crate tracks the current phase deterministically so the
5//! domain crates (crowd, vehicle, transit) only have to react to state
6//! changes, not compute them.
7
8use crate::leg::{Leg, TripPlan};
9use rustsim_modes::TravelMode;
10use rustsim_transit::{RouteId, StopId, VehicleId};
11
12/// Per-traveller context held by the caller.
13#[derive(Debug, Clone)]
14pub struct TravellerContext {
15    /// Traveller (agent) identifier.
16    pub traveller_id: u64,
17    /// Index into the current trip's `legs` list.
18    pub current_leg: usize,
19    /// Current mode state.
20    pub state: ModeState,
21}
22
23impl TravellerContext {
24    /// Start a new traveller at leg 0 in [`ModeState::Idle`].
25    pub fn new(traveller_id: u64) -> Self {
26        Self {
27            traveller_id,
28            current_leg: 0,
29            state: ModeState::Idle,
30        }
31    }
32}
33
34/// High-level mode state.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ModeState {
37    /// No current activity.
38    Idle,
39    /// Walking on foot.
40    Walking,
41    /// Driving a self-operated vehicle of the given mode.
42    Driving(TravelMode),
43    /// Waiting for a transit vehicle at a stop.
44    WaitingAt(StopId, RouteId),
45    /// Riding a transit vehicle toward an alighting stop.
46    RidingTransit {
47        /// Transit vehicle carrying the traveller.
48        vehicle: VehicleId,
49        /// Stop to alight at.
50        alight_at: StopId,
51    },
52    /// Riding a connector (stair/escalator/lift).
53    OnConnector(u64),
54    /// Trip completed.
55    Finished,
56}
57
58/// Controller that advances a traveller through a [`TripPlan`].
59#[derive(Debug, Clone, Copy, Default)]
60pub struct ModeController;
61
62impl ModeController {
63    /// Transition the traveller to the *start* of their current leg.
64    /// Call this whenever the previous leg signals completion.
65    pub fn enter_current_leg(&self, ctx: &mut TravellerContext, plan: &TripPlan) {
66        let Some(leg) = plan.legs.get(ctx.current_leg) else {
67            ctx.state = ModeState::Finished;
68            return;
69        };
70        ctx.state = match leg {
71            Leg::Walk { .. } => ModeState::Walking,
72            Leg::DriveSelf { mode, .. } | Leg::Hail { mode, .. } => ModeState::Driving(*mode),
73            Leg::Transit {
74                route, board_at, ..
75            } => ModeState::WaitingAt(*board_at, *route),
76            Leg::Connector { connector_id } => ModeState::OnConnector(*connector_id),
77        };
78    }
79
80    /// Mark the current leg as complete and advance to the next one.
81    pub fn complete_leg(&self, ctx: &mut TravellerContext, plan: &TripPlan) {
82        ctx.current_leg = ctx.current_leg.saturating_add(1);
83        if ctx.current_leg >= plan.legs.len() {
84            ctx.state = ModeState::Finished;
85        } else {
86            self.enter_current_leg(ctx, plan);
87        }
88    }
89
90    /// Driven by transit code: a transit vehicle has arrived at the
91    /// waiting stop, and the traveller boards it.
92    pub fn board_transit(&self, ctx: &mut TravellerContext, vehicle: VehicleId, alight_at: StopId) {
93        if let ModeState::WaitingAt(_, _) = ctx.state {
94            ctx.state = ModeState::RidingTransit { vehicle, alight_at };
95        }
96    }
97
98    /// Driven by transit code: the alighting stop has been reached.
99    pub fn alight_transit(&self, ctx: &mut TravellerContext, plan: &TripPlan) {
100        if matches!(ctx.state, ModeState::RidingTransit { .. }) {
101            self.complete_leg(ctx, plan);
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::leg::Waypoint;
110
111    fn make_plan() -> TripPlan {
112        TripPlan {
113            id: 1,
114            legs: vec![
115                Leg::Walk {
116                    from: Waypoint::ground(0.0, 0.0),
117                    to: Waypoint::ground(5.0, 0.0),
118                },
119                Leg::Transit {
120                    route: 42,
121                    board_at: 10,
122                    alight_at: 20,
123                },
124                Leg::Walk {
125                    from: Waypoint::ground(100.0, 0.0),
126                    to: Waypoint::ground(105.0, 0.0),
127                },
128            ],
129        }
130    }
131
132    #[test]
133    fn traveller_advances_through_all_legs() {
134        let plan = make_plan();
135        let controller = ModeController;
136        let mut ctx = TravellerContext::new(7);
137        controller.enter_current_leg(&mut ctx, &plan);
138        assert_eq!(ctx.state, ModeState::Walking);
139
140        controller.complete_leg(&mut ctx, &plan);
141        assert_eq!(ctx.state, ModeState::WaitingAt(10, 42));
142
143        controller.board_transit(&mut ctx, 99, 20);
144        assert_eq!(
145            ctx.state,
146            ModeState::RidingTransit {
147                vehicle: 99,
148                alight_at: 20
149            }
150        );
151
152        controller.alight_transit(&mut ctx, &plan);
153        assert_eq!(ctx.state, ModeState::Walking);
154
155        controller.complete_leg(&mut ctx, &plan);
156        assert_eq!(ctx.state, ModeState::Finished);
157    }
158}