//! Declarative competitor configuration: a rule that supplies defaults for //! competitors the history has not seen yet. //! //! [`History::register`](crate::History::register) states configuration for //! *one* competitor, which covers a bot at a known strength or a handful of //! reference points. It does not cover a *rule* — "every layout is static" — //! because enumerating the keys means knowing the full key set up front, which //! a consumer ingesting an event stream generally does not. //! //! ``` //! use trueskill_tt::{Gaussian, History, StartingPoint}; //! //! let mut h = History::builder() //! // Layouts do not improve; everybody else does. //! .default_rating_for(|key: &&'static str| { //! key.starts_with("layout_") //! .then(|| StartingPoint::new().prior(Gaussian::from_ms(0.0, 1.0)).drift_scale(0.0)) //! }) //! .build(); //! //! h.event(1).team(["layout_7"]).team(["alice"]).scores([3.0, 1.0]).commit()?; //! h.converge()?; //! //! // The layout was pinned, so its uncertainty barely moved. //! assert!(h.current_skill("layout_7").unwrap().sigma() < 1.0); //! # Ok::<(), trueskill_tt::InferenceError>(()) //! ``` //! //! # Why a trait, and why a fifth type parameter //! //! The rule is a type parameter on [`History`](crate::History), defaulted to //! [`NoRule`], so it costs a caller who does not use one exactly nothing — //! `History` still spells out in full. A boxed `dyn Fn` would have //! avoided the parameter at the price of `HistoryBuilder`'s derived `Clone` //! and `Debug`. //! //! It is a trait rather than a bare `Fn` bound because a closure's type cannot //! be written down, and the motivating consumer holds its `History` in //! application state — so it has to name the type in a struct field. Implement //! [`RatingRule`] on a named type of your own and that field is spellable. //! //! # What a rule may set, and what it may not //! //! A [`StartingPoint`], which is the same pair a //! [`Member`](crate::Member) may carry: the prior and the drift scale. Not //! `beta` and not the drift model — those describe the *history*, not one //! competitor, and a rule that could vary them would be describing a different //! model per competitor rather than a starting point within one. //! //! Keeping the rule to those two also keeps it independent of the history's //! time and drift types, so [`HistoryBuilder::drift`](crate::HistoryBuilder::drift) //! and [`HistoryBuilder::time_type`](crate::HistoryBuilder::time_type) still //! work after a rule is set. use crate::gaussian::Gaussian; /// What a [`RatingRule`] may say about a competitor. /// /// Both fields are optional and are applied independently, so a rule that sets /// only `drift_scale` does not also assert a prior — the same reason /// `Member`'s configuration is carried as "what was explicitly set" rather /// than as a merged `Rating`. #[derive(Clone, Copy, Debug, Default, PartialEq)] #[must_use] pub struct StartingPoint { pub(crate) prior: Option, pub(crate) drift_scale: Option, } impl StartingPoint { /// A starting point that says nothing yet. pub fn new() -> Self { Self::default() } /// Start this competitor from `prior` instead of the history's /// `mu`/`sigma`. pub fn prior(mut self, prior: Gaussian) -> Self { self.prior = Some(prior); self } /// Scale how fast this competitor drifts, relative to the history's drift /// model. `0.0` pins them still. pub fn drift_scale(mut self, drift_scale: f64) -> Self { self.drift_scale = Some(drift_scale); self } } /// Supplies a [`StartingPoint`] for competitors the history has not seen. /// /// Consulted once per competitor, when that competitor is created — not per /// event and not per sweep. Returning `None` means "no opinion": the /// competitor takes the history's own defaults. /// /// # Precedence /// /// Explicit configuration wins, field by field. A `prior` or `drift_scale` /// from [`History::register`](crate::History::register) or from a /// [`Member`](crate::Member) overrides whatever the rule returned for that /// competitor. The specific beats the general, which is the only reading that /// lets a rule have exceptions — treating the disagreement as /// `ConflictingCompetitorConfig` would make one exceptional competitor /// incompatible with having any rule at all. /// /// Two *explicit* declarations that disagree remain an error. Neither of those /// is more specific than the other, so there is nothing to prefer. pub trait RatingRule { /// Where this competitor should start, or `None` for the history's /// defaults. fn starting_point(&self, key: &K) -> Option; } /// The default rule: no opinion about anybody. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct NoRule; impl RatingRule for NoRule { #[inline] fn starting_point(&self, _key: &K) -> Option { None } } /// A [`RatingRule`] built from a closure by /// [`HistoryBuilder::default_rating_for`](crate::HistoryBuilder::default_rating_for). /// /// Public so it can be named where a closure's own type cannot be, though /// implementing [`RatingRule`] on a named type of your own is the better way /// to get a `History<..>` you can write down in a struct field. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct FnRule(pub F); impl Option> RatingRule for FnRule { #[inline] fn starting_point(&self, key: &K) -> Option { (self.0)(key) } }