//! `HistoryBuilder::default_rating_for`: configuring a *class* of competitors //! rather than one at a time (#53). //! //! Every test carries a control — a key the rule does not match — so none can //! pass by the rule firing for everybody, which would be indistinguishable //! from changing the history defaults. use trueskill_tt::{ ConstantDrift, Gaussian, History, HistoryBuilder, InferenceError, Member, NullObserver, RatingRule, StartingPoint, }; /// Pinned: no drift, and a tight prior at a known strength. fn pinned() -> StartingPoint { StartingPoint::new() .prior(Gaussian::from_ms(5.0, 0.5)) .drift_scale(0.0) } fn play>( h: &mut History<&'static str, i64, ConstantDrift, NullObserver, R>, ) { for t in 1..=6 { h.event(t) .team(["layout_a"]) .team(["alice"]) .scores([3.0, 1.0]) .commit() .expect("ingests"); } h.converge().expect("converges"); } #[test] fn a_rule_configures_every_matching_key_without_naming_them() { let mut ruled = History::builder() .gamma(0.5) .default_rating_for(|key: &&'static str| key.starts_with("layout_").then(pinned)) .build(); play(&mut ruled); let mut plain = History::builder().gamma(0.5).build(); play(&mut plain); let layout = ruled.current_skill("layout_a").expect("played"); // The rule pinned the layout: tight prior, no drift. assert!( layout.sigma() < 0.5, "the layout should stay near its pinned prior, got sigma {}", layout.sigma() ); assert_ne!( layout.sigma(), plain.current_skill("layout_a").unwrap().sigma(), "the rule must actually change the fit" ); // The control is the *configuration*, not the posterior. Alice's posterior // legitimately moves — she is playing a differently-configured opponent, // and what she learns from beating it depends on how sure the model is // about it. What must not move is what the rule was asked about. let alice = ruled.rating("alice").expect("played"); assert_eq!( alice.drift_scale(), 1.0, "a non-matching key keeps the default drift" ); assert_eq!( (alice.prior().mu(), alice.prior().sigma()), { let p = plain.rating("alice").expect("played").prior(); (p.mu(), p.sigma()) }, "a non-matching key keeps the history's prior" ); } #[test] fn a_rule_fires_for_a_competitor_first_seen_through_record_winner() { // `record_winner` cannot carry configuration, which is the case a rule // exists for. let mut h = History::builder() .default_rating_for(|key: &&'static str| key.starts_with("bot_").then(pinned)) .build(); h.record_winner(&"bot_1", &"human", 1).expect("ingests"); h.converge().expect("converges"); assert_eq!(h.rating("bot_1").expect("known").drift_scale(), 0.0); assert_eq!(h.rating("human").expect("known").drift_scale(), 1.0); } #[test] fn explicit_configuration_overrides_a_rule_field_by_field() { let mut h = History::builder() .default_rating_for(|_: &&'static str| Some(pinned())) .build(); // Sets only the prior, so the rule's `drift_scale` must survive. h.register(Member::new("a").with_prior(Gaussian::from_ms(-9.0, 2.0))) .expect("new"); // Sets neither: the rule supplies both. h.register(Member::new("b")).expect("new"); let a = h.rating("a").expect("registered"); assert_eq!(a.prior().mu(), -9.0, "explicit prior wins"); assert_eq!(a.drift_scale(), 0.0, "the rule's drift_scale survives"); let b = h.rating("b").expect("registered"); assert_eq!(b.prior().mu(), 5.0); assert_eq!(b.drift_scale(), 0.0); } #[test] fn two_explicit_declarations_that_disagree_are_still_an_error() { // Precedence resolves rule-vs-explicit. It does not weaken the check // between two explicit declarations, neither of which is more specific. let mut h = History::builder() .default_rating_for(|_: &&'static str| Some(pinned())) .build(); let err = h .add_events(vec![ event(1, "x", Gaussian::from_ms(1.0, 1.0)), event(2, "x", Gaussian::from_ms(2.0, 1.0)), ]) .expect_err("two different priors for one competitor"); assert!( matches!(err, InferenceError::ConflictingCompetitorConfig { .. }), "{err:?}" ); } fn event(time: i64, key: &'static str, prior: Gaussian) -> trueskill_tt::Event { trueskill_tt::Event { time, teams: [ trueskill_tt::Team::with_members([Member::new(key).with_prior(prior)]), trueskill_tt::Team::with_members([Member::new("opponent")]), ] .into_iter() .collect(), outcome: trueskill_tt::Outcome::scores([2.0, 1.0]), } } /// A named rule type, so the `History<..>` can be written down in a field. struct StaticLayouts; impl RatingRule<&'static str> for StaticLayouts { fn starting_point(&self, key: &&'static str) -> Option { key.starts_with("layout_").then(pinned) } } /// The reason this is a trait rather than a bare `Fn` bound: a consumer holds /// its history in application state and has to name the type. struct Ladder { history: History<&'static str, i64, ConstantDrift, NullObserver, StaticLayouts>, } #[test] fn a_named_rule_type_can_be_stored_in_a_struct_field() { let mut ladder = Ladder { history: HistoryBuilder::default().rating_rule(StaticLayouts).build(), }; play(&mut ladder.history); assert!( ladder .history .current_skill("layout_a") .expect("played") .sigma() < 0.5 ); assert_eq!( ladder .history .rating("alice") .expect("played") .drift_scale(), 1.0 ); } #[test] fn no_rule_is_the_default_and_costs_nothing_to_spell() { // The whole point of defaulting the parameter: `History` still works. let h: History = History::builder().key_type::().build(); assert_eq!(h.competitor_count(), 0); }