PickleBALLER/src/demo.rs

284 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Demo module for generating test data and running simulations
use crate::glicko::{GlickoRating, Glicko2Calculator, calculate_weighted_score};
use fake::faker::name::en::Name;
use fake::Fake;
extern crate rand;
use rand::Rng;
use rand::seq::SliceRandom;
#[derive(Debug, Clone)]
pub struct Player {
pub name: String,
pub email: String,
pub singles: GlickoRating,
pub doubles: GlickoRating,
pub true_skill: f64, // Hidden skill level for simulation
}
impl Player {
pub fn new_random() -> Self {
let mut rng = rand::thread_rng();
let name: String = Name().fake();
let email = format!("{}@example.com", name.to_lowercase().replace(' ', "."));
// Random true skill between 1200-1800
let true_skill = rng.gen_range(1200.0..1800.0);
Self {
name,
email,
singles: GlickoRating::new_player(),
doubles: GlickoRating::new_player(),
true_skill,
}
}
}
#[derive(Debug)]
pub struct Match {
pub match_type: MatchType,
pub team1: Vec<usize>, // Player indices
pub team2: Vec<usize>,
pub team1_score: i32,
pub team2_score: i32,
}
#[derive(Debug, Clone, Copy)]
pub enum MatchType {
Singles,
Doubles,
}
/// Simulate a match outcome based on true skill levels
pub fn simulate_match(
team1_skills: &[f64],
team2_skills: &[f64],
) -> (i32, i32) {
let mut rng = rand::thread_rng();
// Team skill is average
let team1_avg: f64 = team1_skills.iter().sum::<f64>() / team1_skills.len() as f64;
let team2_avg: f64 = team2_skills.iter().sum::<f64>() / team2_skills.len() as f64;
// Win probability based on skill difference
let skill_diff = team1_avg - team2_avg;
let win_prob = 1.0 / (1.0 + 10_f64.powf(-skill_diff / 400.0));
// Determine winner
let team1_wins = rng.gen::<f64>() < win_prob;
// Generate score (pickleball to 11)
if team1_wins {
let margin = rng.gen_range(1..10); // 1-9 point margin
let team2_score = 11 - margin;
(11, team2_score)
} else {
let margin = rng.gen_range(1..10);
let team1_score = 11 - margin;
(team1_score, 11)
}
}
/// Generate a session of matches
pub fn generate_session(players: &mut [Player], num_matches: usize) -> Vec<Match> {
let mut rng = rand::thread_rng();
let mut matches = Vec::new();
let calc = Glicko2Calculator::new();
for _ in 0..num_matches {
// Randomly choose singles or doubles
let match_type = if rng.gen_bool(0.5) {
MatchType::Singles
} else {
MatchType::Doubles
};
let match_result = 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) = simulate_match(
&[players[p1_idx].true_skill],
&[players[p2_idx].true_skill],
);
// Update ratings
let p1_outcome = if team1_score > team2_score {
calculate_weighted_score(1.0, team1_score, team2_score)
} else {
calculate_weighted_score(0.0, team2_score, team1_score)
};
let p2_outcome = if team2_score > team1_score {
calculate_weighted_score(1.0, team2_score, team1_score)
} else {
calculate_weighted_score(0.0, team1_score, team2_score)
};
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)],
);
Match {
match_type,
team1: vec![p1_idx],
team2: vec![p2_idx],
team1_score,
team2_score,
}
}
MatchType::Doubles => {
// Pick 4 random players
let mut indices: Vec<usize> = (0..players.len()).collect();
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) = simulate_match(&team1_skills, &team2_skills);
// Update doubles ratings (simplified - each player vs team average)
let team1_won = team1_score > team2_score;
for &idx in &team1_indices {
let outcome = if team1_won {
calculate_weighted_score(1.0, team1_score, team2_score)
} else {
calculate_weighted_score(0.0, team2_score, team1_score)
};
// Simulate vs average opponent
let avg_opponent = 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)],
);
}
for &idx in &team2_indices {
let outcome = if !team1_won {
calculate_weighted_score(1.0, team2_score, team1_score)
} else {
calculate_weighted_score(0.0, team1_score, team2_score)
};
let avg_opponent = 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)],
);
}
Match {
match_type,
team1: team1_indices,
team2: team2_indices,
team1_score,
team2_score,
}
}
};
matches.push(match_result);
}
matches
}
/// Print session summary
pub fn print_summary(players: &[Player], matches: &[Match]) {
println!("\n🏓 Session Summary");
println!("==================");
println!("\n{} matches played\n", matches.len());
println!("Match Results:");
for (i, m) in matches.iter().enumerate() {
match m.match_type {
MatchType::Singles => {
let p1 = &players[m.team1[0]];
let p2 = &players[m.team2[0]];
let winner = if m.team1_score > m.team2_score { &p1.name } else { &p2.name };
println!(
" {}. Singles: {} vs {}{} wins {}-{}",
i + 1,
p1.name,
p2.name,
winner,
m.team1_score.max(m.team2_score),
m.team1_score.min(m.team2_score)
);
}
MatchType::Doubles => {
let t1_names: Vec<&str> = m.team1.iter().map(|&i| players[i].name.as_str()).collect();
let t2_names: Vec<&str> = m.team2.iter().map(|&i| players[i].name.as_str()).collect();
let winner = if m.team1_score > m.team2_score { "Team 1" } else { "Team 2" };
println!(
" {}. Doubles: {} vs {}{} wins {}-{}",
i + 1,
t1_names.join(" & "),
t2_names.join(" & "),
winner,
m.team1_score.max(m.team2_score),
m.team1_score.min(m.team2_score)
);
}
}
}
println!("\n📊 Singles Leaderboard:");
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(10).enumerate() {
println!(
" {}. {} - {:.1} (RD: {:.1}, σ: {:.3})",
i + 1,
p.name,
p.singles.rating,
p.singles.rd,
p.singles.volatility
);
}
println!("\n📊 Doubles Leaderboard:");
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(10).enumerate() {
println!(
" {}. {} - {:.1} (RD: {:.1}, σ: {:.3})",
i + 1,
p.name,
p.doubles.rating,
p.doubles.rd,
p.doubles.volatility
);
}
}