Add chess-style Elo ratings for players

Each player's rating starts at 1500 and updates transactionally with
every finished match: a team's rating is the mean of its two members
and the standard K=32 formula decides the zero-sum delta applied to
both members of a team. Ratings are per game type in a new
player_rating table; match_player records each match's elo_delta.

- GET /api/leaderboard exposes elo and sorts by it
- GET /api/me/matches includes per-player elo deltas
- new GET /api/me/ratings returns the caller's rating per game type
- frontend: Elo column on the leaderboard, per-match delta in the
  history page, current rating in the lobby
- python -m tavolo.backfill_elo recomputes all ratings from the
  recorded match history (one-off backfill for existing matches)
This commit is contained in:
2026-09-18 13:36:17 +00:00
parent 3da0c463de
commit a606f14550
15 changed files with 651 additions and 17 deletions
+29
View File
@@ -184,6 +184,10 @@ pub struct MatchPlayer {
pub seat: usize,
pub team: String,
pub won: bool,
/// Elo change this match produced for the player; absent for matches
/// recorded before ratings existed.
#[serde(default)]
pub elo_delta: Option<i32>,
}
#[derive(Debug, Clone, Deserialize)]
@@ -201,6 +205,9 @@ pub struct MatchSummary {
pub finished_at: String,
#[serde(default)]
pub you_won: bool,
/// The viewer's Elo change in this match; absent when unrated.
#[serde(default)]
pub your_elo_delta: Option<i32>,
#[serde(default)]
pub players: Vec<MatchPlayer>,
}
@@ -213,16 +220,38 @@ pub struct MatchesPage {
pub next_cursor: Option<String>,
}
fn default_elo() -> i32 {
1500
}
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct LeaderboardEntry {
pub user_sub: String,
pub display_name: String,
/// Chess-style Elo rating for the requested game type.
#[serde(default = "default_elo")]
pub elo: i32,
pub matches: i32,
pub wins: i32,
pub points: i32,
}
/// The caller's Elo rating for one game type (GET /api/me/ratings).
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct PlayerRating {
pub game_type: String,
pub rating: i32,
pub matches_played: i32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RatingsPage {
#[serde(default)]
pub results: Vec<PlayerRating>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LeaderboardPage {
#[serde(default)]