//! `quality()` beyond two rating groups. //! //! The historical golden (two equal singletons) is asserted in //! `src/lib.rs::tests::test_quality`. These cover the N-group generalisation, //! which previously panicked with an out-of-bounds index at 3+ groups. use trueskill_tt::{Gaussian, quality}; const BETA: f64 = 25.0 / 3.0 / 2.0; fn rating(mu: f64, sigma: f64) -> Gaussian { Gaussian::from_ms(mu, sigma) } #[test] fn three_equal_groups_is_finite_and_in_range() { let r = rating(25.0, 3.0); let q = quality(&[&[r], &[r], &[r]], BETA); assert!(q.is_finite(), "quality must be finite, got {q}"); assert!((0.0..=1.0).contains(&q), "quality out of range: {q}"); } #[test] fn quality_supports_many_groups() { let r = rating(25.0, 3.0); for n in 2..=8 { let holders: Vec<[Gaussian; 1]> = (0..n).map(|_| [r]).collect(); let groups: Vec<&[Gaussian]> = holders.iter().map(|g| g.as_slice()).collect(); let q = quality(&groups, BETA); assert!(q.is_finite(), "n={n}: quality must be finite, got {q}"); assert!((0.0..=1.0).contains(&q), "n={n}: out of range: {q}"); } } /// Equal-strength groups are the best-matched case: introducing a skill gap /// must lower quality. #[test] fn imbalance_lowers_quality() { let strong = rating(40.0, 3.0); let average = rating(25.0, 3.0); let balanced = quality(&[&[average], &[average], &[average]], BETA); let lopsided = quality(&[&[strong], &[average], &[average]], BETA); assert!( lopsided < balanced, "expected imbalanced quality {lopsided} < balanced {balanced}" ); } /// Quality is a property of the multiset of groups, not their order. #[test] fn quality_is_permutation_invariant() { let a = rating(30.0, 2.0); let b = rating(25.0, 3.0); let c = rating(20.0, 4.0); let forward = quality(&[&[a], &[b], &[c]], BETA); let reversed = quality(&[&[c], &[b], &[a]], BETA); assert!( (forward - reversed).abs() < 1e-9, "permutation changed quality: {forward} vs {reversed}" ); } #[test] fn multi_player_groups_work() { let r = rating(25.0, 3.0); let q = quality(&[&[r, r], &[r, r], &[r, r]], BETA); assert!(q.is_finite()); assert!((0.0..=1.0).contains(&q)); } #[test] fn uneven_group_sizes_work() { let r = rating(25.0, 3.0); let q = quality(&[&[r, r], &[r], &[r, r, r]], BETA); assert!(q.is_finite(), "got {q}"); assert!((0.0..=1.0).contains(&q), "got {q}"); } #[test] #[should_panic(expected = "at least 2 rating groups")] fn single_group_panics_with_clear_message() { let r = rating(25.0, 3.0); let _ = quality(&[&[r]], BETA); } #[test] #[should_panic(expected = "at least 2 rating groups")] fn zero_groups_panics_with_clear_message() { let _ = quality(&[], BETA); } #[test] #[should_panic(expected = "non-empty")] fn empty_group_panics_with_clear_message() { let r = rating(25.0, 3.0); let _ = quality(&[&[r], &[]], BETA); } #[test] fn history_predict_quality_supports_three_teams() { use trueskill_tt::History; let mut h = History::default(); h.record_winner(&"a", &"b", 1).unwrap(); h.record_winner(&"b", &"c", 2).unwrap(); let _ = h.converge().unwrap(); let q = h.predict_quality(&[&[&"a"], &[&"b"], &[&"c"]]).unwrap(); assert!( q.is_finite(), "3-team predict_quality must be finite, got {q}" ); assert!((0.0..=1.0).contains(&q), "out of range: {q}"); } /// `quality()` for N identical teams has a closed form, which pins the N-group /// determinant path across the whole range rather than at a single golden. /// /// For two identical single-player teams the standard result is /// `sqrt(2b^2 / (2b^2 + s1^2 + s2^2))`. With the conventional parameters /// (`sigma = 25/3`, `beta = 25/6`) that ratio is exactly `1/5`, and the N-group /// generalisation is `(1/5)^((n-1)/2)` — one factor per adjacent pair. /// /// The n=3 and n=5 values this produces (0.200 and 0.040) are also what the /// `trueskill` Python package returns for the same configuration, so this /// doubles as the cross-implementation check the README asked for. #[test] fn quality_of_identical_teams_follows_its_closed_form() { let g = Gaussian::from_ms(25.0, 25.0 / 3.0); let beta = 25.0 / 6.0; for n in 2..=10usize { let groups: Vec> = (0..n).map(|_| vec![g]).collect(); let refs: Vec<&[Gaussian]> = groups.iter().map(Vec::as_slice).collect(); let got = quality(&refs, beta); let expected = 0.2f64.powf((n - 1) as f64 / 2.0); assert!( (got - expected).abs() / expected < 1e-9, "n={n}: quality {got}, closed form {expected}" ); } } /// Spot-check against the two values the `trueskill` Python package is known /// to produce for this configuration, stated as literals so a future change to /// the closed-form reasoning above cannot quietly take these with it. #[test] fn quality_matches_the_reference_implementation() { let g = Gaussian::from_ms(25.0, 25.0 / 3.0); let beta = 25.0 / 6.0; let three: Vec> = (0..3).map(|_| vec![g]).collect(); let refs: Vec<&[Gaussian]> = three.iter().map(Vec::as_slice).collect(); assert!((quality(&refs, beta) - 0.200).abs() < 1e-9); let five: Vec> = (0..5).map(|_| vec![g]).collect(); let refs: Vec<&[Gaussian]> = five.iter().map(Vec::as_slice).collect(); assert!((quality(&refs, beta) - 0.040).abs() < 1e-9); }