284 lines
10 KiB
Rust
284 lines
10 KiB
Rust
// 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
|
||
);
|
||
}
|
||
}
|