Skip to main content

elevator_core/
sim.rs

1//! Top-level simulation runner and tick loop.
2//!
3//! # Essential API
4//!
5//! `Simulation` exposes a large surface, but most users only need the
6//! ~15 methods below, grouped by the order they appear in a typical
7//! game loop.
8//!
9//! ### Construction
10//!
11//! - [`SimulationBuilder::demo()`](crate::builder::SimulationBuilder::demo)
12//!   or [`SimulationBuilder::from_config()`](crate::builder::SimulationBuilder::from_config)
13//!   — fluent entry point; call [`.build()`](crate::builder::SimulationBuilder::build)
14//!   to get a `Simulation`.
15//! - [`Simulation::new()`](crate::sim::Simulation::new) — direct construction from
16//!   `&SimConfig` + a dispatch strategy.
17//!
18//! ### Per-tick driving
19//!
20//! - [`Simulation::step()`](crate::sim::Simulation::step) — run all 8 phases.
21//! - [`Simulation::current_tick()`](crate::sim::Simulation::current_tick) — the
22//!   current tick counter.
23//!
24//! ### Spawning and rerouting riders
25//!
26//! - [`Simulation::spawn_rider()`](crate::sim::Simulation::spawn_rider)
27//!   — simple origin/destination/weight spawn (accepts `EntityId` or `StopId`).
28//! - [`Simulation::build_rider()`](crate::sim::Simulation::build_rider)
29//!   — fluent [`RiderBuilder`](crate::sim::RiderBuilder) for patience, preferences, access
30//!   control, explicit groups, multi-leg routes (accepts `EntityId` or `StopId`).
31//! - [`Simulation::reroute()`](crate::sim::Simulation::reroute) — change a waiting
32//!   rider's destination mid-trip.
33//! - [`Simulation::settle_rider()`](crate::sim::Simulation::settle_rider) /
34//!   [`Simulation::despawn_rider()`](crate::sim::Simulation::despawn_rider) —
35//!   terminal-state cleanup for `Arrived`/`Abandoned` riders.
36//!
37//! ### Observability
38//!
39//! - [`Simulation::drain_events()`](crate::sim::Simulation::drain_events) — consume
40//!   the event stream emitted by the last tick.
41//! - [`Simulation::metrics()`](crate::sim::Simulation::metrics) — aggregate
42//!   wait/ride/throughput stats.
43//! - [`Simulation::waiting_at()`](crate::sim::Simulation::waiting_at) /
44//!   [`Simulation::residents_at()`](crate::sim::Simulation::residents_at) — O(1)
45//!   population queries by stop.
46//!
47//! ### Imperative control
48//!
49//! - [`Simulation::push_destination()`](crate::sim::Simulation::push_destination) /
50//!   [`Simulation::push_destination_front()`](crate::sim::Simulation::push_destination_front) /
51//!   [`Simulation::clear_destinations()`](crate::sim::Simulation::clear_destinations)
52//!   — override dispatch by pushing/clearing stops on an elevator's
53//!   [`DestinationQueue`](crate::components::DestinationQueue).
54//! - [`Simulation::abort_movement()`](crate::sim::Simulation::abort_movement)
55//!   — hard-abort an in-flight trip, braking the car to the nearest
56//!   reachable stop without opening doors (riders stay aboard).
57//!
58//! ### Persistence
59//!
60//! - [`Simulation::snapshot()`](crate::sim::Simulation::snapshot) — capture full
61//!   state as a serializable [`WorldSnapshot`](crate::snapshot::WorldSnapshot).
62//! - [`WorldSnapshot::restore()`](crate::snapshot::WorldSnapshot::restore)
63//!   — rebuild a `Simulation` from a snapshot.
64//!
65//! Everything else (phase-runners, world-level accessors, energy, tag
66//! metrics, topology queries) is available for advanced use but is not
67//! required for the common case.
68
69mod accessors;
70mod calls;
71mod construction;
72mod destinations;
73mod eta;
74mod lifecycle;
75mod manual;
76mod rider;
77mod runtime;
78mod substep;
79mod tagging;
80mod topology;
81
82use crate::components::{
83    Accel, AccessControl, Orientation, Patience, Preferences, Route, SpatialPosition, Speed, Weight,
84};
85use crate::dispatch::{BuiltinReposition, DispatchStrategy, ElevatorGroup, RepositionStrategy};
86use crate::entity::{EntityId, RiderId};
87use crate::error::SimError;
88use crate::events::{Event, EventBus};
89use crate::hooks::PhaseHooks;
90use crate::ids::GroupId;
91use crate::metrics::Metrics;
92use crate::rider_index::RiderIndex;
93use crate::stop::StopId;
94use crate::time::TimeAdapter;
95use crate::topology::TopologyGraph;
96use crate::world::World;
97use std::collections::{BTreeMap, HashMap, HashSet};
98use std::fmt;
99use std::sync::Mutex;
100
101/// Parameters for creating a new elevator at runtime.
102#[derive(Debug, Clone)]
103pub struct ElevatorParams {
104    /// Maximum travel speed (distance/tick).
105    pub max_speed: Speed,
106    /// Acceleration rate (distance/tick^2).
107    pub acceleration: Accel,
108    /// Deceleration rate (distance/tick^2).
109    pub deceleration: Accel,
110    /// Maximum weight the car can carry.
111    pub weight_capacity: Weight,
112    /// Ticks for a door open/close transition.
113    pub door_transition_ticks: u32,
114    /// Ticks the door stays fully open.
115    pub door_open_ticks: u32,
116    /// Stop entity IDs this elevator cannot serve (access restriction).
117    pub restricted_stops: HashSet<EntityId>,
118    /// Speed multiplier for Inspection mode (0.0..1.0).
119    pub inspection_speed_factor: f64,
120}
121
122impl Default for ElevatorParams {
123    fn default() -> Self {
124        Self {
125            max_speed: Speed::from(2.0),
126            acceleration: Accel::from(1.5),
127            deceleration: Accel::from(2.0),
128            weight_capacity: Weight::from(800.0),
129            door_transition_ticks: 5,
130            door_open_ticks: 10,
131            restricted_stops: HashSet::new(),
132            inspection_speed_factor: 0.25,
133        }
134    }
135}
136
137/// Parameters for creating a new line at runtime.
138#[derive(Debug, Clone)]
139pub struct LineParams {
140    /// Human-readable name.
141    pub name: String,
142    /// Dispatch group to add this line to.
143    pub group: GroupId,
144    /// Physical orientation.
145    pub orientation: Orientation,
146    /// Lowest reachable position on the line axis.
147    pub min_position: f64,
148    /// Highest reachable position on the line axis.
149    pub max_position: f64,
150    /// Optional floor-plan position.
151    pub position: Option<SpatialPosition>,
152    /// Maximum cars on this line (None = unlimited).
153    pub max_cars: Option<usize>,
154}
155
156impl LineParams {
157    /// Create line parameters with the given name and group, defaulting
158    /// everything else.
159    pub fn new(name: impl Into<String>, group: GroupId) -> Self {
160        Self {
161            name: name.into(),
162            group,
163            orientation: Orientation::default(),
164            min_position: 0.0,
165            max_position: 0.0,
166            position: None,
167            max_cars: None,
168        }
169    }
170}
171
172/// Fluent builder for spawning riders with optional configuration.
173///
174/// Created via [`Simulation::build_rider`].
175///
176/// ```
177/// use elevator_core::prelude::*;
178///
179/// let mut sim = SimulationBuilder::demo().build().unwrap();
180/// let rider = sim.build_rider(StopId(0), StopId(1))
181///     .unwrap()
182///     .weight(80.0)
183///     .spawn()
184///     .unwrap();
185/// ```
186pub struct RiderBuilder<'a> {
187    /// Mutable reference to the simulation (consumed on spawn).
188    sim: &'a mut Simulation,
189    /// Origin stop entity.
190    origin: EntityId,
191    /// Destination stop entity.
192    destination: EntityId,
193    /// Rider weight (default: 75.0).
194    weight: Weight,
195    /// Explicit dispatch group (skips auto-detection).
196    group: Option<GroupId>,
197    /// Explicit multi-leg route.
198    route: Option<Route>,
199    /// Maximum wait ticks before abandoning.
200    patience: Option<u64>,
201    /// Boarding preferences.
202    preferences: Option<Preferences>,
203    /// Per-rider access control.
204    access_control: Option<AccessControl>,
205}
206
207impl RiderBuilder<'_> {
208    /// Set the rider's weight (default: 75.0).
209    #[must_use]
210    pub fn weight(mut self, weight: impl Into<Weight>) -> Self {
211        self.weight = weight.into();
212        self
213    }
214
215    /// Set the dispatch group explicitly, skipping auto-detection.
216    #[must_use]
217    pub const fn group(mut self, group: GroupId) -> Self {
218        self.group = Some(group);
219        self
220    }
221
222    /// Provide an explicit multi-leg route.
223    #[must_use]
224    pub fn route(mut self, route: Route) -> Self {
225        self.route = Some(route);
226        self
227    }
228
229    /// Set maximum wait ticks before the rider abandons.
230    #[must_use]
231    pub const fn patience(mut self, max_wait_ticks: u64) -> Self {
232        self.patience = Some(max_wait_ticks);
233        self
234    }
235
236    /// Set boarding preferences.
237    #[must_use]
238    pub const fn preferences(mut self, prefs: Preferences) -> Self {
239        self.preferences = Some(prefs);
240        self
241    }
242
243    /// Set per-rider access control (allowed stops).
244    #[must_use]
245    pub fn access_control(mut self, ac: AccessControl) -> Self {
246        self.access_control = Some(ac);
247        self
248    }
249
250    /// Spawn the rider with the configured options.
251    ///
252    /// # Errors
253    ///
254    /// Returns [`SimError::NoRoute`] if no group serves both stops (when auto-detecting).
255    /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops (when auto-detecting).
256    /// Returns [`SimError::GroupNotFound`] if an explicit group does not exist.
257    /// Returns [`SimError::RouteOriginMismatch`] if an explicit route's first leg
258    /// does not start at `origin`.
259    pub fn spawn(self) -> Result<RiderId, SimError> {
260        let route = if let Some(route) = self.route {
261            // Validate route origin matches the spawn origin.
262            if let Some(leg) = route.current()
263                && leg.from != self.origin
264            {
265                return Err(SimError::RouteOriginMismatch {
266                    expected_origin: self.origin,
267                    route_origin: leg.from,
268                });
269            }
270            route
271        } else if let Some(group) = self.group {
272            if !self.sim.groups.iter().any(|g| g.id() == group) {
273                return Err(SimError::GroupNotFound(group));
274            }
275            Route::direct(self.origin, self.destination, group)
276        } else {
277            let group = self.sim.auto_detect_group(self.origin, self.destination)?;
278            Route::direct(self.origin, self.destination, group)
279        };
280
281        let eid = self
282            .sim
283            .spawn_rider_inner(self.origin, self.destination, self.weight, route);
284
285        // Apply optional components.
286        if let Some(max_wait) = self.patience {
287            self.sim.world.set_patience(
288                eid,
289                Patience {
290                    max_wait_ticks: max_wait,
291                    waited_ticks: 0,
292                },
293            );
294        }
295        if let Some(prefs) = self.preferences {
296            self.sim.world.set_preferences(eid, prefs);
297        }
298        if let Some(ac) = self.access_control {
299            self.sim.world.set_access_control(eid, ac);
300        }
301
302        Ok(RiderId::from(eid))
303    }
304}
305
306/// The core simulation state, advanced by calling `step()`.
307pub struct Simulation {
308    /// The ECS world containing all entity data.
309    world: World,
310    /// Internal event bus — only holds events from the current tick.
311    events: EventBus,
312    /// Events from completed ticks, available to consumers via `drain_events()`.
313    pending_output: Vec<Event>,
314    /// Current simulation tick.
315    tick: u64,
316    /// Time delta per tick (seconds).
317    dt: f64,
318    /// Elevator groups in this simulation.
319    groups: Vec<ElevatorGroup>,
320    /// Config `StopId` to `EntityId` mapping for spawn helpers.
321    stop_lookup: HashMap<StopId, EntityId>,
322    /// Dispatch strategies keyed by group.
323    dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
324    /// Serializable strategy identifiers (for snapshot).
325    strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
326    /// Reposition strategies keyed by group (optional per group).
327    repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>>,
328    /// Serializable reposition strategy identifiers (for snapshot).
329    reposition_ids: BTreeMap<GroupId, BuiltinReposition>,
330    /// Aggregated metrics.
331    metrics: Metrics,
332    /// Time conversion utility.
333    time: TimeAdapter,
334    /// Lifecycle hooks (before/after each phase).
335    hooks: PhaseHooks,
336    /// Reusable buffer for elevator IDs (avoids per-tick allocation).
337    elevator_ids_buf: Vec<EntityId>,
338    /// Reusable buffer for reposition decisions (avoids per-tick allocation).
339    reposition_buf: Vec<(EntityId, EntityId)>,
340    /// Lazy-rebuilt connectivity graph for cross-line topology queries.
341    topo_graph: Mutex<TopologyGraph>,
342    /// Phase-partitioned reverse index for O(1) population queries.
343    rider_index: RiderIndex,
344}
345
346impl fmt::Debug for Simulation {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        f.debug_struct("Simulation")
349            .field("tick", &self.tick)
350            .field("dt", &self.dt)
351            .field("groups", &self.groups.len())
352            .field("entities", &self.world.entity_count())
353            .finish_non_exhaustive()
354    }
355}