From decbd895a332452d97e2a03047d102f3089ec69f Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 24 Apr 2026 10:48:50 +0200 Subject: [PATCH] refactor(api): rename Agent to Competitor and .player field to .rating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Competitor holds dynamic per-history state (message, last_time) for someone competing; its configuration lives in a Rating. AgentStore renamed to CompetitorStore to match. The internal `clean()` free function's parameter name changed from `agents` to `competitors` for consistency. Local variable names (agent_idx, this_agent) inside history.rs are left unchanged — they represent abstract identifiers, not Competitor instances. Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md. --- benches/batch.rs | 10 +-- src/agent.rs | 47 ------------ src/batch.rs | 45 +++++------ src/competitor.rs | 50 +++++++++++++ src/history.rs | 20 ++--- src/lib.rs | 3 +- src/storage/agent_store.rs | 125 ------------------------------- src/storage/competitor_store.rs | 127 ++++++++++++++++++++++++++++++++ src/storage/mod.rs | 4 +- 9 files changed, 219 insertions(+), 212 deletions(-) delete mode 100644 src/agent.rs create mode 100644 src/competitor.rs delete mode 100644 src/storage/agent_store.rs create mode 100644 src/storage/competitor_store.rs diff --git a/benches/batch.rs b/benches/batch.rs index 48b8879..095c635 100644 --- a/benches/batch.rs +++ b/benches/batch.rs @@ -1,7 +1,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use trueskill_tt::{ - BETA, GAMMA, KeyTable, MU, P_DRAW, Rating, SIGMA, agent::Agent, batch::Batch, - drift::ConstantDrift, gaussian::Gaussian, storage::AgentStore, + BETA, Competitor, GAMMA, KeyTable, MU, P_DRAW, Rating, SIGMA, batch::Batch, + drift::ConstantDrift, gaussian::Gaussian, storage::CompetitorStore, }; fn criterion_benchmark(criterion: &mut Criterion) { @@ -11,13 +11,13 @@ fn criterion_benchmark(criterion: &mut Criterion) { let b = index_map.get_or_create("b"); let c = index_map.get_or_create("c"); - let mut agents: AgentStore = AgentStore::new(); + let mut agents: CompetitorStore = CompetitorStore::new(); for agent in [a, b, c] { agents.insert( agent, - Agent { - player: Rating::new(Gaussian::from_ms(MU, SIGMA), BETA, ConstantDrift(GAMMA)), + Competitor { + rating: Rating::new(Gaussian::from_ms(MU, SIGMA), BETA, ConstantDrift(GAMMA)), ..Default::default() }, ); diff --git a/src/agent.rs b/src/agent.rs deleted file mode 100644 index 0dcd1e8..0000000 --- a/src/agent.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{ - N_INF, - drift::{ConstantDrift, Drift}, - gaussian::Gaussian, - rating::Rating, -}; - -#[derive(Debug)] -pub struct Agent { - pub player: Rating, - pub message: Gaussian, - pub last_time: i64, -} - -impl Agent { - pub(crate) fn receive(&self, elapsed: i64) -> Gaussian { - if self.message != N_INF { - self.message - .forget(self.player.drift.variance_delta(elapsed)) - } else { - self.player.prior - } - } -} - -impl Default for Agent { - fn default() -> Self { - Self { - player: Rating::default(), - message: N_INF, - last_time: i64::MIN, - } - } -} - -pub(crate) fn clean<'a, D: Drift + 'a, A: Iterator>>( - agents: A, - last_time: bool, -) { - for a in agents { - a.message = N_INF; - - if last_time { - a.last_time = i64::MIN; - } - } -} diff --git a/src/batch.rs b/src/batch.rs index 4b38af0..06bedca 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -7,7 +7,7 @@ use crate::{ game::Game, gaussian::Gaussian, rating::Rating, - storage::{AgentStore, SkillStore}, + storage::{CompetitorStore, SkillStore}, tuple_gt, tuple_max, }; @@ -50,9 +50,9 @@ impl Item { online: bool, forward: bool, skills: &SkillStore, - agents: &AgentStore, + agents: &CompetitorStore, ) -> Rating { - let r = &agents[self.agent].player; + let r = &agents[self.agent].rating; let skill = skills.get(self.agent).unwrap(); if online { @@ -91,7 +91,7 @@ impl Event { online: bool, forward: bool, skills: &SkillStore, - agents: &AgentStore, + agents: &CompetitorStore, ) -> Vec>> { self.teams .iter() @@ -130,7 +130,7 @@ impl Batch { composition: Vec>>, results: Vec>, weights: Vec>>, - agents: &AgentStore, + agents: &CompetitorStore, ) { let mut unique = Vec::with_capacity(10); @@ -216,7 +216,7 @@ impl Batch { .collect::>() } - pub fn iteration(&mut self, from: usize, agents: &AgentStore) { + pub fn iteration(&mut self, from: usize, agents: &CompetitorStore) { for event in self.events.iter_mut().skip(from) { let teams = event.within_priors(false, false, &self.skills, agents); let result = event.outputs(); @@ -237,7 +237,7 @@ impl Batch { } #[allow(dead_code)] - pub(crate) fn convergence(&mut self, agents: &AgentStore) -> usize { + pub(crate) fn convergence(&mut self, agents: &CompetitorStore) -> usize { let epsilon = 1e-6; let iterations = 20; @@ -269,21 +269,21 @@ impl Batch { pub(crate) fn backward_prior_out( &self, agent: &Index, - agents: &AgentStore, + agents: &CompetitorStore, ) -> Gaussian { let skill = self.skills.get(*agent).unwrap(); let n = skill.likelihood * skill.backward; - n.forget(agents[*agent].player.drift.variance_delta(skill.elapsed)) + n.forget(agents[*agent].rating.drift.variance_delta(skill.elapsed)) } - pub(crate) fn new_backward_info(&mut self, agents: &AgentStore) { + pub(crate) fn new_backward_info(&mut self, agents: &CompetitorStore) { for (agent, skill) in self.skills.iter_mut() { skill.backward = agents[agent].message; } self.iteration(0, agents); } - pub(crate) fn new_forward_info(&mut self, agents: &AgentStore) { + pub(crate) fn new_forward_info(&mut self, agents: &CompetitorStore) { for (agent, skill) in self.skills.iter_mut() { skill.forward = agents[agent].receive(skill.elapsed); } @@ -295,7 +295,7 @@ impl Batch { online: bool, targets: &[Index], forward: bool, - agents: &AgentStore, + agents: &CompetitorStore, ) -> f64 { // log_evidence is infrequent; a local arena avoids needing &mut self. let mut arena = ScratchArena::new(); @@ -400,7 +400,8 @@ mod tests { use super::*; use crate::{ - KeyTable, agent::Agent, drift::ConstantDrift, rating::Rating, storage::AgentStore, + KeyTable, competitor::Competitor, drift::ConstantDrift, rating::Rating, + storage::CompetitorStore, }; #[test] @@ -414,13 +415,13 @@ mod tests { let e = index_map.get_or_create("e"); let f = index_map.get_or_create("f"); - let mut agents: AgentStore = AgentStore::new(); + let mut agents: CompetitorStore = CompetitorStore::new(); for agent in [a, b, c, d, e, f] { agents.insert( agent, - Agent { - player: Rating::new( + Competitor { + rating: Rating::new( Gaussian::from_ms(25.0, 25.0 / 3.0), 25.0 / 6.0, ConstantDrift(25.0 / 300.0), @@ -490,13 +491,13 @@ mod tests { let e = index_map.get_or_create("e"); let f = index_map.get_or_create("f"); - let mut agents: AgentStore = AgentStore::new(); + let mut agents: CompetitorStore = CompetitorStore::new(); for agent in [a, b, c, d, e, f] { agents.insert( agent, - Agent { - player: Rating::new( + Competitor { + rating: Rating::new( Gaussian::from_ms(25.0, 25.0 / 3.0), 25.0 / 6.0, ConstantDrift(25.0 / 300.0), @@ -569,13 +570,13 @@ mod tests { let e = index_map.get_or_create("e"); let f = index_map.get_or_create("f"); - let mut agents: AgentStore = AgentStore::new(); + let mut agents: CompetitorStore = CompetitorStore::new(); for agent in [a, b, c, d, e, f] { agents.insert( agent, - Agent { - player: Rating::new( + Competitor { + rating: Rating::new( Gaussian::from_ms(25.0, 25.0 / 3.0), 25.0 / 6.0, ConstantDrift(25.0 / 300.0), diff --git a/src/competitor.rs b/src/competitor.rs new file mode 100644 index 0000000..f2f270b --- /dev/null +++ b/src/competitor.rs @@ -0,0 +1,50 @@ +use crate::{ + N_INF, + drift::{ConstantDrift, Drift}, + gaussian::Gaussian, + rating::Rating, +}; + +/// Per-history, temporal state for someone competing. +/// +/// Renamed from `Agent` in T2; the former `.player` field is now +/// `.rating` to match the `Player → Rating` rename. +#[derive(Debug)] +pub struct Competitor { + pub rating: Rating, + pub message: Gaussian, + pub last_time: i64, +} + +impl Competitor { + pub(crate) fn receive(&self, elapsed: i64) -> Gaussian { + if self.message != N_INF { + self.message + .forget(self.rating.drift.variance_delta(elapsed)) + } else { + self.rating.prior + } + } +} + +impl Default for Competitor { + fn default() -> Self { + Self { + rating: Rating::default(), + message: N_INF, + last_time: i64::MIN, + } + } +} + +pub(crate) fn clean<'a, D: Drift + 'a, C: Iterator>>( + competitors: C, + last_time: bool, +) { + for c in competitors { + c.message = N_INF; + if last_time { + c.last_time = i64::MIN; + } + } +} diff --git a/src/history.rs b/src/history.rs index e852aab..2d7c25e 100644 --- a/src/history.rs +++ b/src/history.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; use crate::{ BETA, GAMMA, Index, MU, N_INF, P_DRAW, SIGMA, - agent::{self, Agent}, batch::{self, Batch}, + competitor::{self, Competitor}, drift::{ConstantDrift, Drift}, gaussian::Gaussian, rating::Rating, sort_time, - storage::AgentStore, + storage::CompetitorStore, tuple_gt, tuple_max, }; @@ -70,7 +70,7 @@ impl HistoryBuilder { History { size: 0, batches: Vec::new(), - agents: AgentStore::new(), + agents: CompetitorStore::new(), time: self.time, mu: self.mu, sigma: self.sigma, @@ -106,7 +106,7 @@ impl Default for HistoryBuilder { pub struct History { size: usize, pub(crate) batches: Vec, - agents: AgentStore, + agents: CompetitorStore, time: bool, mu: f64, sigma: f64, @@ -121,7 +121,7 @@ impl Default for History { Self { size: 0, batches: Vec::new(), - agents: AgentStore::new(), + agents: CompetitorStore::new(), time: true, mu: MU, sigma: SIGMA, @@ -143,7 +143,7 @@ impl History { fn iteration(&mut self) -> (f64, f64) { let mut step = (0.0, 0.0); - agent::clean(self.agents.values_mut(), false); + competitor::clean(self.agents.values_mut(), false); for j in (0..self.batches.len() - 1).rev() { for agent in self.batches[j + 1].skills.keys() { @@ -162,7 +162,7 @@ impl History { .fold(step, |step, (a, old)| tuple_max(step, old.delta(new[a]))); } - agent::clean(self.agents.values_mut(), false); + competitor::clean(self.agents.values_mut(), false); for j in 1..self.batches.len() { for agent in self.batches[j - 1].skills.keys() { @@ -287,7 +287,7 @@ impl History { "(length(weights) > 0) & (length(composition) != length(weights))" ); - agent::clean(self.agents.values_mut(), true); + competitor::clean(self.agents.values_mut(), true); let mut this_agent = Vec::with_capacity(1024); @@ -301,8 +301,8 @@ impl History { if !self.agents.contains(*agent) { self.agents.insert( *agent, - Agent { - player: priors.remove(agent).unwrap_or_else(|| { + Competitor { + rating: priors.remove(agent).unwrap_or_else(|| { Rating::new( Gaussian::from_ms(self.mu, self.sigma), self.beta, diff --git a/src/lib.rs b/src/lib.rs index 0c17660..e1ba8d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,11 @@ use std::{ f64::consts::{FRAC_1_SQRT_2, FRAC_2_SQRT_PI, SQRT_2}, }; -pub mod agent; #[cfg(feature = "approx")] mod approx; pub(crate) mod arena; pub mod batch; +mod competitor; pub mod drift; mod error; pub(crate) mod factor; @@ -20,6 +20,7 @@ mod rating; pub(crate) mod schedule; pub mod storage; +pub use competitor::Competitor; pub use drift::{ConstantDrift, Drift}; pub use error::InferenceError; pub use game::Game; diff --git a/src/storage/agent_store.rs b/src/storage/agent_store.rs deleted file mode 100644 index 364a0a9..0000000 --- a/src/storage/agent_store.rs +++ /dev/null @@ -1,125 +0,0 @@ -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>>` so slots can be -/// absent without an explicit present mask. -#[derive(Debug)] -pub struct AgentStore { - agents: Vec>>, - n_present: usize, -} - -impl Default for AgentStore { - fn default() -> Self { - Self { - agents: Vec::new(), - n_present: 0, - } - } -} - -impl AgentStore { - 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) { - 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> { - self.agents.get(idx.0).and_then(|slot| slot.as_ref()) - } - - pub fn get_mut(&mut self, idx: Index) -> Option<&mut Agent> { - 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)> { - self.agents - .iter() - .enumerate() - .filter_map(|(i, slot)| slot.as_ref().map(|a| (Index(i), a))) - } - - pub fn iter_mut(&mut self) -> impl Iterator)> { - 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> { - self.agents.iter_mut().filter_map(|s| s.as_mut()) - } -} - -impl std::ops::Index for AgentStore { - type Output = Agent; - fn index(&self, idx: Index) -> &Agent { - self.get(idx).expect("agent not found at index") - } -} - -impl std::ops::IndexMut for AgentStore { - fn index_mut(&mut self, idx: Index) -> &mut Agent { - self.get_mut(idx).expect("agent not found at index") - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{agent::Agent, drift::ConstantDrift}; - - #[test] - fn insert_then_get() { - let mut store: AgentStore = 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 = AgentStore::new(); - store.insert(Index(2), Agent::default()); - store.insert(Index(0), Agent::default()); - store.insert(Index(5), Agent::default()); - let keys: Vec = 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 = AgentStore::new(); - store.insert(Index(3), Agent::default()); - let _ = &store[Index(3)]; - } -} diff --git a/src/storage/competitor_store.rs b/src/storage/competitor_store.rs new file mode 100644 index 0000000..b8f392f --- /dev/null +++ b/src/storage/competitor_store.rs @@ -0,0 +1,127 @@ +use crate::{Index, competitor::Competitor, drift::Drift}; + +/// Dense Vec-backed store for competitor state in History. +/// +/// Indexed directly by Index.0, eliminating HashMap hashing in the +/// forward/backward sweep. Uses `Vec>>` so slots can be +/// absent without an explicit present mask. +#[derive(Debug)] +pub struct CompetitorStore { + competitors: Vec>>, + n_present: usize, +} + +impl Default for CompetitorStore { + fn default() -> Self { + Self { + competitors: Vec::new(), + n_present: 0, + } + } +} + +impl CompetitorStore { + pub fn new() -> Self { + Self::default() + } + + fn ensure_capacity(&mut self, idx: usize) { + if idx >= self.competitors.len() { + self.competitors.resize_with(idx + 1, || None); + } + } + + pub fn insert(&mut self, idx: Index, competitor: Competitor) { + self.ensure_capacity(idx.0); + if self.competitors[idx.0].is_none() { + self.n_present += 1; + } + self.competitors[idx.0] = Some(competitor); + } + + pub fn get(&self, idx: Index) -> Option<&Competitor> { + self.competitors.get(idx.0).and_then(|slot| slot.as_ref()) + } + + pub fn get_mut(&mut self, idx: Index) -> Option<&mut Competitor> { + self.competitors + .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)> { + self.competitors + .iter() + .enumerate() + .filter_map(|(i, slot)| slot.as_ref().map(|a| (Index(i), a))) + } + + pub fn iter_mut(&mut self) -> impl Iterator)> { + self.competitors + .iter_mut() + .enumerate() + .filter_map(|(i, slot)| slot.as_mut().map(|a| (Index(i), a))) + } + + pub fn values_mut(&mut self) -> impl Iterator> { + self.competitors.iter_mut().filter_map(|s| s.as_mut()) + } +} + +impl std::ops::Index for CompetitorStore { + type Output = Competitor; + fn index(&self, idx: Index) -> &Competitor { + self.get(idx).expect("competitor not found at index") + } +} + +impl std::ops::IndexMut for CompetitorStore { + fn index_mut(&mut self, idx: Index) -> &mut Competitor { + self.get_mut(idx).expect("competitor not found at index") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{competitor::Competitor, drift::ConstantDrift}; + + #[test] + fn insert_then_get() { + let mut store: CompetitorStore = CompetitorStore::new(); + let idx = Index(7); + store.insert(idx, Competitor::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: CompetitorStore = CompetitorStore::new(); + store.insert(Index(2), Competitor::default()); + store.insert(Index(0), Competitor::default()); + store.insert(Index(5), Competitor::default()); + let keys: Vec = store.iter().map(|(i, _)| i).collect(); + assert_eq!(keys, vec![Index(0), Index(2), Index(5)]); + } + + #[test] + fn index_operator_works() { + let mut store: CompetitorStore = CompetitorStore::new(); + store.insert(Index(3), Competitor::default()); + let _ = &store[Index(3)]; + } +} diff --git a/src/storage/mod.rs b/src/storage/mod.rs index a77963d..1d91129 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,5 +1,5 @@ -mod agent_store; +mod competitor_store; mod skill_store; -pub use agent_store::AgentStore; +pub use competitor_store::CompetitorStore; pub(crate) use skill_store::SkillStore;