use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::arrival_log::{ArrivalLog, CurrentTick, DEFAULT_ARRIVAL_WINDOW_TICKS};
use crate::entity::EntityId;
use crate::tagged_metrics::{MetricTags, TaggedMetric};
use crate::world::World;
use super::{ElevatorGroup, RepositionStrategy};
pub const DEFAULT_REPOSITION_COOLDOWN_TICKS: u64 = 240;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RepositionCooldowns {
pub eligible_at: HashMap<EntityId, u64>,
}
impl RepositionCooldowns {
#[must_use]
pub fn is_cooling_down(&self, car: EntityId, tick: u64) -> bool {
self.eligible_at
.get(&car)
.is_some_and(|eligible| tick < *eligible)
}
pub fn record_arrival(&mut self, car: EntityId, arrival_tick: u64) {
self.eligible_at
.insert(car, arrival_tick + DEFAULT_REPOSITION_COOLDOWN_TICKS);
}
pub fn remap_entity_ids(&mut self, id_remap: &HashMap<EntityId, EntityId>) {
let remapped: HashMap<EntityId, u64> = std::mem::take(&mut self.eligible_at)
.into_iter()
.filter_map(|(old, eligible)| id_remap.get(&old).map(|&new| (new, eligible)))
.collect();
self.eligible_at = remapped;
}
}
pub struct SpreadEvenly;
impl RepositionStrategy for SpreadEvenly {
fn reposition(
&mut self,
idle_elevators: &[(EntityId, f64)],
stop_positions: &[(EntityId, f64)],
group: &ElevatorGroup,
world: &World,
out: &mut Vec<(EntityId, EntityId)>,
) {
if idle_elevators.is_empty() || stop_positions.is_empty() {
return;
}
let mut occupied: Vec<f64> = group
.elevator_entities()
.iter()
.filter_map(|&eid| {
if idle_elevators.iter().any(|(ie, _)| *ie == eid) {
return None;
}
intended_position(eid, world)
})
.collect();
for &(elev_eid, elev_pos) in idle_elevators {
let best = stop_positions.iter().max_by(|a, b| {
let min_a = min_distance_to(a.1, &occupied);
let min_b = min_distance_to(b.1, &occupied);
min_a.total_cmp(&min_b).then_with(|| {
let dist_a = (a.1 - elev_pos).abs();
let dist_b = (b.1 - elev_pos).abs();
dist_b.total_cmp(&dist_a)
})
});
if let Some(&(stop_eid, stop_pos)) = best {
if (stop_pos - elev_pos).abs() > 1e-6 {
out.push((elev_eid, stop_eid));
}
occupied.push(stop_pos);
}
}
}
fn builtin_id(&self) -> Option<super::BuiltinReposition> {
Some(super::BuiltinReposition::SpreadEvenly)
}
}
pub struct ReturnToLobby {
pub home_stop_index: usize,
}
impl ReturnToLobby {
#[must_use]
pub const fn new() -> Self {
Self { home_stop_index: 0 }
}
#[must_use]
pub const fn with_home(index: usize) -> Self {
Self {
home_stop_index: index,
}
}
}
impl Default for ReturnToLobby {
fn default() -> Self {
Self::new()
}
}
impl RepositionStrategy for ReturnToLobby {
fn reposition(
&mut self,
idle_elevators: &[(EntityId, f64)],
stop_positions: &[(EntityId, f64)],
_group: &ElevatorGroup,
_world: &World,
out: &mut Vec<(EntityId, EntityId)>,
) {
let Some(&(home_eid, home_pos)) = stop_positions.get(self.home_stop_index) else {
return;
};
out.extend(
idle_elevators
.iter()
.filter(|(_, pos)| (*pos - home_pos).abs() > 1e-6)
.map(|&(eid, _)| (eid, home_eid)),
);
}
fn builtin_id(&self) -> Option<super::BuiltinReposition> {
Some(super::BuiltinReposition::ReturnToLobby)
}
}
pub struct DemandWeighted;
impl RepositionStrategy for DemandWeighted {
fn reposition(
&mut self,
idle_elevators: &[(EntityId, f64)],
stop_positions: &[(EntityId, f64)],
group: &ElevatorGroup,
world: &World,
out: &mut Vec<(EntityId, EntityId)>,
) {
if idle_elevators.is_empty() || stop_positions.is_empty() {
return;
}
let tags = world.resource::<MetricTags>();
let mut scored: Vec<(EntityId, f64, f64)> = stop_positions
.iter()
.map(|&(stop_eid, stop_pos)| {
let demand = tags
.and_then(|t| {
t.tags_for(stop_eid)
.iter()
.filter_map(|tag| t.metric(tag).map(TaggedMetric::total_delivered))
.max()
})
.unwrap_or(0) as f64;
(stop_eid, stop_pos, demand + 1.0)
})
.collect();
scored.sort_by(|a, b| b.2.total_cmp(&a.2));
assign_greedy_by_score(&scored, idle_elevators, group, world, out);
}
fn builtin_id(&self) -> Option<super::BuiltinReposition> {
Some(super::BuiltinReposition::DemandWeighted)
}
}
pub struct PredictiveParking {
window_ticks: u64,
}
impl PredictiveParking {
#[must_use]
pub const fn new() -> Self {
Self {
window_ticks: DEFAULT_ARRIVAL_WINDOW_TICKS,
}
}
#[must_use]
pub const fn with_window_ticks(window_ticks: u64) -> Self {
assert!(
window_ticks > 0,
"PredictiveParking::with_window_ticks requires a positive window"
);
Self { window_ticks }
}
}
impl Default for PredictiveParking {
fn default() -> Self {
Self::new()
}
}
impl RepositionStrategy for PredictiveParking {
fn reposition(
&mut self,
idle_elevators: &[(EntityId, f64)],
stop_positions: &[(EntityId, f64)],
group: &ElevatorGroup,
world: &World,
out: &mut Vec<(EntityId, EntityId)>,
) {
if idle_elevators.is_empty() || stop_positions.is_empty() {
return;
}
let Some(log) = world.resource::<ArrivalLog>() else {
return;
};
let now = world.resource::<CurrentTick>().map_or(0, |ct| ct.0);
let mut scored: Vec<(EntityId, f64, u64)> = stop_positions
.iter()
.filter_map(|&(sid, pos)| {
let count = log.arrivals_in_window(sid, now, self.window_ticks);
(count > 0).then_some((sid, pos, count))
})
.collect();
if scored.is_empty() {
return;
}
scored.sort_by_key(|(_, _, count)| std::cmp::Reverse(*count));
assign_greedy_by_score(&scored, idle_elevators, group, world, out);
}
fn builtin_id(&self) -> Option<super::BuiltinReposition> {
Some(super::BuiltinReposition::PredictiveParking)
}
}
pub struct AdaptiveParking {
return_to_lobby: ReturnToLobby,
predictive: PredictiveParking,
}
impl AdaptiveParking {
#[must_use]
pub const fn new() -> Self {
Self {
return_to_lobby: ReturnToLobby::new(),
predictive: PredictiveParking::new(),
}
}
#[must_use]
pub const fn with_home(mut self, index: usize) -> Self {
self.return_to_lobby = ReturnToLobby::with_home(index);
self
}
#[must_use]
pub const fn with_window_ticks(mut self, window_ticks: u64) -> Self {
self.predictive = PredictiveParking::with_window_ticks(window_ticks);
self
}
}
impl Default for AdaptiveParking {
fn default() -> Self {
Self::new()
}
}
impl RepositionStrategy for AdaptiveParking {
fn reposition(
&mut self,
idle_elevators: &[(EntityId, f64)],
stop_positions: &[(EntityId, f64)],
group: &ElevatorGroup,
world: &World,
out: &mut Vec<(EntityId, EntityId)>,
) {
use crate::traffic_detector::{TrafficDetector, TrafficMode};
let mode = world
.resource::<TrafficDetector>()
.map_or(TrafficMode::InterFloor, TrafficDetector::current_mode);
match mode {
TrafficMode::Idle => {
}
TrafficMode::UpPeak => {
self.return_to_lobby
.reposition(idle_elevators, stop_positions, group, world, out);
}
TrafficMode::DownPeak | TrafficMode::InterFloor => {
self.predictive
.reposition(idle_elevators, stop_positions, group, world, out);
}
}
}
fn builtin_id(&self) -> Option<super::BuiltinReposition> {
Some(super::BuiltinReposition::Adaptive)
}
}
pub struct NearestIdle;
impl RepositionStrategy for NearestIdle {
fn reposition(
&mut self,
_idle_elevators: &[(EntityId, f64)],
_stop_positions: &[(EntityId, f64)],
_group: &ElevatorGroup,
_world: &World,
_out: &mut Vec<(EntityId, EntityId)>,
) {
}
fn builtin_id(&self) -> Option<super::BuiltinReposition> {
Some(super::BuiltinReposition::NearestIdle)
}
}
fn assign_greedy_by_score<S>(
scored: &[(EntityId, f64, S)],
idle_elevators: &[(EntityId, f64)],
group: &ElevatorGroup,
world: &World,
out: &mut Vec<(EntityId, EntityId)>,
) {
let mut occupied: Vec<f64> = group
.elevator_entities()
.iter()
.filter_map(|&eid| {
if idle_elevators.iter().any(|(ie, _)| *ie == eid) {
return None;
}
intended_position(eid, world)
})
.collect();
let mut assigned: Vec<EntityId> = Vec::new();
for (stop_eid, stop_pos, _) in scored {
if min_distance_to(*stop_pos, &occupied) < 1e-6 {
continue;
}
let closest = idle_elevators
.iter()
.filter(|(eid, _)| !assigned.contains(eid))
.min_by(|a, b| (a.1 - stop_pos).abs().total_cmp(&(b.1 - stop_pos).abs()));
if let Some(&(elev_eid, elev_pos)) = closest
&& (elev_pos - stop_pos).abs() > 1e-6
{
out.push((elev_eid, *stop_eid));
assigned.push(elev_eid);
occupied.push(*stop_pos);
}
if assigned.len() == idle_elevators.len() {
break;
}
}
}
fn intended_position(eid: EntityId, world: &World) -> Option<f64> {
if let Some(car) = world.elevator(eid)
&& let Some(target) = car.target_stop()
&& let Some(target_pos) = world.stop_position(target)
{
return Some(target_pos);
}
world.position(eid).map(|p| p.value)
}
fn min_distance_to(pos: f64, others: &[f64]) -> f64 {
if others.is_empty() {
return f64::INFINITY;
}
others
.iter()
.map(|&o| (pos - o).abs())
.fold(f64::INFINITY, f64::min)
}