PickleBALLER/src/simple_demo.rs
Split 9ae1bd37fd Refactor: Implement all four ELO system improvements
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
2026-02-26 10:58:10 -05:00

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,
);
}
}