fix(client_web): end game

This commit is contained in:
Henri Bourcereau 2026-03-30 22:29:34 +02:00
parent 3f8e451974
commit 8fd5b87c95
8 changed files with 103 additions and 9 deletions

View file

@ -170,7 +170,10 @@ pub fn App() -> impl IntoView {
};
if remote_config.is_none() {
run_local_bot_game(screen, &mut cmd_rx).await;
loop {
let restart = run_local_bot_game(screen, &mut cmd_rx).await;
if !restart { break; }
}
screen.set(Screen::Login { error: None });
continue;
}
@ -268,10 +271,11 @@ pub fn App() -> impl IntoView {
}
}
/// Runs one local bot game. Returns `true` if the player wants to play again.
async fn run_local_bot_game(
screen: RwSignal<Screen>,
cmd_rx: &mut futures::channel::mpsc::UnboundedReceiver<NetCommand>,
) {
) -> bool {
let mut backend = TrictracBackend::new(0);
backend.player_arrival(0);
backend.player_arrival(1);
@ -284,7 +288,8 @@ async fn run_local_bot_game(
Some(NetCommand::Action(action)) => {
backend.inform_rpc(0, action);
}
_ => break,
Some(NetCommand::PlayVsBot) => return true,
_ => return false,
}
drain_and_update(&mut backend, &mut vs, screen);

View file

@ -101,6 +101,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
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;
@ -117,6 +119,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
let room_id = state.room_id.clone();
let is_bot_game = state.is_bot_game;
// ── Game-over info ─────────────────────────────────────────────────────────
let stage_is_ended = stage == SerStage::Ended;
let winner_is_me = my_score.holes >= 12;
let opp_name_end = opp_score.name.clone();
view! {
<div class="game-container">
// ── Top bar ──────────────────────────────────────────────────────
@ -219,6 +226,33 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
// ── Player score (below board) ────────────────────────────────────
<PlayerScorePanel score=my_score jans=my_jans is_you=true />
// ── Game-over overlay ─────────────────────────────────────────────
{stage_is_ended.then(|| {
let winner_text = if winner_is_me {
t_string!(i18n, you_win).to_owned()
} else {
t_string!(i18n, opp_wins, name = opp_name_end.as_str())
};
view! {
<div class="game-over-overlay">
<div class="game-over-box">
<h2>{t!(i18n, game_over)}</h2>
<p class="game-over-winner">{winner_text}</p>
<div class="game-over-actions">
<button class="btn btn-secondary" on:click=move |_| {
cmd_tx_end_quit.unbounded_send(NetCommand::Disconnect).ok();
}>{t!(i18n, quit)}</button>
{is_bot_game.then(|| view! {
<button class="btn btn-primary" on:click=move |_| {
cmd_tx_end_replay.unbounded_send(NetCommand::PlayVsBot).ok();
}>{t!(i18n, play_again)}</button>
})}
</div>
</div>
</div>
}
})}
</div>
}
}

View file

@ -46,6 +46,12 @@ impl TrictracBackend {
/// (MarkPoints, MarkAdvPoints).
fn drive_automatic_stages(&mut self) {
loop {
// Stop if the game has already ended (stage transitions to Ended but
// turn_stage may still be MarkPoints when schools_enabled=false, which
// makes consume(Mark) a no-op and would cause an infinite loop).
if self.game.stage == trictrac_store::Stage::Ended {
break;
}
let player_id = self.game.active_player_id;
match self.game.turn_stage {
TurnStage::MarkPoints | TurnStage::MarkAdvPoints => {

View file

@ -1,5 +1,5 @@
use rand::prelude::IndexedRandom;
use trictrac_store::{CheckerMove, Color, GameState, MoveRules, TurnStage};
use trictrac_store::{CheckerMove, Color, GameState, MoveRules, Stage, TurnStage};
use crate::trictrac::types::PlayerAction;
@ -7,6 +7,9 @@ const GUEST_PLAYER_ID: u64 = 2;
/// Returns the next action for the bot (mp_player 1 / guest), or None if it is not the bot's turn.
pub fn bot_decide(game: &GameState) -> Option<PlayerAction> {
if game.stage == Stage::Ended {
return None;
}
if game.active_player_id != GUEST_PLAYER_ID {
return None;
}