Add configurable napola rule with instant win on a full denari sweep

This commit is contained in:
2026-09-18 09:18:17 +08:00
parent f7fa8e78e5
commit db7ba30d13
12 changed files with 250 additions and 5 deletions
+2 -2
View File
@@ -35,9 +35,9 @@ pub async fn game_types() -> Result<Vec<GameTypeInfo>, String> {
Ok(page.results)
}
pub async fn create_game(game_type: &str, target_score: i32) -> Result<GameView, String> {
pub async fn create_game(game_type: &str, target_score: i32, napola: bool) -> Result<GameView, String> {
let resp = Request::post("/api/games")
.json(&serde_json::json!({ "game_type": game_type, "target_score": target_score }))
.json(&serde_json::json!({ "game_type": game_type, "target_score": target_score, "napola": napola }))
.map_err(|e| e.to_string())?
.send()
.await
+39
View File
@@ -113,6 +113,45 @@ pub fn summary_rows(summary: HandSummary) -> View {
summary.award.primiera.clone(),
"+1",
),
// Napola (only when the rule is enabled for this match)
match summary.napola {
None => view! {},
Some(napola) => {
let n = match summary.award.napola.as_deref() {
Some("A") => napola.a,
Some("B") => napola.b,
_ => 0,
};
let text = match (&summary.award.napola, n) {
(Some(t), 10) => format!(
"Team {t} swept the whole denari suit — napola! Instant match win"
),
(Some(t), n) => format!(
"Team {t} captured {n} consecutive denari from the ace"
),
(None, _) => "No napola this hand".to_string(),
};
let chip = match &summary.award.napola {
Some(t) => format!("Team {t} +{n}"),
None => "tie".to_string(),
};
let cls = match summary.award.napola.as_deref() {
Some("A") => "score-row team-a",
Some("B") => "score-row team-b",
_ => "score-row tie",
};
view! {
div(class=cls) {
div(class="score-icon") { (card_img("01D".to_string(), "score-mini")) }
div(class="score-body") {
div(class="score-title") { "Napola" }
div(class="score-text") { (text) }
}
div(class="score-points") { (chip) }
}
}
}
},
// Scope
{
let a = summary.scope.a;
+13
View File
@@ -2,6 +2,10 @@
use serde::Deserialize;
use std::collections::HashMap;
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct User {
@@ -75,6 +79,8 @@ pub struct Award {
pub settebello: Option<String>,
#[serde(default)]
pub primiera: Option<String>,
#[serde(default)]
pub napola: Option<String>,
}
/// The scoring breakdown of one completed hand.
@@ -86,6 +92,10 @@ pub struct HandSummary {
pub settebello: TeamBools,
pub primiera: TeamCounts,
pub scope: TeamCounts,
/// Napola run lengths per team; absent when the rule is disabled (or
/// the summary predates the option).
#[serde(default)]
pub napola: Option<TeamCounts>,
pub award: Award,
#[serde(default)]
pub hand: i32,
@@ -104,6 +114,9 @@ pub struct GameView {
/// Which card game this match is (id from /api/game-types).
#[serde(default)]
pub game_type: String,
/// Whether the napola rule is scored in this match.
#[serde(default = "default_true")]
pub napola: bool,
pub phase: String,
#[serde(default)]
pub target_score: i32,
+12 -1
View File
@@ -24,6 +24,7 @@ pub fn LobbyPage() -> View {
let code = create_signal(String::new());
let game_types = create_signal(fallback_game_types());
let selected_game = create_signal("scopone_scientifico".to_string());
let napola = create_signal(true);
spawn_local(async move {
match api::me().await {
@@ -47,8 +48,9 @@ pub fn LobbyPage() -> View {
let on_create = move |target: i32| {
let game_type = selected_game.get_clone();
let napola = napola.get();
spawn_local(async move {
match api::create_game(&game_type, target).await {
match api::create_game(&game_type, target, napola).await {
Ok(game) => navigate(&format!("/game/{}", game.id)),
Err(e) => error.set(Some(e)),
}
@@ -101,6 +103,15 @@ pub fn LobbyPage() -> View {
key=|g| g.id.clone(),
)
}
label(class="check", r#for="napola") {
input(id="napola", r#type="checkbox", bind:checked=napola)
" Napola"
}
p(class="hint") {
"A-2-3 of denari scores 3 points, plus 1 per extra "
"consecutive denari card; sweeping the whole suit "
"wins the match instantly."
}
p { "First team to reach the target score wins." }
div(class="target-buttons") {
button(class="button", on:click=move |_| on_create(11)) { "Target 11" }
+6
View File
@@ -123,6 +123,12 @@ body {
gap: 0.5rem;
}
.check {
display: flex;
align-items: center;
gap: 0.4rem;
}
.join-form {
display: flex;
gap: 0.5rem;