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, O: Observer, K: Eq + std::hash::Hash + Clone, { history: &'h mut History, event: Event, current_team_idx: Option, /// First validation failure seen while building, surfaced by `commit`. /// /// The setters return `Self` so the chain stays fluent; they cannot return /// a `Result` without breaking that. Recording the failure and reporting it /// at `commit` keeps the check enforced in release, where the previous /// `debug_assert!` was compiled out and a mismatched event was ingested /// silently. error: Option, } impl<'h, T, D, O, K> EventBuilder<'h, T, D, O, K> where T: Time, D: Drift, O: Observer, K: Eq + std::hash::Hash + Clone, { pub(crate) fn new(history: &'h mut History, time: T) -> Self { Self { history, event: Event { time, teams: SmallVec::new(), outcome: Outcome::Ranked(SmallVec::new()), }, current_team_idx: None, error: None, } } /// Add a team by its member keys (weight 1.0 each, no prior overrides). /// /// Use [`EventBuilder::members`] to set `prior` or `drift_scale`. pub fn team>(mut self, keys: I) -> Self { let members: SmallVec<[Member; 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 } /// Add a team from fully-specified [`Member`] values. /// /// [`EventBuilder::team`] is the common case and builds members with /// `Member::new`, which leaves `prior` and `drift_scale` unset. This is the /// escape hatch for when they matter: /// /// ``` /// # use trueskill_tt::{Gaussian, History, Member}; /// # let mut h = History::builder().build(); /// h.event(0) /// .team(["player"]) /// .members([Member::new("layout_7") /// .with_drift_scale(0.0) /// .with_prior(Gaussian::from_ms(0.0, 1.0))]) /// .ranking([0, 1]) /// .commit()?; /// # Ok::<(), trueskill_tt::InferenceError>(()) /// ``` /// /// One method rather than a `priors` and a `drift_scales` setter beside /// `weights`: those would have to grow a parallel array — and a parallel /// length check — every time `Member` gains a field, and each one would be /// a new way to get the lengths wrong. `Member`'s own builder already /// expresses all of it. /// /// `prior` and `drift_scale` are competitor configuration rather than /// per-event values; see [`Member`] for what that means for a key the /// history already knows. pub fn members>>(mut self, members: I) -> Self { self.event.teams.push(Team::with_members(members)); self.current_team_idx = Some(self.event.teams.len() - 1); self } /// Set per-member weights for the most recently added team. /// /// A length mismatch is recorded and returned by [`EventBuilder::commit`] /// as `InferenceError::MismatchedShape`, in both debug and release. The /// weights are not applied in that case, so a partially-weighted team /// cannot reach the history. /// /// # Panics /// /// Panics if called before any `.team(...)`. pub fn weights>(mut self, weights: I) -> Self { let idx = self .current_team_idx .expect(".weights(...) called before any .team(...)"); let ws: Vec = weights.into_iter().collect(); let team = &mut self.event.teams[idx]; if ws.len() != team.members.len() { self.error.get_or_insert(InferenceError::MismatchedShape { kind: "weights", expected: team.members.len(), got: ws.len(), }); return self; } 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>(mut self, ranks: I) -> Self { self.event.outcome = Outcome::ranking(ranks); self } /// Set explicit per-team continuous scores; higher = better. pub fn scores>(mut self, scores: I) -> Self { self.event.outcome = crate::Outcome::scores(scores); self } /// Set explicit per-team continuous scores with a per-event noise override. /// /// `sigma` overrides `HistoryBuilder::score_sigma` for this event only. /// Must be `> 0.0`. Constructing the outcome with a non-positive or NaN /// sigma is allowed; the value is rejected with /// `InferenceError::InvalidParameter` when the event is ingested, so /// callers get an error from `commit` rather than a panic. pub fn scores_with_sigma>(mut self, scores: I, sigma: f64) -> Self { self.event.outcome = crate::Outcome::scores_with_sigma(scores, sigma); 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. /// /// # Errors /// /// Returns the first validation failure recorded while building — see /// [`EventBuilder::weights`] — otherwise forwards to /// [`History::add_events`] and returns its errors. pub fn commit(self) -> Result<(), InferenceError> { if let Some(error) = self.error { return Err(error); } self.history.add_events(std::iter::once(self.event)) } }