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