From 87677a09b0652935cf2bfdebb9935c8fe43c0912 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sat, 18 Apr 2026 16:12:38 +0200 Subject: [PATCH] fix(client_web): pre-game : allow guest to roll die without waiting for host --- client_web/src/components/game_screen.rs | 11 +++--- client_web/src/trictrac/backend.rs | 48 +++++++++++------------- client_web/src/trictrac/types.rs | 2 +- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/client_web/src/components/game_screen.rs b/client_web/src/components/game_screen.rs index a94194f..909e266 100644 --- a/client_web/src/components/game_screen.rs +++ b/client_web/src/components/game_screen.rs @@ -5,10 +5,10 @@ use futures::channel::mpsc::UnboundedSender; use leptos::prelude::*; use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveRules}; +use super::die::Die; use crate::app::{GameUiState, NetCommand, PauseReason}; use crate::i18n::*; use crate::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage}; -use super::die::Die; use super::board::Board; use super::score_panel::PlayerScorePanel; @@ -81,9 +81,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { // wait until the buffer is drained and the live screen state is shown. // Guard: never auto-roll during the pre-game ceremony (the ceremony overlay // has its own Roll button for PlayerAction::PreGameRoll). - let show_roll = is_my_turn - && vs.turn_stage == SerTurnStage::RollDice - && vs.stage != SerStage::PreGameRoll; + let show_roll = + is_my_turn && vs.turn_stage == SerTurnStage::RollDice && vs.stage != SerStage::PreGameRoll; if show_roll && !waiting_for_confirm { let cmd_tx_auto = cmd_tx.clone(); Effect::new(move |_| { @@ -374,7 +373,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { }); let my_die = if player_id == 0 { pgr.host_die } else { pgr.guest_die }; let opp_die = if player_id == 0 { pgr.guest_die } else { pgr.host_die }; - let can_roll = is_my_turn && !waiting_for_confirm; + let can_roll = my_die.is_none() && !waiting_for_confirm; let show_tie = pgr.tie_count > 0; view! {
@@ -385,7 +384,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { })}
- {my_name_ceremony} + {my_name_ceremony}{t!(i18n, you_suffix)}
diff --git a/client_web/src/trictrac/backend.rs b/client_web/src/trictrac/backend.rs index 288f5e7..7f3b1a6 100644 --- a/client_web/src/trictrac/backend.rs +++ b/client_web/src/trictrac/backend.rs @@ -32,12 +32,8 @@ impl TrictracBackend { guest_die: self.pre_game_dice[1], tie_count: self.tie_count, }); - // The active mp player is whoever hasn't rolled yet (host rolls first). - vs.active_mp_player = match self.pre_game_dice { - [None, _] => Some(0), - [Some(_), None] => Some(1), - _ => None, - }; + // Both players roll independently; no single "active" player. + vs.active_mp_player = None; } self.view_state = vs; } @@ -52,16 +48,11 @@ impl TrictracBackend { /// Process one ceremony die-roll for `mp_player` (0 = host, 1 = guest). fn handle_pre_game_roll(&mut self, mp_player: u16) { - // Enforce turn order: host rolls first, then guest. - let expected: u16 = match self.pre_game_dice { - [None, _] => 0, - [Some(_), None] => 1, - _ => return, // both already rolled (shouldn't happen) - }; - if mp_player != expected { + let idx = mp_player as usize; + // Ignore if this player already rolled. + if self.pre_game_dice[idx].is_some() { return; } - let idx = mp_player as usize; let single = self.dice_roller.roll().values.0; self.pre_game_dice[idx] = Some(single); @@ -132,8 +123,8 @@ impl TrictracBackend { impl BackEndArchitecture for TrictracBackend { fn new(_rule_variation: u16) -> Self { let mut game = GameState::new(false); - game.init_player("Host"); - game.init_player("Guest"); + game.init_player("Blancs"); + game.init_player("Noirs"); let view_state = ViewState::from_game_state(&game, HOST_PLAYER_ID, GUEST_PLAYER_ID); @@ -309,11 +300,14 @@ mod tests { if b.get_view_state().stage != SerStage::PreGameRoll { break; } - match b.get_view_state().active_mp_player { - Some(0) => b.inform_rpc(0, PlayerAction::PreGameRoll), - Some(1) => b.inform_rpc(1, PlayerAction::PreGameRoll), - _ => break, + let pgr = b.get_view_state().pre_game_roll.clone().unwrap_or_default(); + let host_needs = pgr.host_die.is_none(); + let guest_needs = pgr.guest_die.is_none(); + if !host_needs && !guest_needs { + break; // both rolled but stage not yet resolved — shouldn't happen } + if host_needs { b.inform_rpc(0, PlayerAction::PreGameRoll); } + if guest_needs { b.inform_rpc(1, PlayerAction::PreGameRoll); } b.drain_commands(); } } @@ -349,19 +343,19 @@ mod tests { } #[test] - fn ceremony_wrong_order_ignored() { + fn ceremony_any_order_allowed() { let mut b = make_backend(); b.player_arrival(0); b.player_arrival(1); b.drain_commands(); - // Guest tries to roll before host (host goes first in ceremony). + // Guest may roll before host. b.inform_rpc(1, PlayerAction::PreGameRoll); - let cmds = b.drain_commands(); - assert!( - cmds.is_empty(), - "guest PreGameRoll should be ignored when it is host's turn" - ); + let states = drain_deltas(&mut b); + assert!(!states.is_empty(), "guest PreGameRoll should broadcast a state"); + let pgr = states.last().unwrap().pre_game_roll.as_ref().unwrap(); + assert!(pgr.guest_die.is_some(), "guest die should be set after guest rolls"); + assert!(pgr.host_die.is_none(), "host die should still be blank"); } #[test] diff --git a/client_web/src/trictrac/types.rs b/client_web/src/trictrac/types.rs index b6f43da..3c0dfe2 100644 --- a/client_web/src/trictrac/types.rs +++ b/client_web/src/trictrac/types.rs @@ -31,7 +31,7 @@ pub struct GameDelta { /// State of the pre-game ceremony where each player rolls one die to decide /// who goes first. Present only when `stage == SerStage::PreGameRoll`. -#[derive(Clone, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Default, PartialEq, Serialize, Deserialize)] pub struct PreGameRollState { /// Die value (1–6) rolled by the host; `None` = not yet rolled this round. pub host_die: Option,