Add briscola chiamata

Five-player briscola with a descending calling auction and a secret
partner. Starting with the player after the dealer, each player bids a
lower rank (A > 3 > K > H > J > 7 > 6 > 5 > 4 > 2) or passes; once the
standing bid is the two the auction continues on points (62, 64, ...).
The last player standing must bid, and the winner names any card of the
won rank: its holder is the secret partner, or the caller plays alone
when it is one of their own cards, and its suit becomes the trump. The
calling side must reach 61 (or the raised target); a 60-60 deal with the
plain target is a patta and is replayed. A match is a single deal, so
the caller and partner (or the lone caller) against the other players
map onto the platform's two-team result, Elo and match history.

The rules live in tavolo.briscola.chiamata with the
BriscolaChiamataEngine adapter registered alongside the other briscola
variants; the partner's role and the side totals stay hidden until the
called card is played. The web client gets a five-seat table, the
auction panel with legal bids and point raises, the call picker, the
reveal bands and a chiamata-aware final summary.
This commit is contained in:
2026-10-08 04:27:15 +00:00
parent 6427afd12b
commit 43d5a6e2f9
21 changed files with 4036 additions and 73 deletions
+57 -10
View File
@@ -12,10 +12,11 @@ use crate::components::copy::CopyButton;
use crate::components::summary::{hand_summary_modal, summary_rows};
use crate::components::toast::toast;
use crate::model::{
card_label, hand_sort_key, is_briscola, is_individual_game, GameView, MoveView,
card_label, hand_sort_key, is_briscola, is_chiamata, is_individual_game, GameView, MoveView,
PlayerView, Scores, ServerMessage,
};
use crate::pages::briscola::briscola_table;
use crate::pages::chiamata::{call_picker, chiamata_table};
use crate::ws::{self, GameSocket};
fn send_play(socket: Signal<Option<GameSocket>>, card: String, capture: Option<Vec<String>>) {
@@ -43,7 +44,7 @@ fn seat_panel(game: GameView, seat: usize, position: &'static str) -> View {
if active { " active" } else { "" }
);
let name = player.name.clone();
let team = player.team.clone();
let team = player.team.clone().unwrap_or_default();
let captured = player.captured_count;
let scope = player.scope;
let backs = (0..player.cards_left)
@@ -324,6 +325,32 @@ pub fn GamePage(id: String) -> View {
}
};
// Briscola chiamata: auction and call actions.
let on_bid_rank = move |rank: i32| {
if closed.get() || game.get_clone().and_then(|g| g.your_turn) != Some(true) {
return;
}
if let Some(s) = socket.get_clone() {
s.bid_rank(rank);
}
};
let on_bid_points = move |points: i32| {
if closed.get() || game.get_clone().and_then(|g| g.your_turn) != Some(true) {
return;
}
if let Some(s) = socket.get_clone() {
s.bid_points(points);
}
};
let on_pass = move || {
if closed.get() || game.get_clone().and_then(|g| g.your_turn) != Some(true) {
return;
}
if let Some(s) = socket.get_clone() {
s.pass();
}
};
let reconnect_banner = reconnect.clone();
view! {
div(class="game-page") {
@@ -392,6 +419,11 @@ pub fn GamePage(id: String) -> View {
}
}
Some(g) if g.phase == "lobby" => view! { LobbyView(game=g) },
// Briscola chiamata has its own five-seat table with the
// calling auction and the secret-partner reveal.
Some(g) if is_chiamata(&g.game_type) => {
chiamata_table(g, on_hand_card, on_bid_rank, on_bid_points, on_pass, now)
}
// Briscola has its own table view for both variants.
Some(g) if is_briscola(&g.game_type) => {
briscola_table(g, on_hand_card, now)
@@ -406,6 +438,17 @@ pub fn GamePage(id: String) -> View {
(move || capture_choice.get_clone().map(|(card, options)| {
capture_picker(card, options, socket, capture_choice)
}))
// Briscola chiamata: the auction winner names the called card.
(move || match game.get_clone() {
Some(g)
if is_chiamata(&g.game_type)
&& g.phase == "calling"
&& g.your_turn == Some(true) =>
{
call_picker(g, socket)
}
_ => view! {},
})
(move || match game.get_clone() {
Some(g) if g.phase == "hand_end" && g.game_type == "scopone_scientifico" => {
hand_summary_modal(g, socket, now)
@@ -440,10 +483,11 @@ fn LobbyView(game: GameView) -> View {
.map(|p| {
let name = p.name.clone();
let seat = p.seat;
let team_badge = show_teams.then(|| {
let team = p.team.clone();
view! { span(class="badge text-bg-info") { "Team " (team) } }
});
let team_badge = show_teams
.then(|| p.team.clone())
.flatten()
.filter(|team| !team.is_empty())
.map(|team| view! { span(class="badge text-bg-info") { "Team " (team) } });
view! {
li(class="list-group-item d-flex justify-content-between align-items-center") {
strong { (name) }
@@ -734,8 +778,9 @@ fn capture_picker(
/// End-of-match overlay: the final hand/deal breakdown and the winner.
fn game_over_view(over: Option<(Scores, Option<String>)>, game: Option<GameView>) -> View {
let briscola = game.as_ref().is_some_and(|g| is_briscola(&g.game_type));
let chiamata = game.as_ref().is_some_and(|g| is_chiamata(&g.game_type));
if let Some(g) = &game {
if g.game_type != "scopone_scientifico" && !briscola {
if g.game_type != "scopone_scientifico" && !briscola && !chiamata {
return view! {};
}
}
@@ -749,18 +794,20 @@ fn game_over_view(over: Option<(Scores, Option<String>)>, game: Option<GameView>
Some((scores, winner)) => {
let winner = winner.unwrap_or_else(|| "?".to_string());
// Two-player briscola has no teams: name the winner instead.
// Briscola chiamata names the caller's side (or the defenders).
let winner_label = match game.as_ref() {
Some(g) if is_individual_game(&g.game_type) => {
g.player_name_for_team(&winner)
}
Some(g) if is_chiamata(&g.game_type) => winner.clone(),
_ => format!("Team {winner}"),
};
let line = format!("{winner_label} wins {} – {}", scores.a, scores.b);
// Spectators have no match history to look at.
let spectator = game.as_ref().is_some_and(|g| g.spectator);
// Explain the final hand's (scopone) or deal's (briscola)
// scoring before the result.
let final_summary: View = if briscola {
// Explain the final hand's (scopone) or deal's (briscola,
// briscola chiamata) scoring before the result.
let final_summary: View = if briscola || chiamata {
match game.as_ref() {
Some(g) => match g.last_deal.clone() {
Some(deal) => deal_rows(g, &deal),