Skip to main content

elevator_core/
builder.rs

1//! Fluent builder for constructing a [`Simulation`](crate::sim::Simulation)
2//! programmatically.
3
4use serde::{Serialize, de::DeserializeOwned};
5
6use crate::config::{
7    BuildingConfig, ElevatorConfig, GroupConfig, LineConfig, PassengerSpawnConfig, SimConfig,
8    SimulationParams,
9};
10use crate::dispatch::scan::ScanDispatch;
11use crate::dispatch::{BuiltinReposition, DispatchStrategy, RepositionStrategy};
12use crate::error::SimError;
13use crate::hooks::{Phase, PhaseHooks};
14use crate::ids::GroupId;
15use crate::sim::Simulation;
16use crate::stop::{StopConfig, StopId};
17use crate::world::World;
18use std::collections::BTreeMap;
19
20/// A deferred extension registration closure.
21type ExtRegistration = Box<dyn FnOnce(&mut World) + Send>;
22
23/// Fluent builder for constructing a [`Simulation`].
24///
25/// Builds a [`SimConfig`] internally and delegates to [`Simulation::new()`].
26/// Provides a more ergonomic API for programmatic construction compared to
27/// assembling a config struct manually.
28///
29/// # Constructors
30///
31/// - [`SimulationBuilder::new`] — empty builder. You must add at least one
32///   stop and at least one elevator before `.build()`, or it errors.
33///   `ScanDispatch` is the default strategy, 60 ticks/s the default rate.
34/// - [`SimulationBuilder::demo`] — pre-populated with two stops (Ground at
35///   0.0, Top at 10.0) and one elevator, for doctests and quick
36///   prototyping. Override any piece with the fluent methods.
37pub struct SimulationBuilder {
38    /// Simulation configuration (stops, elevators, timing).
39    config: SimConfig,
40    /// Per-group dispatch strategies.
41    dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
42    /// Per-group reposition strategies.
43    repositioners: Vec<(GroupId, Box<dyn RepositionStrategy>, BuiltinReposition)>,
44    /// Lifecycle hooks for before/after each tick phase.
45    hooks: PhaseHooks,
46    /// Deferred extension registrations (applied after build).
47    ext_registrations: Vec<ExtRegistration>,
48}
49
50impl Default for SimulationBuilder {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl SimulationBuilder {
57    /// Create an empty builder — no stops, no elevators, `ScanDispatch` as
58    /// the default strategy, and 60 ticks per second.
59    ///
60    /// You must add at least one stop and at least one elevator (via
61    /// [`stops`](Self::stops) / [`stop`](Self::stop) and
62    /// [`elevators`](Self::elevators) / [`elevator`](Self::elevator))
63    /// before [`build`](Self::build), or the build fails with
64    /// [`SimError::InvalidConfig`].
65    ///
66    /// If you want a quick, already-valid sim for prototyping or examples,
67    /// use [`demo`](Self::demo).
68    ///
69    /// ```
70    /// use elevator_core::prelude::*;
71    /// use elevator_core::config::ElevatorConfig;
72    /// use elevator_core::stop::StopConfig;
73    ///
74    /// // An empty builder errors on build — you must configure it first.
75    /// assert!(SimulationBuilder::new().build().is_err());
76    ///
77    /// // Minimum valid configuration: at least one stop and one elevator.
78    /// let sim = SimulationBuilder::new()
79    ///     .stops(vec![
80    ///         StopConfig { id: StopId(0), name: "Ground".into(), position: 0.0 },
81    ///         StopConfig { id: StopId(1), name: "Top".into(), position: 10.0 },
82    ///     ])
83    ///     .elevator(ElevatorConfig {
84    ///         id: 0,
85    ///         name: "Main".into(),
86    ///         max_speed: 2.0,
87    ///         acceleration: 1.5,
88    ///         deceleration: 2.0,
89    ///         weight_capacity: 800.0,
90    ///         starting_stop: StopId(0),
91    ///         door_open_ticks: 10,
92    ///         door_transition_ticks: 5,
93    ///         restricted_stops: Vec::new(),
94    ///         # #[cfg(feature = "energy")]
95    ///         # energy_profile: None,
96    ///         service_mode: None,
97    ///         inspection_speed_factor: 0.25,
98    ///     })
99    ///     .build()
100    ///     .unwrap();
101    /// assert_eq!(sim.current_tick(), 0);
102    /// ```
103    #[must_use]
104    pub fn new() -> Self {
105        let config = SimConfig {
106            building: BuildingConfig {
107                name: "Untitled".into(),
108                stops: Vec::new(),
109                lines: None,
110                groups: None,
111            },
112            elevators: Vec::new(),
113            simulation: SimulationParams {
114                ticks_per_second: 60.0,
115            },
116            passenger_spawning: PassengerSpawnConfig {
117                mean_interval_ticks: 120,
118                weight_range: (50.0, 100.0),
119            },
120        };
121
122        let mut dispatchers = BTreeMap::new();
123        dispatchers.insert(
124            GroupId(0),
125            Box::new(ScanDispatch::new()) as Box<dyn DispatchStrategy>,
126        );
127
128        Self {
129            config,
130            dispatchers,
131            repositioners: Vec::new(),
132            hooks: PhaseHooks::default(),
133            ext_registrations: Vec::new(),
134        }
135    }
136
137    /// Pre-populated builder for zero-config examples, doctests, and quick
138    /// prototyping where the building layout isn't the point.
139    ///
140    /// Provides two stops (Ground at 0.0, Top at 10.0) and one elevator
141    /// with SCAN dispatch. Use this when you want a working `Simulation`
142    /// in one call and don't care about the specific stops.
143    ///
144    /// ```
145    /// use elevator_core::prelude::*;
146    ///
147    /// let sim = SimulationBuilder::demo().build().unwrap();
148    /// assert_eq!(sim.current_tick(), 0);
149    /// ```
150    ///
151    /// If you need a specific stop layout or elevator physics, use
152    /// [`new`](Self::new) and configure every field explicitly — it reads
153    /// more clearly than threading overrides on top of `demo`'s defaults.
154    /// [`.stop()`](Self::stop) is a *push* onto the current stops list,
155    /// so calling it after `demo()` appends to the two defaults rather
156    /// than replacing them.
157    #[must_use]
158    pub fn demo() -> Self {
159        let mut b = Self::new();
160        b.config.building.name = "Demo".into();
161        b.config.building.stops = vec![
162            StopConfig {
163                id: StopId(0),
164                name: "Ground".into(),
165                position: 0.0,
166            },
167            StopConfig {
168                id: StopId(1),
169                name: "Top".into(),
170                position: 10.0,
171            },
172        ];
173        b.config.elevators = vec![ElevatorConfig {
174            id: 0,
175            name: "Elevator 1".into(),
176            max_speed: 2.0,
177            acceleration: 1.5,
178            deceleration: 2.0,
179            weight_capacity: 800.0,
180            starting_stop: StopId(0),
181            door_open_ticks: 10,
182            door_transition_ticks: 5,
183            restricted_stops: Vec::new(),
184            #[cfg(feature = "energy")]
185            energy_profile: None,
186            service_mode: None,
187            inspection_speed_factor: 0.25,
188        }];
189        b
190    }
191
192    /// Create a builder from an existing [`SimConfig`].
193    ///
194    /// Uses `ScanDispatch` as the default strategy. Call [`.dispatch()`](Self::dispatch)
195    /// to override.
196    #[must_use]
197    pub fn from_config(config: SimConfig) -> Self {
198        let mut dispatchers = BTreeMap::new();
199        dispatchers.insert(
200            GroupId(0),
201            Box::new(ScanDispatch::new()) as Box<dyn DispatchStrategy>,
202        );
203
204        Self {
205            config,
206            dispatchers,
207            repositioners: Vec::new(),
208            hooks: PhaseHooks::default(),
209            ext_registrations: Vec::new(),
210        }
211    }
212
213    /// Replace all stops with the given list.
214    ///
215    /// Clears any previously added stops.
216    #[must_use]
217    pub fn stops(mut self, stops: Vec<StopConfig>) -> Self {
218        self.config.building.stops = stops;
219        self
220    }
221
222    /// Add a single stop to the building.
223    #[must_use]
224    pub fn stop(mut self, id: StopId, name: impl Into<String>, position: f64) -> Self {
225        self.config.building.stops.push(StopConfig {
226            id,
227            name: name.into(),
228            position,
229        });
230        self
231    }
232
233    /// Replace all elevators with the given list.
234    ///
235    /// Clears any previously added elevators.
236    #[must_use]
237    pub fn elevators(mut self, elevators: Vec<ElevatorConfig>) -> Self {
238        self.config.elevators = elevators;
239        self
240    }
241
242    /// Add a single elevator configuration.
243    #[must_use]
244    pub fn elevator(mut self, config: ElevatorConfig) -> Self {
245        self.config.elevators.push(config);
246        self
247    }
248
249    /// Add a single line configuration.
250    ///
251    /// Switches from legacy flat-elevator mode to explicit topology.
252    #[must_use]
253    pub fn line(mut self, config: LineConfig) -> Self {
254        self.config
255            .building
256            .lines
257            .get_or_insert_with(Vec::new)
258            .push(config);
259        self
260    }
261
262    /// Replace all lines with the given list.
263    ///
264    /// Switches from legacy flat-elevator mode to explicit topology.
265    #[must_use]
266    pub fn lines(mut self, lines: Vec<LineConfig>) -> Self {
267        self.config.building.lines = Some(lines);
268        self
269    }
270
271    /// Add a single group configuration.
272    #[must_use]
273    pub fn group(mut self, config: GroupConfig) -> Self {
274        self.config
275            .building
276            .groups
277            .get_or_insert_with(Vec::new)
278            .push(config);
279        self
280    }
281
282    /// Replace all groups with the given list.
283    #[must_use]
284    pub fn groups(mut self, groups: Vec<GroupConfig>) -> Self {
285        self.config.building.groups = Some(groups);
286        self
287    }
288
289    /// Set the simulation tick rate (ticks per second).
290    #[must_use]
291    pub const fn ticks_per_second(mut self, tps: f64) -> Self {
292        self.config.simulation.ticks_per_second = tps;
293        self
294    }
295
296    /// Set the building name.
297    #[must_use]
298    pub fn building_name(mut self, name: impl Into<String>) -> Self {
299        self.config.building.name = name.into();
300        self
301    }
302
303    /// Set the default dispatch strategy for the default group.
304    #[must_use]
305    pub fn dispatch(mut self, strategy: impl DispatchStrategy + 'static) -> Self {
306        self.dispatchers.insert(GroupId(0), Box::new(strategy));
307        self
308    }
309
310    /// Set a dispatch strategy for a specific group.
311    #[must_use]
312    pub fn dispatch_for_group(
313        mut self,
314        group: GroupId,
315        strategy: impl DispatchStrategy + 'static,
316    ) -> Self {
317        self.dispatchers.insert(group, Box::new(strategy));
318        self
319    }
320
321    /// Register a hook to run before a simulation phase.
322    #[must_use]
323    pub fn before(
324        mut self,
325        phase: Phase,
326        hook: impl Fn(&mut World) + Send + Sync + 'static,
327    ) -> Self {
328        self.hooks.add_before(phase, Box::new(hook));
329        self
330    }
331
332    /// Register a hook to run after a simulation phase.
333    #[must_use]
334    pub fn after(
335        mut self,
336        phase: Phase,
337        hook: impl Fn(&mut World) + Send + Sync + 'static,
338    ) -> Self {
339        self.hooks.add_after(phase, Box::new(hook));
340        self
341    }
342
343    /// Register a hook to run before a phase for a specific group.
344    #[must_use]
345    pub fn before_group(
346        mut self,
347        phase: Phase,
348        group: GroupId,
349        hook: impl Fn(&mut World) + Send + Sync + 'static,
350    ) -> Self {
351        self.hooks.add_before_group(phase, group, Box::new(hook));
352        self
353    }
354
355    /// Register a hook to run after a phase for a specific group.
356    #[must_use]
357    pub fn after_group(
358        mut self,
359        phase: Phase,
360        group: GroupId,
361        hook: impl Fn(&mut World) + Send + Sync + 'static,
362    ) -> Self {
363        self.hooks.add_after_group(phase, group, Box::new(hook));
364        self
365    }
366
367    /// Set a reposition strategy for the default group.
368    ///
369    /// Enables the reposition phase, which runs after dispatch to
370    /// move idle elevators for better coverage.
371    #[must_use]
372    pub fn reposition(
373        self,
374        strategy: impl RepositionStrategy + 'static,
375        id: BuiltinReposition,
376    ) -> Self {
377        self.reposition_for_group(GroupId(0), strategy, id)
378    }
379
380    /// Set a reposition strategy for a specific group.
381    #[must_use]
382    pub fn reposition_for_group(
383        mut self,
384        group: GroupId,
385        strategy: impl RepositionStrategy + 'static,
386        id: BuiltinReposition,
387    ) -> Self {
388        self.repositioners.push((group, Box::new(strategy), id));
389        self
390    }
391
392    /// Pre-register an extension type for snapshot deserialization.
393    ///
394    /// Extensions registered here will be available immediately after [`build()`](Self::build)
395    /// without needing to call `register_ext` manually.
396    #[must_use]
397    pub fn with_ext<T: 'static + Send + Sync + Serialize + DeserializeOwned>(
398        mut self,
399        name: &str,
400    ) -> Self {
401        let name = name.to_owned();
402        self.ext_registrations
403            .push(Box::new(move |world: &mut World| {
404                world.register_ext::<T>(&name);
405            }));
406        self
407    }
408
409    /// Validate the configuration without building the simulation.
410    ///
411    /// Runs the same validation as [`build()`](Self::build) but does not
412    /// allocate entities or construct the simulation. Useful for CLI tools,
413    /// config editors, and dry-run checks.
414    ///
415    /// # Errors
416    ///
417    /// Returns [`SimError::InvalidConfig`] if the configuration is invalid.
418    pub fn validate(&self) -> Result<(), SimError> {
419        Simulation::validate_config(&self.config)
420    }
421
422    /// Build the simulation, validating the configuration.
423    ///
424    /// Returns `Err(SimError)` if the configuration is invalid.
425    ///
426    /// # Errors
427    ///
428    /// Returns [`SimError::InvalidConfig`] if the assembled configuration is invalid.
429    ///
430    /// # Examples
431    ///
432    /// ```
433    /// use elevator_core::prelude::*;
434    /// use elevator_core::stop::StopConfig;
435    ///
436    /// let mut sim = SimulationBuilder::demo()
437    ///     .stops(vec![
438    ///         StopConfig { id: StopId(0), name: "Lobby".into(), position: 0.0 },
439    ///         StopConfig { id: StopId(1), name: "Roof".into(), position: 20.0 },
440    ///     ])
441    ///     .build()
442    ///     .unwrap();
443    ///
444    /// sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 75.0).unwrap();
445    ///
446    /// for _ in 0..1000 {
447    ///     sim.step();
448    /// }
449    ///
450    /// assert!(sim.metrics().total_delivered() > 0);
451    /// ```
452    pub fn build(self) -> Result<Simulation, SimError> {
453        let mut sim = Simulation::new_with_hooks(&self.config, self.dispatchers, self.hooks)?;
454
455        for (group, strategy, id) in self.repositioners {
456            sim.set_reposition(group, strategy, id);
457        }
458
459        for register in self.ext_registrations {
460            register(sim.world_mut());
461        }
462
463        Ok(sim)
464    }
465}