Add optional asso pigliatutto rule with a lobby toggle
CI / Build and push docker image (push) Successful in 3m16s
CI / Build and push docker image (push) Successful in 3m16s
An ace played when no equal rank is on the table sweeps the whole table; the sweep never scores a scopa, and an ace already on the table still forces the mandatory equal-rank capture. The rule is off by default, selectable per match via the schema-driven creation form, and announced in the game view.
This commit is contained in:
+36
-2
@@ -49,6 +49,9 @@ pub struct MoveView {
|
||||
pub captured: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub scopa: bool,
|
||||
/// The play was an asso pigliatutto sweep (it took the whole table).
|
||||
#[serde(default)]
|
||||
pub pigliatutto: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
@@ -117,6 +120,9 @@ pub struct GameView {
|
||||
/// Whether the napola rule is scored in this match.
|
||||
#[serde(default = "default_true")]
|
||||
pub napola: bool,
|
||||
/// Whether the asso pigliatutto rule is scored in this match.
|
||||
#[serde(default)]
|
||||
pub asso_pigliatutto: bool,
|
||||
pub phase: String,
|
||||
#[serde(default)]
|
||||
pub target_score: i32,
|
||||
@@ -563,6 +569,7 @@ mod tests {
|
||||
"default": 11,
|
||||
},
|
||||
"napola": {"type": "boolean", "default": true},
|
||||
"asso_pigliatutto": {"type": "boolean", "default": false},
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -572,7 +579,7 @@ mod tests {
|
||||
#[test]
|
||||
fn option_fields_parse_scopone_schema() {
|
||||
let fields = scopone_game_type().option_fields();
|
||||
assert_eq!(2, fields.len());
|
||||
assert_eq!(3, fields.len());
|
||||
let by_key: std::collections::HashMap<_, _> =
|
||||
fields.into_iter().map(|f| (f.key.clone(), f)).collect();
|
||||
let target = &by_key["target_score"];
|
||||
@@ -583,6 +590,8 @@ mod tests {
|
||||
assert_eq!(json!(11), target.default);
|
||||
assert_eq!(OptionKind::Boolean, by_key["napola"].kind);
|
||||
assert_eq!(json!(true), by_key["napola"].default);
|
||||
assert_eq!(OptionKind::Boolean, by_key["asso_pigliatutto"].kind);
|
||||
assert_eq!(json!(false), by_key["asso_pigliatutto"].default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -614,13 +623,14 @@ mod tests {
|
||||
let opts = scopone_game_type().default_options();
|
||||
assert_eq!(Some(&json!(11)), opts.get("target_score"));
|
||||
assert_eq!(Some(&json!(true)), opts.get("napola"));
|
||||
assert_eq!(Some(&json!(false)), opts.get("asso_pigliatutto"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_view_parses_new_lobby_payload() {
|
||||
// Exact shape of the platform's lobby payload: envelope fields,
|
||||
// seats with team labels, seats_open, plus the engine's
|
||||
// lobby_view (phase/target_score/napola for scopone).
|
||||
// lobby_view (phase/target_score/napola/asso_pigliatutto).
|
||||
let g: GameView = serde_json::from_value(json!({
|
||||
"id": "11111111-2222-3333-4444-555555555555",
|
||||
"join_code": "ABC123",
|
||||
@@ -632,6 +642,7 @@ mod tests {
|
||||
"phase": "lobby",
|
||||
"target_score": 16,
|
||||
"napola": false,
|
||||
"asso_pigliatutto": true,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!("ABC123", g.join_code);
|
||||
@@ -639,6 +650,7 @@ mod tests {
|
||||
assert_eq!("lobby", g.phase);
|
||||
assert_eq!(16, g.target_score);
|
||||
assert!(!g.napola);
|
||||
assert!(g.asso_pigliatutto);
|
||||
assert_eq!("A", g.players[0].team);
|
||||
}
|
||||
|
||||
@@ -652,6 +664,7 @@ mod tests {
|
||||
"phase": "playing",
|
||||
"target_score": 11,
|
||||
"napola": true,
|
||||
"asso_pigliatutto": true,
|
||||
"hand_number": 1,
|
||||
"dealer": 0,
|
||||
"turn": 1,
|
||||
@@ -675,11 +688,32 @@ mod tests {
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!("playing", g.phase);
|
||||
assert!(g.asso_pigliatutto);
|
||||
assert_eq!(None, g.players[0].hand);
|
||||
assert_eq!(Some(vec!["01D".to_string()]), g.players[1].hand);
|
||||
assert_eq!(Some(true), g.your_turn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_view_parses_pigliatutto() {
|
||||
let mv: MoveView = serde_json::from_value(json!({
|
||||
"seat": 0,
|
||||
"name": "Alice",
|
||||
"card": "01D",
|
||||
"captured": ["05C", "02D"],
|
||||
"scopa": false,
|
||||
"pigliatutto": true,
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(mv.pigliatutto);
|
||||
// Older payloads without the flag default to a normal capture.
|
||||
let legacy: MoveView = serde_json::from_value(json!({
|
||||
"seat": 0, "name": "Alice", "card": "01D",
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(!legacy.pigliatutto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_type_without_schema_has_no_fields() {
|
||||
let g: GameTypeInfo = serde_json::from_value(json!({
|
||||
|
||||
@@ -63,6 +63,9 @@ fn move_banner(mv: MoveView) -> View {
|
||||
.map(|c| card_img(c.clone(), "move-card"))
|
||||
.collect::<Vec<_>>();
|
||||
let scopa = mv.scopa.then(|| view! { span(class="scopa-badge") { "Scopa!" } });
|
||||
let pigliatutto = mv
|
||||
.pigliatutto
|
||||
.then(|| view! { span(class="pigliatutto-badge") { "Asso pigliatutto!" } });
|
||||
view! {
|
||||
div(class="move-banner") {
|
||||
strong { (name) }
|
||||
@@ -70,6 +73,7 @@ fn move_banner(mv: MoveView) -> View {
|
||||
(played)
|
||||
(captured)
|
||||
(scopa)
|
||||
(pigliatutto)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,6 +569,9 @@ fn table_view(
|
||||
let banner = game.last_move.clone().map(move_banner);
|
||||
let hand_number = game.hand_number;
|
||||
let target_score = game.target_score;
|
||||
let rule_badge = game.asso_pigliatutto.then(|| view! {
|
||||
span(class="pigliatutto-badge") { "Asso pigliatutto" }
|
||||
});
|
||||
|
||||
let viewer = player_for_seat(&game, viewer_seat);
|
||||
let my_captured = viewer.as_ref().map(|p| p.captured_count).unwrap_or(0);
|
||||
@@ -602,6 +609,7 @@ fn table_view(
|
||||
div(class="hud") {
|
||||
a(href="/") { "← Lobby" }
|
||||
span { "Hand " (hand_number) }
|
||||
(rule_badge)
|
||||
span(class="hud-scores") {
|
||||
"Team A " (scores.a) " — " (scores.b) " Team B (target "
|
||||
(target_score) ")"
|
||||
|
||||
@@ -257,6 +257,15 @@ body {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.pigliatutto-badge {
|
||||
background: var(--danger, #d9534f);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
border-radius: 999px;
|
||||
padding: 0.15rem 0.6rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* ---------- overlays ---------- */
|
||||
|
||||
.overlay {
|
||||
|
||||
Reference in New Issue
Block a user