Add briscola chiamata
CI / Build and push docker image (push) Successful in 5m35s

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 21:50:56 +08:00
parent 6427afd12b
commit 353f012906
21 changed files with 4036 additions and 73 deletions
+35 -9
View File
@@ -7,14 +7,16 @@
use sycamore::prelude::*;
use crate::components::card::{card_back, card_img};
use crate::model::{is_individual_game, DealSummary, GameView};
use crate::model::{card_label, is_chiamata, is_individual_game, DealSummary, GameView};
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.
/// Names of the two sides: the caller and partner for briscola chiamata,
/// 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) {
if is_chiamata(&game.game_type) {
game.chiamata_sides()
} else if is_individual_game(&game.game_type) {
(
game.player_name_for_team("A"),
game.player_name_for_team("B"),
@@ -66,6 +68,25 @@ pub fn deal_rows(game: &GameView, summary: &DealSummary) -> View {
"60–60: patta — the deal is replayed and its points do not count."
}
});
let called_row = if is_chiamata(&game.game_type) {
game.called_card.clone().map(|code| {
let label = card_label(&code);
let text = if game.solo {
format!("{name_a} called {label} in hand and plays alone.")
} else {
format!("{label}: its holder is the secret partner; its suit is trump.")
};
summary_row(
card_img(code, "score-mini"),
"Chiamata",
text,
"score-row chiamata-row",
label,
)
})
} else {
None
};
let points_row = summary_row(
card_img("01D".to_string(), "score-mini"),
"Deal points",
@@ -84,6 +105,7 @@ pub fn deal_rows(game: &GameView, summary: &DealSummary) -> View {
});
view! {
div(class="score-rows") {
(called_row)
(points_row)
(patta_note)
(cards)
@@ -94,9 +116,13 @@ pub fn deal_rows(game: &GameView, summary: &DealSummary) -> View {
/// Running totals with progress toward the target, including the points
/// gained in the deal just played (zero for a patta).
fn totals(game: &GameView, summary: &DealSummary) -> View {
// Briscola chiamata plays a single deal: there is no running total.
let Some(running) = summary.totals else {
return view! {};
};
let target = game.target_score.max(1);
let pct_a = (100 * summary.totals.a / target).min(100);
let pct_b = (100 * summary.totals.b / target).min(100);
let pct_a = (100 * running.a / target).min(100);
let pct_b = (100 * running.b / target).min(100);
let (gained_a, gained_b) = if summary.patta {
(0, 0)
} else {
@@ -111,7 +137,7 @@ fn totals(game: &GameView, summary: &DealSummary) -> View {
div(class="progress-fill", style=format!("width: {pct_a}%")) {}
}
span(class="total-value") {
(summary.totals.a) " / " (target)
(running.a) " / " (target)
span(class="gained") { "+" (gained_a) }
}
}
@@ -121,7 +147,7 @@ fn totals(game: &GameView, summary: &DealSummary) -> View {
div(class="progress-fill", style=format!("width: {pct_b}%")) {}
}
span(class="total-value") {
(summary.totals.b) " / " (target)
(running.b) " / " (target)
span(class="gained") { "+" (gained_b) }
}
}