Paginate the leaderboard from materialized player ratings
Store each player's leaderboard totals (wins, accumulated points and the latest display name) on player_rating alongside the Elo rating, updated in the same transaction as the match, so the board is a single indexed table query instead of aggregating every participation on each request. GET /api/leaderboard is now page-number paginated (page/limit) and returns the page, page count and total, plus the caller's own rank and page when authenticated; focus=me returns the caller's page directly. The migration adds the columns, backfills them from the match history (inserting rows for players from pre-Elo matches at the initial rating) and replaces the index. backfill_elo rebuilds the aggregates too. The web page opens on the page containing the signed-in player, marks their row and adds a numbered footer pager.
This commit is contained in:
@@ -466,6 +466,10 @@ fn default_elo() -> i32 {
|
||||
1500
|
||||
}
|
||||
|
||||
fn default_page() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct LeaderboardEntry {
|
||||
@@ -512,10 +516,30 @@ pub struct RatingsPage {
|
||||
pub results: Vec<PlayerRating>,
|
||||
}
|
||||
|
||||
/// Page size requested by the leaderboard page. Also used to turn a rank
|
||||
/// into the row number on a page (and back).
|
||||
pub const LEADERBOARD_PAGE_SIZE: u32 = 20;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct LeaderboardPage {
|
||||
#[serde(default)]
|
||||
pub results: Vec<LeaderboardEntry>,
|
||||
/// 1-based page actually returned (clamped to the last page).
|
||||
#[serde(default = "default_page")]
|
||||
pub page: u32,
|
||||
/// Total number of pages; 0 when the board has no rated players.
|
||||
#[serde(default)]
|
||||
pub pages: u32,
|
||||
/// Total number of rated players.
|
||||
#[serde(default)]
|
||||
pub total: u32,
|
||||
/// The viewer's own rank, when authenticated and rated for this game.
|
||||
#[serde(default)]
|
||||
pub your_rank: Option<u32>,
|
||||
/// The page holding the viewer's row.
|
||||
#[serde(default)]
|
||||
pub your_page: Option<u32>,
|
||||
}
|
||||
|
||||
/// A card game the platform can host (GET /api/game-types).
|
||||
@@ -1113,6 +1137,31 @@ mod tests {
|
||||
assert_eq!(1500, e.elo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaderboard_page_parses_pagination_fields() {
|
||||
let p: LeaderboardPage = serde_json::from_value(json!({
|
||||
"results": [],
|
||||
"page": 3,
|
||||
"pages": 12,
|
||||
"total": 234,
|
||||
"your_rank": 47,
|
||||
"your_page": 3,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(3, p.page);
|
||||
assert_eq!(12, p.pages);
|
||||
assert_eq!(234, p.total);
|
||||
assert_eq!(Some(47), p.your_rank);
|
||||
assert_eq!(Some(3), p.your_page);
|
||||
// The pagination fields are optional for older payloads.
|
||||
let legacy: LeaderboardPage =
|
||||
serde_json::from_value(json!({"results": []})).unwrap();
|
||||
assert_eq!(1, legacy.page);
|
||||
assert_eq!(0, legacy.pages);
|
||||
assert_eq!(None, legacy.your_rank);
|
||||
assert_eq!(None, legacy.your_page);
|
||||
}
|
||||
|
||||
fn scopone_game_type() -> GameTypeInfo {
|
||||
serde_json::from_value(json!({
|
||||
"id": "scopone_scientifico",
|
||||
|
||||
Reference in New Issue
Block a user