Refactor: unified rating (use singles_rating for all matches, update leaderboard)

This commit is contained in:
Split 2026-02-26 12:15:00 -05:00
parent 2533b589a0
commit 189ea1b037

View File

@ -916,10 +916,9 @@ async fn delete_match(
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Revert each player's rating
for (player_id, match_type, rating_before, _rating_after) in &participants {
let rating_col = if match_type == "doubles" { "doubles_rating" } else { "singles_rating" };
sqlx::query(&format!("UPDATE players SET {} = ? WHERE id = ?", rating_col))
// Revert each player's rating (UNIFIED: always use singles_rating)
for (player_id, _match_type, rating_before, _rating_after) in &participants {
sqlx::query("UPDATE players SET singles_rating = ? WHERE id = ?")
.bind(rating_before)
.bind(player_id)
.execute(&state.pool)
@ -1298,7 +1297,8 @@ 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";
let rating_col = if is_doubles { "doubles" } else { "singles" };
// UNIFIED RATING: Always use singles_rating as the unified rating column
let rating_col = "singles";
let mut team1_players = vec![match_data.team1_player1];
if is_doubles { if let Some(p2) = match_data.team1_player2 { team1_players.push(p2); } }
@ -1575,10 +1575,10 @@ async fn players_list_handler(State(state): State<AppState>) -> Html<String> {
///
/// **Parameters:** None
///
/// **Returns:** HTML page with dual leaderboards
/// **Returns:** HTML page with unified leaderboard
async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
// Only include players who have played at least one match
let singles: Vec<(i64, String, f64)> = sqlx::query_as(
// UNIFIED RATING: Single leaderboard using singles_rating as unified rating
let players: Vec<(i64, String, f64)> = sqlx::query_as(
r#"SELECT DISTINCT p.id, p.name, p.singles_rating
FROM players p
JOIN match_participants mp ON p.id = mp.player_id
@ -1588,27 +1588,7 @@ async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
.await
.unwrap_or_default();
let doubles: Vec<(i64, String, f64)> = sqlx::query_as(
r#"SELECT DISTINCT p.id, p.name, p.doubles_rating
FROM players p
JOIN match_participants mp ON p.id = mp.player_id
ORDER BY p.doubles_rating DESC LIMIT 10"#
)
.fetch_all(&state.pool)
.await
.unwrap_or_default();
let singles_rows: String = singles.iter().enumerate()
.map(|(i, (id, name, rating))| {
let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
format!(r#"<div style="padding: 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between;">
<div><span style="font-size: 18px; margin-right: 8px;">{}</span><strong>{}.</strong> <a href="/players/{}">{}</a></div>
<strong>{:.0}</strong>
</div>"#, medal, i + 1, id, name, rating)
})
.collect();
let doubles_rows: String = doubles.iter().enumerate()
let player_rows: String = players.iter().enumerate()
.map(|(i, (id, name, rating))| {
let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
format!(r#"<div style="padding: 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between;">
@ -1627,9 +1607,7 @@ async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
<title>Leaderboard - Pickleball ELO</title>
<style>
{}
.leaderboards {{ display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }}
@media (max-width: 768px) {{ .leaderboards {{ grid-template-columns: 1fr; }} }}
.leaderboard {{ background: #f9f9f9; border-radius: 8px; overflow: hidden; }}
.leaderboard {{ background: #f9f9f9; border-radius: 8px; overflow: hidden; max-width: 500px; margin: 0 auto; }}
</style>
</head>
<body>
@ -1637,20 +1615,17 @@ async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
<h1>🏓 Leaderboard</h1>
{}
<div class="leaderboards">
<div>
<h2>📊 Top Singles</h2>
<div class="leaderboard">{}</div>
</div>
<div>
<h2>📊 Top Doubles</h2>
<div class="leaderboard">{}</div>
</div>
</div>
<h2 style="text-align: center;">📊 Top Players (Unified ELO)</h2>
<div class="leaderboard">{}</div>
<p style="text-align: center; color: #666; margin-top: 20px; font-size: 14px;">
Unified rating: singles and doubles matches both contribute to one rating.<br>
<a href="/about" style="color: #003594;">Learn how ratings work </a>
</p>
</div>
</body>
</html>
"#, COMMON_CSS, nav_html(), singles_rows, doubles_rows);
"#, COMMON_CSS, nav_html(), player_rows);
Html(html)
}
@ -1659,15 +1634,15 @@ async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
///
/// **Endpoint:** `GET /api/leaderboard`
///
/// **Description:** Provides leaderboard data as JSON for external applications, including top 10 in singles and doubles with ratings and uncertainty (RD).
/// **Description:** Provides unified leaderboard data as JSON for external applications.
///
/// **Parameters:** None
///
/// **Returns:** JSON object with `singles` and `doubles` arrays, each containing rank, name, rating, and RD
/// **Returns:** JSON object with `players` array containing rank, name, and rating
async fn api_leaderboard_handler(State(state): State<AppState>) -> axum::Json<serde_json::Value> {
// Only include players who have played at least one match
let singles: Vec<(String, f64, f64)> = sqlx::query_as(
r#"SELECT DISTINCT p.name, p.singles_rating, p.singles_rd
// UNIFIED RATING: Single leaderboard
let players: Vec<(String, f64)> = sqlx::query_as(
r#"SELECT DISTINCT p.name, p.singles_rating
FROM players p
JOIN match_participants mp ON p.id = mp.player_id
ORDER BY p.singles_rating DESC LIMIT 10"#
@ -1676,22 +1651,9 @@ async fn api_leaderboard_handler(State(state): State<AppState>) -> axum::Json<se
.await
.unwrap_or_default();
let doubles: Vec<(String, f64, f64)> = sqlx::query_as(
r#"SELECT DISTINCT p.name, p.doubles_rating, p.doubles_rd
FROM players p
JOIN match_participants mp ON p.id = mp.player_id
ORDER BY p.doubles_rating DESC LIMIT 10"#
)
.fetch_all(&state.pool)
.await
.unwrap_or_default();
axum::Json(serde_json::json!({
"singles": singles.iter().enumerate().map(|(i, (n, r, rd))| {
serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32, "rd": *rd as i32})
}).collect::<Vec<_>>(),
"doubles": doubles.iter().enumerate().map(|(i, (n, r, rd))| {
serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32, "rd": *rd as i32})
"players": players.iter().enumerate().map(|(i, (n, r))| {
serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32})
}).collect::<Vec<_>>(),
}))
}