//! The `Time` generic, exercised end to end. //! //! `History` has always been generic over the time axis, `Untimed` //! has always been exported, and `Drift` is generic specifically so that //! "seasonal or calendar-aware drift is expressible without going through //! `i64`". None of it was reachable: every construction route pinned `T = i64`, //! `HistoryBuilder`'s fields are private, and its `Default` existed only for the //! `i64` instantiation. //! //! Nothing in the repository constructed a non-`i64` history, which is why that //! went unnoticed. This file is the guard against it recurring — it is as much //! about the generic being *exercised* as about any single assertion. use trueskill_tt::{ConstantDrift, Drift, History, HistoryBuilder, Time, Untimed}; /// A domain time type: a season number. Exactly what the `Time` trait exists /// to support, and what a consumer with `chrono` dates would write. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] struct Season(u16); impl Time for Season { fn elapsed_to(&self, later: &Self) -> i64 { i64::from(later.0.saturating_sub(self.0)) } } /// Drift that only accumulates between seasons, not within one — the /// calendar-aware case the trait's own docs cite. #[derive(Copy, Clone, Debug)] struct SeasonalDrift { per_season: f64, } impl Drift for SeasonalDrift { fn variance_delta(&self, from: &Season, to: &Season) -> f64 { self.variance_for_elapsed(from.elapsed_to(to)) } fn variance_for_elapsed(&self, elapsed: i64) -> f64 { elapsed.max(0) as f64 * self.per_season * self.per_season } } #[test] fn an_untimed_history_fits_through_the_builder() { let mut h = History::builder().time_type::().build(); for _ in 0..5 { h.record_winner(&"alice", &"bob", Untimed).unwrap(); } assert!(h.converge().unwrap().converged); let alice = h.current_skill(&"alice").unwrap(); let bob = h.current_skill(&"bob").unwrap(); assert!(alice.mu() > bob.mu(), "{alice:?} vs {bob:?}"); assert!(alice.sigma().is_finite() && alice.sigma() > 0.0); } /// `Untimed::elapsed_to` is always 0, so no drift accumulates however many /// events there are. That is the property the type exists for, and it had never /// been checked. #[test] fn untimed_accumulates_no_drift() { fn final_sigma(time: T, drift: ConstantDrift) -> f64 { let mut h = History::builder().time_type::().drift(drift).build(); for _ in 0..8 { h.record_winner(&"a", &"b", time).unwrap(); } let _ = h.converge().unwrap(); h.current_skill(&"a").unwrap().sigma() } // Under Untimed the drift setting cannot matter, because elapsed is always 0. let none = final_sigma(Untimed, ConstantDrift::new(0.0)); let large = final_sigma(Untimed, ConstantDrift::new(5.0)); assert_eq!( none.to_bits(), large.to_bits(), "Untimed must ignore drift entirely: {none} vs {large}" ); } #[test] fn a_custom_time_type_and_a_custom_drift_work_together() { let mut h = History::builder() .time_type::() .drift(SeasonalDrift { per_season: 0.5 }) .build(); for season in 1..=4u16 { for _ in 0..3 { h.record_winner(&"veteran", &"rookie", Season(season)) .unwrap(); } } assert!(h.converge().unwrap().converged); let curve = h.learning_curve(&"veteran"); assert_eq!(curve.len(), 4, "one point per season: {curve:?}"); for (season, g) in &curve { assert!( g.mu().is_finite() && g.sigma() > 0.0, "season {season:?}: {g:?}" ); } // Times come back as the domain type, not as an integer. assert_eq!(curve[0].0, Season(1)); assert_eq!(curve[3].0, Season(4)); } /// Seasonal drift must actually widen a gap across seasons — otherwise the /// custom `Drift` is being ignored and the test above would pass regardless. #[test] fn a_custom_drift_is_actually_consulted() { fn sigma_with(per_season: f64) -> f64 { let mut h = History::builder() .time_type::() .drift(SeasonalDrift { per_season }) .build(); for season in 1..=6u16 { h.record_winner(&"a", &"b", Season(season)).unwrap(); } let _ = h.converge().unwrap(); h.current_skill(&"a").unwrap().sigma() } let still = sigma_with(0.0); let drifting = sigma_with(2.0); assert!( drifting > still * 1.05, "a drifting fit must be less certain: {drifting} vs {still}" ); } /// The other axis: a custom key type, through the same mechanism. #[test] fn key_type_replaces_builder_with_key() { let mut h = History::builder().key_type::().build(); h.record_winner(&"alice".to_string(), &"bob".to_string(), 1) .unwrap(); assert!(h.converge().unwrap().converged); assert!(h.current_skill("alice").is_some()); } /// Both axes at once, via the explicit constructor rather than the setters. #[test] fn new_constructs_on_any_axis_directly() { let mut h = HistoryBuilder::::new().build(); h.record_winner(&"a".to_string(), &"b".to_string(), Season(7)) .unwrap(); assert!(h.converge().unwrap().converged); assert_eq!(h.learning_curve("a")[0].0, Season(7)); }