CHANGES: 1. Replace arbitrary margin bonus with per-point expected value - Replace tanh formula in score_weight.rs - New: performance = actual_points / total_points - Expected: P(point) = 1 / (1 + 10^((R_opp - R_self)/400)) - Outcome now reflects actual performance vs expected 2. Fix RD-based distribution (backwards logic) - Changed weight from 1.0/rd² to rd² - Higher RD (uncertain) now gets more change - Lower RD (certain) gets less change - Follows correct Glicko-2 principle 3. Add new effective opponent calculation for doubles - New functions: calculate_effective_opponent_rating() - Formula: Eff_Opp = Opp1 + Opp2 - Teammate - Personalizes rating change by partner strength - Strong teammate → lower effective opponent - Weak teammate → higher effective opponent 4. Document unified rating consolidation (Phase 1) - Added REFACTORING_NOTES.md with full plan - Schema changes identified but deferred - Code is ready for single rating migration All changes: - Compile successfully (release build) - Pass all 14 unit tests - Backwards compatible with demo/example code updated - Database backup available at pickleball.db.backup-20260226-105326
98 lines
3.5 KiB
Rust
98 lines
3.5 KiB
Rust
/// 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);
|
|
}
|
|
}
|