Refactor: unified rating (use singles_rating for all matches, update leaderboard)
This commit is contained in:
parent
2533b589a0
commit
189ea1b037
88
src/main.rs
88
src/main.rs
@ -916,10 +916,9 @@ async fn delete_match(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Revert each player's rating
|
// Revert each player's rating (UNIFIED: always use singles_rating)
|
||||||
for (player_id, match_type, rating_before, _rating_after) in &participants {
|
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("UPDATE players SET singles_rating = ? WHERE id = ?")
|
||||||
sqlx::query(&format!("UPDATE players SET {} = ? WHERE id = ?", rating_col))
|
|
||||||
.bind(rating_before)
|
.bind(rating_before)
|
||||||
.bind(player_id)
|
.bind(player_id)
|
||||||
.execute(&state.pool)
|
.execute(&state.pool)
|
||||||
@ -1298,7 +1297,8 @@ async fn create_match(
|
|||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create match: {}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create match: {}", e)))?;
|
||||||
|
|
||||||
let is_doubles = match_data.match_type == "doubles";
|
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];
|
let mut team1_players = vec![match_data.team1_player1];
|
||||||
if is_doubles { if let Some(p2) = match_data.team1_player2 { team1_players.push(p2); } }
|
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
|
/// **Parameters:** None
|
||||||
///
|
///
|
||||||
/// **Returns:** HTML page with dual leaderboards
|
/// **Returns:** HTML page with unified leaderboard
|
||||||
async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
|
async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
|
||||||
// Only include players who have played at least one match
|
// UNIFIED RATING: Single leaderboard using singles_rating as unified rating
|
||||||
let singles: Vec<(i64, String, f64)> = sqlx::query_as(
|
let players: Vec<(i64, String, f64)> = sqlx::query_as(
|
||||||
r#"SELECT DISTINCT p.id, p.name, p.singles_rating
|
r#"SELECT DISTINCT p.id, p.name, p.singles_rating
|
||||||
FROM players p
|
FROM players p
|
||||||
JOIN match_participants mp ON p.id = mp.player_id
|
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
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let doubles: Vec<(i64, String, f64)> = sqlx::query_as(
|
let player_rows: String = players.iter().enumerate()
|
||||||
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()
|
|
||||||
.map(|(i, (id, name, rating))| {
|
.map(|(i, (id, name, rating))| {
|
||||||
let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
|
let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" };
|
||||||
format!(r#"<div style="padding: 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between;">
|
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>
|
<title>Leaderboard - Pickleball ELO</title>
|
||||||
<style>
|
<style>
|
||||||
{}
|
{}
|
||||||
.leaderboards {{ display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }}
|
.leaderboard {{ background: #f9f9f9; border-radius: 8px; overflow: hidden; max-width: 500px; margin: 0 auto; }}
|
||||||
@media (max-width: 768px) {{ .leaderboards {{ grid-template-columns: 1fr; }} }}
|
|
||||||
.leaderboard {{ background: #f9f9f9; border-radius: 8px; overflow: hidden; }}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -1637,20 +1615,17 @@ async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
|
|||||||
<h1>🏓 Leaderboard</h1>
|
<h1>🏓 Leaderboard</h1>
|
||||||
{}
|
{}
|
||||||
|
|
||||||
<div class="leaderboards">
|
<h2 style="text-align: center;">📊 Top Players (Unified ELO)</h2>
|
||||||
<div>
|
<div class="leaderboard">{}</div>
|
||||||
<h2>📊 Top Singles</h2>
|
|
||||||
<div class="leaderboard">{}</div>
|
<p style="text-align: center; color: #666; margin-top: 20px; font-size: 14px;">
|
||||||
</div>
|
Unified rating: singles and doubles matches both contribute to one rating.<br>
|
||||||
<div>
|
<a href="/about" style="color: #003594;">Learn how ratings work →</a>
|
||||||
<h2>📊 Top Doubles</h2>
|
</p>
|
||||||
<div class="leaderboard">{}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"#, COMMON_CSS, nav_html(), singles_rows, doubles_rows);
|
"#, COMMON_CSS, nav_html(), player_rows);
|
||||||
|
|
||||||
Html(html)
|
Html(html)
|
||||||
}
|
}
|
||||||
@ -1659,15 +1634,15 @@ async fn leaderboard_handler(State(state): State<AppState>) -> Html<String> {
|
|||||||
///
|
///
|
||||||
/// **Endpoint:** `GET /api/leaderboard`
|
/// **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
|
/// **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> {
|
async fn api_leaderboard_handler(State(state): State<AppState>) -> axum::Json<serde_json::Value> {
|
||||||
// Only include players who have played at least one match
|
// UNIFIED RATING: Single leaderboard
|
||||||
let singles: Vec<(String, f64, f64)> = sqlx::query_as(
|
let players: Vec<(String, f64)> = sqlx::query_as(
|
||||||
r#"SELECT DISTINCT p.name, p.singles_rating, p.singles_rd
|
r#"SELECT DISTINCT p.name, p.singles_rating
|
||||||
FROM players p
|
FROM players p
|
||||||
JOIN match_participants mp ON p.id = mp.player_id
|
JOIN match_participants mp ON p.id = mp.player_id
|
||||||
ORDER BY p.singles_rating DESC LIMIT 10"#
|
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
|
.await
|
||||||
.unwrap_or_default();
|
.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!({
|
axum::Json(serde_json::json!({
|
||||||
"singles": singles.iter().enumerate().map(|(i, (n, r, rd))| {
|
"players": players.iter().enumerate().map(|(i, (n, r))| {
|
||||||
serde_json::json!({"rank": i+1, "name": n, "rating": *r as i32, "rd": *rd as i32})
|
serde_json::json!({"rank": i+1, "name": n, "rating": *r 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})
|
|
||||||
}).collect::<Vec<_>>(),
|
}).collect::<Vec<_>>(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user