//! Comprehensive integration tests for the ELO rating system //! //! These tests verify end-to-end rating calculations for singles and doubles matches, //! including edge cases and real-world scenarios. #[cfg(test)] mod tests { use crate::elo::calculator::EloCalculator; use crate::elo::doubles::calculate_effective_opponent_rating; use crate::elo::rating::EloRating; use crate::elo::score_weight::calculate_weighted_score; // ============================================ // SINGLES MATCH TESTS // ============================================ #[test] fn test_singles_equal_ratings_close_win() { // Two 1500-rated players, winner wins 11-9 let calc = EloCalculator::new(); let winner = EloRating::new_player(); let loser = EloRating::new_player(); let performance = calculate_weighted_score(winner.rating, loser.rating, 11, 9); let new_winner = calc.update_rating(&winner, &loser, performance); let new_loser = calc.update_rating(&loser, &winner, 1.0 - performance); // Winner should gain a small amount (~1.6 points for 55% performance vs 50% expected) assert!(new_winner.rating > winner.rating); assert!(new_loser.rating < loser.rating); // Changes should be symmetric let winner_gain = new_winner.rating - winner.rating; let loser_loss = loser.rating - new_loser.rating; assert!((winner_gain - loser_loss).abs() < 0.01); // For close game, change should be small assert!(winner_gain < 5.0); println!("Singles 1500 vs 1500, 11-9: Winner {} -> {} (+{:.1})", winner.rating, new_winner.rating, winner_gain); } #[test] fn test_singles_equal_ratings_blowout_win() { // Two 1500-rated players, winner wins 11-2 let calc = EloCalculator::new(); let winner = EloRating::new_player(); let loser = EloRating::new_player(); let performance = calculate_weighted_score(winner.rating, loser.rating, 11, 2); let new_winner = calc.update_rating(&winner, &loser, performance); let winner_gain = new_winner.rating - winner.rating; // Blowout should yield bigger gains than close game assert!(winner_gain > 10.0); assert!(winner_gain < 16.0); // Max is 16 for K=32 println!("Singles 1500 vs 1500, 11-2: Winner {} -> {} (+{:.1})", winner.rating, new_winner.rating, winner_gain); } #[test] fn test_singles_upset_win() { // Lower rated (1400) beats higher rated (1600), close game 11-9 let calc = EloCalculator::new(); let underdog = EloRating::new_with_rating(1400.0); let favorite = EloRating::new_with_rating(1600.0); let performance = calculate_weighted_score(underdog.rating, favorite.rating, 11, 9); let new_underdog = calc.update_rating(&underdog, &favorite, performance); let new_favorite = calc.update_rating(&favorite, &underdog, 1.0 - performance); let underdog_gain = new_underdog.rating - underdog.rating; let favorite_loss = favorite.rating - new_favorite.rating; // Upset should yield big gains assert!(underdog_gain > 5.0); println!("Singles upset 1400 vs 1600, 11-9: Underdog {} -> {} (+{:.1}), Favorite {} -> {} ({:.1})", underdog.rating, new_underdog.rating, underdog_gain, favorite.rating, new_favorite.rating, -favorite_loss); } #[test] fn test_singles_expected_win() { // Higher rated (1600) beats lower rated (1400), close game 11-9 let calc = EloCalculator::new(); let favorite = EloRating::new_with_rating(1600.0); let underdog = EloRating::new_with_rating(1400.0); let performance = calculate_weighted_score(favorite.rating, underdog.rating, 11, 9); let new_favorite = calc.update_rating(&favorite, &underdog, performance); let favorite_gain = new_favorite.rating - favorite.rating; // Expected win should yield small gains (underperformed expectations) // Expected ~64% of points, got 55% assert!(favorite_gain < 0.0); // Actually loses rating for close expected win! println!("Singles expected 1600 vs 1400, 11-9: Favorite {} -> {} ({:.1})", favorite.rating, new_favorite.rating, favorite_gain); } #[test] fn test_singles_expected_blowout_win() { // Higher rated (1600) blows out lower rated (1400), 11-2 let calc = EloCalculator::new(); let favorite = EloRating::new_with_rating(1600.0); let underdog = EloRating::new_with_rating(1400.0); let performance = calculate_weighted_score(favorite.rating, underdog.rating, 11, 2); let new_favorite = calc.update_rating(&favorite, &underdog, performance); let favorite_gain = new_favorite.rating - favorite.rating; // Blowout exceeds expectations (85% vs expected 64%), should gain assert!(favorite_gain > 0.0); println!("Singles expected blowout 1600 vs 1400, 11-2: Favorite {} -> {} (+{:.1})", favorite.rating, new_favorite.rating, favorite_gain); } #[test] fn test_singles_shutout() { // Complete shutout 11-0 let calc = EloCalculator::new(); let winner = EloRating::new_player(); let loser = EloRating::new_player(); let performance = calculate_weighted_score(winner.rating, loser.rating, 11, 0); assert!((performance - 1.0).abs() < 0.001); // 100% performance let new_winner = calc.update_rating(&winner, &loser, performance); // Should be max gain of K/2 = 16 (since expected is 0.5) let gain = new_winner.rating - winner.rating; assert!((gain - 16.0).abs() < 0.1); println!("Singles shutout 11-0: {} -> {} (+{:.1})", winner.rating, new_winner.rating, gain); } #[test] fn test_singles_get_shutout() { // Complete shutout loss 0-11 let calc = EloCalculator::new(); let winner = EloRating::new_player(); let loser = EloRating::new_player(); let performance = calculate_weighted_score(loser.rating, winner.rating, 0, 11); assert!(performance.abs() < 0.001); // 0% performance let new_loser = calc.update_rating(&loser, &winner, performance); // Should be max loss of K/2 = 16 let loss = loser.rating - new_loser.rating; assert!((loss - 16.0).abs() < 0.1); println!("Singles get shutout 0-11: {} -> {} (-{:.1})", loser.rating, new_loser.rating, loss); } // ============================================ // DOUBLES MATCH TESTS // ============================================ #[test] fn test_doubles_equal_teams() { // All four players rated 1500 let calc = EloCalculator::new(); let player1 = EloRating::new_player(); // 1500 let teammate1 = EloRating::new_player(); // 1500 let opp1 = EloRating::new_player(); // 1500 let opp2 = EloRating::new_player(); // 1500 // Player 1's effective opponent let eff_opp = calculate_effective_opponent_rating(opp1.rating, opp2.rating, teammate1.rating); assert!((eff_opp - 1500.0).abs() < 0.01); // 1500+1500-1500 = 1500 // Win 11-9 let performance = calculate_weighted_score(player1.rating, eff_opp, 11, 9); let new_player1 = calc.update_rating(&player1, &EloRating::new_with_rating(eff_opp), performance); let gain = new_player1.rating - player1.rating; println!("Doubles equal teams, 11-9: {} -> {} (+{:.1})", player1.rating, new_player1.rating, gain); } #[test] fn test_doubles_carried_by_strong_teammate() { // Player (1400) with strong teammate (1600) vs two 1500s let calc = EloCalculator::new(); let player = EloRating::new_with_rating(1400.0); let teammate = EloRating::new_with_rating(1600.0); let opp1 = EloRating::new_player(); // 1500 let opp2 = EloRating::new_player(); // 1500 // Player's effective opponent: 1500+1500-1600 = 1400 let eff_opp = calculate_effective_opponent_rating(opp1.rating, opp2.rating, teammate.rating); assert!((eff_opp - 1400.0).abs() < 0.01); // Teammate's effective opponent: 1500+1500-1400 = 1600 let teammate_eff_opp = calculate_effective_opponent_rating(opp1.rating, opp2.rating, player.rating); assert!((teammate_eff_opp - 1600.0).abs() < 0.01); // Win 11-9 let player_perf = calculate_weighted_score(player.rating, eff_opp, 11, 9); let teammate_perf = calculate_weighted_score(teammate.rating, teammate_eff_opp, 11, 9); let new_player = calc.update_rating(&player, &EloRating::new_with_rating(eff_opp), player_perf); let new_teammate = calc.update_rating(&teammate, &EloRating::new_with_rating(teammate_eff_opp), teammate_perf); let player_gain = new_player.rating - player.rating; let teammate_gain = new_teammate.rating - teammate.rating; // Player faces easier effective opponent (1400), should gain less // Teammate faces harder effective opponent (1600), should lose rating (underperformed) println!("Doubles carry: Player (1400) eff_opp=1400, gain={:.1}; Teammate (1600) eff_opp=1600, gain={:.1}", player_gain, teammate_gain); // The weaker player benefits less from wins with strong partner assert!(player_gain < 3.0); } #[test] fn test_doubles_carrying_weak_teammate() { // Strong player (1600) with weak teammate (1400) vs two 1500s let calc = EloCalculator::new(); let strong_player = EloRating::new_with_rating(1600.0); let weak_teammate = EloRating::new_with_rating(1400.0); let opp1 = EloRating::new_player(); // 1500 let opp2 = EloRating::new_player(); // 1500 // Strong player's effective opponent: 1500+1500-1400 = 1600 let eff_opp = calculate_effective_opponent_rating(opp1.rating, opp2.rating, weak_teammate.rating); assert!((eff_opp - 1600.0).abs() < 0.01); // Win 11-9 - strong player carrying let performance = calculate_weighted_score(strong_player.rating, eff_opp, 11, 9); let new_strong = calc.update_rating(&strong_player, &EloRating::new_with_rating(eff_opp), performance); let gain = new_strong.rating - strong_player.rating; // Strong player faces harder effective opponent, gains less for close win // (or even loses points since 55% < expected) println!("Doubles carrying: Strong (1600) eff_opp=1600, 11-9 win, change={:.1}", gain); } #[test] fn test_doubles_all_different_ratings() { // Realistic scenario: 1550+1450 vs 1520+1480 let calc = EloCalculator::new(); let p1 = EloRating::new_with_rating(1550.0); let p1_teammate = EloRating::new_with_rating(1450.0); let p2 = EloRating::new_with_rating(1520.0); let p2_teammate = EloRating::new_with_rating(1480.0); // P1's effective opponent: 1520+1480-1450 = 1550 let p1_eff = calculate_effective_opponent_rating(p2.rating, p2_teammate.rating, p1_teammate.rating); // P1's teammate's effective opponent: 1520+1480-1550 = 1450 let p1t_eff = calculate_effective_opponent_rating(p2.rating, p2_teammate.rating, p1.rating); // P2's effective opponent: 1550+1450-1480 = 1520 let p2_eff = calculate_effective_opponent_rating(p1.rating, p1_teammate.rating, p2_teammate.rating); // P2's teammate's effective opponent: 1550+1450-1520 = 1480 let p2t_eff = calculate_effective_opponent_rating(p1.rating, p1_teammate.rating, p2.rating); println!("Team 1 (1550+1450) vs Team 2 (1520+1480):"); println!(" P1 (1550) eff_opp: {:.0}", p1_eff); println!(" P1 teammate (1450) eff_opp: {:.0}", p1t_eff); println!(" P2 (1520) eff_opp: {:.0}", p2_eff); println!(" P2 teammate (1480) eff_opp: {:.0}", p2t_eff); // Each player's effective opponent equals their own rating! // This is a property of balanced teams assert!((p1_eff - p1.rating).abs() < 0.01); assert!((p1t_eff - p1_teammate.rating).abs() < 0.01); } // ============================================ // K-FACTOR TESTS // ============================================ #[test] fn test_different_k_factors() { let player = EloRating::new_player(); let opponent = EloRating::new_player(); let calc_k16 = EloCalculator::new_with_k_factor(16.0); let calc_k32 = EloCalculator::new_with_k_factor(32.0); let calc_k64 = EloCalculator::new_with_k_factor(64.0); let new_k16 = calc_k16.update_rating(&player, &opponent, 1.0); let new_k32 = calc_k32.update_rating(&player, &opponent, 1.0); let new_k64 = calc_k64.update_rating(&player, &opponent, 1.0); // Higher K = more volatile ratings let gain_k16 = new_k16.rating - player.rating; let gain_k32 = new_k32.rating - player.rating; let gain_k64 = new_k64.rating - player.rating; assert!((gain_k16 - 8.0).abs() < 0.1); // K=16, win = +8 assert!((gain_k32 - 16.0).abs() < 0.1); // K=32, win = +16 assert!((gain_k64 - 32.0).abs() < 0.1); // K=64, win = +32 println!("K-factor comparison for 1.0 performance:"); println!(" K=16: +{:.1}", gain_k16); println!(" K=32: +{:.1}", gain_k32); println!(" K=64: +{:.1}", gain_k64); } // ============================================ // EDGE CASES // ============================================ #[test] fn test_extreme_rating_difference() { // 2000 vs 1000 - extreme mismatch let calc = EloCalculator::new(); let elite = EloRating::new_with_rating(2000.0); let beginner = EloRating::new_with_rating(1000.0); // Elite wins as expected 11-3 let perf = calculate_weighted_score(elite.rating, beginner.rating, 11, 3); let new_elite = calc.update_rating(&elite, &beginner, perf); // Expected performance is ~0.99, actual is 0.786 // Should actually lose rating! let change = new_elite.rating - elite.rating; assert!(change < 0.0); println!("Extreme mismatch 2000 vs 1000, 11-3: Elite change = {:.1}", change); } #[test] fn test_beginner_beats_elite() { // Major upset: 1000 beats 2000 let calc = EloCalculator::new(); let beginner = EloRating::new_with_rating(1000.0); let elite = EloRating::new_with_rating(2000.0); let perf = calculate_weighted_score(beginner.rating, elite.rating, 11, 9); let new_beginner = calc.update_rating(&beginner, &elite, perf); let gain = new_beginner.rating - beginner.rating; // Massive upset - expected only ~1% of points, got 55%! assert!(gain > 15.0); println!("Major upset 1000 beats 2000, 11-9: Beginner gain = +{:.1}", gain); } #[test] fn test_rating_conservation_singles() { // In a match, total rating change should sum to approximately zero let calc = EloCalculator::new(); let p1 = EloRating::new_with_rating(1500.0); let p2 = EloRating::new_with_rating(1500.0); let p1_perf = calculate_weighted_score(p1.rating, p2.rating, 11, 7); let p2_perf = calculate_weighted_score(p2.rating, p1.rating, 7, 11); let new_p1 = calc.update_rating(&p1, &p2, p1_perf); let new_p2 = calc.update_rating(&p2, &p1, p2_perf); let total_change = (new_p1.rating - p1.rating) + (new_p2.rating - p2.rating); // Should sum to zero (rating conserved in the system) assert!(total_change.abs() < 0.01); println!("Rating conservation: P1 {:.1}, P2 {:.1}, sum = {:.3}", new_p1.rating - p1.rating, new_p2.rating - p2.rating, total_change); } #[test] fn test_multiple_matches_convergence() { // After many matches, better player should have higher rating let calc = EloCalculator::new(); let mut strong = EloRating::new_player(); // Actually wins 70% of points let mut weak = EloRating::new_player(); // Actually wins 30% of points // Simulate 20 matches where strong player gets ~70% of points for _ in 0..20 { let strong_points = 11; let weak_points = 5; // ~70-30 split let strong_perf = calculate_weighted_score(strong.rating, weak.rating, strong_points, weak_points); let weak_perf = calculate_weighted_score(weak.rating, strong.rating, weak_points, strong_points); strong = calc.update_rating(&strong, &weak, strong_perf); weak = calc.update_rating(&weak, &strong, weak_perf); } // Strong player should be significantly higher rated now assert!(strong.rating > weak.rating + 100.0); println!("After 20 matches (70-30 split): Strong={:.0}, Weak={:.0}, Diff={:.0}", strong.rating, weak.rating, strong.rating - weak.rating); } // ============================================ // SCORE WEIGHT TESTS // ============================================ #[test] fn test_score_weight_various_margins() { // Compare performance scores for different margins let scores = vec![ (11, 0, "11-0 shutout"), (11, 1, "11-1"), (11, 5, "11-5"), (11, 9, "11-9 close"), (11, 10, "11-10 tiebreak"), ]; println!("Performance scores (1500 vs 1500):"); for (won, lost, label) in scores { let perf = calculate_weighted_score(1500.0, 1500.0, won, lost); println!(" {}: {:.3}", label, perf); } } #[test] fn test_performance_symmetry() { // Winner and loser performances should sum to 1.0 let winner_perf = calculate_weighted_score(1500.0, 1500.0, 11, 7); let loser_perf = calculate_weighted_score(1500.0, 1500.0, 7, 11); assert!((winner_perf + loser_perf - 1.0).abs() < 0.001); println!("Performance symmetry: Winner {:.3} + Loser {:.3} = {:.3}", winner_perf, loser_perf, winner_perf + loser_perf); } }