diff --git a/logs/pickleball.log b/logs/pickleball.log index 8eb350e..9fcd48d 100644 --- a/logs/pickleball.log +++ b/logs/pickleball.log @@ -1042,3 +1042,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 + diff --git a/pickleball-elo b/pickleball-elo index fc67e3a..15a4215 100755 Binary files a/pickleball-elo and b/pickleball-elo differ diff --git a/src/db/mod.rs b/src/db/mod.rs index f28fa20..7fc4ec4 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -19,7 +19,7 @@ use std::path::Path; pub async fn create_pool(db_path: &str) -> Result { // Create database file if it doesn't exist let path = Path::new(db_path); - let db_exists = path.exists(); + let _db_exists = path.exists(); // Ensure parent directory exists if let Some(parent) = path.parent() { @@ -54,7 +54,7 @@ pub async fn create_pool(db_path: &str) -> Result { /// All tables include foreign keys and appropriate indexes for query performance. /// Idempotent - safe to call multiple times. pub async fn run_migrations(pool: &SqlitePool) -> Result<(), sqlx::Error> { - let schema = include_str!("../../migrations/001_initial_schema.sql"); + let _schema = include_str!("../../migrations/001_initial_schema.sql"); // Execute each statement let statements = vec![ diff --git a/src/glicko/calculator.rs b/src/glicko/calculator.rs index 5582337..023693c 100644 --- a/src/glicko/calculator.rs +++ b/src/glicko/calculator.rs @@ -144,7 +144,7 @@ impl Glicko2Calculator { }; let mut fa = fa_init; - let mut fb = compute_f(b); + let fb = compute_f(b); // Ensure proper bracket if fa * fb >= 0.0 { diff --git a/src/main.rs b/src/main.rs index 06fa970..87b3820 100644 --- a/src/main.rs +++ b/src/main.rs @@ -229,6 +229,7 @@ async fn run_server() { .route("/daily/send", post(send_daily_summary)) .route("/api/leaderboard", get(api_leaderboard_handler)) .route("/api/players", get(api_players_handler)) + .route("/about", get(about_handler)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); @@ -252,8 +253,9 @@ fn nav_html() -> &'static str { ๐Ÿ“œ History ๐Ÿ‘ฅ Players โš–๏ธ Balance - ๐Ÿ“ง Daily Summary - ๐ŸŽพ Record Match + ๐Ÿ“ง Daily + โ“ About + ๐ŸŽพ Record "# } @@ -374,11 +376,22 @@ async fn match_history_handler(State(state): State) -> Html { let team1: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 1).collect(); let team2: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 2).collect(); - let team1_names: String = team1.iter().map(|(n, _, _)| n.as_str()).collect::>().join(" & "); - let team2_names: String = team2.iter().map(|(n, _, _)| n.as_str()).collect::>().join(" & "); + // Format player names with individual rating changes + let format_player = |name: &str, change: f64| { + let change_class = if change >= 0.0 { "rating-up" } else { "rating-down" }; + let change_sign = if change >= 0.0 { "+" } else { "" }; + format!("{} ({}{})", + name, change_class, change_sign, change as i32) + }; - let team1_change: f64 = team1.first().map(|(_, _, c)| *c).unwrap_or(0.0); - let team2_change: f64 = team2.first().map(|(_, _, c)| *c).unwrap_or(0.0); + let team1_display: String = team1.iter() + .map(|(n, _, c)| format_player(n, *c)) + .collect::>() + .join(" & "); + let team2_display: String = team2.iter() + .map(|(n, _, c)| format_player(n, *c)) + .collect::>() + .join(" & "); let winner_badge = if t1_score > t2_score { ("W", "L") @@ -386,19 +399,14 @@ async fn match_history_handler(State(state): State) -> Html { ("L", "W") }; - let change1_class = if team1_change >= 0.0 { "rating-up" } else { "rating-down" }; - let change2_class = if team2_change >= 0.0 { "rating-up" } else { "rating-down" }; - let change1_sign = if team1_change >= 0.0 { "+" } else { "" }; - let change2_sign = if team2_change >= 0.0 { "+" } else { "" }; - let type_emoji = if match_type == "doubles" { "๐Ÿ‘ฅ" } else { "๐ŸŽพ" }; match_rows.push_str(&format!(r#" {} {} - {} {} ({}{}) + {} {} {} - {} - {} {} ({}{}) + {} {} {}
) -> Html { - "#, type_emoji, match_type, winner_badge.0, team1_names, change1_class, change1_sign, team1_change as i32, + "#, type_emoji, match_type, winner_badge.0, team1_display, t1_score, t2_score, - winner_badge.1, team2_names, change2_class, change2_sign, team2_change as i32, + winner_badge.1, team2_display, ×tamp[..16], match_id)); } @@ -472,7 +480,7 @@ async fn player_profile_handler( .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, singles_rating, _singles_rd, _doubles_rating, _doubles_rd) = player .ok_or((StatusCode::NOT_FOUND, "Player not found".to_string()))?; // Get match stats @@ -1321,7 +1329,7 @@ async fn create_match( let mut team2_players = vec![match_data.team2_player1]; if is_doubles { if let Some(p2) = match_data.team2_player2 { team2_players.push(p2); } } - let team1_wins = match_data.team1_score > match_data.team2_score; + let _team1_wins = match_data.team1_score > match_data.team2_score; let calc = EloCalculator::new(); // Calculate per-point performance for team 1 @@ -1928,7 +1936,7 @@ async fn session_preview_handler( }) .collect(); - let doubles_list: String = top_doubles.iter().enumerate() + let _doubles_list: String = top_doubles.iter().enumerate() .map(|(i, (name, rating))| { let medal = match i { 0 => "๐Ÿฅ‡", 1 => "๐Ÿฅˆ", 2 => "๐Ÿฅ‰", _ => "" }; format!("
{}{}. {} - {:.0}
", medal, i+1, name, rating) @@ -2119,7 +2127,7 @@ async fn send_session_email( }) .collect(); - let doubles_html: String = top_doubles.iter().enumerate() + let _doubles_html: String = top_doubles.iter().enumerate() .map(|(i, (name, rating))| { let medal = match i { 0 => "๐Ÿฅ‡", 1 => "๐Ÿฅˆ", 2 => "๐Ÿฅ‰", _ => "" }; format!("{} {}. {}{:.0}", medal, i+1, name, rating) @@ -2335,36 +2343,21 @@ async fn daily_summary_handler( }) .collect(); - let doubles_list: String = top_doubles.iter().enumerate() + let _doubles_list: String = top_doubles.iter().enumerate() .map(|(i, (name, rating))| { let medal = match i { 0 => "๐Ÿฅ‡", 1 => "๐Ÿฅˆ", 2 => "๐Ÿฅ‰", _ => "" }; format!("
{}{}. {} - {:.0}
", medal, i+1, name, rating) }) .collect(); - // === ELO HISTORY CHART DATA (SINGLES) === - let singles_history: Vec<(i64, String, String, f64)> = sqlx::query_as( + // === ELO HISTORY CHART DATA (UNIFIED) === + let elo_history: Vec<(i64, String, String, f64)> = sqlx::query_as( r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str, p.name, mp.rating_after FROM matches m JOIN match_participants mp ON m.id = mp.match_id JOIN players p ON mp.player_id = p.id - WHERE date(m.timestamp) = ? AND m.match_type = 'singles' - ORDER BY m.timestamp, p.name"# - ) - .bind(&target_date) - .fetch_all(&state.pool) - .await - .unwrap_or_default(); - - // === ELO HISTORY CHART DATA (DOUBLES) === - let doubles_history: Vec<(i64, String, String, f64)> = sqlx::query_as( - r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str, - p.name, mp.rating_after - FROM matches m - JOIN match_participants mp ON m.id = mp.match_id - JOIN players p ON mp.player_id = p.id - WHERE date(m.timestamp) = ? AND m.match_type = 'doubles' + WHERE date(m.timestamp) = ? ORDER BY m.timestamp, p.name"# ) .bind(&target_date) @@ -2418,8 +2411,7 @@ async fn daily_summary_handler( (labels, datasets) } - let (singles_labels, singles_datasets) = build_chart_data(&singles_history, &colors); - let (doubles_labels, doubles_datasets) = build_chart_data(&doubles_history, &colors); + let (elo_labels, elo_datasets) = build_chart_data(&elo_history, &colors); // === PARTNER SYNERGY HEATMAP DATA === // Calculate win rate for each partner pair (all time, for doubles) @@ -2577,32 +2569,17 @@ async fn daily_summary_handler(

๐Ÿ“ˆ ELO Journey

-
- -
- +

๐Ÿ“Š Current Leaderboard

-
-
-

๐ŸŽพ Singles

- {} -
-
-

๐Ÿ‘ฅ Doubles

- {} -
+
+

๐Ÿ“ Top Players (Unified ELO)

+ {}

๐Ÿค Partner Synergy

@@ -3502,12 +3441,149 @@ async fn daily_public_handler( player_rating_changes.len(), matches_html, players_html, - singles_labels, singles_labels, singles_datasets, - doubles_labels, doubles_labels, doubles_datasets, + elo_labels, elo_labels, elo_datasets, singles_list, - doubles_list, heatmap_html ); Html(html) } + +/// About page explaining the ELO rating system +/// +/// **Endpoint:** `GET /about` +async fn about_handler() -> Html { + let html = format!(r#" + + + + + + About - Pickleball ELO + + + +
+ {} +
+

๐Ÿ“Š How the Rating System Works

+

A unified ELO system designed for recreational pickleball

+ +
+

๐ŸŽฏ The Basics

+

One rating for everything. Whether you play singles or doubles, all matches contribute to a single ELO rating. Everyone starts at 1500.

+

The system rewards skill and adapts to your performance. Beat higher-rated players? Big gains. Lose to lower-rated players? Bigger losses. It's that simple.

+
+ +
+

๐Ÿ“ˆ Per-Point Scoring

+

Unlike traditional ELO that only cares about winning or losing, our system considers how you played:

+
+ Performance = Points Won รท Total Points +
+

This means:

+
    +
  • Winning 11-2 earns more than winning 11-9
  • +
  • Losing 9-11 costs less than losing 2-11
  • +
  • Close games = smaller rating swings for everyone
  • +
+
+ Example: You win 11-7 against someone equally rated.
+ Performance = 11 รท 18 = 0.611
+ Expected (equal ratings) = 0.500
+ You outperformed expectations โ†’ rating goes up! +
+
+ +
+

๐Ÿ‘ฅ The Doubles Problem

+

In doubles, you have a partner. If your partner is much stronger than you, you'll probably win more โ€” but how much credit should you get?

+

We solve this with the Effective Opponent formula:

+
+ Effective Opponent = Opponent 1 + Opponent 2 โˆ’ Teammate +
+

This creates a personalized opponent rating for each player. Here's how it works:

+
    +
  • Strong teammate โ†’ Lower effective opponent โ†’ Less credit for winning, less blame for losing
  • +
  • Weak teammate โ†’ Higher effective opponent โ†’ More credit for winning, more blame for losing
  • +
+
+ Example: You're 1500 playing with a 1600 partner against two 1550 opponents.
+ Your effective opponent = 1550 + 1550 โˆ’ 1600 = 1500
+ Your partner's effective opponent = 1550 + 1550 โˆ’ 1500 = 1600

+ If you win, you gain less than your partner because their effective opponent was harder! +
+
+ +
+

๐Ÿ”ข The Math

+

For those who want the details:

+ +

Expected Performance

+
+ Expected = 1 / (1 + 10^((Opponent โˆ’ You) / 400)) +
+

This is the standard ELO expectation formula. If you're 200 points above your opponent, you're expected to score about 76% of points.

+ +

Rating Change

+
+ ฮ” Rating = K ร— (Actual โˆ’ Expected) +
+

We use K = 32, which is standard for casual/club play. This means:

+
    +
  • Maximum gain/loss per match: ยฑ32 points
  • +
  • Typical swing for competitive matches: ยฑ10-15 points
  • +
  • Close match against equal opponent: ยฑ2-5 points
  • +
+
+ +
+

โ“ Why This System?

+

We tried Glicko-2 first (with rating deviation and volatility), but it was:

+
    +
  • Confusing โ€” nobody understood what "RD 150" meant
  • +
  • Opaque โ€” the math was hidden behind complexity
  • +
  • Overkill โ€” designed for chess with thousands of games, not rec pickleball
  • +
+

Pure ELO with per-point scoring is:

+
    +
  • Transparent โ€” you can calculate changes by hand
  • +
  • Fair โ€” accounts for margin of victory and partner strength
  • +
  • Simple โ€” one number that goes up when you play well
  • +
+
+ +
+

๐Ÿ† Rating Tiers

+ + + + + + + +
Below 1400Developing
1400-1500Intermediate
1500-1600Solid
1600-1700Strong
1700+โญ Rising Star
1900+๐Ÿ‘‘ Elite
+
+ +

+ โ† Back to Home +

+
+
+ + + "#, COMMON_CSS, nav_html()); + + Html(html) +}