use std::collections::HashMap; use crate::{Index, time_slice::Skill}; /// Compact per-slice store for skill state, addressed by a slice-local slot. /// /// `skills` holds one entry per competitor **in this slice**, so memory is /// O(competitors in the slice). It used to be a dense `Vec` indexed by /// the global `Index.0`, which made a slice's footprint O(largest index it /// touches): a single 1v1 game between competitors 19998 and 19999 reserved /// 20,000 slots. /// /// The dense layout existed to keep `HashMap` hashing out of the inner /// convergence loop, and that property is preserved. `slots` is consulted only /// while building a slice; every hot-path access goes through /// [`SkillStore::at`] / [`SkillStore::at_mut`] with a slot resolved once at /// ingestion and cached on the event's `Item`. #[derive(Debug, Default)] pub struct SkillStore { skills: Vec, /// Slot -> global index, parallel to `skills`, so iteration can report the /// global index without a reverse lookup. indices: Vec, slots: HashMap, } impl SkillStore { pub fn new() -> Self { Self::default() } /// Resolve a global index to this slice's slot, if the competitor is here. /// /// This hashes. Call it at ingestion and cache the result; do not call it /// from the convergence loop. pub fn slot_of(&self, idx: Index) -> Option { self.slots.get(&idx).copied() } /// Skill at a slot resolved earlier by [`SkillStore::slot_of`]. /// /// # Panics /// /// Panics if `slot` is out of range, which means it came from a different /// slice's store. pub fn at(&self, slot: u32) -> &Skill { &self.skills[slot as usize] } /// Mutable counterpart to [`SkillStore::at`]. /// /// # Panics /// /// Panics if `slot` is out of range. pub fn at_mut(&mut self, slot: u32) -> &mut Skill { &mut self.skills[slot as usize] } /// Insert or overwrite a competitor's skill, returning its slot. pub fn insert(&mut self, idx: Index, skill: Skill) -> u32 { match self.slots.get(&idx) { Some(&slot) => { self.skills[slot as usize] = skill; slot } None => { let slot = u32::try_from(self.skills.len()) .expect("a time slice cannot hold more than u32::MAX competitors"); self.skills.push(skill); self.indices.push(idx); self.slots.insert(idx, slot); slot } } } pub fn get(&self, idx: Index) -> Option<&Skill> { self.slot_of(idx).map(|slot| self.at(slot)) } pub fn get_mut(&mut self, idx: Index) -> Option<&mut Skill> { self.slot_of(idx) .map(|slot| &mut self.skills[slot as usize]) } /// Whether a competitor is present in this slice. Test-only. #[cfg(test)] pub fn contains(&self, idx: Index) -> bool { self.slots.contains_key(&idx) } /// Number of competitors in this slice. Test-only. #[cfg(test)] pub fn len(&self) -> usize { self.skills.len() } /// Slots actually allocated — the quantity #17 is about, and NOT the same /// as `len` for every possible implementation. /// /// A store indexed by the global `Index` must report `max_index + 1` here /// while reporting the true competitor count from `len`, which is exactly /// how the original defect hid. Tests that mean to pin the footprint must /// assert on this. #[cfg(test)] pub fn allocated_slots(&self) -> usize { self.skills.len() } /// Iterate in slot order — the order competitors were first seen in this /// slice. Deterministic for a given event order, which is what the /// cross-thread determinism test relies on. pub fn iter(&self) -> impl Iterator { self.indices.iter().copied().zip(self.skills.iter()) } pub fn iter_mut(&mut self) -> impl Iterator { self.indices.iter().copied().zip(self.skills.iter_mut()) } pub fn keys(&self) -> impl Iterator + '_ { self.indices.iter().copied() } } #[cfg(test)] mod tests { use super::*; #[test] fn insert_then_get() { let mut store = SkillStore::new(); let idx = Index(3); store.insert(idx, Skill::default()); assert!(store.contains(idx)); assert_eq!(store.len(), 1); assert!(store.get(idx).is_some()); } #[test] fn missing_returns_none() { let store = SkillStore::new(); assert!(store.get(Index(0)).is_none()); assert!(!store.contains(Index(42))); } #[test] fn iter_reports_global_indices() { let mut store = SkillStore::new(); store.insert(Index(0), Skill::default()); store.insert(Index(5), Skill::default()); let keys: Vec = store.keys().collect(); assert_eq!(keys, vec![Index(0), Index(5)]); } #[test] fn double_insert_does_not_double_count() { let mut store = SkillStore::new(); store.insert(Index(2), Skill::default()); store.insert(Index(2), Skill::default()); assert_eq!(store.len(), 1); } /// The defect in #17: a slice holding two competitors must cost the same /// whether their indices are small or large. #[test] fn footprint_is_independent_of_index_magnitude() { let mut low = SkillStore::new(); low.insert(Index(0), Skill::default()); low.insert(Index(1), Skill::default()); let mut high = SkillStore::new(); high.insert(Index(19_998), Skill::default()); high.insert(Index(19_999), Skill::default()); assert_eq!(low.len(), high.len()); assert_eq!(low.skills.capacity(), high.skills.capacity()); } #[test] fn slot_survives_reinsert() { let mut store = SkillStore::new(); let first = store.insert(Index(7), Skill::default()); let again = store.insert(Index(7), Skill::default()); assert_eq!(first, again); } }