Files
trueskill-tt/src/event.rs
Anders Olsson f5a486329e feat(api): add Event<T, K>, Team<K>, Member<K> typed event description
Replaces the old nested Vec<Vec<Vec<_>>> event description on the
public API boundary. Member<K>::from(K) enables ergonomic literal
lists. Member::with_weight / with_prior are builder methods for the
optional per-event overrides.

Fully additive — no existing call sites updated. Consumed by
History::add_events(iter) in Task 15.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 12:14:58 +02:00

133 lines
3.1 KiB
Rust

//! Typed event description for bulk ingestion.
//!
//! `Event<T, K>` is the new public event shape (spec Section 4). Replaces
//! the nested `Vec<Vec<Vec<Index>>>`, `Vec<Vec<f64>>`, `Vec<Vec<Vec<f64>>>`
//! that the old `add_events_with_prior` took.
use smallvec::SmallVec;
use crate::{gaussian::Gaussian, outcome::Outcome, time::Time};
/// A single match at time `time` involving some number of teams.
#[derive(Clone, Debug)]
pub struct Event<T: Time, K> {
pub time: T,
pub teams: SmallVec<[Team<K>; 4]>,
pub outcome: Outcome,
}
/// A team: list of members competing together.
#[derive(Clone, Debug)]
pub struct Team<K> {
pub members: SmallVec<[Member<K>; 4]>,
}
impl<K> Team<K> {
pub fn new() -> Self {
Self {
members: SmallVec::new(),
}
}
pub fn with_members<I: IntoIterator<Item = Member<K>>>(members: I) -> Self {
Self {
members: members.into_iter().collect(),
}
}
}
impl<K> Default for Team<K> {
fn default() -> Self {
Self::new()
}
}
/// One member of a team, identified by user key `K`.
///
/// `weight` defaults to 1.0; a per-event `prior` can override the competitor's
/// current skill estimate for this event only.
#[derive(Clone, Debug)]
pub struct Member<K> {
pub key: K,
pub weight: f64,
pub prior: Option<Gaussian>,
}
impl<K> Member<K> {
pub fn new(key: K) -> Self {
Self {
key,
weight: 1.0,
prior: None,
}
}
pub fn with_weight(mut self, weight: f64) -> Self {
self.weight = weight;
self
}
pub fn with_prior(mut self, prior: Gaussian) -> Self {
self.prior = Some(prior);
self
}
}
/// Convenience: a member is a user key with default weight 1.0 and no prior.
impl<K> From<K> for Member<K> {
fn from(key: K) -> Self {
Self::new(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Outcome;
#[test]
fn member_new_has_unit_weight_no_prior() {
let m = Member::new("alice");
assert_eq!(m.key, "alice");
assert_eq!(m.weight, 1.0);
assert!(m.prior.is_none());
}
#[test]
fn member_builder_methods_chain() {
let m = Member::new("alice")
.with_weight(0.5)
.with_prior(Gaussian::from_ms(20.0, 5.0));
assert_eq!(m.weight, 0.5);
assert!(m.prior.is_some());
}
#[test]
fn member_from_key() {
let m: Member<&str> = "bob".into();
assert_eq!(m.key, "bob");
assert_eq!(m.weight, 1.0);
}
#[test]
fn team_with_members_collects() {
let t: Team<&str> = Team::with_members([Member::new("a"), Member::new("b")]);
assert_eq!(t.members.len(), 2);
}
#[test]
fn event_construction() {
use smallvec::smallvec;
let e: Event<i64, &str> = Event {
time: 1,
teams: smallvec![
Team::with_members([Member::new("a")]),
Team::with_members([Member::new("b")]),
],
outcome: Outcome::winner(0, 2),
};
assert_eq!(e.teams.len(), 2);
assert_eq!(e.time, 1);
}
}