use crate::stats::calculators::standard_soccar_boost_pad_layout;
use crate::*;
use std::collections::{HashMap, HashSet};
const STANDARD_PAD_MATCH_RADIUS_SMALL: f32 = 450.0;
const STANDARD_PAD_MATCH_RADIUS_BIG: f32 = 1000.0;
#[derive(Debug, Clone, Default)]
pub(crate) struct BoostPadResolutionState {
observed_pad_positions: HashMap<String, PadPositionEstimate>,
known_pad_indices: HashMap<String, usize>,
}
impl BoostPadResolutionState {
pub(crate) fn observe_pickup(&mut self, pad_id: &str, observed_position: glam::Vec3) {
self.observed_pad_positions
.entry(pad_id.to_owned())
.or_default()
.observe(observed_position);
if self.known_pad_indices.contains_key(pad_id) {
return;
}
if let Some((index, _)) = self.infer_pad_details_from_position(pad_id, observed_position) {
self.known_pad_indices.insert(pad_id.to_owned(), index);
}
}
pub(crate) fn resolved_boost_pads(&self) -> Vec<ResolvedBoostPad> {
standard_soccar_boost_pad_layout()
.iter()
.enumerate()
.map(|(index, (position, size))| ResolvedBoostPad {
index,
pad_id: self
.known_pad_indices
.iter()
.find_map(|(pad_id, pad_index)| (*pad_index == index).then(|| pad_id.clone())),
size: *size,
position: glam_to_vec(position),
})
.collect()
}
fn estimated_pad_position(&self, pad_id: &str) -> Option<glam::Vec3> {
self.observed_pad_positions
.get(pad_id)
.and_then(PadPositionEstimate::mean)
}
fn observed_pad_positions(&self, pad_id: &str) -> &[glam::Vec3] {
self.observed_pad_positions
.get(pad_id)
.map(PadPositionEstimate::observations)
.unwrap_or(&[])
}
fn pad_match_radius(pad_size: BoostPadSize) -> f32 {
match pad_size {
BoostPadSize::Big => STANDARD_PAD_MATCH_RADIUS_BIG,
BoostPadSize::Small => STANDARD_PAD_MATCH_RADIUS_SMALL,
}
}
fn infer_pad_details_from_position(
&self,
pad_id: &str,
observed_position: glam::Vec3,
) -> Option<(usize, BoostPadSize)> {
if let Some(index) = self.known_pad_indices.get(pad_id).copied() {
let (_, size) = standard_soccar_boost_pad_layout().get(index)?;
return Some((index, *size));
}
let observed_position = self
.estimated_pad_position(pad_id)
.unwrap_or(observed_position);
let layout = standard_soccar_boost_pad_layout();
let used_indices: HashSet<usize> = self.known_pad_indices.values().copied().collect();
let observed_positions = self.observed_pad_positions(pad_id);
let best_candidate = |allow_used: bool| {
layout
.iter()
.enumerate()
.filter(|(index, _)| allow_used || !used_indices.contains(index))
.filter_map(|(index, (candidate_position, size))| {
let radius = Self::pad_match_radius(*size);
let mut vote_count = 0usize;
let mut total_vote_distance = 0.0f32;
let mut best_vote_distance = f32::INFINITY;
for position in observed_positions {
let distance = position.distance(*candidate_position);
if distance <= radius {
vote_count += 1;
total_vote_distance += distance;
best_vote_distance = best_vote_distance.min(distance);
}
}
if vote_count == 0 {
return None;
}
let representative_distance = observed_position.distance(*candidate_position);
Some((
index,
*size,
vote_count,
total_vote_distance / vote_count as f32,
best_vote_distance,
representative_distance,
))
})
.max_by(|left, right| {
left.2
.cmp(&right.2)
.then_with(|| right.3.partial_cmp(&left.3).unwrap())
.then_with(|| right.4.partial_cmp(&left.4).unwrap())
.then_with(|| right.5.partial_cmp(&left.5).unwrap())
})
.map(|(index, size, _, _, _, _)| (index, size))
};
best_candidate(false).or_else(|| best_candidate(true))
}
}
#[derive(Debug, Clone, Default)]
struct PadPositionEstimate {
observations: Vec<glam::Vec3>,
}
impl PadPositionEstimate {
fn observe(&mut self, position: glam::Vec3) {
self.observations.push(position);
}
fn observations(&self) -> &[glam::Vec3] {
self.observations.as_slice()
}
fn mean(&self) -> Option<glam::Vec3> {
if self.observations.is_empty() {
return None;
}
let sum = self
.observations
.iter()
.copied()
.fold(glam::Vec3::ZERO, |acc, position| acc + position);
Some(sum / self.observations.len() as f32)
}
}