refactor(api): generify Drift, Rating, Competitor, TimeSlice, CompetitorStore, History over T: Time
Drift now takes &T -> &T and is generic over the time axis. Untimed impls return elapsed=0. ConstantDrift impl covers all T via the Time trait. An additional variance_for_elapsed(i64) method on the trait serves callers that work with the pre-cached i64 elapsed count. Competitor.last_time moves from i64 with MIN sentinel to Option<T> with None sentinel. receive(&T) computes variance from last_time dynamically; receive_for_elapsed(i64) uses a pre-cached elapsed count (needed in convergence sweeps where last_time has already advanced). TimeSlice.time changes from i64 to T. compute_elapsed is now generic over T and takes Option<&T> for the last-seen time. new_forward_info uses receive_for_elapsed to preserve the cached elapsed during sweeps. History<D> becomes History<T, D>; HistoryBuilder<D> becomes HistoryBuilder<T, D>; Game<D> becomes Game<T, D>. Defaults keep existing call sites compiling with zero changes: T = i64, D = ConstantDrift. add_events / add_events_with_prior stay on impl History<i64, D> since times: Vec<i64> is i64-specific (Task 8 will generalise this). In !self.time mode the old i64::MAX sentinel guaranteed elapsed=1 for every slice transition regardless of time gaps. Replaced by advancing all previously-seen agents' last_time to Some(current_slice_time) at the end of each slice; this preserves elapsed=1 between adjacent slices in sequential-integer untimed mode. The time: bool field on History and .time(bool) on HistoryBuilder are NOT removed by this task — deferred to Task 8 so this commit is purely a type-level generification. Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use crate::{
|
||||
gaussian::Gaussian,
|
||||
rating::Rating,
|
||||
storage::{CompetitorStore, SkillStore},
|
||||
time::Time,
|
||||
tuple_gt, tuple_max,
|
||||
};
|
||||
|
||||
@@ -49,13 +50,13 @@ struct Item {
|
||||
}
|
||||
|
||||
impl Item {
|
||||
fn within_prior<D: Drift>(
|
||||
fn within_prior<T: Time, D: Drift<T>>(
|
||||
&self,
|
||||
online: bool,
|
||||
forward: bool,
|
||||
skills: &SkillStore,
|
||||
agents: &CompetitorStore<D>,
|
||||
) -> Rating<D> {
|
||||
agents: &CompetitorStore<T, D>,
|
||||
) -> Rating<T, D> {
|
||||
let r = &agents[self.agent].rating;
|
||||
let skill = skills.get(self.agent).unwrap();
|
||||
|
||||
@@ -90,13 +91,13 @@ impl Event {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub(crate) fn within_priors<D: Drift>(
|
||||
pub(crate) fn within_priors<T: Time, D: Drift<T>>(
|
||||
&self,
|
||||
online: bool,
|
||||
forward: bool,
|
||||
skills: &SkillStore,
|
||||
agents: &CompetitorStore<D>,
|
||||
) -> Vec<Vec<Rating<D>>> {
|
||||
agents: &CompetitorStore<T, D>,
|
||||
) -> Vec<Vec<Rating<T, D>>> {
|
||||
self.teams
|
||||
.iter()
|
||||
.map(|team| {
|
||||
@@ -110,16 +111,16 @@ impl Event {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TimeSlice {
|
||||
pub struct TimeSlice<T: Time = i64> {
|
||||
pub(crate) events: Vec<Event>,
|
||||
pub(crate) skills: SkillStore,
|
||||
pub(crate) time: i64,
|
||||
pub(crate) time: T,
|
||||
p_draw: f64,
|
||||
arena: ScratchArena,
|
||||
}
|
||||
|
||||
impl TimeSlice {
|
||||
pub fn new(time: i64, p_draw: f64) -> Self {
|
||||
impl<T: Time> TimeSlice<T> {
|
||||
pub fn new(time: T, p_draw: f64) -> Self {
|
||||
Self {
|
||||
events: Vec::new(),
|
||||
skills: SkillStore::new(),
|
||||
@@ -129,12 +130,12 @@ impl TimeSlice {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_events<D: Drift>(
|
||||
pub fn add_events<D: Drift<T>>(
|
||||
&mut self,
|
||||
composition: Vec<Vec<Vec<Index>>>,
|
||||
results: Vec<Vec<f64>>,
|
||||
weights: Vec<Vec<Vec<f64>>>,
|
||||
agents: &CompetitorStore<D>,
|
||||
agents: &CompetitorStore<T, D>,
|
||||
) {
|
||||
let mut unique = Vec::with_capacity(10);
|
||||
|
||||
@@ -149,16 +150,16 @@ impl TimeSlice {
|
||||
});
|
||||
|
||||
for idx in this_agent {
|
||||
let elapsed = compute_elapsed(agents[*idx].last_time, self.time);
|
||||
let elapsed = compute_elapsed(agents[*idx].last_time.as_ref(), &self.time);
|
||||
|
||||
if let Some(skill) = self.skills.get_mut(*idx) {
|
||||
skill.elapsed = elapsed;
|
||||
skill.forward = agents[*idx].receive(elapsed);
|
||||
skill.forward = agents[*idx].receive(&self.time);
|
||||
} else {
|
||||
self.skills.insert(
|
||||
*idx,
|
||||
Skill {
|
||||
forward: agents[*idx].receive(elapsed),
|
||||
forward: agents[*idx].receive(&self.time),
|
||||
elapsed,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -220,7 +221,7 @@ impl TimeSlice {
|
||||
.collect::<HashMap<_, _>>()
|
||||
}
|
||||
|
||||
pub fn iteration<D: Drift>(&mut self, from: usize, agents: &CompetitorStore<D>) {
|
||||
pub fn iteration<D: Drift<T>>(&mut self, from: usize, agents: &CompetitorStore<T, D>) {
|
||||
for event in self.events.iter_mut().skip(from) {
|
||||
let teams = event.within_priors(false, false, &self.skills, agents);
|
||||
let result = event.outputs();
|
||||
@@ -241,7 +242,7 @@ impl TimeSlice {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn convergence<D: Drift>(&mut self, agents: &CompetitorStore<D>) -> usize {
|
||||
pub(crate) fn convergence<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) -> usize {
|
||||
let epsilon = 1e-6;
|
||||
let iterations = 20;
|
||||
|
||||
@@ -270,36 +271,41 @@ impl TimeSlice {
|
||||
skill.forward * skill.likelihood
|
||||
}
|
||||
|
||||
pub(crate) fn backward_prior_out<D: Drift>(
|
||||
pub(crate) fn backward_prior_out<D: Drift<T>>(
|
||||
&self,
|
||||
agent: &Index,
|
||||
agents: &CompetitorStore<D>,
|
||||
agents: &CompetitorStore<T, D>,
|
||||
) -> Gaussian {
|
||||
let skill = self.skills.get(*agent).unwrap();
|
||||
let n = skill.likelihood * skill.backward;
|
||||
n.forget(agents[*agent].rating.drift.variance_delta(skill.elapsed))
|
||||
n.forget(
|
||||
agents[*agent]
|
||||
.rating
|
||||
.drift
|
||||
.variance_for_elapsed(skill.elapsed),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn new_backward_info<D: Drift>(&mut self, agents: &CompetitorStore<D>) {
|
||||
pub(crate) fn new_backward_info<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) {
|
||||
for (agent, skill) in self.skills.iter_mut() {
|
||||
skill.backward = agents[agent].message;
|
||||
}
|
||||
self.iteration(0, agents);
|
||||
}
|
||||
|
||||
pub(crate) fn new_forward_info<D: Drift>(&mut self, agents: &CompetitorStore<D>) {
|
||||
pub(crate) fn new_forward_info<D: Drift<T>>(&mut self, agents: &CompetitorStore<T, D>) {
|
||||
for (agent, skill) in self.skills.iter_mut() {
|
||||
skill.forward = agents[agent].receive(skill.elapsed);
|
||||
skill.forward = agents[agent].receive_for_elapsed(skill.elapsed);
|
||||
}
|
||||
self.iteration(0, agents);
|
||||
}
|
||||
|
||||
pub(crate) fn log_evidence<D: Drift>(
|
||||
pub(crate) fn log_evidence<D: Drift<T>>(
|
||||
&self,
|
||||
online: bool,
|
||||
targets: &[Index],
|
||||
forward: bool,
|
||||
agents: &CompetitorStore<D>,
|
||||
agents: &CompetitorStore<T, D>,
|
||||
) -> f64 {
|
||||
// log_evidence is infrequent; a local arena avoids needing &mut self.
|
||||
let mut arena = ScratchArena::new();
|
||||
@@ -388,14 +394,8 @@ impl TimeSlice {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compute_elapsed(last_time: i64, actual_time: i64) -> i64 {
|
||||
if last_time == i64::MIN {
|
||||
0
|
||||
} else if last_time == i64::MAX {
|
||||
1
|
||||
} else {
|
||||
actual_time - last_time
|
||||
}
|
||||
pub(crate) fn compute_elapsed<T: Time>(last: Option<&T>, current: &T) -> i64 {
|
||||
last.map(|l| l.elapsed_to(current).max(0)).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -419,7 +419,7 @@ mod tests {
|
||||
let e = index_map.get_or_create("e");
|
||||
let f = index_map.get_or_create("f");
|
||||
|
||||
let mut agents: CompetitorStore<ConstantDrift> = CompetitorStore::new();
|
||||
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
|
||||
|
||||
for agent in [a, b, c, d, e, f] {
|
||||
agents.insert(
|
||||
@@ -435,7 +435,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let mut time_slice = TimeSlice::new(0, 0.0);
|
||||
let mut time_slice = TimeSlice::new(0i64, 0.0);
|
||||
|
||||
time_slice.add_events(
|
||||
vec![
|
||||
@@ -495,7 +495,7 @@ mod tests {
|
||||
let e = index_map.get_or_create("e");
|
||||
let f = index_map.get_or_create("f");
|
||||
|
||||
let mut agents: CompetitorStore<ConstantDrift> = CompetitorStore::new();
|
||||
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
|
||||
|
||||
for agent in [a, b, c, d, e, f] {
|
||||
agents.insert(
|
||||
@@ -511,7 +511,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let mut time_slice = TimeSlice::new(0, 0.0);
|
||||
let mut time_slice = TimeSlice::new(0i64, 0.0);
|
||||
|
||||
time_slice.add_events(
|
||||
vec![
|
||||
@@ -574,7 +574,7 @@ mod tests {
|
||||
let e = index_map.get_or_create("e");
|
||||
let f = index_map.get_or_create("f");
|
||||
|
||||
let mut agents: CompetitorStore<ConstantDrift> = CompetitorStore::new();
|
||||
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
|
||||
|
||||
for agent in [a, b, c, d, e, f] {
|
||||
agents.insert(
|
||||
@@ -590,7 +590,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let mut time_slice = TimeSlice::new(0, 0.0);
|
||||
let mut time_slice = TimeSlice::new(0i64, 0.0);
|
||||
|
||||
time_slice.add_events(
|
||||
vec![
|
||||
|
||||
Reference in New Issue
Block a user