use crate::components::{CarCall, ElevatorPhase};
use crate::traffic_detector::{TrafficDetector, TrafficMode};
use super::{DispatchStrategy, RankContext, pair_is_useful};
fn peak_scaling(ctx: &RankContext<'_>, multiplier: f64) -> f64 {
let mode = ctx
.world
.resource::<TrafficDetector>()
.map_or(TrafficMode::Idle, TrafficDetector::current_mode);
match mode {
TrafficMode::UpPeak | TrafficMode::DownPeak => multiplier,
_ => 1.0,
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct RsrDispatch {
pub eta_weight: f64,
pub wrong_direction_penalty: f64,
pub coincident_car_call_bonus: f64,
pub load_penalty_coeff: f64,
pub peak_direction_multiplier: f64,
#[serde(default)]
pub age_linear_weight: f64,
}
impl RsrDispatch {
#[must_use]
pub const fn new() -> Self {
Self {
eta_weight: 1.0,
wrong_direction_penalty: 0.0,
coincident_car_call_bonus: 0.0,
load_penalty_coeff: 0.0,
peak_direction_multiplier: 1.0,
age_linear_weight: 0.0,
}
}
#[must_use]
pub const fn tuned() -> Self {
Self {
eta_weight: 1.0,
wrong_direction_penalty: 15.0,
coincident_car_call_bonus: 5.0,
load_penalty_coeff: 3.0,
peak_direction_multiplier: 2.0,
age_linear_weight: 0.002,
}
}
#[must_use]
pub fn with_wrong_direction_penalty(mut self, weight: f64) -> Self {
assert!(
weight.is_finite() && weight >= 0.0,
"wrong_direction_penalty must be finite and non-negative, got {weight}"
);
self.wrong_direction_penalty = weight;
self
}
#[must_use]
pub fn with_coincident_car_call_bonus(mut self, weight: f64) -> Self {
assert!(
weight.is_finite() && weight >= 0.0,
"coincident_car_call_bonus must be finite and non-negative, got {weight}"
);
self.coincident_car_call_bonus = weight;
self
}
#[must_use]
pub fn with_load_penalty_coeff(mut self, weight: f64) -> Self {
assert!(
weight.is_finite() && weight >= 0.0,
"load_penalty_coeff must be finite and non-negative, got {weight}"
);
self.load_penalty_coeff = weight;
self
}
#[must_use]
pub fn with_eta_weight(mut self, weight: f64) -> Self {
assert!(
weight.is_finite() && weight >= 0.0,
"eta_weight must be finite and non-negative, got {weight}"
);
self.eta_weight = weight;
self
}
#[must_use]
pub fn with_age_linear_weight(mut self, weight: f64) -> Self {
assert!(
weight.is_finite() && weight >= 0.0,
"age_linear_weight must be finite and non-negative, got {weight}"
);
self.age_linear_weight = weight;
self
}
#[must_use]
pub fn with_peak_direction_multiplier(mut self, factor: f64) -> Self {
assert!(
factor.is_finite() && factor >= 1.0,
"peak_direction_multiplier must be finite and ≥ 1.0, got {factor}"
);
self.peak_direction_multiplier = factor;
self
}
}
impl Default for RsrDispatch {
fn default() -> Self {
Self::tuned()
}
}
impl DispatchStrategy for RsrDispatch {
fn rank(&self, ctx: &RankContext<'_>) -> Option<f64> {
if !pair_is_useful(ctx, true) {
return None;
}
let car = ctx.world.elevator(ctx.car)?;
let distance = (ctx.car_position - ctx.stop_position).abs();
let max_speed = car.max_speed.value();
if max_speed <= 0.0 {
return None;
}
let travel_time = distance / max_speed;
let mut cost = self.eta_weight * travel_time;
if self.wrong_direction_penalty > 0.0
&& let Some(target) = car.phase.moving_target()
&& let Some(target_pos) = ctx.world.stop_position(target)
{
let car_going_up = target_pos > ctx.car_position;
let car_going_down = target_pos < ctx.car_position;
let cand_above = ctx.stop_position > ctx.car_position;
let cand_below = ctx.stop_position < ctx.car_position;
if (car_going_up && cand_below) || (car_going_down && cand_above) {
let scaled = self.wrong_direction_penalty
* peak_scaling(ctx, self.peak_direction_multiplier);
cost += scaled;
}
}
if self.coincident_car_call_bonus > 0.0
&& ctx
.manifest
.car_calls_for(ctx.car)
.iter()
.any(|c: &CarCall| c.floor == ctx.stop)
{
cost -= self.coincident_car_call_bonus;
}
if self.load_penalty_coeff > 0.0 && car.phase() != ElevatorPhase::Idle {
let capacity = car.weight_capacity().value();
if capacity > 0.0 {
let load_ratio = (car.current_load().value() / capacity).clamp(0.0, 1.0);
cost += self.load_penalty_coeff * load_ratio;
}
}
if self.age_linear_weight > 0.0 {
let wait_sum: f64 = ctx
.manifest
.waiting_riders_at(ctx.stop)
.iter()
.map(|r| r.wait_ticks as f64)
.sum();
cost = crate::fp::fma(self.age_linear_weight, -wait_sum, cost);
}
let cost = cost.max(0.0);
if cost.is_finite() { Some(cost) } else { None }
}
fn builtin_id(&self) -> Option<super::BuiltinStrategy> {
Some(super::BuiltinStrategy::Rsr)
}
fn snapshot_config(&self) -> Option<String> {
ron::to_string(self).ok()
}
fn restore_config(&mut self, serialized: &str) -> Result<(), String> {
let restored: Self = ron::from_str(serialized).map_err(|e| e.to_string())?;
*self = restored;
Ok(())
}
}