diff --git a/client_web/locales/en.json b/client_web/locales/en.json index e5058ef..799dbbb 100644 --- a/client_web/locales/en.json +++ b/client_web/locales/en.json @@ -38,9 +38,5 @@ "vs_bot_label": "vs Bot", "you_win": "You win!", "opp_wins": "{{ name }} wins!", - "play_again": "Play again", - "after_opponent_roll": "Opponent rolled", - "after_opponent_go": "Opponent chose to continue", - "after_opponent_move": "Opponent moved — your turn", - "continue_btn": "Continue" + "play_again": "Play again" } diff --git a/client_web/locales/fr.json b/client_web/locales/fr.json index 5d27d02..df6a2b5 100644 --- a/client_web/locales/fr.json +++ b/client_web/locales/fr.json @@ -38,9 +38,5 @@ "vs_bot_label": "contre le bot", "you_win": "Vous avez gagné !", "opp_wins": "{{ name }} gagne !", - "play_again": "Rejouer", - "after_opponent_roll": "L'adversaire a lancé les dés", - "after_opponent_go": "L'adversaire s'en va", - "after_opponent_move": "L'adversaire a joué — à vous", - "continue_btn": "Continuer" + "play_again": "Rejouer" } diff --git a/client_web/src/app.rs b/client_web/src/app.rs index 1c04891..ae4c22f 100644 --- a/client_web/src/app.rs +++ b/client_web/src/app.rs @@ -12,9 +12,7 @@ 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, PlayerAction, SerTurnStage, ViewState}; - -use std::collections::VecDeque; +use crate::trictrac::types::{GameDelta, PlayerAction, ViewState}; const RELAY_URL: &str = "ws://127.0.0.1:8080/ws"; const GAME_ID: &str = "trictrac"; @@ -28,18 +26,6 @@ pub struct GameUiState { pub player_id: u16, pub room_id: String, pub is_bot_game: bool, - /// True when this state is a buffered snapshot awaiting player confirmation. - pub waiting_for_confirm: bool, - /// Why we are paused — drives the status-bar message in GameScreen. - pub pause_reason: Option, -} - -/// Reason the UI is paused waiting for the player to click Continue. -#[derive(Clone, Debug, PartialEq)] -pub enum PauseReason { - AfterOpponentRoll, - AfterOpponentGo, - AfterOpponentMove, } /// Which screen is currently shown. @@ -106,8 +92,6 @@ pub fn App() -> impl IntoView { let screen = RwSignal::new(initial_screen); let (cmd_tx, mut cmd_rx) = mpsc::unbounded::(); - let pending: RwSignal> = RwSignal::new(VecDeque::new()); - provide_context(pending); provide_context(cmd_tx.clone()); if let Some(s) = stored { @@ -187,10 +171,9 @@ pub fn App() -> impl IntoView { if remote_config.is_none() { loop { - let restart = run_local_bot_game(screen, &mut cmd_rx, pending).await; + let restart = run_local_bot_game(screen, &mut cmd_rx).await; if !restart { break; } } - pending.update(|q| q.clear()); screen.set(Screen::Login { error: None }); continue; } @@ -236,14 +219,12 @@ pub fn App() -> impl IntoView { _ => { clear_session(); session.disconnect(); - pending.update(|q| q.clear()); screen.set(Screen::Login { error: None }); break; } }, event = session.next_event().fuse() => match event { Some(SessionEvent::Update(u)) => { - let prev_vs = vs.clone(); match u { ViewStateUpdate::Full(state) => vs = state, ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta), @@ -258,27 +239,18 @@ pub fn App() -> impl IntoView { view_state: Some(vs.clone()), }); } - push_or_show( - &prev_vs, - GameUiState { - view_state: vs.clone(), - player_id, - room_id: room_id_for_storage.clone(), - is_bot_game: false, - waiting_for_confirm: false, - pause_reason: None, - }, - pending, - screen, - ); + screen.set(Screen::Playing(GameUiState { + view_state: vs.clone(), + player_id, + room_id: room_id_for_storage.clone(), + is_bot_game: false, + })); } Some(SessionEvent::Disconnected(reason)) => { - pending.update(|q| q.clear()); screen.set(Screen::Login { error: reason }); break; } None => { - pending.update(|q| q.clear()); screen.set(Screen::Login { error: None }); break; } @@ -290,17 +262,10 @@ pub fn App() -> impl IntoView { view! { - {move || { - let q = pending.get(); - if let Some(front) = q.front() { - view! { }.into_any() - } else { - match screen.get() { - Screen::Login { error } => view! { }.into_any(), - Screen::Connecting => view! { }.into_any(), - Screen::Playing(state) => view! { }.into_any(), - } - } + {move || match screen.get() { + Screen::Login { error } => view! { }.into_any(), + Screen::Connecting => view! { }.into_any(), + Screen::Playing(state) => view! { }.into_any(), }} } @@ -310,7 +275,6 @@ pub fn App() -> impl IntoView { async fn run_local_bot_game( screen: RwSignal, cmd_rx: &mut futures::channel::mpsc::UnboundedReceiver, - pending: RwSignal>, ) -> bool { let mut backend = TrictracBackend::new(0); backend.player_arrival(0); @@ -334,80 +298,14 @@ async fn run_local_bot_game( match bot_decide(backend.get_game()) { None => break, Some(action) => { - let prev_vs = vs.clone(); backend.inform_rpc(1, action); - for cmd in backend.drain_commands() { - if let BackendCommand::Delta(delta) = cmd { - vs.apply_delta(&delta); - } - } - push_or_show( - &prev_vs, - GameUiState { - view_state: vs.clone(), - player_id: 0, - room_id: String::new(), - is_bot_game: true, - waiting_for_confirm: false, - pause_reason: None, - }, - pending, - screen, - ); + drain_and_update(&mut backend, &mut vs, screen); } } } } } -/// Either queues the state as a buffered confirmation step (when the transition -/// warrants a pause) or shows it immediately. Always updates `screen` to the -/// live state so the UI falls through to the right content once pending drains. -fn push_or_show( - prev_vs: &ViewState, - new_state: GameUiState, - pending: RwSignal>, - screen: RwSignal, -) { - if let Some(reason) = infer_pause_reason(prev_vs, &new_state.view_state, new_state.player_id) { - pending.update(|q| { - q.push_back(GameUiState { - waiting_for_confirm: true, - pause_reason: Some(reason), - ..new_state.clone() - }); - }); - } - screen.set(Screen::Playing(new_state)); -} - -/// Compares the previous and next ViewState to decide whether the transition -/// warrants a confirmation pause. Returns None when it is the local player's -/// own action (no pause needed). -fn infer_pause_reason(prev: &ViewState, next: &ViewState, player_id: u16) -> Option { - let opponent_id = 1 - player_id; - - if next.active_mp_player == Some(opponent_id) { - // Dice changed → opponent just rolled. - if next.dice != prev.dice { - 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 - { - return Some(PauseReason::AfterOpponentGo); - } - } - - // Turn switched to us → opponent moved. - if next.active_mp_player == Some(player_id) && prev.active_mp_player == Some(opponent_id) { - return Some(PauseReason::AfterOpponentMove); - } - - None -} - fn drain_and_update( backend: &mut TrictracBackend, vs: &mut ViewState, @@ -429,66 +327,5 @@ fn drain_and_update( player_id: 0, room_id: String::new(), is_bot_game: true, - waiting_for_confirm: false, - pause_reason: 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 } - } - - fn vs(dice: (u8, u8), turn_stage: SerTurnStage, active: Option) -> ViewState { - ViewState { - board: [0i8; 24], - stage: SerStage::InGame, - turn_stage, - active_mp_player: active, - scores: [score(), score()], - dice, - dice_jans: Vec::new(), - } - } - - #[test] - 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)); - } - - #[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)); - } - - #[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)); - } - - #[test] - fn own_action_returns_none() { - let prev = vs((0, 0), SerTurnStage::RollDice, Some(0)); - let next = vs((2, 4), SerTurnStage::Move, Some(0)); - assert_eq!(infer_pause_reason(&prev, &next, 0), None); - } - - #[test] - fn no_active_player_returns_none() { - let mut prev = vs((0, 0), SerTurnStage::RollDice, None); - prev.stage = SerStage::PreGame; - let mut next = prev.clone(); - next.active_mp_player = Some(0); - assert_eq!(infer_pause_reason(&prev, &next, 0), None); - } -} diff --git a/client_web/src/components/game_screen.rs b/client_web/src/components/game_screen.rs index 1855560..8a43399 100644 --- a/client_web/src/components/game_screen.rs +++ b/client_web/src/components/game_screen.rs @@ -1,10 +1,8 @@ -use std::collections::VecDeque; - use futures::channel::mpsc::UnboundedSender; use leptos::prelude::*; use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, MoveRules}; -use crate::app::{GameUiState, NetCommand, PauseReason}; +use crate::app::{GameUiState, NetCommand}; use crate::i18n::*; use crate::trictrac::types::{JanEntry, PlayerAction, SerStage, SerTurnStage}; @@ -76,8 +74,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { vs.turn_stage, SerTurnStage::Move | SerTurnStage::HoldOrGoChoice ); - let waiting_for_confirm = state.waiting_for_confirm; - let pause_reason = state.pause_reason.clone(); // ── Hovered jan moves (shown as arrows on the board) ────────────────────── let hovered_jan_moves: RwSignal> = RwSignal::new(vec![]); @@ -89,8 +85,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let cmd_tx = use_context::>() .expect("UnboundedSender not found in context"); - let pending = use_context::>>() - .expect("pending not found in context"); let cmd_tx_effect = cmd_tx.clone(); Effect::new(move |_| { let moves = staged_moves.get(); @@ -109,29 +103,16 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { } }); - // ── Auto-roll effect ───────────────────────────────────────────────────── - // GameScreen is fully re-mounted on every ViewState update (state is a - // plain prop, not a signal), so this effect fires exactly once per - // RollDice phase entry and will not double-send. - // Guard: suppressed while waiting_for_confirm — the AfterOpponentMove - // buffered state shows the human's RollDice turn but the auto-roll must - // wait until the buffer is drained and the live screen state is shown. - let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice; - if show_roll && !waiting_for_confirm { - let cmd_tx_auto = cmd_tx.clone(); - Effect::new(move |_| { - cmd_tx_auto.unbounded_send(NetCommand::Action(PlayerAction::Roll)).ok(); - }); - } - let dice = vs.dice; let show_dice = dice != (0, 0); // ── Button senders ───────────────────────────────────────────────────────── + let cmd_tx_roll = cmd_tx.clone(); let cmd_tx_go = cmd_tx.clone(); let cmd_tx_quit = cmd_tx.clone(); let cmd_tx_end_quit = cmd_tx.clone(); let cmd_tx_end_replay = cmd_tx.clone(); + let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice; let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice; // ── Valid move sequences for this turn ───────────────────────────────────── @@ -216,13 +197,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { // Status message
{move || { - if let Some(ref reason) = pause_reason { - return String::from(match reason { - PauseReason::AfterOpponentRoll => t_string!(i18n, after_opponent_roll), - PauseReason::AfterOpponentGo => t_string!(i18n, after_opponent_go), - PauseReason::AfterOpponentMove => t_string!(i18n, after_opponent_move), - }); - } let n = staged_moves.get().len(); if is_move_stage { t_string!(i18n, select_move, n = n + 1) @@ -258,10 +232,10 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { // Action buttons
- {waiting_for_confirm.then(|| view! { + {show_roll.then(|| view! { + cmd_tx_roll.unbounded_send(NetCommand::Action(PlayerAction::Roll)).ok(); + }>{t!(i18n, roll_dice)} })} {show_hold_go.then(|| view! {