From 05e09fba9589c8b451229b5025ca6465bbc5e43c Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sun, 12 Apr 2026 21:02:59 +0200 Subject: [PATCH] feat(web_client): debug message --- client_web/src/app.rs | 64 +++++++++++++++++------- client_web/src/components/game_screen.rs | 4 ++ client_web/src/trictrac/backend.rs | 1 + client_web/src/trictrac/types.rs | 49 ++++++++++++------ store/src/game.rs | 13 ++++- 5 files changed, 96 insertions(+), 35 deletions(-) diff --git a/client_web/src/app.rs b/client_web/src/app.rs index 4ae4ad1..5ade2c3 100644 --- a/client_web/src/app.rs +++ b/client_web/src/app.rs @@ -12,7 +12,9 @@ use crate::components::{ConnectingScreen, GameScreen, LoginScreen}; use crate::i18n::I18nContextProvider; use crate::trictrac::backend::TrictracBackend; use crate::trictrac::bot_local::bot_decide; -use crate::trictrac::types::{GameDelta, JanEntry, PlayerAction, ScoredEvent, SerTurnStage, ViewState}; +use crate::trictrac::types::{ + GameDelta, JanEntry, PlayerAction, ScoredEvent, SerTurnStage, ViewState, +}; use trictrac_store::CheckerMove; use std::collections::VecDeque; @@ -194,7 +196,9 @@ pub fn App() -> impl IntoView { if remote_config.is_none() { loop { let restart = run_local_bot_game(screen, &mut cmd_rx, pending).await; - if !restart { break; } + if !restart { + break; + } } pending.update(|q| q.clear()); screen.set(Screen::Login { error: None }); @@ -328,8 +332,12 @@ async fn run_local_bot_game( let mut vs = ViewState::default_with_names("You", "Bot"); for cmd in backend.drain_commands() { match cmd { - BackendCommand::ResetViewState => { vs = backend.get_view_state().clone(); } - BackendCommand::Delta(delta) => { vs.apply_delta(&delta); } + BackendCommand::ResetViewState => { + vs = backend.get_view_state().clone(); + } + BackendCommand::Delta(delta) => { + vs.apply_delta(&delta); + } _ => {} } } @@ -440,15 +448,21 @@ fn compute_scored_event(prev: &ViewState, next: &ViewState, player_id: u16) -> O && prev.active_mp_player == Some(player_id) { // My own roll: positive totals are mine. - next.dice_jans.iter().filter(|e| e.total > 0).cloned().collect() - } else if next.active_mp_player == Some(player_id) - && prev.active_mp_player != Some(player_id) - { + next.dice_jans + .iter() + .filter(|e| e.total > 0) + .cloned() + .collect() + } else if next.active_mp_player == Some(player_id) && prev.active_mp_player != Some(player_id) { // Opponent just moved: negative totals (their penalty) are scored for me. next.dice_jans .iter() .filter(|e| e.total < 0) - .map(|e| JanEntry { total: -e.total, points_per: -e.points_per, ..e.clone() }) + .map(|e| JanEntry { + total: -e.total, + points_per: -e.points_per, + ..e.clone() + }) .collect() } else { return None; @@ -496,7 +510,10 @@ fn push_or_show( }); // Animation belongs to the buffered confirmation step; clear it on the // fallback live state so it doesn't fire again after the queue drains. - screen.set(Screen::Playing(GameUiState { last_moves: None, ..new_state })); + screen.set(Screen::Playing(GameUiState { + last_moves: None, + ..new_state + })); } else { // No pause: show scoring directly on the live state. screen.set(Screen::Playing(GameUiState { @@ -519,8 +536,7 @@ fn infer_pause_reason(prev: &ViewState, next: &ViewState, player_id: u16) -> Opt return Some(PauseReason::AfterOpponentRoll); } // Was at HoldOrGoChoice, now Move, opponent still active → opponent went. - if prev.turn_stage == SerTurnStage::HoldOrGoChoice - && next.turn_stage == SerTurnStage::Move + if prev.turn_stage == SerTurnStage::HoldOrGoChoice && next.turn_stage == SerTurnStage::Move { return Some(PauseReason::AfterOpponentGo); } @@ -534,14 +550,18 @@ fn infer_pause_reason(prev: &ViewState, next: &ViewState, player_id: u16) -> Opt None } - #[cfg(test)] mod tests { use super::*; use crate::trictrac::types::{PlayerScore, SerStage, SerTurnStage}; fn score() -> PlayerScore { - PlayerScore { name: String::new(), points: 0, holes: 0, can_bredouille: false } + PlayerScore { + name: String::new(), + points: 0, + holes: 0, + can_bredouille: false, + } } fn vs(dice: (u8, u8), turn_stage: SerTurnStage, active: Option) -> ViewState { @@ -554,6 +574,7 @@ mod tests { dice, dice_jans: Vec::new(), dice_moves: (CheckerMove::default(), CheckerMove::default()), + message: "".into(), } } @@ -561,21 +582,30 @@ mod tests { fn dice_change_is_after_roll() { let prev = vs((0, 0), SerTurnStage::RollDice, Some(1)); let next = vs((3, 5), SerTurnStage::Move, Some(1)); - assert_eq!(infer_pause_reason(&prev, &next, 0), Some(PauseReason::AfterOpponentRoll)); + assert_eq!( + infer_pause_reason(&prev, &next, 0), + Some(PauseReason::AfterOpponentRoll) + ); } #[test] fn hold_to_move_is_after_go() { let prev = vs((3, 5), SerTurnStage::HoldOrGoChoice, Some(1)); let next = vs((3, 5), SerTurnStage::Move, Some(1)); - assert_eq!(infer_pause_reason(&prev, &next, 0), Some(PauseReason::AfterOpponentGo)); + assert_eq!( + infer_pause_reason(&prev, &next, 0), + Some(PauseReason::AfterOpponentGo) + ); } #[test] fn turn_switch_is_after_move() { let prev = vs((3, 5), SerTurnStage::Move, Some(1)); let next = vs((3, 5), SerTurnStage::RollDice, Some(0)); - assert_eq!(infer_pause_reason(&prev, &next, 0), Some(PauseReason::AfterOpponentMove)); + assert_eq!( + infer_pause_reason(&prev, &next, 0), + Some(PauseReason::AfterOpponentMove) + ); } #[test] diff --git a/client_web/src/components/game_screen.rs b/client_web/src/components/game_screen.rs index 24042be..8bf9a5b 100644 --- a/client_web/src/components/game_screen.rs +++ b/client_web/src/components/game_screen.rs @@ -18,6 +18,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let i18n = use_i18n(); let vs = state.view_state.clone(); + let message = format!("{}", vs.message); let player_id = state.player_id; let is_my_turn = vs.active_mp_player == Some(player_id); let is_move_stage = is_my_turn @@ -351,6 +352,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { // ── Player score (below board) ──────────────────────────────────── +
+ {format!("{message}")} +
// ── Game-over overlay ───────────────────────────────────────────── {stage_is_ended.then(|| { diff --git a/client_web/src/trictrac/backend.rs b/client_web/src/trictrac/backend.rs index e96f080..7d60873 100644 --- a/client_web/src/trictrac/backend.rs +++ b/client_web/src/trictrac/backend.rs @@ -167,6 +167,7 @@ impl BackEndArchitecture for TrictracBackend moves: (m1, m2), }; if self.game.validate(&event) { + self.game.debug_message = format!("Event {:?} validated", event); let _ = self.game.consume(&event); self.drive_automatic_stages(); } diff --git a/client_web/src/trictrac/types.rs b/client_web/src/trictrac/types.rs index 82f9a2d..927584e 100644 --- a/client_web/src/trictrac/types.rs +++ b/client_web/src/trictrac/types.rs @@ -43,6 +43,7 @@ pub struct ViewState { pub dice_jans: Vec, /// Last two checker moves played; default when no move has occurred yet. pub dice_moves: (CheckerMove, CheckerMove), + pub message: String, } /// One scoring event from a dice roll. @@ -70,12 +71,23 @@ impl ViewState { turn_stage: SerTurnStage::RollDice, active_mp_player: None, scores: [ - PlayerScore { name: host_name.to_string(), points: 0, holes: 0, can_bredouille: false }, - PlayerScore { name: guest_name.to_string(), points: 0, holes: 0, can_bredouille: false }, + PlayerScore { + name: host_name.to_string(), + points: 0, + holes: 0, + can_bredouille: false, + }, + PlayerScore { + name: guest_name.to_string(), + points: 0, + holes: 0, + can_bredouille: false, + }, ], dice: (0, 0), dice_jans: Vec::new(), dice_moves: (CheckerMove::default(), CheckerMove::default()), + message: "".into(), } } @@ -86,25 +98,21 @@ impl ViewState { /// Convert a store `GameState` to a `ViewState`. /// `host_store_id` and `guest_store_id` are the trictrac `PlayerId`s assigned /// to the host (mp player 0) and guest (mp player 1) respectively. - pub fn from_game_state( - gs: &GameState, - host_store_id: u64, - guest_store_id: u64, - ) -> Self { + pub fn from_game_state(gs: &GameState, host_store_id: u64, guest_store_id: u64) -> Self { let board_vec = gs.board.to_vec(); let board: [i8; 24] = board_vec.try_into().expect("board is always 24 fields"); let stage = match gs.stage { Stage::PreGame => SerStage::PreGame, - Stage::InGame => SerStage::InGame, - Stage::Ended => SerStage::Ended, + Stage::InGame => SerStage::InGame, + Stage::Ended => SerStage::Ended, }; let turn_stage = match gs.turn_stage { - TurnStage::RollDice => SerTurnStage::RollDice, - TurnStage::RollWaiting => SerTurnStage::RollWaiting, - TurnStage::MarkPoints => SerTurnStage::MarkPoints, + TurnStage::RollDice => SerTurnStage::RollDice, + TurnStage::RollWaiting => SerTurnStage::RollWaiting, + TurnStage::MarkPoints => SerTurnStage::MarkPoints, TurnStage::HoldOrGoChoice => SerTurnStage::HoldOrGoChoice, - TurnStage::Move => SerTurnStage::Move, + TurnStage::Move => SerTurnStage::Move, TurnStage::MarkAdvPoints => SerTurnStage::MarkAdvPoints, }; @@ -125,7 +133,12 @@ impl ViewState { holes: p.holes, can_bredouille: p.can_bredouille, }) - .unwrap_or_else(|| PlayerScore { name: String::new(), points: 0, holes: 0, can_bredouille: false }) + .unwrap_or_else(|| PlayerScore { + name: String::new(), + points: 0, + holes: 0, + can_bredouille: false, + }) }; // is_double for scoring: dice show the same value (both dice identical). @@ -134,13 +147,16 @@ impl ViewState { // Build JanEntry list from the PossibleJans map. let empty_move = CheckerMove::new(0, 0).unwrap_or_default(); - let mut dice_jans: Vec = gs.dice_jans + let mut dice_jans: Vec = gs + .dice_jans .iter() .map(|(jan, moves)| { // HelplessMan: is_double = true only when *both* dice are unplayable // (the moves list contains a single (empty, empty) sentinel). let is_double = if *jan == Jan::HelplessMan { - moves.first().map(|&(m1, m2)| m1 == empty_move && m2 == empty_move) + moves + .first() + .map(|&(m1, m2)| m1 == empty_move && m2 == empty_move) .unwrap_or(false) } else { dice_are_double @@ -170,6 +186,7 @@ impl ViewState { dice: (gs.dice.values.0, gs.dice.values.1), dice_jans, dice_moves: gs.dice_moves, + message: gs.get_debug_message(), } } } diff --git a/store/src/game.rs b/store/src/game.rs index 9c5233f..da074b9 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -80,6 +80,7 @@ pub struct GameState { roll_first: bool, // NOTE: add to a Setting struct if other fields needed pub schools_enabled: bool, + pub debug_message: String, } // implement Display trait @@ -119,6 +120,7 @@ impl Default for GameState { dice_jans: PossibleJans::default(), roll_first: true, schools_enabled: false, + debug_message: "".into(), } } } @@ -147,6 +149,11 @@ impl GameState { game } + pub fn get_debug_message(&self) -> String { + // format!("{:?}", self.history.last()) + format!("{:?}", self.debug_message) + } + pub fn mirror(&self) -> GameState { let mirrored_active_player = if self.active_player_id == 1 { 2 } else { 1 }; let mut mirrored_players = HashMap::new(); @@ -171,6 +178,7 @@ impl GameState { dice_jans: self.dice_jans.mirror(), roll_first: self.roll_first, schools_enabled: self.schools_enabled, + debug_message: self.debug_message.clone(), } } @@ -594,8 +602,9 @@ impl GameState { dice_points: (0, 0), dice_moves: (CheckerMove::default(), CheckerMove::default()), dice_jans: PossibleJans::default(), - roll_first: false, // Assume not first roll - schools_enabled: false, // Assume disabled + roll_first: false, // Assume not first roll + schools_enabled: false, // Assume disabled + debug_message: "".into(), // Assume disabled }) }