feat(api): typed add_events(iter); generify internal path over T
Public API gains:
History::add_events<I: IntoIterator<Item = Event<T, K>>>(events)
-> Result<(), InferenceError>
which accepts the typed Event<T, K> shape added in Task 10. Ranks
from Outcome::Ranked are mapped to the legacy "higher f64 = better"
results internally.
add_events_with_prior now takes Vec<T> for times (was Vec<i64>),
generifying the whole internal path over T in a single fully-generic
impl<T: Time, D: Drift<T>, O: Observer<T>, K> block. The i64-specific
block is gone; record_winner/record_draw are now generic over T.
add_events_with_prior stays pub (not pub(crate)) because the ATP
example calls it directly with pre-built Index-based composition;
the new typed add_events is the primary public API going forward.
In-crate tests updated to call add_events_with_prior with an empty
HashMap. tests/api_shape.rs added with 3 integration tests covering
bulk ingest, draw, and mismatched-outcome error.
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:
84
tests/api_shape.rs
Normal file
84
tests/api_shape.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! Tests for the new T2 public API surface: typed add_events(iter) and the
|
||||
//! fluent event builder (added in Task 16).
|
||||
|
||||
use smallvec::smallvec;
|
||||
use trueskill_tt::{ConstantDrift, ConvergenceOptions, Event, History, Member, Outcome, Team};
|
||||
|
||||
#[test]
|
||||
fn add_events_bulk_via_iter() {
|
||||
let mut h = History::builder()
|
||||
.mu(0.0)
|
||||
.sigma(2.0)
|
||||
.beta(1.0)
|
||||
.p_draw(0.0)
|
||||
.drift(ConstantDrift(0.0))
|
||||
.convergence(ConvergenceOptions {
|
||||
max_iter: 30,
|
||||
epsilon: 1e-6,
|
||||
})
|
||||
.build();
|
||||
|
||||
let events: Vec<Event<i64, &'static str>> = vec![
|
||||
Event {
|
||||
time: 1,
|
||||
teams: smallvec![
|
||||
Team::with_members([Member::new("a")]),
|
||||
Team::with_members([Member::new("b")]),
|
||||
],
|
||||
outcome: Outcome::winner(0, 2),
|
||||
},
|
||||
Event {
|
||||
time: 2,
|
||||
teams: smallvec![
|
||||
Team::with_members([Member::new("b")]),
|
||||
Team::with_members([Member::new("c")]),
|
||||
],
|
||||
outcome: Outcome::winner(0, 2),
|
||||
},
|
||||
];
|
||||
|
||||
h.add_events(events).unwrap();
|
||||
let report = h.converge().unwrap();
|
||||
assert!(report.converged);
|
||||
assert!(h.lookup(&"a").is_some());
|
||||
assert!(h.lookup(&"b").is_some());
|
||||
assert!(h.lookup(&"c").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_events_draw() {
|
||||
let mut h = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.25)
|
||||
.drift(ConstantDrift(25.0 / 300.0))
|
||||
.build();
|
||||
|
||||
let events: Vec<Event<i64, &'static str>> = vec![Event {
|
||||
time: 1,
|
||||
teams: smallvec![
|
||||
Team::with_members([Member::new("alice")]),
|
||||
Team::with_members([Member::new("bob")]),
|
||||
],
|
||||
outcome: Outcome::draw(2),
|
||||
}];
|
||||
h.add_events(events).unwrap();
|
||||
h.converge().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_events_rejects_mismatched_outcome_ranks() {
|
||||
use trueskill_tt::InferenceError;
|
||||
let mut h: History = History::builder().build();
|
||||
let events: Vec<Event<i64, &'static str>> = vec![Event {
|
||||
time: 1,
|
||||
teams: smallvec![
|
||||
Team::with_members([Member::new("a")]),
|
||||
Team::with_members([Member::new("b")]),
|
||||
],
|
||||
outcome: Outcome::ranking([0, 1, 2]), // 3 ranks but 2 teams
|
||||
}];
|
||||
let err = h.add_events(events).unwrap_err();
|
||||
assert!(matches!(err, InferenceError::MismatchedShape { .. }));
|
||||
}
|
||||
Reference in New Issue
Block a user