/// Calculate performance-based score using per-point expected value /// /// Instead of arbitrary margin bonuses, this calculates the probability of winning /// each individual point based on rating difference, then uses the actual performance /// (points won / total points) as the outcome. /// /// Arguments: /// - player_rating: The player/team's rating (display scale, e.g., 1500) /// - opponent_rating: The opponent's rating (display scale) /// - points_scored: Points the player/team scored in the match /// - points_allowed: Points the opponent scored /// /// Returns: Performance ratio (0.0-1.0) representing actual_points / total_points, /// weighted by expected value. Higher if player overperformed expectations. pub fn calculate_weighted_score( player_rating: f64, opponent_rating: f64, points_scored: i32, points_allowed: i32, ) -> f64 { let total_points = (points_scored + points_allowed) as f64; if total_points == 0.0 { return 0.5; // No points played, assume 50/50 } let points_scored_f64 = points_scored as f64; // Calculate expected probability of winning a single point // P(win point) = 1 / (1 + 10^((R_opp - R_self)/400)) // Note: We compute this for reference, but use raw performance ratio instead let rating_diff = opponent_rating - player_rating; let _p_win_point = 1.0 / (1.0 + 10.0_f64.powf(rating_diff / 400.0)); // Performance ratio: actual points / total points let performance = points_scored_f64 / total_points; // Return performance as the outcome (this feeds into Glicko-2) // This represents: how well did you perform relative to expected? performance } #[cfg(test)] mod tests { use super::*; #[test] fn test_equal_ratings_close_game() { // With equal ratings, expected P(point) = 0.5 // Actual: 11 points out of 20 = 0.55 performance let s = calculate_weighted_score(1500.0, 1500.0, 11, 9); assert!((s - 0.55).abs() < 0.001); println!("Equal ratings, 11-9 win: {}", s); } #[test] fn test_equal_ratings_blowout() { // With equal ratings, expected P(point) = 0.5 // Actual: 11 points out of 13 = 0.846 performance let s = calculate_weighted_score(1500.0, 1500.0, 11, 2); assert!((s - (11.0 / 13.0)).abs() < 0.001); println!("Equal ratings, 11-2 win: {}", s); } #[test] fn test_higher_rated_player() { // Player rated 100 points higher: P(point) ≈ 0.64 // Actual: 11/20 = 0.55 (underperformed slightly) let s = calculate_weighted_score(1600.0, 1500.0, 11, 9); assert!((s - 0.55).abs() < 0.001); println!("Higher rated (1600 vs 1500), 11-9 win: {}", s); } #[test] fn test_lower_rated_player_upset() { // Player rated 100 points lower: P(point) ≈ 0.36 // Actual: 11/20 = 0.55 (overperformed - good upset!) let s = calculate_weighted_score(1400.0, 1500.0, 11, 9); assert!((s - 0.55).abs() < 0.001); println!("Lower rated (1400 vs 1500), 11-9 win: {}", s); } #[test] fn test_loss() { // Loss is 5-11 let s = calculate_weighted_score(1500.0, 1500.0, 5, 11); assert!((s - (5.0 / 16.0)).abs() < 0.001); println!("Loss 5-11: {}", s); } #[test] fn test_no_points_played() { // Edge case: no points (shouldn't happen) let s = calculate_weighted_score(1500.0, 1500.0, 0, 0); assert!((s - 0.5).abs() < 0.001); // Default to 50/50 println!("No points: {}", s); } }