feat(api): add fluent history.event(t).team(...).commit() builder
Third tier of the ingestion API (spec Section 4). Powers one-off events with irregular shapes where neither record_winner (too simple) nor typed add_events (too verbose) fits cleanly. EventBuilder accumulates teams, weights, and outcome. Supports: - .team([keys]) — add a team - .weights([w..]) — per-member weights on the most-recently-added team - .ranking([ranks]) — explicit per-team ranks - .winner(i) — convenience: team i wins, others tied - .draw() — all teams tied - .commit() — finalize into an Event<T, K> and delegate to add_events 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:
94
src/event_builder.rs
Normal file
94
src/event_builder.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
InferenceError, Outcome,
|
||||
drift::Drift,
|
||||
event::{Event, Member, Team},
|
||||
history::History,
|
||||
observer::Observer,
|
||||
time::Time,
|
||||
};
|
||||
|
||||
pub struct EventBuilder<'h, T, D, O, K>
|
||||
where
|
||||
T: Time,
|
||||
D: Drift<T>,
|
||||
O: Observer<T>,
|
||||
K: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
history: &'h mut History<T, D, O, K>,
|
||||
event: Event<T, K>,
|
||||
current_team_idx: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'h, T, D, O, K> EventBuilder<'h, T, D, O, K>
|
||||
where
|
||||
T: Time,
|
||||
D: Drift<T>,
|
||||
O: Observer<T>,
|
||||
K: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
pub(crate) fn new(history: &'h mut History<T, D, O, K>, time: T) -> Self {
|
||||
Self {
|
||||
history,
|
||||
event: Event {
|
||||
time,
|
||||
teams: SmallVec::new(),
|
||||
outcome: Outcome::Ranked(SmallVec::new()),
|
||||
},
|
||||
current_team_idx: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a team by its member keys (weight 1.0 each, no prior overrides).
|
||||
pub fn team<I: IntoIterator<Item = K>>(mut self, keys: I) -> Self {
|
||||
let members: SmallVec<[Member<K>; 4]> = keys.into_iter().map(Member::new).collect();
|
||||
self.event.teams.push(Team { members });
|
||||
self.current_team_idx = Some(self.event.teams.len() - 1);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set per-member weights for the most recently added team.
|
||||
///
|
||||
/// Panics in debug builds if called before `.team(...)` or if the length
|
||||
/// doesn't match the team's member count.
|
||||
pub fn weights<I: IntoIterator<Item = f64>>(mut self, weights: I) -> Self {
|
||||
let idx = self
|
||||
.current_team_idx
|
||||
.expect(".weights(...) called before any .team(...)");
|
||||
let ws: Vec<f64> = weights.into_iter().collect();
|
||||
let team = &mut self.event.teams[idx];
|
||||
debug_assert_eq!(
|
||||
ws.len(),
|
||||
team.members.len(),
|
||||
"weights length must match team size"
|
||||
);
|
||||
for (m, w) in team.members.iter_mut().zip(ws) {
|
||||
m.weight = w;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set explicit ranks per team (length must equal number of teams).
|
||||
pub fn ranking<I: IntoIterator<Item = u32>>(mut self, ranks: I) -> Self {
|
||||
self.event.outcome = Outcome::ranking(ranks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark team `winner_idx` as winner; others tied for last.
|
||||
pub fn winner(mut self, winner_idx: u32) -> Self {
|
||||
self.event.outcome = Outcome::winner(winner_idx, self.event.teams.len() as u32);
|
||||
self
|
||||
}
|
||||
|
||||
/// All teams tied.
|
||||
pub fn draw(mut self) -> Self {
|
||||
self.event.outcome = Outcome::draw(self.event.teams.len() as u32);
|
||||
self
|
||||
}
|
||||
|
||||
/// Commit the event to the history.
|
||||
pub fn commit(self) -> Result<(), InferenceError> {
|
||||
self.history.add_events(std::iter::once(self.event))
|
||||
}
|
||||
}
|
||||
@@ -535,6 +535,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
)
|
||||
}
|
||||
|
||||
/// Start a fluent event builder for a single match at `time`.
|
||||
pub fn event(&mut self, time: T) -> crate::event_builder::EventBuilder<'_, T, D, O, K> {
|
||||
crate::event_builder::EventBuilder::new(self, time)
|
||||
}
|
||||
|
||||
/// Bulk-ingest typed events.
|
||||
pub fn add_events<I>(&mut self, events: I) -> Result<(), InferenceError>
|
||||
where
|
||||
|
||||
@@ -14,6 +14,7 @@ mod convergence;
|
||||
pub mod drift;
|
||||
mod error;
|
||||
mod event;
|
||||
mod event_builder;
|
||||
pub(crate) mod factor;
|
||||
mod game;
|
||||
pub mod gaussian;
|
||||
@@ -31,6 +32,7 @@ pub use convergence::{ConvergenceOptions, ConvergenceReport};
|
||||
pub use drift::{ConstantDrift, Drift};
|
||||
pub use error::InferenceError;
|
||||
pub use event::{Event, Member, Team};
|
||||
pub use event_builder::EventBuilder;
|
||||
pub use game::Game;
|
||||
pub use gaussian::Gaussian;
|
||||
pub use history::History;
|
||||
|
||||
@@ -82,3 +82,63 @@ fn add_events_rejects_mismatched_outcome_ranks() {
|
||||
let err = h.add_events(events).unwrap_err();
|
||||
assert!(matches!(err, InferenceError::MismatchedShape { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fluent_event_builder_basic() {
|
||||
let mut h = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.0)
|
||||
.build();
|
||||
|
||||
h.event(1)
|
||||
.team(["alice", "bob"])
|
||||
.weights([1.0, 0.7])
|
||||
.team(["carol"])
|
||||
.ranking([1, 0])
|
||||
.commit()
|
||||
.unwrap();
|
||||
|
||||
let report = h.converge().unwrap();
|
||||
assert!(report.converged);
|
||||
assert!(h.lookup(&"alice").is_some());
|
||||
assert!(h.lookup(&"bob").is_some());
|
||||
assert!(h.lookup(&"carol").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fluent_event_builder_winner_convenience() {
|
||||
let mut h = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.0)
|
||||
.build();
|
||||
|
||||
h.event(1)
|
||||
.team(["alice"])
|
||||
.team(["bob"])
|
||||
.winner(0)
|
||||
.commit()
|
||||
.unwrap();
|
||||
h.converge().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fluent_event_builder_draw() {
|
||||
let mut h = History::builder()
|
||||
.mu(25.0)
|
||||
.sigma(25.0 / 3.0)
|
||||
.beta(25.0 / 6.0)
|
||||
.p_draw(0.25)
|
||||
.build();
|
||||
|
||||
h.event(1)
|
||||
.team(["alice"])
|
||||
.team(["bob"])
|
||||
.draw()
|
||||
.commit()
|
||||
.unwrap();
|
||||
h.converge().unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user