use crate::types::*;
use crate::tire::TireCompound;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RaceStrategy {
pub id: String,
pub starting_compound: TireCompound,
pub pit_stops: Vec<PitStop>,
pub fuel_strategy: FuelStrategy,
pub ers_plan: ErsDeploymentPlan,
pub expected_lap_times: BTreeMap<StintNumber, Vec<f32>>,
pub predicted_race_time: f32,
pub confidence: f32,
pub metadata: StrategyMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PitStop {
pub lap: LapNumber,
pub compound: TireCompound,
pub pit_loss: f32,
pub reason: PitStopReason,
pub confidence: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PitStopReason {
Mandatory,
TireDegradation,
TireDamage,
WeatherChange,
Undercut,
Overcut,
SafetyCar,
VirtualSafetyCar,
Opportunistic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StintNumber(pub u8);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FuelStrategy {
pub starting_fuel: f32,
pub fuel_saving_per_lap: f32,
pub fuel_saving_laps: Vec<LapNumber>,
pub minimum_buffer: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErsDeploymentPlan {
pub default_mode: ErsMode,
pub lap_overrides: BTreeMap<LapNumber, ErsMode>,
pub overtake_laps: Vec<LapNumber>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErsMode {
None,
Low,
Medium,
High,
Hotlap,
Overtake,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyMetadata {
pub generated_at: chrono::DateTime<chrono::Utc>,
pub num_simulations: u64,
pub contributing_agents: Vec<String>,
pub version_hash: Option<String>,
pub parent_strategy_id: Option<String>,
}
impl RaceStrategy {
pub fn num_pit_stops(&self) -> usize {
self.pit_stops.len()
}
pub fn pit_stop_on_lap(&self, lap: LapNumber) -> Option<&PitStop> {
self.pit_stops.iter().find(|ps| ps.lap == lap)
}
pub fn is_valid(&self, total_race_laps: u16) -> bool {
if self.pit_stops.is_empty() {
return false;
}
if self.pit_stops.iter().any(|ps| ps.lap.0 > total_race_laps) {
return false;
}
let mut compounds: Vec<_> = self.pit_stops
.iter()
.map(|ps| ps.compound)
.collect();
compounds.push(self.starting_compound);
compounds.sort();
compounds.dedup();
compounds.len() >= 2 || compounds.contains(&TireCompound::Intermediate) || compounds.contains(&TireCompound::Wet)
}
pub fn total_pit_loss(&self) -> f32 {
self.pit_stops.iter().map(|ps| ps.pit_loss).sum()
}
pub fn stint_for_lap(&self, lap: LapNumber) -> StintNumber {
let stint_count = self.pit_stops.iter().filter(|ps| ps.lap.0 < lap.0).count();
StintNumber(stint_count as u8)
}
pub fn compound_for_lap(&self, lap: LapNumber) -> TireCompound {
self.pit_stops
.iter()
.rev()
.find(|ps| ps.lap.0 < lap.0)
.map(|ps| ps.compound)
.unwrap_or(self.starting_compound)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyComparison {
pub strategy_a_id: String,
pub strategy_b_id: String,
pub time_delta: f32, pub risk_delta: f32, pub recommendation: ComparisonRecommendation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComparisonRecommendation {
PreferA,
PreferB,
Equivalent,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strategy_validity() {
let mut strategy = RaceStrategy {
id: "test-strat-1".to_string(),
starting_compound: TireCompound::C3,
pit_stops: vec![
PitStop {
lap: LapNumber(25),
compound: TireCompound::C2,
pit_loss: 22.5,
reason: PitStopReason::Mandatory,
confidence: 0.95,
},
],
fuel_strategy: FuelStrategy {
starting_fuel: 110.0,
fuel_saving_per_lap: 0.0,
fuel_saving_laps: vec![],
minimum_buffer: 2.0,
},
ers_plan: ErsDeploymentPlan {
default_mode: ErsMode::Medium,
lap_overrides: BTreeMap::new(),
overtake_laps: vec![],
},
expected_lap_times: BTreeMap::new(),
predicted_race_time: 5400.0,
confidence: 0.85,
metadata: StrategyMetadata {
generated_at: chrono::Utc::now(),
num_simulations: 10_000,
contributing_agents: vec!["strategy-agent".to_string()],
version_hash: None,
parent_strategy_id: None,
},
};
assert!(strategy.is_valid(50));
assert_eq!(strategy.num_pit_stops(), 1);
assert_eq!(strategy.total_pit_loss(), 22.5);
assert_eq!(strategy.compound_for_lap(LapNumber(20)), TireCompound::C3);
assert_eq!(strategy.compound_for_lap(LapNumber(30)), TireCompound::C2);
strategy.pit_stops[0].lap = LapNumber(100);
assert!(!strategy.is_valid(50));
}
}