pub mod destination;
pub mod etd;
pub mod look;
pub mod nearest_car;
pub mod reposition;
pub mod rsr;
pub mod scan;
pub(crate) mod sweep;
pub use destination::{AssignedCar, DestinationDispatch};
pub use etd::EtdDispatch;
pub use look::LookDispatch;
pub use nearest_car::NearestCarDispatch;
pub use rsr::RsrDispatch;
pub use scan::ScanDispatch;
use serde::{Deserialize, Serialize};
use crate::components::{
CallDirection, CarCall, ElevatorPhase, HallCall, Route, TransportMode, Weight,
};
use crate::entity::EntityId;
use crate::ids::GroupId;
use crate::world::World;
use std::collections::{BTreeMap, HashSet};
#[must_use]
pub fn pair_is_useful(ctx: &RankContext<'_>, respect_aboard_path: bool) -> bool {
let Some(car) = ctx.world.elevator(ctx.car) else {
return false;
};
let can_exit_here = car
.riders()
.iter()
.any(|&rid| ctx.world.route(rid).and_then(Route::current_destination) == Some(ctx.stop));
if can_exit_here {
return true;
}
if bypass_in_current_direction(car, ctx) {
return false;
}
let remaining_capacity = car.weight_capacity.value() - car.current_load.value();
if remaining_capacity <= 0.0 {
return false;
}
let waiting = ctx.manifest.waiting_riders_at(ctx.stop);
let servable = if waiting.is_empty() {
ctx.manifest
.hall_calls_at_stop
.get(&ctx.stop)
.is_some_and(|calls| calls.iter().any(|c| c.pending_riders.is_empty()))
} else {
waiting
.iter()
.any(|r| rider_can_board(r, car, ctx, remaining_capacity))
};
if !servable {
return false;
}
if !respect_aboard_path || car.riders().is_empty() {
return true;
}
let has_routed_rider = car.riders().iter().any(|&rid| {
ctx.world
.route(rid)
.and_then(Route::current_destination)
.is_some()
});
if !has_routed_rider {
return true;
}
let to_cand = ctx.stop_position - ctx.car_position;
car.riders().iter().any(|&rid| {
let Some(dest) = ctx.world.route(rid).and_then(Route::current_destination) else {
return false;
};
let Some(dest_pos) = ctx.world.stop_position(dest) else {
return false;
};
let to_dest = dest_pos - ctx.car_position;
to_dest * to_cand >= 0.0 && to_cand.abs() <= to_dest.abs()
})
}
fn rider_can_board(
rider: &RiderInfo,
car: &crate::components::Elevator,
ctx: &RankContext<'_>,
remaining_capacity: f64,
) -> bool {
if rider.weight.value() > remaining_capacity {
return false;
}
let Some(dest) = rider.destination else {
return true;
};
let Some(dest_pos) = ctx.world.stop_position(dest) else {
return true;
};
if dest_pos > ctx.stop_position && !car.going_up() {
return false;
}
if dest_pos < ctx.stop_position && !car.going_down() {
return false;
}
true
}
fn bypass_in_current_direction(car: &crate::components::Elevator, ctx: &RankContext<'_>) -> bool {
let Some(target) = car.phase().moving_target() else {
return false;
};
let Some(target_pos) = ctx.world.stop_position(target) else {
return false;
};
let going_up = target_pos > ctx.car_position;
let going_down = target_pos < ctx.car_position;
if !going_up && !going_down {
return false;
}
let threshold = if going_up {
car.bypass_load_up_pct()
} else {
car.bypass_load_down_pct()
};
let Some(pct) = threshold else {
return false;
};
let capacity = car.weight_capacity().value();
if capacity <= 0.0 {
return false;
}
let load_ratio = car.current_load().value() / capacity;
if load_ratio < pct {
return false;
}
let stop_above = ctx.stop_position > ctx.car_position;
let stop_below = ctx.stop_position < ctx.car_position;
(going_up && stop_above) || (going_down && stop_below)
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RiderInfo {
pub id: EntityId,
pub destination: Option<EntityId>,
pub weight: Weight,
pub wait_ticks: u64,
}
#[derive(Debug, Clone, Default)]
pub struct DispatchManifest {
pub(crate) waiting_at_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
pub(crate) riding_to_stop: BTreeMap<EntityId, Vec<RiderInfo>>,
pub(crate) resident_count_at_stop: BTreeMap<EntityId, usize>,
pub(crate) hall_calls_at_stop: BTreeMap<EntityId, Vec<HallCall>>,
pub(crate) car_calls_by_car: BTreeMap<EntityId, Vec<CarCall>>,
pub(crate) arrivals_at_stop: BTreeMap<EntityId, u64>,
pub(crate) arrival_window_ticks: u64,
}
impl DispatchManifest {
#[must_use]
pub fn waiting_count_at(&self, stop: EntityId) -> usize {
self.waiting_at_stop.get(&stop).map_or(0, Vec::len)
}
#[must_use]
pub fn total_weight_at(&self, stop: EntityId) -> f64 {
self.waiting_at_stop
.get(&stop)
.map_or(0.0, |riders| riders.iter().map(|r| r.weight.value()).sum())
}
#[must_use]
pub fn riding_count_to(&self, stop: EntityId) -> usize {
self.riding_to_stop.get(&stop).map_or(0, Vec::len)
}
#[must_use]
pub fn has_demand(&self, stop: EntityId) -> bool {
self.waiting_count_at(stop) > 0
|| self.riding_count_to(stop) > 0
|| self
.hall_calls_at_stop
.get(&stop)
.is_some_and(|calls| calls.iter().any(|c| c.pending_riders.is_empty()))
}
#[must_use]
pub fn resident_count_at(&self, stop: EntityId) -> usize {
self.resident_count_at_stop.get(&stop).copied().unwrap_or(0)
}
#[must_use]
pub fn arrivals_at(&self, stop: EntityId) -> u64 {
self.arrivals_at_stop.get(&stop).copied().unwrap_or(0)
}
#[must_use]
pub const fn arrival_window_ticks(&self) -> u64 {
self.arrival_window_ticks
}
#[must_use]
pub fn hall_call_at(&self, stop: EntityId, direction: CallDirection) -> Option<&HallCall> {
self.hall_calls_at_stop
.get(&stop)?
.iter()
.find(|c| c.direction == direction)
}
pub fn iter_hall_calls(&self) -> impl Iterator<Item = &HallCall> {
self.hall_calls_at_stop.values().flatten()
}
#[must_use]
pub fn car_calls_for(&self, car: EntityId) -> &[CarCall] {
self.car_calls_by_car.get(&car).map_or(&[], Vec::as_slice)
}
#[must_use]
pub fn waiting_riders_at(&self, stop: EntityId) -> &[RiderInfo] {
self.waiting_at_stop.get(&stop).map_or(&[], Vec::as_slice)
}
pub fn iter_waiting_stops(&self) -> impl Iterator<Item = (&EntityId, &[RiderInfo])> {
self.waiting_at_stop
.iter()
.map(|(stop, riders)| (stop, riders.as_slice()))
}
#[must_use]
pub fn riding_riders_to(&self, stop: EntityId) -> &[RiderInfo] {
self.riding_to_stop.get(&stop).map_or(&[], Vec::as_slice)
}
pub fn iter_riding_stops(&self) -> impl Iterator<Item = (&EntityId, &[RiderInfo])> {
self.riding_to_stop
.iter()
.map(|(stop, riders)| (stop, riders.as_slice()))
}
pub fn iter_hall_call_stops(&self) -> impl Iterator<Item = (&EntityId, &[HallCall])> {
self.hall_calls_at_stop
.iter()
.map(|(stop, calls)| (stop, calls.as_slice()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinStrategy {
Scan,
Look,
NearestCar,
Etd,
Destination,
Rsr,
Custom(String),
}
impl std::fmt::Display for BuiltinStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Scan => write!(f, "Scan"),
Self::Look => write!(f, "Look"),
Self::NearestCar => write!(f, "NearestCar"),
Self::Etd => write!(f, "Etd"),
Self::Destination => write!(f, "Destination"),
Self::Rsr => write!(f, "Rsr"),
Self::Custom(name) => write!(f, "Custom({name})"),
}
}
}
impl BuiltinStrategy {
#[must_use]
pub fn instantiate(&self) -> Option<Box<dyn DispatchStrategy>> {
match self {
Self::Scan => Some(Box::new(scan::ScanDispatch::new())),
Self::Look => Some(Box::new(look::LookDispatch::new())),
Self::NearestCar => Some(Box::new(nearest_car::NearestCarDispatch::new())),
Self::Etd => Some(Box::new(etd::EtdDispatch::default())),
Self::Destination => Some(Box::new(destination::DestinationDispatch::new())),
Self::Rsr => Some(Box::new(rsr::RsrDispatch::default())),
Self::Custom(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DispatchDecision {
GoToStop(EntityId),
Idle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineInfo {
entity: EntityId,
elevators: Vec<EntityId>,
serves: Vec<EntityId>,
}
impl LineInfo {
#[must_use]
pub const fn new(entity: EntityId, elevators: Vec<EntityId>, serves: Vec<EntityId>) -> Self {
Self {
entity,
elevators,
serves,
}
}
#[must_use]
pub const fn entity(&self) -> EntityId {
self.entity
}
#[must_use]
pub fn elevators(&self) -> &[EntityId] {
&self.elevators
}
#[must_use]
pub fn serves(&self) -> &[EntityId] {
&self.serves
}
pub(crate) const fn set_entity(&mut self, entity: EntityId) {
self.entity = entity;
}
pub(crate) const fn elevators_mut(&mut self) -> &mut Vec<EntityId> {
&mut self.elevators
}
pub(crate) const fn serves_mut(&mut self) -> &mut Vec<EntityId> {
&mut self.serves
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum HallCallMode {
#[default]
Classic,
Destination,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElevatorGroup {
id: GroupId,
name: String,
lines: Vec<LineInfo>,
hall_call_mode: HallCallMode,
ack_latency_ticks: u32,
elevator_entities: Vec<EntityId>,
stop_entities: Vec<EntityId>,
}
impl ElevatorGroup {
#[must_use]
pub fn new(id: GroupId, name: String, lines: Vec<LineInfo>) -> Self {
let mut group = Self {
id,
name,
lines,
hall_call_mode: HallCallMode::default(),
ack_latency_ticks: 0,
elevator_entities: Vec::new(),
stop_entities: Vec::new(),
};
group.rebuild_caches();
group
}
#[must_use]
pub const fn with_hall_call_mode(mut self, mode: HallCallMode) -> Self {
self.hall_call_mode = mode;
self
}
#[must_use]
pub const fn with_ack_latency_ticks(mut self, ticks: u32) -> Self {
self.ack_latency_ticks = ticks;
self
}
pub const fn set_hall_call_mode(&mut self, mode: HallCallMode) {
self.hall_call_mode = mode;
}
pub const fn set_ack_latency_ticks(&mut self, ticks: u32) {
self.ack_latency_ticks = ticks;
}
#[must_use]
pub const fn hall_call_mode(&self) -> HallCallMode {
self.hall_call_mode
}
#[must_use]
pub const fn ack_latency_ticks(&self) -> u32 {
self.ack_latency_ticks
}
#[must_use]
pub const fn id(&self) -> GroupId {
self.id
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn lines(&self) -> &[LineInfo] {
&self.lines
}
pub const fn lines_mut(&mut self) -> &mut Vec<LineInfo> {
&mut self.lines
}
#[must_use]
pub fn elevator_entities(&self) -> &[EntityId] {
&self.elevator_entities
}
#[must_use]
pub fn stop_entities(&self) -> &[EntityId] {
&self.stop_entities
}
#[must_use]
pub fn accepts_leg(&self, leg: &crate::components::RouteLeg) -> bool {
match leg.via {
crate::components::TransportMode::Group(g) => g == self.id,
crate::components::TransportMode::Line(l) => {
self.lines.iter().any(|li| li.entity() == l)
}
crate::components::TransportMode::Walk => false,
}
}
pub(crate) fn push_stop(&mut self, stop: EntityId) {
if !self.stop_entities.contains(&stop) {
self.stop_entities.push(stop);
}
}
pub(crate) fn push_elevator(&mut self, elevator: EntityId) {
if !self.elevator_entities.contains(&elevator) {
self.elevator_entities.push(elevator);
}
}
pub fn rebuild_caches(&mut self) {
self.elevator_entities = self
.lines
.iter()
.flat_map(|li| li.elevators.iter().copied())
.collect();
let mut stops: Vec<EntityId> = self
.lines
.iter()
.flat_map(|li| li.serves.iter().copied())
.collect();
stops.sort_unstable();
stops.dedup();
self.stop_entities = stops;
}
}
#[must_use]
pub fn elevator_line_serves<'a>(
world: &World,
groups: &'a [ElevatorGroup],
elevator: EntityId,
) -> Option<&'a [EntityId]> {
let line_eid = world.elevator(elevator)?.line();
groups
.iter()
.flat_map(ElevatorGroup::lines)
.find(|li| li.entity() == line_eid)
.map(LineInfo::serves)
}
pub type LineServesIndex<'a> = std::collections::HashMap<EntityId, &'a [EntityId]>;
#[must_use]
pub fn build_line_serves_index(groups: &[ElevatorGroup]) -> LineServesIndex<'_> {
let mut idx: LineServesIndex<'_> = std::collections::HashMap::new();
for li in groups.iter().flat_map(ElevatorGroup::lines) {
idx.insert(li.entity(), li.serves());
}
idx
}
#[must_use]
pub fn elevator_line_serves_indexed<'a>(
world: &World,
index: &LineServesIndex<'a>,
elevator: EntityId,
) -> Option<&'a [EntityId]> {
let line_eid = world.elevator(elevator)?.line();
index.get(&line_eid).copied()
}
#[non_exhaustive]
pub struct RankContext<'a> {
pub car: EntityId,
pub car_position: f64,
pub stop: EntityId,
pub stop_position: f64,
pub group: &'a ElevatorGroup,
pub manifest: &'a DispatchManifest,
pub world: &'a World,
}
impl std::fmt::Debug for RankContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RankContext")
.field("car", &self.car)
.field("car_position", &self.car_position)
.field("stop", &self.stop)
.field("stop_position", &self.stop_position)
.field("group", &self.group)
.field("manifest", &self.manifest)
.field("world", &"World { .. }")
.finish()
}
}
pub trait DispatchStrategy: Send + Sync {
fn pre_dispatch(
&mut self,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &mut World,
) {
}
fn prepare_car(
&mut self,
_car: EntityId,
_car_position: f64,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &World,
) {
}
fn rank(&self, ctx: &RankContext<'_>) -> Option<f64>;
fn fallback(
&mut self,
_car: EntityId,
_car_position: f64,
_group: &ElevatorGroup,
_manifest: &DispatchManifest,
_world: &World,
) -> DispatchDecision {
DispatchDecision::Idle
}
fn notify_removed(&mut self, _elevator: EntityId) {}
#[must_use]
fn builtin_id(&self) -> Option<BuiltinStrategy> {
None
}
#[must_use]
fn snapshot_config(&self) -> Option<String> {
None
}
fn restore_config(&mut self, _serialized: &str) -> Result<(), String> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AssignmentResult {
pub decisions: Vec<(EntityId, DispatchDecision)>,
}
#[derive(Default)]
pub(crate) struct DispatchScratch {
pub cost_matrix_mx: Option<pathfinding::matrix::Matrix<i64>>,
pub servicing: Vec<(EntityId, EntityId, f64)>,
pub pending_stops: Vec<(EntityId, f64)>,
pub idle_rider_destinations: HashSet<EntityId>,
pub lines_here: Vec<EntityId>,
pub pinned_pairs: Vec<(EntityId, EntityId)>,
pub committed_pairs: Vec<(EntityId, EntityId)>,
pub idle_elevators: Vec<(EntityId, f64)>,
}
impl DispatchScratch {
pub fn clear_all(&mut self) {
self.servicing.clear();
self.pending_stops.clear();
self.idle_rider_destinations.clear();
self.lines_here.clear();
self.pinned_pairs.clear();
self.committed_pairs.clear();
self.idle_elevators.clear();
}
}
const ASSIGNMENT_SENTINEL: i64 = 1 << 48;
const ASSIGNMENT_SCALE: f64 = 1_000_000.0;
fn scale_cost(cost: f64) -> i64 {
if !cost.is_finite() || cost < 0.0 {
debug_assert!(
cost.is_finite() && cost >= 0.0,
"DispatchStrategy::rank() returned invalid cost {cost}; must be finite and non-negative"
);
return ASSIGNMENT_SENTINEL;
}
(cost * ASSIGNMENT_SCALE)
.round()
.clamp(0.0, (ASSIGNMENT_SENTINEL - 1) as f64) as i64
}
fn pending_stops_minus_covered(
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
idle_cars: &[(EntityId, f64)],
scratch: &mut DispatchScratch,
) {
scratch.servicing.clear();
for &eid in group.elevator_entities() {
let Some(car) = world.elevator(eid) else {
continue;
};
let Some(target) = car.target_stop() else {
continue;
};
if !matches!(
car.phase(),
ElevatorPhase::MovingToStop(_)
| ElevatorPhase::DoorOpening
| ElevatorPhase::Loading
| ElevatorPhase::DoorClosing
) {
continue;
}
let remaining = car.weight_capacity().value() - car.current_load().value();
scratch.servicing.push((target, car.line(), remaining));
}
scratch.idle_rider_destinations.clear();
for &(car_eid, _) in idle_cars {
if let Some(car) = world.elevator(car_eid) {
for &rid in car.riders() {
if let Some(dest) = world.route(rid).and_then(Route::current_destination) {
scratch.idle_rider_destinations.insert(dest);
}
}
}
}
let mut lines_here: Vec<EntityId> = std::mem::take(&mut scratch.lines_here);
let servicing = &scratch.servicing;
let is_covered = |stop_eid: EntityId, lines_here: &mut Vec<EntityId>| -> bool {
lines_here.clear();
let mut capacity_here = 0.0;
for &(stop, line, rem) in servicing {
if stop == stop_eid {
lines_here.push(line);
capacity_here += rem;
}
}
if lines_here.is_empty() {
return false;
}
let mut total_weight = 0.0;
for rider in manifest.waiting_riders_at(stop_eid) {
let required_line = world
.route(rider.id)
.and_then(Route::current)
.and_then(|leg| match leg.via {
TransportMode::Line(l) => Some(l),
_ => None,
});
if let Some(required) = required_line
&& !lines_here.contains(&required)
{
return false;
}
total_weight += rider.weight.value();
}
total_weight <= capacity_here
};
scratch.pending_stops.clear();
for &stop in group.stop_entities() {
if !manifest.has_demand(stop) {
continue;
}
let keep =
scratch.idle_rider_destinations.contains(&stop) || !is_covered(stop, &mut lines_here);
if !keep {
continue;
}
if let Some(pos) = world.stop_position(stop) {
scratch.pending_stops.push((stop, pos));
}
}
scratch.lines_here = lines_here;
}
#[cfg(test)]
pub(crate) fn assign(
strategy: &mut dyn DispatchStrategy,
idle_cars: &[(EntityId, f64)],
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
) -> AssignmentResult {
let mut scratch = DispatchScratch::default();
assign_with_scratch(strategy, idle_cars, group, manifest, world, &mut scratch)
}
pub(crate) fn assign_with_scratch(
strategy: &mut dyn DispatchStrategy,
idle_cars: &[(EntityId, f64)],
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &World,
scratch: &mut DispatchScratch,
) -> AssignmentResult {
pending_stops_minus_covered(group, manifest, world, idle_cars, scratch);
let n = idle_cars.len();
let m = scratch.pending_stops.len();
if n == 0 {
return AssignmentResult {
decisions: Vec::new(),
};
}
let mut decisions: Vec<(EntityId, DispatchDecision)> = Vec::with_capacity(n);
if m == 0 {
for &(eid, pos) in idle_cars {
let d = strategy.fallback(eid, pos, group, manifest, world);
decisions.push((eid, d));
}
return AssignmentResult { decisions };
}
let cols = n.max(m);
match &mut scratch.cost_matrix_mx {
Some(mx) if mx.rows == n && mx.columns == cols => {
mx.fill(ASSIGNMENT_SENTINEL);
}
slot => {
*slot = Some(pathfinding::matrix::Matrix::new(
n,
cols,
ASSIGNMENT_SENTINEL,
));
}
}
let matrix_ref = scratch
.cost_matrix_mx
.as_mut()
.unwrap_or_else(|| unreachable!("cost_matrix_mx populated by match above"));
{
let pending_stops = &scratch.pending_stops;
for (i, &(car_eid, car_pos)) in idle_cars.iter().enumerate() {
strategy.prepare_car(car_eid, car_pos, group, manifest, world);
let restricted = world
.elevator(car_eid)
.map(crate::components::Elevator::restricted_stops);
let car_serves: Option<&[EntityId]> = world
.elevator(car_eid)
.map(crate::components::Elevator::line)
.and_then(|line_eid| {
group
.lines()
.iter()
.find(|li| li.entity() == line_eid)
.map(LineInfo::serves)
});
debug_assert!(
world.elevator(car_eid).is_none() || car_serves.is_some(),
"car {car_eid:?} on line not present in its group's lines list"
);
for (j, &(stop_eid, stop_pos)) in pending_stops.iter().enumerate() {
if restricted.is_some_and(|r| r.contains(&stop_eid)) {
continue; }
if car_serves.is_some_and(|s| !s.contains(&stop_eid)) {
continue; }
let ctx = RankContext {
car: car_eid,
car_position: car_pos,
stop: stop_eid,
stop_position: stop_pos,
group,
manifest,
world,
};
let scaled = strategy.rank(&ctx).map_or(ASSIGNMENT_SENTINEL, scale_cost);
matrix_ref[(i, j)] = scaled;
}
}
}
let matrix = &*matrix_ref;
let (_, assignments) = pathfinding::kuhn_munkres::kuhn_munkres_min(matrix);
for (i, &(car_eid, car_pos)) in idle_cars.iter().enumerate() {
let col = assignments[i];
if col < m && matrix[(i, col)] < ASSIGNMENT_SENTINEL {
let (stop_eid, _) = scratch.pending_stops[col];
decisions.push((car_eid, DispatchDecision::GoToStop(stop_eid)));
} else {
let d = strategy.fallback(car_eid, car_pos, group, manifest, world);
decisions.push((car_eid, d));
}
}
AssignmentResult { decisions }
}
pub trait RepositionStrategy: Send + Sync {
fn reposition(
&mut self,
idle_elevators: &[(EntityId, f64)],
stop_positions: &[(EntityId, f64)],
group: &ElevatorGroup,
world: &World,
out: &mut Vec<(EntityId, EntityId)>,
);
#[must_use]
fn builtin_id(&self) -> Option<BuiltinReposition> {
None
}
#[must_use]
fn min_arrival_log_window(&self) -> u64 {
0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BuiltinReposition {
SpreadEvenly,
ReturnToLobby,
DemandWeighted,
NearestIdle,
PredictiveParking,
Adaptive,
Custom(String),
}
impl std::fmt::Display for BuiltinReposition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SpreadEvenly => write!(f, "SpreadEvenly"),
Self::ReturnToLobby => write!(f, "ReturnToLobby"),
Self::DemandWeighted => write!(f, "DemandWeighted"),
Self::NearestIdle => write!(f, "NearestIdle"),
Self::PredictiveParking => write!(f, "PredictiveParking"),
Self::Adaptive => write!(f, "Adaptive"),
Self::Custom(name) => write!(f, "Custom({name})"),
}
}
}
impl BuiltinReposition {
#[must_use]
pub fn instantiate(&self) -> Option<Box<dyn RepositionStrategy>> {
match self {
Self::SpreadEvenly => Some(Box::new(reposition::SpreadEvenly)),
Self::ReturnToLobby => Some(Box::new(reposition::ReturnToLobby::new())),
Self::DemandWeighted => Some(Box::new(reposition::DemandWeighted)),
Self::NearestIdle => Some(Box::new(reposition::NearestIdle)),
Self::PredictiveParking => Some(Box::new(reposition::PredictiveParking::new())),
Self::Adaptive => Some(Box::new(reposition::AdaptiveParking::new())),
Self::Custom(_) => None,
}
}
}