Use player names in two-player briscola
Classic briscola has no partnerships: its "teams" are just the two players, but the UI labelled them Team A/Team B. - add is_individual_game() and GameView::player_name_for_team() to the model; - the briscola table HUD reads "Alice 0 — 0 Bob (target 61)" and the seat panels drop the Team badge for two-player games; - the deal summary and game-over screen name the sides and the winner; - the waiting room and join preview hide the Team badge. briscola a quattro and scopone keep their team labels, and the match history table is unchanged.
This commit is contained in:
@@ -7,9 +7,23 @@
|
|||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
use crate::components::card::{card_back, card_img};
|
use crate::components::card::{card_back, card_img};
|
||||||
use crate::model::{DealSummary, GameView};
|
use crate::model::{is_individual_game, DealSummary, GameView};
|
||||||
use crate::ws::GameSocket;
|
use crate::ws::GameSocket;
|
||||||
|
|
||||||
|
/// Names of the two sides: the players' names for games without
|
||||||
|
/// partnerships (classic two-player briscola), "Team A"/"Team B"
|
||||||
|
/// otherwise.
|
||||||
|
fn side_names(game: &GameView) -> (String, String) {
|
||||||
|
if is_individual_game(&game.game_type) {
|
||||||
|
(
|
||||||
|
game.player_name_for_team("A"),
|
||||||
|
game.player_name_for_team("B"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
("Team A".to_string(), "Team B".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// One explanatory row: icon, title, plain-language sentence, points chip.
|
/// One explanatory row: icon, title, plain-language sentence, points chip.
|
||||||
fn summary_row(
|
fn summary_row(
|
||||||
icon: View,
|
icon: View,
|
||||||
@@ -31,9 +45,10 @@ fn summary_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The scoring rows of one completed deal (also used by the final screen).
|
/// The scoring rows of one completed deal (also used by the final screen).
|
||||||
pub fn deal_rows(summary: &DealSummary) -> View {
|
pub fn deal_rows(game: &GameView, summary: &DealSummary) -> View {
|
||||||
let a = summary.points.a;
|
let a = summary.points.a;
|
||||||
let b = summary.points.b;
|
let b = summary.points.b;
|
||||||
|
let (name_a, name_b) = side_names(game);
|
||||||
let cls = if summary.patta {
|
let cls = if summary.patta {
|
||||||
"score-row tie"
|
"score-row tie"
|
||||||
} else if a > b {
|
} else if a > b {
|
||||||
@@ -54,7 +69,7 @@ pub fn deal_rows(summary: &DealSummary) -> View {
|
|||||||
let points_row = summary_row(
|
let points_row = summary_row(
|
||||||
card_img("01D".to_string(), "score-mini"),
|
card_img("01D".to_string(), "score-mini"),
|
||||||
"Deal points",
|
"Deal points",
|
||||||
format!("Team A {a} · Team B {b}"),
|
format!("{name_a} {a} · {name_b} {b}"),
|
||||||
cls,
|
cls,
|
||||||
chip,
|
chip,
|
||||||
);
|
);
|
||||||
@@ -62,7 +77,7 @@ pub fn deal_rows(summary: &DealSummary) -> View {
|
|||||||
summary_row(
|
summary_row(
|
||||||
card_back("score-mini"),
|
card_back("score-mini"),
|
||||||
"Cards captured",
|
"Cards captured",
|
||||||
format!("Team A {} · Team B {}", c.a, c.b),
|
format!("{name_a} {} · {name_b} {}", c.a, c.b),
|
||||||
"score-row",
|
"score-row",
|
||||||
format!("{} · {}", c.a, c.b),
|
format!("{} · {}", c.a, c.b),
|
||||||
)
|
)
|
||||||
@@ -87,10 +102,11 @@ fn totals(game: &GameView, summary: &DealSummary) -> View {
|
|||||||
} else {
|
} else {
|
||||||
(summary.points.a, summary.points.b)
|
(summary.points.a, summary.points.b)
|
||||||
};
|
};
|
||||||
|
let (name_a, name_b) = side_names(game);
|
||||||
view! {
|
view! {
|
||||||
div(class="totals") {
|
div(class="totals") {
|
||||||
div(class="total-row team-a") {
|
div(class="total-row team-a") {
|
||||||
span(class="total-label") { "Team A" }
|
span(class="total-label") { (name_a) }
|
||||||
div(class="progress") {
|
div(class="progress") {
|
||||||
div(class="progress-fill", style=format!("width: {pct_a}%")) {}
|
div(class="progress-fill", style=format!("width: {pct_a}%")) {}
|
||||||
}
|
}
|
||||||
@@ -100,7 +116,7 @@ fn totals(game: &GameView, summary: &DealSummary) -> View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(class="total-row team-b") {
|
div(class="total-row team-b") {
|
||||||
span(class="total-label") { "Team B" }
|
span(class="total-label") { (name_b) }
|
||||||
div(class="progress") {
|
div(class="progress") {
|
||||||
div(class="progress-fill", style=format!("width: {pct_b}%")) {}
|
div(class="progress-fill", style=format!("width: {pct_b}%")) {}
|
||||||
}
|
}
|
||||||
@@ -151,7 +167,7 @@ pub fn deal_summary_modal(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let title = format!("Deal {} — results", summary.deal);
|
let title = format!("Deal {} — results", summary.deal);
|
||||||
let rows = deal_rows(&summary);
|
let rows = deal_rows(&game, &summary);
|
||||||
let totals_view = totals(&game, &summary);
|
let totals_view = totals(&game, &summary);
|
||||||
|
|
||||||
let action = if game.spectator {
|
let action = if game.spectator {
|
||||||
|
|||||||
@@ -229,6 +229,18 @@ pub struct GameView {
|
|||||||
pub last_deal: Option<DealSummary>,
|
pub last_deal: Option<DealSummary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GameView {
|
||||||
|
/// Display name of the player on team `team` ("A"/"B"), for games
|
||||||
|
/// without partnerships; falls back to the team label itself.
|
||||||
|
pub fn player_name_for_team(&self, team: &str) -> String {
|
||||||
|
self.players
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.team == team)
|
||||||
|
.map(|p| p.name.clone())
|
||||||
|
.unwrap_or_else(|| team.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -663,6 +675,13 @@ pub fn is_briscola(game_type: &str) -> bool {
|
|||||||
game_type == "briscola" || game_type == "briscola_a_quattro"
|
game_type == "briscola" || game_type == "briscola_a_quattro"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a game is played by two individuals rather than partnerships
|
||||||
|
/// (classic two-player briscola): team labels and team messages are
|
||||||
|
/// replaced by the players' names.
|
||||||
|
pub fn is_individual_game(game_type: &str) -> bool {
|
||||||
|
game_type == "briscola"
|
||||||
|
}
|
||||||
|
|
||||||
/// Human-readable suit name for a one-letter code (`D` -> "denari").
|
/// Human-readable suit name for a one-letter code (`D` -> "denari").
|
||||||
pub fn suit_name(suit: &str) -> String {
|
pub fn suit_name(suit: &str) -> String {
|
||||||
match suit {
|
match suit {
|
||||||
@@ -1174,6 +1193,31 @@ mod tests {
|
|||||||
assert_eq!("?", suit_name("?"));
|
assert_eq!("?", suit_name("?"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_individual_game_only_matches_two_player_briscola() {
|
||||||
|
assert!(is_individual_game("briscola"));
|
||||||
|
assert!(!is_individual_game("briscola_a_quattro"));
|
||||||
|
assert!(!is_individual_game("scopone_scientifico"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn player_name_for_team_resolves_names() {
|
||||||
|
let g: GameView = serde_json::from_value(json!({
|
||||||
|
"id": "b1",
|
||||||
|
"game_type": "briscola",
|
||||||
|
"phase": "playing",
|
||||||
|
"players": [
|
||||||
|
{"sub": "alice", "name": "Alice", "seat": 0, "team": "A"},
|
||||||
|
{"sub": "bob", "name": "Bob", "seat": 1, "team": "B"},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!("Alice", g.player_name_for_team("A"));
|
||||||
|
assert_eq!("Bob", g.player_name_for_team("B"));
|
||||||
|
// Unknown labels fall back to the label itself.
|
||||||
|
assert_eq!("C", g.player_name_for_team("C"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fallback_game_types_cover_the_registered_games() {
|
fn fallback_game_types_cover_the_registered_games() {
|
||||||
let types = fallback_game_types();
|
let types = fallback_game_types();
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ use sycamore::prelude::*;
|
|||||||
|
|
||||||
use crate::components::card::{card_back, card_img};
|
use crate::components::card::{card_back, card_img};
|
||||||
use crate::components::copy::CopyButton;
|
use crate::components::copy::CopyButton;
|
||||||
use crate::model::{hand_sort_key, suit_name, GameView, PlayerView, Scores};
|
use crate::model::{
|
||||||
|
hand_sort_key, is_individual_game, suit_name, GameView, PlayerView, Scores,
|
||||||
|
};
|
||||||
use crate::pages::game::watch_link;
|
use crate::pages::game::watch_link;
|
||||||
|
|
||||||
fn player_for_seat(game: &GameView, seat: usize) -> Option<PlayerView> {
|
fn player_for_seat(game: &GameView, seat: usize) -> Option<PlayerView> {
|
||||||
@@ -25,7 +27,10 @@ fn seat_panel(game: GameView, seat: usize, position: &'static str) -> View {
|
|||||||
if active { " active" } else { "" }
|
if active { " active" } else { "" }
|
||||||
);
|
);
|
||||||
let name = player.name.clone();
|
let name = player.name.clone();
|
||||||
let team = player.team.clone();
|
let team_badge = (!is_individual_game(&game.game_type)).then(|| {
|
||||||
|
let team = player.team.clone();
|
||||||
|
view! { span(class="team-badge") { "Team " (team) } }
|
||||||
|
});
|
||||||
let points = player.points;
|
let points = player.points;
|
||||||
let captured = player.captured_count;
|
let captured = player.captured_count;
|
||||||
let backs = (0..player.cards_left)
|
let backs = (0..player.cards_left)
|
||||||
@@ -35,7 +40,7 @@ fn seat_panel(game: GameView, seat: usize, position: &'static str) -> View {
|
|||||||
div(class=cls) {
|
div(class=cls) {
|
||||||
div(class="seat-name") {
|
div(class="seat-name") {
|
||||||
(name)
|
(name)
|
||||||
span(class="team-badge") { "Team " (team) }
|
(team_badge)
|
||||||
}
|
}
|
||||||
div(class="seat-cards") { (backs) }
|
div(class="seat-cards") { (backs) }
|
||||||
div(class="seat-stats") {
|
div(class="seat-stats") {
|
||||||
@@ -160,6 +165,19 @@ pub fn briscola_table(
|
|||||||
let scores = game.scores.unwrap_or(Scores { a: 0, b: 0 });
|
let scores = game.scores.unwrap_or(Scores { a: 0, b: 0 });
|
||||||
let target = game.target_score;
|
let target = game.target_score;
|
||||||
let deal = game.hand_number;
|
let deal = game.hand_number;
|
||||||
|
// Two-player briscola has no partnerships: show the players' names.
|
||||||
|
let score_line = if is_individual_game(&game.game_type) {
|
||||||
|
format!(
|
||||||
|
"{} {} — {} {} (target {})",
|
||||||
|
game.player_name_for_team("A"),
|
||||||
|
scores.a,
|
||||||
|
scores.b,
|
||||||
|
game.player_name_for_team("B"),
|
||||||
|
target
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Team A {} — {} Team B (target {})", scores.a, scores.b, target)
|
||||||
|
};
|
||||||
|
|
||||||
let (grid_cls, top, left, right) = if player_count == 2 {
|
let (grid_cls, top, left, right) = if player_count == 2 {
|
||||||
(
|
(
|
||||||
@@ -234,10 +252,7 @@ pub fn briscola_table(
|
|||||||
span { "Deal " (deal) }
|
span { "Deal " (deal) }
|
||||||
(trump_badge)
|
(trump_badge)
|
||||||
(spectator_badge)
|
(spectator_badge)
|
||||||
span(class="hud-scores") {
|
span(class="hud-scores") { (score_line) }
|
||||||
"Team A " (scores.a) " — " (scores.b) " Team B (target "
|
|
||||||
(target) ")"
|
|
||||||
}
|
|
||||||
span(class=turn_cls) { (turn_note) }
|
span(class=turn_cls) { (turn_note) }
|
||||||
(countdown)
|
(countdown)
|
||||||
CopyButton(text=watch_link, label="Copy watch link".to_string())
|
CopyButton(text=watch_link, label="Copy watch link".to_string())
|
||||||
|
|||||||
+21
-7
@@ -12,8 +12,8 @@ use crate::components::copy::CopyButton;
|
|||||||
use crate::components::summary::{hand_summary_modal, summary_rows};
|
use crate::components::summary::{hand_summary_modal, summary_rows};
|
||||||
use crate::components::toast::toast;
|
use crate::components::toast::toast;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
card_label, hand_sort_key, is_briscola, GameView, MoveView, PlayerView, Scores,
|
card_label, hand_sort_key, is_briscola, is_individual_game, GameView, MoveView,
|
||||||
ServerMessage,
|
PlayerView, Scores, ServerMessage,
|
||||||
};
|
};
|
||||||
use crate::pages::briscola::briscola_table;
|
use crate::pages::briscola::briscola_table;
|
||||||
use crate::ws::{self, GameSocket};
|
use crate::ws::{self, GameSocket};
|
||||||
@@ -433,19 +433,23 @@ fn LobbyView(game: GameView) -> View {
|
|||||||
let join_code = game.join_code.clone();
|
let join_code = game.join_code.clone();
|
||||||
let watch_link = watch_link(&game.id);
|
let watch_link = watch_link(&game.id);
|
||||||
|
|
||||||
|
let show_teams = !is_individual_game(&game.game_type);
|
||||||
let players = game
|
let players = game
|
||||||
.players
|
.players
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
let name = p.name.clone();
|
let name = p.name.clone();
|
||||||
let seat = p.seat;
|
let seat = p.seat;
|
||||||
let team = p.team.clone();
|
let team_badge = show_teams.then(|| {
|
||||||
|
let team = p.team.clone();
|
||||||
|
view! { span(class="badge text-bg-info") { "Team " (team) } }
|
||||||
|
});
|
||||||
view! {
|
view! {
|
||||||
li(class="list-group-item d-flex justify-content-between align-items-center") {
|
li(class="list-group-item d-flex justify-content-between align-items-center") {
|
||||||
strong { (name) }
|
strong { (name) }
|
||||||
span {
|
span {
|
||||||
span(class="badge text-bg-secondary me-2") { "Seat " (seat) }
|
span(class="badge text-bg-secondary me-2") { "Seat " (seat) }
|
||||||
span(class="badge text-bg-info") { "Team " (team) }
|
(team_badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -744,14 +748,24 @@ fn game_over_view(over: Option<(Scores, Option<String>)>, game: Option<GameView>
|
|||||||
None => view! {},
|
None => view! {},
|
||||||
Some((scores, winner)) => {
|
Some((scores, winner)) => {
|
||||||
let winner = winner.unwrap_or_else(|| "?".to_string());
|
let winner = winner.unwrap_or_else(|| "?".to_string());
|
||||||
let line = format!("Team {winner} wins {} – {}", scores.a, scores.b);
|
// Two-player briscola has no teams: name the winner instead.
|
||||||
|
let winner_label = match game.as_ref() {
|
||||||
|
Some(g) if is_individual_game(&g.game_type) => {
|
||||||
|
g.player_name_for_team(&winner)
|
||||||
|
}
|
||||||
|
_ => format!("Team {winner}"),
|
||||||
|
};
|
||||||
|
let line = format!("{winner_label} wins {} – {}", scores.a, scores.b);
|
||||||
// Spectators have no match history to look at.
|
// Spectators have no match history to look at.
|
||||||
let spectator = game.as_ref().is_some_and(|g| g.spectator);
|
let spectator = game.as_ref().is_some_and(|g| g.spectator);
|
||||||
// Explain the final hand's (scopone) or deal's (briscola)
|
// Explain the final hand's (scopone) or deal's (briscola)
|
||||||
// scoring before the result.
|
// scoring before the result.
|
||||||
let final_summary: View = if briscola {
|
let final_summary: View = if briscola {
|
||||||
match game.as_ref().and_then(|g| g.last_deal.clone()) {
|
match game.as_ref() {
|
||||||
Some(deal) => deal_rows(&deal),
|
Some(g) => match g.last_deal.clone() {
|
||||||
|
Some(deal) => deal_rows(g, &deal),
|
||||||
|
None => view! {},
|
||||||
|
},
|
||||||
None => view! {},
|
None => view! {},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+11
-6
@@ -15,8 +15,8 @@ use crate::api;
|
|||||||
use crate::components::card::card_back;
|
use crate::components::card::card_back;
|
||||||
use crate::components::toast::toast;
|
use crate::components::toast::toast;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
fallback_game_types, AuthProvider, GameTypeInfo, JoinPreview, OptionField, OptionKind,
|
fallback_game_types, is_individual_game, AuthProvider, GameTypeInfo, JoinPreview,
|
||||||
PlayerRating, User,
|
OptionField, OptionKind, PlayerRating, User,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Human-readable label for a provider name ("google" -> "Google").
|
/// Human-readable label for a provider name ("google" -> "Google").
|
||||||
@@ -237,15 +237,20 @@ fn join_preview_panel(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let seated = p.players.len();
|
let seated = p.players.len();
|
||||||
|
let show_teams = !is_individual_game(&p.game_type);
|
||||||
let players = p
|
let players = p
|
||||||
.players
|
.players
|
||||||
.iter()
|
.iter()
|
||||||
.map(|player| {
|
.map(|player| {
|
||||||
let name = player.name.clone();
|
let name = player.name.clone();
|
||||||
let team = player.team.as_ref().map(|t| {
|
let team = if show_teams {
|
||||||
let label = t.clone();
|
player.team.as_ref().map(|t| {
|
||||||
view! { span(class="badge text-bg-info ms-2") { "Team " (label) } }
|
let label = t.clone();
|
||||||
});
|
view! { span(class="badge text-bg-info ms-2") { "Team " (label) } }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
view! {
|
view! {
|
||||||
li(
|
li(
|
||||||
class="list-group-item d-flex justify-content-between \
|
class="list-group-item d-flex justify-content-between \
|
||||||
|
|||||||
Reference in New Issue
Block a user