feat(api): add record_winner, record_draw, intern, lookup on History
Spec Section 4 "three-tier event ingestion" tier 2: one-off match convenience. Spec open question 3: expose Index + intern/lookup for power users. History and HistoryBuilder gain a 4th generic parameter K: Eq + Hash + Clone = &'static str. The default ensures existing tests using Index-based add_events compile unchanged. History internally owns a KeyTable<K>. intern(&Q) creates or returns an Index for the given key; lookup(&Q) returns Option<Index> without creating. record_winner and record_draw are thin 1v1 wrappers around the internal add_events_with_prior. 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:
105
src/history.rs
105
src/history.rs
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{borrow::Borrow, collections::HashMap, hash::Hash, marker::PhantomData};
|
||||
|
||||
use crate::{
|
||||
BETA, GAMMA, Index, MU, N_INF, P_DRAW, SIGMA,
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
drift::{ConstantDrift, Drift},
|
||||
error::InferenceError,
|
||||
gaussian::Gaussian,
|
||||
key_table::KeyTable,
|
||||
observer::{NullObserver, Observer},
|
||||
rating::Rating,
|
||||
sort_time,
|
||||
@@ -17,8 +18,12 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HistoryBuilder<T: Time = i64, D: Drift<T> = ConstantDrift, O: Observer<T> = NullObserver>
|
||||
{
|
||||
pub struct HistoryBuilder<
|
||||
T: Time = i64,
|
||||
D: Drift<T> = ConstantDrift,
|
||||
O: Observer<T> = NullObserver,
|
||||
K: Eq + Hash + Clone = &'static str,
|
||||
> {
|
||||
mu: f64,
|
||||
sigma: f64,
|
||||
beta: f64,
|
||||
@@ -27,10 +32,11 @@ pub struct HistoryBuilder<T: Time = i64, D: Drift<T> = ConstantDrift, O: Observe
|
||||
online: bool,
|
||||
convergence: ConvergenceOptions,
|
||||
observer: O,
|
||||
_time: std::marker::PhantomData<T>,
|
||||
_time: PhantomData<T>,
|
||||
_key: PhantomData<K>,
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>> HistoryBuilder<T, D, O> {
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> HistoryBuilder<T, D, O, K> {
|
||||
pub fn mu(mut self, mu: f64) -> Self {
|
||||
self.mu = mu;
|
||||
self
|
||||
@@ -46,7 +52,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>> HistoryBuilder<T, D, O> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn drift<D2: Drift<T>>(self, drift: D2) -> HistoryBuilder<T, D2, O> {
|
||||
pub fn drift<D2: Drift<T>>(self, drift: D2) -> HistoryBuilder<T, D2, O, K> {
|
||||
HistoryBuilder {
|
||||
drift,
|
||||
mu: self.mu,
|
||||
@@ -57,6 +63,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>> HistoryBuilder<T, D, O> {
|
||||
convergence: self.convergence,
|
||||
observer: self.observer,
|
||||
_time: self._time,
|
||||
_key: self._key,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +82,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>> HistoryBuilder<T, D, O> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn observer<O2: Observer<T>>(self, observer: O2) -> HistoryBuilder<T, D, O2> {
|
||||
pub fn observer<O2: Observer<T>>(self, observer: O2) -> HistoryBuilder<T, D, O2, K> {
|
||||
HistoryBuilder {
|
||||
mu: self.mu,
|
||||
sigma: self.sigma,
|
||||
@@ -86,14 +93,16 @@ impl<T: Time, D: Drift<T>, O: Observer<T>> HistoryBuilder<T, D, O> {
|
||||
convergence: self.convergence,
|
||||
observer,
|
||||
_time: self._time,
|
||||
_key: self._key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(self) -> History<T, D, O> {
|
||||
pub fn build(self) -> History<T, D, O, K> {
|
||||
History {
|
||||
size: 0,
|
||||
time_slices: Vec::new(),
|
||||
agents: CompetitorStore::new(),
|
||||
keys: KeyTable::new(),
|
||||
mu: self.mu,
|
||||
sigma: self.sigma,
|
||||
beta: self.beta,
|
||||
@@ -106,14 +115,14 @@ impl<T: Time, D: Drift<T>, O: Observer<T>> HistoryBuilder<T, D, O> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: Observer<i64>> HistoryBuilder<i64, ConstantDrift, O> {
|
||||
impl<O: Observer<i64>, K: Eq + Hash + Clone> HistoryBuilder<i64, ConstantDrift, O, K> {
|
||||
pub fn gamma(mut self, gamma: f64) -> Self {
|
||||
self.drift = ConstantDrift(gamma);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HistoryBuilder<i64, ConstantDrift, NullObserver> {
|
||||
impl Default for HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mu: MU,
|
||||
@@ -124,15 +133,22 @@ impl Default for HistoryBuilder<i64, ConstantDrift, NullObserver> {
|
||||
online: false,
|
||||
convergence: ConvergenceOptions::default(),
|
||||
observer: NullObserver,
|
||||
_time: std::marker::PhantomData,
|
||||
_time: PhantomData,
|
||||
_key: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct History<T: Time = i64, D: Drift<T> = ConstantDrift, O: Observer<T> = NullObserver> {
|
||||
pub struct History<
|
||||
T: Time = i64,
|
||||
D: Drift<T> = ConstantDrift,
|
||||
O: Observer<T> = NullObserver,
|
||||
K: Eq + Hash + Clone = &'static str,
|
||||
> {
|
||||
size: usize,
|
||||
pub(crate) time_slices: Vec<TimeSlice<T>>,
|
||||
pub(crate) agents: CompetitorStore<T, D>,
|
||||
keys: KeyTable<K>,
|
||||
mu: f64,
|
||||
sigma: f64,
|
||||
beta: f64,
|
||||
@@ -143,19 +159,37 @@ pub struct History<T: Time = i64, D: Drift<T> = ConstantDrift, O: Observer<T> =
|
||||
observer: O,
|
||||
}
|
||||
|
||||
impl Default for History<i64, ConstantDrift, NullObserver> {
|
||||
impl Default for History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
fn default() -> Self {
|
||||
HistoryBuilder::default().build()
|
||||
}
|
||||
}
|
||||
|
||||
impl History<i64, ConstantDrift, NullObserver> {
|
||||
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver> {
|
||||
impl History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
HistoryBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>> History<T, D, O> {
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O, K> {
|
||||
pub fn intern<Q>(&mut self, key: &Q) -> Index
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
|
||||
{
|
||||
self.keys.get_or_create(key)
|
||||
}
|
||||
|
||||
pub fn lookup<Q>(&self, key: &Q) -> Option<Index>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
|
||||
{
|
||||
self.keys.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O, K> {
|
||||
fn iteration(&mut self) -> (f64, f64) {
|
||||
let mut step = (0.0, 0.0);
|
||||
|
||||
@@ -298,7 +332,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>> History<T, D, O> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Drift<i64>, O: Observer<i64>> History<i64, D, O> {
|
||||
impl<D: Drift<i64>, O: Observer<i64>, K: Eq + Hash + Clone> History<i64, D, O, K> {
|
||||
pub fn add_events(
|
||||
&mut self,
|
||||
composition: Vec<Vec<Vec<Index>>>,
|
||||
@@ -478,6 +512,43 @@ impl<D: Drift<i64>, O: Observer<i64>> History<i64, D, O> {
|
||||
self.size += n;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_winner<Q>(
|
||||
&mut self,
|
||||
winner: &Q,
|
||||
loser: &Q,
|
||||
time: i64,
|
||||
) -> Result<(), InferenceError>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
|
||||
{
|
||||
let w = self.intern(winner);
|
||||
let l = self.intern(loser);
|
||||
self.add_events_with_prior(
|
||||
vec![vec![vec![w], vec![l]]],
|
||||
vec![vec![1.0, 0.0]],
|
||||
vec![time],
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn record_draw<Q>(&mut self, a: &Q, b: &Q, time: i64) -> Result<(), InferenceError>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ToOwned<Owned = K> + ?Sized,
|
||||
{
|
||||
let a_idx = self.intern(a);
|
||||
let b_idx = self.intern(b);
|
||||
self.add_events_with_prior(
|
||||
vec![vec![vec![a_idx], vec![b_idx]]],
|
||||
vec![vec![0.0, 0.0]],
|
||||
vec![time],
|
||||
vec![],
|
||||
HashMap::new(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
54
tests/record_winner.rs
Normal file
54
tests/record_winner.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use trueskill_tt::{ConstantDrift, ConvergenceOptions, History};
|
||||
|
||||
#[test]
|
||||
fn record_winner_builds_history() {
|
||||
let mut h = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 30,
|
||||
epsilon: 1e-6,
|
||||
})
|
||||
.build();
|
||||
|
||||
h.record_winner(&"alice", &"bob", 1).unwrap();
|
||||
h.converge().unwrap();
|
||||
|
||||
let a_idx = h.lookup(&"alice").unwrap();
|
||||
let b_idx = h.lookup(&"bob").unwrap();
|
||||
|
||||
assert_ne!(a_idx, b_idx);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intern_is_idempotent() {
|
||||
let mut h: History = History::builder().build();
|
||||
let a1 = h.intern(&"alice");
|
||||
let a2 = h.intern(&"alice");
|
||||
assert_eq!(a1, a2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_returns_none_for_missing() {
|
||||
let h: History = History::builder().build();
|
||||
assert!(h.lookup(&"nobody").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_draw_with_p_draw_set() {
|
||||
let mut h = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.p_draw(0.25)
|
||||
.build();
|
||||
|
||||
h.record_draw(&"alice", &"bob", 1).unwrap();
|
||||
h.converge().unwrap();
|
||||
|
||||
assert!(h.lookup(&"alice").is_some());
|
||||
assert!(h.lookup(&"bob").is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user