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
//! Error types for configuration validation and runtime failures.
use crate::entity::EntityId;
use crate::ids::GroupId;
use crate::stop::StopId;
use ordered_float::OrderedFloat;
use std::fmt;
/// Errors that can occur during simulation setup or operation.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SimError {
/// Configuration is invalid.
InvalidConfig {
/// Which config field is problematic.
field: &'static str,
/// Human-readable explanation.
reason: String,
},
/// A referenced entity does not exist.
EntityNotFound(EntityId),
/// A referenced stop ID does not exist in the config.
StopNotFound(StopId),
/// A referenced group does not exist.
GroupNotFound(GroupId),
/// An operation was attempted on an entity in an invalid state.
InvalidState {
/// The entity in the wrong state.
entity: EntityId,
/// Human-readable explanation.
reason: String,
},
/// A line entity was not found.
LineNotFound(EntityId),
/// No route exists between origin and destination across any group.
NoRoute {
/// The origin stop.
origin: EntityId,
/// The destination stop.
destination: EntityId,
/// Groups that serve the origin (if any).
origin_groups: Vec<GroupId>,
/// Groups that serve the destination (if any).
destination_groups: Vec<GroupId>,
},
/// Multiple groups serve both origin and destination — caller must specify.
AmbiguousRoute {
/// The origin stop.
origin: EntityId,
/// The destination stop.
destination: EntityId,
/// The groups that serve both stops.
groups: Vec<GroupId>,
},
}
impl fmt::Display for SimError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfig { field, reason } => {
write!(f, "invalid config '{field}': {reason}")
}
Self::EntityNotFound(id) => write!(f, "entity not found: {id:?}"),
Self::StopNotFound(id) => write!(f, "stop not found: {id}"),
Self::GroupNotFound(id) => write!(f, "group not found: {id}"),
Self::InvalidState { entity, reason } => {
write!(f, "invalid state for {entity:?}: {reason}")
}
Self::LineNotFound(id) => write!(f, "line entity {id:?} not found"),
Self::NoRoute {
origin,
destination,
origin_groups,
destination_groups,
} => {
write!(
f,
"no route from {origin:?} to {destination:?} (origin served by {}, destination served by {})",
format_group_list(origin_groups),
format_group_list(destination_groups),
)
}
Self::AmbiguousRoute {
origin,
destination,
groups,
} => {
write!(
f,
"ambiguous route from {origin:?} to {destination:?}: served by groups {}",
format_group_list(groups),
)
}
}
}
}
impl std::error::Error for SimError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
/// Format a list of `GroupId`s as `[GroupId(0), GroupId(1)]` or `[]` if empty.
fn format_group_list(groups: &[GroupId]) -> String {
if groups.is_empty() {
return "[]".to_string();
}
let parts: Vec<String> = groups.iter().map(GroupId::to_string).collect();
format!("[{}]", parts.join(", "))
}
/// Reason a rider was rejected from boarding an elevator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum RejectionReason {
/// Rider's weight exceeds remaining elevator capacity.
OverCapacity,
/// Rider's boarding preferences prevented boarding (e.g., crowding threshold).
PreferenceBased,
/// Rider lacks access to the destination stop, or the elevator cannot serve it.
AccessDenied,
}
impl fmt::Display for RejectionReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::OverCapacity => write!(f, "over capacity"),
Self::PreferenceBased => write!(f, "rider preference"),
Self::AccessDenied => write!(f, "access denied"),
}
}
}
/// Additional context for a rider rejection.
///
/// Provides the numeric details that led to the rejection decision.
/// Separated from [`RejectionReason`] to preserve `Eq` on the reason enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RejectionContext {
/// Weight the rider attempted to add.
pub attempted_weight: OrderedFloat<f64>,
/// Current load on the elevator at rejection time.
pub current_load: OrderedFloat<f64>,
/// Maximum weight capacity of the elevator.
pub capacity: OrderedFloat<f64>,
}
impl fmt::Display for RejectionContext {
/// Compact summary for game feedback.
///
/// ```
/// # use elevator_core::error::RejectionContext;
/// # use ordered_float::OrderedFloat;
/// let ctx = RejectionContext {
/// attempted_weight: OrderedFloat(80.0),
/// current_load: OrderedFloat(750.0),
/// capacity: OrderedFloat(800.0),
/// };
/// assert_eq!(format!("{ctx}"), "over capacity by 30.0kg (750.0/800.0 + 80.0)");
/// ```
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let excess = (*self.current_load + *self.attempted_weight) - *self.capacity;
if excess > 0.0 {
write!(
f,
"over capacity by {excess:.1}kg ({:.1}/{:.1} + {:.1})",
*self.current_load, *self.capacity, *self.attempted_weight,
)
} else {
write!(
f,
"load {:.1}kg/{:.1}kg + {:.1}kg",
*self.current_load, *self.capacity, *self.attempted_weight,
)
}
}
}
impl From<EntityId> for SimError {
fn from(id: EntityId) -> Self {
Self::EntityNotFound(id)
}
}
impl From<StopId> for SimError {
fn from(id: StopId) -> Self {
Self::StopNotFound(id)
}
}
impl From<GroupId> for SimError {
fn from(id: GroupId) -> Self {
Self::GroupNotFound(id)
}
}