elevator_core/components/patience.rs
1//! Patience and boarding preference components.
2
3use serde::{Deserialize, Serialize};
4
5/// Tracks how long a rider will wait before abandoning.
6#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
7pub struct Patience {
8 /// Maximum ticks the rider will wait before abandoning.
9 pub(crate) max_wait_ticks: u64,
10 /// Ticks waited so far (incremented while in `Waiting` phase).
11 pub(crate) waited_ticks: u64,
12}
13
14impl Patience {
15 /// Maximum ticks the rider will wait before abandoning.
16 #[must_use]
17 pub const fn max_wait_ticks(&self) -> u64 {
18 self.max_wait_ticks
19 }
20
21 /// Ticks waited so far (incremented while in `Waiting` phase).
22 #[must_use]
23 pub const fn waited_ticks(&self) -> u64 {
24 self.waited_ticks
25 }
26}
27
28impl Default for Patience {
29 fn default() -> Self {
30 Self {
31 max_wait_ticks: 600,
32 waited_ticks: 0,
33 }
34 }
35}
36
37/// Boarding preferences for a rider.
38#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
39pub struct Preferences {
40 /// If true, the rider will skip a crowded elevator and wait for the next.
41 pub(crate) skip_full_elevator: bool,
42 /// Maximum load factor (0.0-1.0) the rider will tolerate when boarding.
43 pub(crate) max_crowding_factor: f64,
44 /// Wait budget before the rider abandons. `None` disables balking-
45 /// based abandonment; `Some(n)` causes the rider to enter
46 /// [`RiderPhase::Abandoned`](crate::components::RiderPhase) after
47 /// `n` ticks of being [`Waiting`](crate::components::RiderPhase::Waiting).
48 ///
49 /// The counter consulted is [`Patience::waited_ticks`] when a
50 /// [`Patience`] component is attached — that counter only
51 /// increments during `Waiting` and correctly excludes ride time for
52 /// multi-leg routes. Without `Patience`, the budget degrades to
53 /// lifetime ticks since spawn, which matches single-leg behavior.
54 pub(crate) balk_threshold_ticks: Option<u32>,
55 /// When a full car arrives and this rider skips it, should that
56 /// count as a balk-and-abandon rather than a silent pass? When
57 /// `true`, the rider abandons immediately instead of waiting for
58 /// `balk_threshold_ticks` to elapse. Default `false`.
59 pub(crate) rebalk_on_full: bool,
60}
61
62impl Preferences {
63 /// If true, the rider will skip a crowded elevator and wait for the next.
64 #[must_use]
65 pub const fn skip_full_elevator(&self) -> bool {
66 self.skip_full_elevator
67 }
68
69 /// Maximum load factor (0.0-1.0) the rider will tolerate when boarding.
70 #[must_use]
71 pub const fn max_crowding_factor(&self) -> f64 {
72 self.max_crowding_factor
73 }
74
75 /// Wait budget before the rider abandons. `None` disables balking-
76 /// based abandonment.
77 #[must_use]
78 pub const fn balk_threshold_ticks(&self) -> Option<u32> {
79 self.balk_threshold_ticks
80 }
81
82 /// Should balking a full car convert directly to abandonment?
83 #[must_use]
84 pub const fn rebalk_on_full(&self) -> bool {
85 self.rebalk_on_full
86 }
87
88 /// Builder: set `balk_threshold_ticks`.
89 #[must_use]
90 pub const fn with_balk_threshold_ticks(mut self, ticks: Option<u32>) -> Self {
91 self.balk_threshold_ticks = ticks;
92 self
93 }
94
95 /// Builder: set `rebalk_on_full`.
96 #[must_use]
97 pub const fn with_rebalk_on_full(mut self, rebalk: bool) -> Self {
98 self.rebalk_on_full = rebalk;
99 self
100 }
101}
102
103impl Default for Preferences {
104 fn default() -> Self {
105 Self {
106 skip_full_elevator: false,
107 max_crowding_factor: 0.8,
108 balk_threshold_ticks: None,
109 rebalk_on_full: false,
110 }
111 }
112}