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
189 lines
7.7 KiB
Rust
189 lines
7.7 KiB
Rust
// Simple in-memory demo without database for testing
|
|
|
|
use crate::demo::{self, Player, MatchType};
|
|
use crate::glicko::{Glicko2Calculator, calculate_weighted_score};
|
|
use rand::Rng;
|
|
|
|
pub async fn run_simple_demo() {
|
|
println!("🏓 Pickleball ELO Tracker v2.0 - Simple Demo");
|
|
println!("==========================================\n");
|
|
|
|
// Generate 20 players
|
|
println!("👥 Generating 20 players...");
|
|
let mut players: Vec<Player> = (0..20)
|
|
.map(|_| Player::new_random())
|
|
.collect();
|
|
println!("✅ Players generated\n");
|
|
|
|
println!("Session 1: Opening Tournament");
|
|
println!("=============================\n");
|
|
|
|
run_session(&mut players, 1, 50);
|
|
|
|
println!("\nSession 2: Mid-Tournament");
|
|
println!("========================\n");
|
|
|
|
run_session(&mut players, 2, 55);
|
|
|
|
println!("\nSession 3: Finals");
|
|
println!("================\n");
|
|
|
|
run_session(&mut players, 3, 52);
|
|
|
|
// Print final leaderboards
|
|
println!("\n📧 Final Leaderboards:\n");
|
|
print_leaderboard(&players);
|
|
|
|
println!("\n✅ Demo Complete!");
|
|
println!("\nTotal Players: {}", players.len());
|
|
println!("Total Matches Across 3 Sessions: {}", 50 + 55 + 52);
|
|
}
|
|
|
|
fn run_session(players: &mut [Player], session_num: usize, num_matches: usize) {
|
|
println!("Starting session {}...", session_num);
|
|
let mut rng = rand::thread_rng();
|
|
let calc = Glicko2Calculator::new();
|
|
|
|
for i in 0..num_matches {
|
|
if i % 20 == 0 && i > 0 {
|
|
println!(" {} matches completed...", i);
|
|
}
|
|
|
|
// Randomly choose singles or doubles
|
|
let match_type = if rng.gen_bool(0.5) {
|
|
MatchType::Singles
|
|
} else {
|
|
MatchType::Doubles
|
|
};
|
|
|
|
match match_type {
|
|
MatchType::Singles => {
|
|
// Pick 2 random players
|
|
let p1_idx = rng.gen_range(0..players.len());
|
|
let mut p2_idx = rng.gen_range(0..players.len());
|
|
while p2_idx == p1_idx {
|
|
p2_idx = rng.gen_range(0..players.len());
|
|
}
|
|
|
|
let (team1_score, team2_score) = demo::simulate_match(
|
|
&[players[p1_idx].true_skill],
|
|
&[players[p2_idx].true_skill],
|
|
);
|
|
|
|
// Calculate outcomes with performance-based weighting
|
|
let p1_outcome = if team1_score > team2_score {
|
|
calculate_weighted_score(players[p1_idx].singles.rating, players[p2_idx].singles.rating, team1_score, team2_score)
|
|
} else {
|
|
calculate_weighted_score(players[p1_idx].singles.rating, players[p2_idx].singles.rating, team1_score, team2_score)
|
|
};
|
|
|
|
let p2_outcome = if team1_score > team2_score {
|
|
calculate_weighted_score(players[p2_idx].singles.rating, players[p1_idx].singles.rating, team2_score, team1_score)
|
|
} else {
|
|
calculate_weighted_score(players[p2_idx].singles.rating, players[p1_idx].singles.rating, team2_score, team1_score)
|
|
};
|
|
|
|
// Update ratings
|
|
players[p1_idx].singles = calc.update_rating(
|
|
&players[p1_idx].singles,
|
|
&[(players[p2_idx].singles, p1_outcome)],
|
|
);
|
|
|
|
players[p2_idx].singles = calc.update_rating(
|
|
&players[p2_idx].singles,
|
|
&[(players[p1_idx].singles, p2_outcome)],
|
|
);
|
|
}
|
|
MatchType::Doubles => {
|
|
// Pick 4 random players
|
|
let mut indices: Vec<usize> = (0..players.len()).collect();
|
|
use rand::seq::SliceRandom;
|
|
indices[..].shuffle(&mut rng);
|
|
let team1_indices = vec![indices[0], indices[1]];
|
|
let team2_indices = vec![indices[2], indices[3]];
|
|
|
|
let team1_skills: Vec<f64> = team1_indices.iter()
|
|
.map(|&i| players[i].true_skill)
|
|
.collect();
|
|
let team2_skills: Vec<f64> = team2_indices.iter()
|
|
.map(|&i| players[i].true_skill)
|
|
.collect();
|
|
|
|
let (team1_score, team2_score) = demo::simulate_match(&team1_skills, &team2_skills);
|
|
let team1_won = team1_score > team2_score;
|
|
|
|
// Update team 1
|
|
for &idx in &team1_indices {
|
|
let outcome = if team1_won {
|
|
calculate_weighted_score(players[idx].doubles.rating, team2_indices.iter().map(|&i| players[i].doubles.rating).sum::<f64>() / 2.0, team1_score, team2_score)
|
|
} else {
|
|
calculate_weighted_score(players[idx].doubles.rating, team2_indices.iter().map(|&i| players[i].doubles.rating).sum::<f64>() / 2.0, team1_score, team2_score)
|
|
};
|
|
|
|
let avg_opponent = crate::glicko::GlickoRating {
|
|
rating: team2_indices.iter().map(|&i| players[i].doubles.rating).sum::<f64>() / 2.0,
|
|
rd: team2_indices.iter().map(|&i| players[i].doubles.rd).sum::<f64>() / 2.0,
|
|
volatility: 0.06,
|
|
};
|
|
|
|
players[idx].doubles = calc.update_rating(
|
|
&players[idx].doubles,
|
|
&[(avg_opponent, outcome)],
|
|
);
|
|
}
|
|
|
|
// Update team 2
|
|
for &idx in &team2_indices {
|
|
let outcome = if !team1_won {
|
|
calculate_weighted_score(players[idx].doubles.rating, team1_indices.iter().map(|&i| players[i].doubles.rating).sum::<f64>() / 2.0, team2_score, team1_score)
|
|
} else {
|
|
calculate_weighted_score(players[idx].doubles.rating, team1_indices.iter().map(|&i| players[i].doubles.rating).sum::<f64>() / 2.0, team2_score, team1_score)
|
|
};
|
|
|
|
let avg_opponent = crate::glicko::GlickoRating {
|
|
rating: team1_indices.iter().map(|&i| players[i].doubles.rating).sum::<f64>() / 2.0,
|
|
rd: team1_indices.iter().map(|&i| players[i].doubles.rd).sum::<f64>() / 2.0,
|
|
volatility: 0.06,
|
|
};
|
|
|
|
players[idx].doubles = calc.update_rating(
|
|
&players[idx].doubles,
|
|
&[(avg_opponent, outcome)],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
println!("✅ Completed {} matches", num_matches);
|
|
print_leaderboard(players);
|
|
}
|
|
|
|
fn print_leaderboard(players: &[Player]) {
|
|
println!("\n📊 Top 5 Singles:");
|
|
let mut singles_sorted = players.to_vec();
|
|
singles_sorted.sort_by(|a, b| b.singles.rating.partial_cmp(&a.singles.rating).unwrap());
|
|
for (i, p) in singles_sorted.iter().take(5).enumerate() {
|
|
println!(
|
|
" {}. {} - {:.1} (RD: {:.1})",
|
|
i + 1,
|
|
p.name,
|
|
p.singles.rating,
|
|
p.singles.rd,
|
|
);
|
|
}
|
|
|
|
println!("\n📊 Top 5 Doubles:");
|
|
let mut doubles_sorted = players.to_vec();
|
|
doubles_sorted.sort_by(|a, b| b.doubles.rating.partial_cmp(&a.doubles.rating).unwrap());
|
|
for (i, p) in doubles_sorted.iter().take(5).enumerate() {
|
|
println!(
|
|
" {}. {} - {:.1} (RD: {:.1})",
|
|
i + 1,
|
|
p.name,
|
|
p.doubles.rating,
|
|
p.doubles.rd,
|
|
);
|
|
}
|
|
}
|