Scope the leaderboard to a selectable game

Elo ratings are stored per game type, so the leaderboard now always
requests /api/leaderboard?game_type=<id> and shows only that game's
players, Elo, wins, matches and points instead of mixing all games'
aggregates with the default game's rating.

- api::leaderboard takes the game type;
- a "Game" dropdown next to the heading lists every registered game and
  reloads the board on change (stale in-flight responses are dropped);
- move fallback_game_types() from the lobby to model.rs so both the
  lobby and leaderboard selectors work when /api/game-types fails.
This commit is contained in:
2026-10-06 16:55:51 +08:00
parent 7bb6b74fdc
commit 97d4b3f217
4 changed files with 112 additions and 39 deletions
+44
View File
@@ -408,6 +408,38 @@ pub struct GameTypesPage {
pub results: Vec<GameTypeInfo>,
}
/// The games to show in the lobby's and leaderboard's game selectors when
/// the `/api/game-types` fetch fails: match creation and per-game views
/// must still work.
pub fn fallback_game_types() -> Vec<GameTypeInfo> {
vec![
GameTypeInfo {
id: "scopone_scientifico".to_string(),
name: "Scopone scientifico".to_string(),
description: String::new(),
min_players: 4,
max_players: 4,
options_schema: serde_json::Value::Null,
},
GameTypeInfo {
id: "briscola".to_string(),
name: "Briscola".to_string(),
description: String::new(),
min_players: 2,
max_players: 2,
options_schema: serde_json::Value::Null,
},
GameTypeInfo {
id: "briscola_a_quattro".to_string(),
name: "Briscola a quattro".to_string(),
description: String::new(),
min_players: 4,
max_players: 4,
options_schema: serde_json::Value::Null,
},
]
}
/// One configured OIDC login provider (GET /api/auth/providers).
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct AuthProvider {
@@ -1142,6 +1174,18 @@ mod tests {
assert_eq!("?", suit_name("?"));
}
#[test]
fn fallback_game_types_cover_the_registered_games() {
let types = fallback_game_types();
let ids: Vec<&str> = types.iter().map(|g| g.id.as_str()).collect();
assert_eq!(
vec!["scopone_scientifico", "briscola", "briscola_a_quattro"],
ids
);
// The lobby's first game (the creation default) must exist.
assert_eq!("scopone_scientifico", types[0].id);
}
#[test]
fn game_view_parses_briscola_payload() {
let g: GameView = serde_json::from_value(json!({