diff --git a/web/src/model.rs b/web/src/model.rs index 38a75f3..9010bb2 100644 --- a/web/src/model.rs +++ b/web/src/model.rs @@ -551,6 +551,21 @@ pub fn card_asset(code: &str) -> String { pub const CARD_BACK: &str = "/assets/cards/back.svg"; +/// Display order key for a hand: rank ascending (01..10, i.e. 1..7 then +/// Fante, Cavallo, Re), then suit (denari, coppe, spade, bastoni). +/// Malformed codes sort last. +pub fn hand_sort_key(code: &str) -> (u8, u8) { + let rank = code.get(..2).and_then(|r| r.parse().ok()).unwrap_or(u8::MAX); + let suit = match code.get(2..3) { + Some("D") => 0, + Some("C") => 1, + Some("S") => 2, + Some("B") => 3, + _ => u8::MAX, + }; + (rank, suit) +} + /// Human-friendly rank+suit label, e.g. `07D` -> "7 of denari". pub fn card_label(code: &str) -> String { if code.len() != 3 { @@ -600,6 +615,38 @@ mod tests { assert_eq!("7 di denari", card_label("07D")); } + #[test] + fn hand_sort_orders_ranks_ascending() { + let mut hand = vec![ + "10B".to_string(), + "01S".to_string(), + "07D".to_string(), + "08C".to_string(), + ]; + hand.sort_by_key(|c| hand_sort_key(c)); + assert_eq!(vec!["01S", "07D", "08C", "10B"], hand); + } + + #[test] + fn hand_sort_breaks_ties_by_suit() { + let mut hand = vec![ + "02B".to_string(), + "02S".to_string(), + "02C".to_string(), + "02D".to_string(), + "01S".to_string(), + ]; + hand.sort_by_key(|c| hand_sort_key(c)); + assert_eq!(vec!["01S", "02D", "02C", "02S", "02B"], hand); + } + + #[test] + fn hand_sort_puts_malformed_codes_last() { + let mut hand = vec!["07D".to_string(), "xx".to_string(), "AB1".to_string()]; + hand.sort_by_key(|c| hand_sort_key(c)); + assert_eq!(vec!["07D", "xx", "AB1"], hand); + } + fn scopone_match() -> MatchSummary { serde_json::from_value(json!({ "id": "m1", diff --git a/web/src/pages/game.rs b/web/src/pages/game.rs index 4fb33f1..0994915 100644 --- a/web/src/pages/game.rs +++ b/web/src/pages/game.rs @@ -10,7 +10,7 @@ use crate::api; use crate::components::card::{card_back, card_img}; use crate::components::summary::{hand_summary_modal, summary_rows}; use crate::components::toast::toast; -use crate::model::{card_label, GameView, MoveView, PlayerView, Scores, ServerMessage}; +use crate::model::{card_label, hand_sort_key, GameView, MoveView, PlayerView, Scores, ServerMessage}; use crate::ws::{self, GameSocket}; fn send_play(socket: Signal>, card: String, capture: Option>) { @@ -593,7 +593,8 @@ fn table_view( let viewer = player_for_seat(&game, viewer_seat); let my_captured = viewer.as_ref().map(|p| p.captured_count).unwrap_or(0); let my_scope = viewer.as_ref().map(|p| p.scope).unwrap_or(0); - let hand = viewer.and_then(|p| p.hand).unwrap_or_default(); + let mut hand = viewer.and_then(|p| p.hand).unwrap_or_default(); + hand.sort_by_key(|c| hand_sort_key(c)); let current_selection = selected.get_clone(); let hand_cards = hand .into_iter()