refactor(history): replace HashMap<Index, Agent<D>> with dense AgentStore<D>

AgentStore<D> is a Vec<Option<Agent<D>>>-backed store indexed directly
by Index.0, eliminating per-iteration hashing in the cross-history
forward/backward sweep. Implements Index<Index>/IndexMut<Index> for
ergonomic agent access.

AgentStore is public (so benches/batch.rs can use it). SkillStore
remains pub(crate) since Skill is pub(crate) in batch.rs.

HashMap<Index, _> is now only used for the posteriors() return value
(temporary; will be replaced in T2 with a proper typed return) and
for the add_events_with_prior(priors: HashMap<Index, Player<D>>) API
(also T2 target).

Part of T0 engine redesign.
This commit is contained in:
2026-04-24 07:15:21 +02:00
parent 8f60258dba
commit 49d2b317da
6 changed files with 179 additions and 60 deletions

125
src/storage/agent_store.rs Normal file
View File

@@ -0,0 +1,125 @@
use crate::{Index, agent::Agent, drift::Drift};
/// Dense Vec-backed store for agent state in History.
///
/// Indexed directly by Index.0, eliminating HashMap hashing in the
/// forward/backward sweep. Uses `Vec<Option<Agent<D>>>` so slots can be
/// absent without an explicit present mask.
#[derive(Debug)]
pub struct AgentStore<D: Drift> {
agents: Vec<Option<Agent<D>>>,
n_present: usize,
}
impl<D: Drift> Default for AgentStore<D> {
fn default() -> Self {
Self {
agents: Vec::new(),
n_present: 0,
}
}
}
impl<D: Drift> AgentStore<D> {
pub fn new() -> Self {
Self::default()
}
fn ensure_capacity(&mut self, idx: usize) {
if idx >= self.agents.len() {
self.agents.resize_with(idx + 1, || None);
}
}
pub fn insert(&mut self, idx: Index, agent: Agent<D>) {
self.ensure_capacity(idx.0);
if self.agents[idx.0].is_none() {
self.n_present += 1;
}
self.agents[idx.0] = Some(agent);
}
pub fn get(&self, idx: Index) -> Option<&Agent<D>> {
self.agents.get(idx.0).and_then(|slot| slot.as_ref())
}
pub fn get_mut(&mut self, idx: Index) -> Option<&mut Agent<D>> {
self.agents.get_mut(idx.0).and_then(|slot| slot.as_mut())
}
pub fn contains(&self, idx: Index) -> bool {
self.get(idx).is_some()
}
pub fn len(&self) -> usize {
self.n_present
}
pub fn is_empty(&self) -> bool {
self.n_present == 0
}
pub fn iter(&self) -> impl Iterator<Item = (Index, &Agent<D>)> {
self.agents
.iter()
.enumerate()
.filter_map(|(i, slot)| slot.as_ref().map(|a| (Index(i), a)))
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Index, &mut Agent<D>)> {
self.agents
.iter_mut()
.enumerate()
.filter_map(|(i, slot)| slot.as_mut().map(|a| (Index(i), a)))
}
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Agent<D>> {
self.agents.iter_mut().filter_map(|s| s.as_mut())
}
}
impl<D: Drift> std::ops::Index<Index> for AgentStore<D> {
type Output = Agent<D>;
fn index(&self, idx: Index) -> &Agent<D> {
self.get(idx).expect("agent not found at index")
}
}
impl<D: Drift> std::ops::IndexMut<Index> for AgentStore<D> {
fn index_mut(&mut self, idx: Index) -> &mut Agent<D> {
self.get_mut(idx).expect("agent not found at index")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{agent::Agent, drift::ConstantDrift, player::Player};
#[test]
fn insert_then_get() {
let mut store: AgentStore<ConstantDrift> = AgentStore::new();
let idx = Index(7);
store.insert(idx, Agent::default());
assert!(store.contains(idx));
assert_eq!(store.len(), 1);
assert!(store.get(idx).is_some());
}
#[test]
fn iter_in_index_order() {
let mut store: AgentStore<ConstantDrift> = AgentStore::new();
store.insert(Index(2), Agent::default());
store.insert(Index(0), Agent::default());
store.insert(Index(5), Agent::default());
let keys: Vec<Index> = store.iter().map(|(i, _)| i).collect();
assert_eq!(keys, vec![Index(0), Index(2), Index(5)]);
}
#[test]
fn index_operator_works() {
let mut store: AgentStore<ConstantDrift> = AgentStore::new();
store.insert(Index(3), Agent::default());
let _ = &store[Index(3)];
}
}

View File

@@ -1,3 +1,5 @@
mod agent_store;
mod skill_store;
pub use agent_store::AgentStore;
pub(crate) use skill_store::SkillStore;