Skip to main content

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, PartialEq, Eq, 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, PartialEq, 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 time-
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    #[serde(alias = "balk_threshold_ticks")]
55    pub(crate) abandon_after_ticks: Option<u32>,
56    /// Abandon on the first full-car skip, rather than silently
57    /// passing and continuing to wait. Default `false`.
58    ///
59    /// This is an **independent** abandonment axis from
60    /// [`abandon_after_ticks`](Self::abandon_after_ticks) — the two
61    /// do not compose or gate each other:
62    ///
63    /// - `abandon_on_full` is *event-triggered* from the loading phase
64    ///   (`systems::loading`), firing on a full-car skip.
65    /// - `abandon_after_ticks` is *time-triggered* from the transient
66    ///   phase (`systems::advance_transient`), firing when the rider's
67    ///   wait budget elapses.
68    ///
69    /// Both paths set [`RiderPhase::Abandoned`](crate::components::RiderPhase);
70    /// whichever condition is reached first wins. Setting
71    /// `abandon_on_full = true` with `abandon_after_ticks = None` is
72    /// valid and abandons on the first full-car skip regardless of
73    /// wait time.
74    pub(crate) abandon_on_full: bool,
75}
76
77impl Preferences {
78    /// If true, the rider will skip a crowded elevator and wait for the next.
79    #[must_use]
80    pub const fn skip_full_elevator(&self) -> bool {
81        self.skip_full_elevator
82    }
83
84    /// Maximum load factor (0.0-1.0) the rider will tolerate when boarding.
85    #[must_use]
86    pub const fn max_crowding_factor(&self) -> f64 {
87        self.max_crowding_factor
88    }
89
90    /// Wait budget before the rider abandons. `None` disables time-
91    /// based abandonment.
92    #[must_use]
93    pub const fn abandon_after_ticks(&self) -> Option<u32> {
94        self.abandon_after_ticks
95    }
96
97    /// Should skipping a full car convert directly to abandonment?
98    #[must_use]
99    pub const fn abandon_on_full(&self) -> bool {
100        self.abandon_on_full
101    }
102
103    /// Builder: set `skip_full_elevator`.
104    #[must_use]
105    pub const fn with_skip_full_elevator(mut self, skip: bool) -> Self {
106        self.skip_full_elevator = skip;
107        self
108    }
109
110    /// Builder: set `max_crowding_factor`.
111    #[must_use]
112    pub const fn with_max_crowding_factor(mut self, factor: f64) -> Self {
113        self.max_crowding_factor = factor;
114        self
115    }
116
117    /// Builder: set `abandon_after_ticks`.
118    #[must_use]
119    pub const fn with_abandon_after_ticks(mut self, ticks: Option<u32>) -> Self {
120        self.abandon_after_ticks = ticks;
121        self
122    }
123
124    /// Builder: set `abandon_on_full`.
125    #[must_use]
126    pub const fn with_abandon_on_full(mut self, abandon: bool) -> Self {
127        self.abandon_on_full = abandon;
128        self
129    }
130}
131
132impl Default for Preferences {
133    fn default() -> Self {
134        Self {
135            skip_full_elevator: false,
136            max_crowding_factor: 0.8,
137            abandon_after_ticks: None,
138            abandon_on_full: false,
139        }
140    }
141}