//! Serde types mirroring the backend API payloads. use serde::Deserialize; use std::collections::HashMap; fn default_true() -> bool { true } #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct User { pub sub: String, pub name: String, } #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct PlayerView { pub sub: String, pub name: String, pub seat: usize, pub team: String, #[serde(default)] pub cards_left: usize, #[serde(default)] pub captured_count: usize, #[serde(default)] pub scope: i32, /// Own hand only; absent for the other players. #[serde(default)] pub hand: Option>, } #[derive(Debug, Clone, Copy, Deserialize)] pub struct Scores { #[serde(rename = "A")] pub a: i32, #[serde(rename = "B")] pub b: i32, } #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct MoveView { pub seat: usize, pub name: String, pub card: String, #[serde(default)] pub captured: Vec, #[serde(default)] pub scopa: bool, } #[derive(Debug, Clone, Copy, Deserialize)] pub struct TeamCounts { #[serde(rename = "A")] pub a: i32, #[serde(rename = "B")] pub b: i32, } #[derive(Debug, Clone, Copy, Deserialize)] #[allow(dead_code)] pub struct TeamBools { #[serde(rename = "A")] pub a: bool, #[serde(rename = "B")] pub b: bool, } /// Which team (if any) won each scoring category of a hand. #[derive(Debug, Clone, Deserialize)] pub struct Award { #[serde(default)] pub carte: Option, #[serde(default)] pub denara: Option, #[serde(default)] pub settebello: Option, #[serde(default)] pub primiera: Option, #[serde(default)] pub napola: Option, } /// The scoring breakdown of one completed hand. #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct HandSummary { pub cards: TeamCounts, pub denara: TeamCounts, pub settebello: TeamBools, pub primiera: TeamCounts, pub scope: TeamCounts, /// Napola run lengths per team; absent when the rule is disabled (or /// the summary predates the option). #[serde(default)] pub napola: Option, pub award: Award, #[serde(default)] pub hand: i32, #[serde(default)] pub team_a_points: i32, #[serde(default)] pub team_b_points: i32, } #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct GameView { pub id: String, #[serde(default)] pub join_code: String, /// Which card game this match is (id from /api/game-types). #[serde(default)] pub game_type: String, /// Whether the napola rule is scored in this match. #[serde(default = "default_true")] pub napola: bool, pub phase: String, #[serde(default)] pub target_score: i32, #[serde(default)] pub hand_number: i32, #[serde(default)] pub dealer: usize, #[serde(default)] pub turn: usize, #[serde(default)] pub scores: Option, #[serde(default)] pub winner: Option, #[serde(default)] pub table: Vec, #[serde(default)] pub players: Vec, #[serde(default)] pub seats_open: Option, #[serde(default)] pub last_move: Option, /// Scoring breakdown of the most recent hand (present once a hand has /// been completed). #[serde(default)] pub last_hand: Option, /// Seats that acknowledged the hand-end summary. #[serde(default)] pub acknowledged: Vec, /// ISO-8601 instant at which the next hand is dealt automatically. #[serde(default)] pub hand_end_deadline: Option, /// ISO-8601 instant at which the server plays a random legal card for /// the player on turn. #[serde(default)] pub turn_deadline: Option, #[serde(default)] pub your_turn: Option, /// Legal captures per hand card; present only for the player on turn. #[serde(default)] pub legal_moves: Option>>>, } #[derive(Debug, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[allow(dead_code)] pub enum ServerMessage { State { game: GameView }, GameOver { scores: Scores, #[serde(default)] winner: Option, }, Error { #[serde(default)] code: Option, message: String, }, } #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct MatchPlayer { pub user_sub: String, pub display_name: String, 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, } #[derive(Debug, Clone, Deserialize)] #[allow(dead_code)] pub struct MatchSummary { pub id: String, #[serde(default)] pub game_type: String, pub team_a_score: i32, pub team_b_score: i32, pub winner_team: String, pub target_score: i32, pub hands_played: i32, pub started_at: String, 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, #[serde(default)] pub players: Vec, } #[derive(Debug, Clone, Deserialize)] pub struct MatchesPage { #[serde(default)] pub results: Vec, #[serde(default)] pub next_cursor: Option, } 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, } #[derive(Debug, Clone, Deserialize)] pub struct LeaderboardPage { #[serde(default)] pub results: Vec, } /// A card game the platform can host (GET /api/game-types). #[derive(Debug, Clone, PartialEq, Deserialize)] #[allow(dead_code)] pub struct GameTypeInfo { pub id: String, pub name: String, #[serde(default)] pub description: String, } #[derive(Debug, Clone, Deserialize)] pub struct GameTypesPage { #[serde(default)] pub results: Vec, } /// Map a card code (e.g. `07D`) to its asset path. pub fn card_asset(code: &str) -> String { format!("/assets/cards/{code}.svg") } pub const CARD_BACK: &str = "/assets/cards/back.svg"; /// Human-friendly rank+suit label, e.g. `07D` -> "7 of denari". pub fn card_label(code: &str) -> String { if code.len() != 3 { return code.to_string(); } let rank = match &code[..2] { "01" => "Asso", "08" => "Fante", "09" => "Cavallo", "10" => "Re", other => match other.trim_start_matches('0') { "2" => "2", "3" => "3", "4" => "4", "5" => "5", "6" => "6", "7" => "7", _ => other, }, }; let suit = match &code[2..] { "D" => "denari", "C" => "coppe", "S" => "spade", "B" => "bastoni", _ => "?", }; format!("{rank} di {suit}") } #[cfg(test)] mod tests { use super::*; #[test] fn card_asset_maps_code() { assert_eq!("/assets/cards/07D.svg", card_asset("07D")); } #[test] fn card_label_names_courts() { assert_eq!("Asso di denari", card_label("01D")); assert_eq!("Fante di coppe", card_label("08C")); assert_eq!("Cavallo di spade", card_label("09S")); assert_eq!("Re di bastoni", card_label("10B")); assert_eq!("7 di denari", card_label("07D")); } }