Unified ELO: single rating column, recalculated all matches
- Added 'rating' column to players table - Renamed old columns to _deprecated_* - Created recalculate_ratings.rs tool to replay all matches - Updated all queries and structs to use unified rating - Match form now shows single rating per player - API returns single rating field Final ratings after recalculation: - Andrew: 1538 - David: 1522 - Jacklyn: 1515 - Eliana: 1497 - Krzysztof: 1476 - Dane: 1449
This commit is contained in:
parent
a1f96b9af4
commit
d605000c28
@ -1094,3 +1094,16 @@ Starting Pickleball ELO Tracker Server on port 3000...
|
||||
➕ Add Player: http://localhost:3000/players/new
|
||||
🎾 Record Match: http://localhost:3000/matches/new
|
||||
|
||||
🏓 Pickleball ELO Tracker v3.0
|
||||
==============================
|
||||
|
||||
Starting Pickleball ELO Tracker Server on port 3000...
|
||||
|
||||
✅ Server running at http://localhost:3000
|
||||
📊 Leaderboard: http://localhost:3000/leaderboard
|
||||
📜 Match History: http://localhost:3000/matches
|
||||
👥 Players: http://localhost:3000/players
|
||||
⚖️ Team Balancer: http://localhost:3000/balance
|
||||
➕ Add Player: http://localhost:3000/players/new
|
||||
🎾 Record Match: http://localhost:3000/matches/new
|
||||
|
||||
|
||||
BIN
pickleball-elo
BIN
pickleball-elo
Binary file not shown.
BIN
pickleball.db
BIN
pickleball.db
Binary file not shown.
BIN
pickleball.db.backup-pre-unified-20260226-131223
Normal file
BIN
pickleball.db.backup-pre-unified-20260226-131223
Normal file
Binary file not shown.
202
src/bin/recalculate_ratings.rs
Normal file
202
src/bin/recalculate_ratings.rs
Normal file
@ -0,0 +1,202 @@
|
||||
//! Recalculate all ratings from scratch using unified ELO
|
||||
//!
|
||||
//! This script maintains all state in memory, then writes to DB at the end.
|
||||
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const K_FACTOR: f64 = 32.0;
|
||||
const STARTING_RATING: f64 = 1500.0;
|
||||
|
||||
fn expected_score(player_rating: f64, opponent_rating: f64) -> f64 {
|
||||
1.0 / (1.0 + 10.0_f64.powf((opponent_rating - player_rating) / 400.0))
|
||||
}
|
||||
|
||||
fn calculate_performance(points_won: i32, points_lost: i32) -> f64 {
|
||||
let total = (points_won + points_lost) as f64;
|
||||
if total == 0.0 { return 0.5; }
|
||||
points_won as f64 / total
|
||||
}
|
||||
|
||||
fn calculate_new_rating(current: f64, opponent: f64, performance: f64) -> f64 {
|
||||
let expected = expected_score(current, opponent);
|
||||
let change = K_FACTOR * (performance - expected);
|
||||
(current + change).max(1.0)
|
||||
}
|
||||
|
||||
fn effective_opponent(opp1: f64, opp2: f64, teammate: f64) -> f64 {
|
||||
opp1 + opp2 - teammate
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MatchUpdate {
|
||||
match_id: i64,
|
||||
player_id: i64,
|
||||
rating_before: f64,
|
||||
rating_after: f64,
|
||||
rating_change: f64,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🔄 Recalculating all ratings with unified ELO...\n");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect("sqlite:pickleball.db")
|
||||
.await?;
|
||||
|
||||
// Step 1: Get all players and initialize ratings in memory
|
||||
println!("Step 1: Loading players");
|
||||
let players: Vec<(i64, String)> = sqlx::query_as("SELECT id, name FROM players")
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
let mut ratings: HashMap<i64, f64> = HashMap::new();
|
||||
for (id, name) in &players {
|
||||
ratings.insert(*id, STARTING_RATING);
|
||||
println!(" {} (id={}) -> {}", name, id, STARTING_RATING);
|
||||
}
|
||||
|
||||
// Step 2: Get all matches in chronological order
|
||||
println!("\nStep 2: Loading matches");
|
||||
let matches: Vec<(i64, String, i32, i32)> = sqlx::query_as(
|
||||
"SELECT id, match_type, team1_score, team2_score FROM matches ORDER BY timestamp ASC"
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
println!(" Found {} matches\n", matches.len());
|
||||
|
||||
// Step 3: Get all participants for all matches
|
||||
let all_participants: Vec<(i64, i64, i32)> = sqlx::query_as(
|
||||
"SELECT match_id, player_id, team FROM match_participants ORDER BY match_id, team"
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
|
||||
// Group participants by match
|
||||
let mut match_participants: HashMap<i64, Vec<(i64, i32)>> = HashMap::new();
|
||||
for (match_id, player_id, team) in all_participants {
|
||||
match_participants.entry(match_id).or_insert_with(Vec::new).push((player_id, team));
|
||||
}
|
||||
|
||||
// Step 4: Process each match
|
||||
println!("Step 3: Processing matches...");
|
||||
let mut all_updates: Vec<MatchUpdate> = Vec::new();
|
||||
|
||||
for (match_id, match_type, team1_score, team2_score) in &matches {
|
||||
let participants = match match_participants.get(match_id) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
println!(" ⚠️ Match {} has no participants", match_id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let team1: Vec<i64> = participants.iter().filter(|(_, t)| *t == 1).map(|(id, _)| *id).collect();
|
||||
let team2: Vec<i64> = participants.iter().filter(|(_, t)| *t == 2).map(|(id, _)| *id).collect();
|
||||
|
||||
if team1.is_empty() || team2.is_empty() {
|
||||
println!(" ⚠️ Match {} missing team members", match_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
let team1_perf = calculate_performance(*team1_score, *team2_score);
|
||||
let team2_perf = 1.0 - team1_perf;
|
||||
let is_doubles = match_type == "doubles" && team1.len() >= 2 && team2.len() >= 2;
|
||||
|
||||
// Calculate new ratings for team 1
|
||||
let mut team1_new_ratings: Vec<(i64, f64, f64, f64)> = Vec::new();
|
||||
for &player_id in &team1 {
|
||||
let current = ratings[&player_id];
|
||||
let opp_rating = if is_doubles && team1.len() == 2 && team2.len() == 2 {
|
||||
if let Some(&teammate_id) = team1.iter().find(|&&id| id != player_id) {
|
||||
effective_opponent(ratings[&team2[0]], ratings[&team2[1]], ratings[&teammate_id])
|
||||
} else {
|
||||
team2.iter().map(|id| ratings[id]).sum::<f64>() / team2.len() as f64
|
||||
}
|
||||
} else {
|
||||
// Singles or incomplete doubles: just average opponent ratings
|
||||
team2.iter().map(|id| ratings[id]).sum::<f64>() / team2.len() as f64
|
||||
};
|
||||
let new_rating = calculate_new_rating(current, opp_rating, team1_perf);
|
||||
team1_new_ratings.push((player_id, current, new_rating, new_rating - current));
|
||||
}
|
||||
|
||||
// Calculate new ratings for team 2
|
||||
let mut team2_new_ratings: Vec<(i64, f64, f64, f64)> = Vec::new();
|
||||
for &player_id in &team2 {
|
||||
let current = ratings[&player_id];
|
||||
let opp_rating = if is_doubles && team1.len() == 2 && team2.len() == 2 {
|
||||
if let Some(&teammate_id) = team2.iter().find(|&&id| id != player_id) {
|
||||
effective_opponent(ratings[&team1[0]], ratings[&team1[1]], ratings[&teammate_id])
|
||||
} else {
|
||||
team1.iter().map(|id| ratings[id]).sum::<f64>() / team1.len() as f64
|
||||
}
|
||||
} else {
|
||||
// Singles or incomplete doubles: just average opponent ratings
|
||||
team1.iter().map(|id| ratings[id]).sum::<f64>() / team1.len() as f64
|
||||
};
|
||||
let new_rating = calculate_new_rating(current, opp_rating, team2_perf);
|
||||
team2_new_ratings.push((player_id, current, new_rating, new_rating - current));
|
||||
}
|
||||
|
||||
// Apply updates to our in-memory ratings
|
||||
for (player_id, before, after, change) in team1_new_ratings.iter().chain(team2_new_ratings.iter()) {
|
||||
ratings.insert(*player_id, *after);
|
||||
all_updates.push(MatchUpdate {
|
||||
match_id: *match_id,
|
||||
player_id: *player_id,
|
||||
rating_before: *before,
|
||||
rating_after: *after,
|
||||
rating_change: *change,
|
||||
});
|
||||
}
|
||||
|
||||
print!(".");
|
||||
}
|
||||
|
||||
// Step 5: Write all updates to database
|
||||
println!("\n\nStep 4: Writing {} updates to database...", all_updates.len());
|
||||
|
||||
// Update players table
|
||||
for (player_id, rating) in &ratings {
|
||||
sqlx::query("UPDATE players SET rating = ? WHERE id = ?")
|
||||
.bind(rating)
|
||||
.bind(player_id)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Update match_participants table
|
||||
for update in &all_updates {
|
||||
sqlx::query(
|
||||
"UPDATE match_participants SET rating_before = ?, rating_after = ?, rating_change = ? WHERE match_id = ? AND player_id = ?"
|
||||
)
|
||||
.bind(update.rating_before)
|
||||
.bind(update.rating_after)
|
||||
.bind(update.rating_change)
|
||||
.bind(update.match_id)
|
||||
.bind(update.player_id)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Step 6: Display final ratings
|
||||
println!("\n✅ Final ratings:");
|
||||
let mut sorted: Vec<_> = ratings.iter().collect();
|
||||
sorted.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
|
||||
|
||||
let player_names: HashMap<i64, String> = players.into_iter().collect();
|
||||
|
||||
println!("\n{:<25} {:>10}", "Player", "Rating");
|
||||
println!("{}", "-".repeat(37));
|
||||
for (id, rating) in sorted {
|
||||
let name = player_names.get(id).map(|s| s.as_str()).unwrap_or("Unknown");
|
||||
println!("{:<25} {:>10.0}", name, rating);
|
||||
}
|
||||
|
||||
println!("\n🎉 Recalculation complete!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
103
src/main.rs
103
src/main.rs
@ -30,8 +30,7 @@ struct EditPlayer {
|
||||
name: String,
|
||||
#[serde(default, deserialize_with = "empty_string_as_none_string")]
|
||||
email: Option<String>,
|
||||
singles_rating: f64,
|
||||
doubles_rating: f64,
|
||||
rating: f64,
|
||||
}
|
||||
|
||||
fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
|
||||
@ -83,8 +82,7 @@ struct BalanceQuery {
|
||||
struct PlayerJson {
|
||||
id: i64,
|
||||
name: String,
|
||||
singles_rating: f64,
|
||||
doubles_rating: f64,
|
||||
rating: f64,
|
||||
}
|
||||
|
||||
// Common CSS used across pages - Pitt colors (Blue #003594, Gold #FFB81C)
|
||||
@ -201,7 +199,6 @@ struct PlayerData {
|
||||
id: i64,
|
||||
name: String,
|
||||
rating: f64,
|
||||
singles_rating: f64,
|
||||
email: String,
|
||||
rating_display: String,
|
||||
has_email: bool,
|
||||
@ -511,15 +508,15 @@ async fn player_profile_handler(
|
||||
Path(player_id): Path<i64>,
|
||||
) -> Result<Html<String>, (StatusCode, String)> {
|
||||
// Get player info
|
||||
let player: Option<(i64, String, Option<String>, f64, f64, f64, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, email, singles_rating, singles_rd, doubles_rating, doubles_rd FROM players WHERE id = ?"
|
||||
let player: Option<(i64, String, Option<String>, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, email, rating FROM players WHERE id = ?"
|
||||
)
|
||||
.bind(player_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let (_id, name, email, singles_rating, _singles_rd, _doubles_rating, _doubles_rd) = player
|
||||
let (_id, name, email, rating) = player
|
||||
.ok_or((StatusCode::NOT_FOUND, "Player not found".to_string()))?;
|
||||
|
||||
// Get match stats
|
||||
@ -611,8 +608,8 @@ async fn player_profile_handler(
|
||||
if wins >= 1 { achievements.push("✨ First Win"); }
|
||||
if wins >= 10 { achievements.push("🔥 10 Wins"); }
|
||||
if win_rate >= 60 && total_matches >= 5 { achievements.push("💪 60% Win Rate"); }
|
||||
if singles_rating >= 1700.0 { achievements.push("⭐ Rising Star (1700+)"); }
|
||||
if singles_rating >= 1900.0 { achievements.push("👑 Elite (1900+)"); }
|
||||
if rating >= 1700.0 { achievements.push("⭐ Rising Star (1700+)"); }
|
||||
if rating >= 1900.0 { achievements.push("👑 Elite (1900+)"); }
|
||||
|
||||
let achievements_html: String = achievements.iter()
|
||||
.map(|a| format!(r#"<span class="achievement">{}</span>"#, a))
|
||||
@ -794,7 +791,7 @@ async fn player_profile_handler(
|
||||
</html>
|
||||
"#, name, COMMON_CSS, nav_html(), name,
|
||||
email.as_deref().unwrap_or("No email"), player_id,
|
||||
singles_rating, total_matches, wins, losses, win_rate,
|
||||
rating, total_matches, wins, losses, win_rate,
|
||||
achievements_html, h2h_rows, partners_rows, recent_rows, chart_data, chart_data);
|
||||
|
||||
Ok(Html(html))
|
||||
@ -814,7 +811,7 @@ async fn team_balancer_handler(
|
||||
Query(params): Query<BalanceQuery>,
|
||||
) -> Html<String> {
|
||||
let players: Vec<(i64, String, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, singles_rating FROM players ORDER BY name"
|
||||
"SELECT id, name, rating FROM players ORDER BY name"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
@ -983,9 +980,9 @@ async fn delete_match(
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Revert each player's rating (UNIFIED: always use singles_rating)
|
||||
// Revert each player's rating (UNIFIED: always use rating)
|
||||
for (player_id, _match_type, rating_before, _rating_after) in &participants {
|
||||
sqlx::query("UPDATE players SET singles_rating = ? WHERE id = ?")
|
||||
sqlx::query("UPDATE players SET rating = ? WHERE id = ?")
|
||||
.bind(rating_before)
|
||||
.bind(player_id)
|
||||
.execute(&state.pool)
|
||||
@ -1096,15 +1093,15 @@ async fn edit_player_form(
|
||||
State(state): State<AppState>,
|
||||
Path(player_id): Path<i64>,
|
||||
) -> Result<Html<String>, (StatusCode, String)> {
|
||||
let player: Option<(i64, String, Option<String>, f64, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, email, singles_rating, doubles_rating FROM players WHERE id = ?"
|
||||
let player: Option<(i64, String, Option<String>, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, email, rating FROM players WHERE id = ?"
|
||||
)
|
||||
.bind(player_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let (id, name, email, singles_rating, doubles_rating) = player
|
||||
let (id, name, email, rating) = player
|
||||
.ok_or((StatusCode::NOT_FOUND, "Player not found".to_string()))?;
|
||||
|
||||
let email_value = email.unwrap_or_default();
|
||||
@ -1138,9 +1135,8 @@ async fn edit_player_form(
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: bold; margin-bottom: 8px;">ELO Rating</label>
|
||||
<input type="number" name="singles_rating" step="0.1" value="{:.1}"
|
||||
<input type="number" name="rating" step="0.1" value="{:.1}"
|
||||
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;">
|
||||
<input type="hidden" name="doubles_rating" value="{:.1}">
|
||||
<p style="color: #666; font-size: 12px; margin-top: 5px;">Unified rating for all match types</p>
|
||||
</div>
|
||||
|
||||
@ -1151,7 +1147,7 @@ async fn edit_player_form(
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"#, name, COMMON_CSS, nav_html(), id, name, email_value, singles_rating, doubles_rating, id);
|
||||
"#, name, COMMON_CSS, nav_html(), id, name, email_value, rating, id);
|
||||
|
||||
Ok(Html(html))
|
||||
}
|
||||
@ -1167,8 +1163,7 @@ async fn edit_player_form(
|
||||
/// **Form Fields:**
|
||||
/// - `name` (required): Updated player name
|
||||
/// - `email` (optional): Updated email address
|
||||
/// - `singles_rating` (required): Updated singles rating value
|
||||
/// - `doubles_rating` (required): Updated doubles rating value
|
||||
/// - `rating` (required): Updated unified ELO rating value
|
||||
///
|
||||
/// **Returns:** Redirect to player profile on success, or error response on failure
|
||||
async fn update_player(
|
||||
@ -1179,12 +1174,12 @@ async fn update_player(
|
||||
let email = player.email.filter(|e| !e.is_empty());
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE players SET name = ?, email = ?, singles_rating = ?, doubles_rating = ? WHERE id = ?"
|
||||
"UPDATE players SET name = ?, email = ?, rating = ? WHERE id = ?"
|
||||
)
|
||||
.bind(&player.name)
|
||||
.bind(&email)
|
||||
.bind(player.singles_rating)
|
||||
.bind(player.doubles_rating)
|
||||
.bind(player.rating)
|
||||
.bind(player_id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?;
|
||||
@ -1202,16 +1197,16 @@ async fn update_player(
|
||||
///
|
||||
/// **Returns:** HTML form page with player dropdown lists
|
||||
async fn new_match_form(State(state): State<AppState>) -> Html<String> {
|
||||
let players: Vec<(i64, String, f64, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, singles_rating, doubles_rating FROM players ORDER BY name"
|
||||
let players: Vec<(i64, String, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, rating FROM players ORDER BY name"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let player_options: String = players.iter()
|
||||
.map(|(id, name, sr, dr)| {
|
||||
format!(r#"<option value="{}">{} (S:{:.0} / D:{:.0})</option>"#, id, name, sr, dr)
|
||||
.map(|(id, name, rating)| {
|
||||
format!(r#"<option value="{}">{} ({:.0})</option>"#, id, name, rating)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -1359,7 +1354,7 @@ async fn create_match(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create match: {}", e)))?;
|
||||
|
||||
let is_doubles = match_data.match_type == "doubles";
|
||||
// UNIFIED RATING: Always use singles_rating as the unified rating column
|
||||
// UNIFIED RATING: Always use rating as the unified rating column
|
||||
let rating_col = "singles";
|
||||
|
||||
let mut team1_players = vec![match_data.team1_player1];
|
||||
@ -1609,9 +1604,8 @@ async fn players_list_handler(State(state): State<AppState>) -> impl IntoRespons
|
||||
id,
|
||||
name,
|
||||
rating,
|
||||
singles_rating: rating,
|
||||
email: email.unwrap_or_default(),
|
||||
rating_display: format!("{:.1}", rating),
|
||||
rating_display: format!("{:.0}", rating),
|
||||
has_email,
|
||||
wins,
|
||||
losses,
|
||||
@ -1661,9 +1655,8 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
|
||||
id,
|
||||
name,
|
||||
rating,
|
||||
singles_rating: rating,
|
||||
email: email.unwrap_or_default(),
|
||||
rating_display: format!("{:.1}", rating),
|
||||
rating_display: format!("{:.0}", rating),
|
||||
has_email,
|
||||
wins,
|
||||
losses,
|
||||
@ -1687,10 +1680,10 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
|
||||
async fn api_leaderboard_handler(State(state): State<AppState>) -> axum::Json<serde_json::Value> {
|
||||
// UNIFIED RATING: Single leaderboard
|
||||
let players: Vec<(String, f64)> = sqlx::query_as(
|
||||
r#"SELECT DISTINCT p.name, p.singles_rating
|
||||
r#"SELECT DISTINCT p.name, p.rating
|
||||
FROM players p
|
||||
JOIN match_participants mp ON p.id = mp.player_id
|
||||
ORDER BY p.singles_rating DESC LIMIT 10"#
|
||||
ORDER BY p.rating DESC LIMIT 10"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
@ -1711,17 +1704,17 @@ async fn api_leaderboard_handler(State(state): State<AppState>) -> axum::Json<se
|
||||
///
|
||||
/// **Parameters:** None
|
||||
///
|
||||
/// **Returns:** JSON array of player objects with id, name, singles_rating, and doubles_rating
|
||||
/// **Returns:** JSON array of player objects with id, name, and rating
|
||||
async fn api_players_handler(State(state): State<AppState>) -> axum::Json<Vec<PlayerJson>> {
|
||||
let players: Vec<(i64, String, f64, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, singles_rating, doubles_rating FROM players ORDER BY name"
|
||||
let players: Vec<(i64, String, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, rating FROM players ORDER BY name"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
axum::Json(players.into_iter().map(|(id, name, sr, dr)| PlayerJson {
|
||||
id, name, singles_rating: sr, doubles_rating: dr,
|
||||
axum::Json(players.into_iter().map(|(id, name, rating)| PlayerJson {
|
||||
id, name, rating,
|
||||
}).collect())
|
||||
}
|
||||
|
||||
@ -1928,14 +1921,14 @@ async fn session_preview_handler(
|
||||
|
||||
// Get top players for this session
|
||||
let top_singles: Vec<(String, f64)> = sqlx::query_as(
|
||||
"SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
|
||||
"SELECT name, rating FROM players ORDER BY rating DESC LIMIT 5"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let top_doubles: Vec<(String, f64)> = sqlx::query_as(
|
||||
"SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
|
||||
"SELECT name, rating FROM players ORDER BY rating DESC LIMIT 5"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
@ -2128,14 +2121,14 @@ async fn send_session_email(
|
||||
|
||||
// Get leaderboard data for email
|
||||
let top_singles: Vec<(String, f64)> = sqlx::query_as(
|
||||
"SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
|
||||
"SELECT name, rating FROM players ORDER BY rating DESC LIMIT 5"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let top_doubles: Vec<(String, f64)> = sqlx::query_as(
|
||||
"SELECT name, singles_rating FROM players ORDER BY singles_rating DESC LIMIT 5"
|
||||
"SELECT name, rating FROM players ORDER BY rating DESC LIMIT 5"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
@ -2339,20 +2332,20 @@ async fn daily_summary_handler(
|
||||
|
||||
// Get top 5 for leaderboard preview
|
||||
let top_singles: Vec<(String, f64)> = sqlx::query_as(
|
||||
r#"SELECT DISTINCT p.name, p.singles_rating
|
||||
r#"SELECT DISTINCT p.name, p.rating
|
||||
FROM players p
|
||||
JOIN match_participants mp ON p.id = mp.player_id
|
||||
ORDER BY p.singles_rating DESC LIMIT 5"#
|
||||
ORDER BY p.rating DESC LIMIT 5"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let top_doubles: Vec<(String, f64)> = sqlx::query_as(
|
||||
r#"SELECT DISTINCT p.name, p.singles_rating
|
||||
r#"SELECT DISTINCT p.name, p.rating
|
||||
FROM players p
|
||||
JOIN match_participants mp ON p.id = mp.player_id
|
||||
ORDER BY p.singles_rating DESC LIMIT 5"#
|
||||
ORDER BY p.rating DESC LIMIT 5"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
@ -2827,10 +2820,10 @@ async fn send_daily_summary(
|
||||
|
||||
// Get unified leaderboard data
|
||||
let top_players: Vec<(String, f64)> = sqlx::query_as(
|
||||
r#"SELECT DISTINCT p.name, p.singles_rating
|
||||
r#"SELECT DISTINCT p.name, p.rating
|
||||
FROM players p
|
||||
JOIN match_participants mp ON p.id = mp.player_id
|
||||
ORDER BY p.singles_rating DESC LIMIT 5"#
|
||||
ORDER BY p.rating DESC LIMIT 5"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
@ -3096,20 +3089,20 @@ async fn daily_public_handler(
|
||||
|
||||
// Leaderboards
|
||||
let top_singles: Vec<(String, f64)> = sqlx::query_as(
|
||||
r#"SELECT DISTINCT p.name, p.singles_rating
|
||||
r#"SELECT DISTINCT p.name, p.rating
|
||||
FROM players p
|
||||
JOIN match_participants mp ON p.id = mp.player_id
|
||||
ORDER BY p.singles_rating DESC LIMIT 5"#
|
||||
ORDER BY p.rating DESC LIMIT 5"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let top_doubles: Vec<(String, f64)> = sqlx::query_as(
|
||||
r#"SELECT DISTINCT p.name, p.singles_rating
|
||||
r#"SELECT DISTINCT p.name, p.rating
|
||||
FROM players p
|
||||
JOIN match_participants mp ON p.id = mp.player_id
|
||||
ORDER BY p.singles_rating DESC LIMIT 5"#
|
||||
ORDER BY p.rating DESC LIMIT 5"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user