fix(client_web): end game
This commit is contained in:
parent
3f8e451974
commit
8fd5b87c95
8 changed files with 103 additions and 9 deletions
|
|
@ -44,6 +44,7 @@ input[type="text"] {
|
||||||
.btn:disabled { opacity: 0.4; cursor: default; }
|
.btn:disabled { opacity: 0.4; cursor: default; }
|
||||||
.btn-primary { background: #3a6b3a; color: #fff; }
|
.btn-primary { background: #3a6b3a; color: #fff; }
|
||||||
.btn-secondary { background: #5a4a2a; color: #fff; }
|
.btn-secondary { background: #5a4a2a; color: #fff; }
|
||||||
|
.btn-bot { background: #2a5a7a; color: #fff; }
|
||||||
.btn:not(:disabled):hover { opacity: 0.85; }
|
.btn:not(:disabled):hover { opacity: 0.85; }
|
||||||
|
|
||||||
/* ── Game container ─────────────────────────────────────────────────── */
|
/* ── Game container ─────────────────────────────────────────────────── */
|
||||||
|
|
@ -253,6 +254,45 @@ input[type="text"] {
|
||||||
.jan-moves.hidden { display: none; }
|
.jan-moves.hidden { display: none; }
|
||||||
.jan-move-line { font-family: monospace; font-size: 0.8rem; color: #444; }
|
.jan-move-line { font-family: monospace; font-size: 0.8rem; color: #444; }
|
||||||
|
|
||||||
|
/* ── Game-over overlay ──────────────────────────────────────────────── */
|
||||||
|
.game-over-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over-box {
|
||||||
|
background: #f5edd8;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem 2.5rem;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over-box h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over-winner {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3a6b3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Board ──────────────────────────────────────────────────────────── */
|
/* ── Board ──────────────────────────────────────────────────────────── */
|
||||||
.board {
|
.board {
|
||||||
background: #2e6b2e;
|
background: #2e6b2e;
|
||||||
|
|
|
||||||
|
|
@ -35,5 +35,8 @@
|
||||||
"jan_contre_mezeas": "Contre mezeas",
|
"jan_contre_mezeas": "Contre mezeas",
|
||||||
"jan_helpless_man": "Helpless man",
|
"jan_helpless_man": "Helpless man",
|
||||||
"play_vs_bot": "Play vs Bot",
|
"play_vs_bot": "Play vs Bot",
|
||||||
"vs_bot_label": "vs Bot"
|
"vs_bot_label": "vs Bot",
|
||||||
|
"you_win": "You win!",
|
||||||
|
"opp_wins": "{{ name }} wins!",
|
||||||
|
"play_again": "Play again"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,5 +35,8 @@
|
||||||
"jan_contre_mezeas": "Contre mezeas",
|
"jan_contre_mezeas": "Contre mezeas",
|
||||||
"jan_helpless_man": "Dame impuissante",
|
"jan_helpless_man": "Dame impuissante",
|
||||||
"play_vs_bot": "Jouer contre le bot",
|
"play_vs_bot": "Jouer contre le bot",
|
||||||
"vs_bot_label": "contre le bot"
|
"vs_bot_label": "contre le bot",
|
||||||
|
"you_win": "Vous gagnez !",
|
||||||
|
"opp_wins": "{{ name }} gagne !",
|
||||||
|
"play_again": "Rejouer"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,10 @@ pub fn App() -> impl IntoView {
|
||||||
};
|
};
|
||||||
|
|
||||||
if remote_config.is_none() {
|
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 });
|
screen.set(Screen::Login { error: None });
|
||||||
continue;
|
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(
|
async fn run_local_bot_game(
|
||||||
screen: RwSignal<Screen>,
|
screen: RwSignal<Screen>,
|
||||||
cmd_rx: &mut futures::channel::mpsc::UnboundedReceiver<NetCommand>,
|
cmd_rx: &mut futures::channel::mpsc::UnboundedReceiver<NetCommand>,
|
||||||
) {
|
) -> bool {
|
||||||
let mut backend = TrictracBackend::new(0);
|
let mut backend = TrictracBackend::new(0);
|
||||||
backend.player_arrival(0);
|
backend.player_arrival(0);
|
||||||
backend.player_arrival(1);
|
backend.player_arrival(1);
|
||||||
|
|
@ -284,7 +288,8 @@ async fn run_local_bot_game(
|
||||||
Some(NetCommand::Action(action)) => {
|
Some(NetCommand::Action(action)) => {
|
||||||
backend.inform_rpc(0, action);
|
backend.inform_rpc(0, action);
|
||||||
}
|
}
|
||||||
_ => break,
|
Some(NetCommand::PlayVsBot) => return true,
|
||||||
|
_ => return false,
|
||||||
}
|
}
|
||||||
|
|
||||||
drain_and_update(&mut backend, &mut vs, screen);
|
drain_and_update(&mut backend, &mut vs, screen);
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
let cmd_tx_roll = cmd_tx.clone();
|
let cmd_tx_roll = cmd_tx.clone();
|
||||||
let cmd_tx_go = cmd_tx.clone();
|
let cmd_tx_go = cmd_tx.clone();
|
||||||
let cmd_tx_quit = 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_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice;
|
||||||
let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice;
|
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 room_id = state.room_id.clone();
|
||||||
let is_bot_game = state.is_bot_game;
|
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! {
|
view! {
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
// ── Top bar ──────────────────────────────────────────────────────
|
// ── Top bar ──────────────────────────────────────────────────────
|
||||||
|
|
@ -219,6 +226,33 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
|
||||||
|
|
||||||
// ── Player score (below board) ────────────────────────────────────
|
// ── Player score (below board) ────────────────────────────────────
|
||||||
<PlayerScorePanel score=my_score jans=my_jans is_you=true />
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,12 @@ impl TrictracBackend {
|
||||||
/// (MarkPoints, MarkAdvPoints).
|
/// (MarkPoints, MarkAdvPoints).
|
||||||
fn drive_automatic_stages(&mut self) {
|
fn drive_automatic_stages(&mut self) {
|
||||||
loop {
|
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;
|
let player_id = self.game.active_player_id;
|
||||||
match self.game.turn_stage {
|
match self.game.turn_stage {
|
||||||
TurnStage::MarkPoints | TurnStage::MarkAdvPoints => {
|
TurnStage::MarkPoints | TurnStage::MarkAdvPoints => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use rand::prelude::IndexedRandom;
|
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;
|
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.
|
/// 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> {
|
pub fn bot_decide(game: &GameState) -> Option<PlayerAction> {
|
||||||
|
if game.stage == Stage::Ended {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
if game.active_player_id != GUEST_PLAYER_ID {
|
if game.active_player_id != GUEST_PLAYER_ID {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -890,7 +890,7 @@ impl GameState {
|
||||||
return Err("No active player".into());
|
return Err("No active player".into());
|
||||||
};
|
};
|
||||||
debug!("new hole -> {holes_count:?}");
|
debug!("new hole -> {holes_count:?}");
|
||||||
if holes_count > 12 {
|
if holes_count >= 12 {
|
||||||
self.stage = Stage::Ended;
|
self.stage = Stage::Ended;
|
||||||
} else {
|
} else {
|
||||||
self.turn_stage = TurnStage::HoldOrGoChoice;
|
self.turn_stage = TurnStage::HoldOrGoChoice;
|
||||||
|
|
@ -907,7 +907,7 @@ impl GameState {
|
||||||
let Some(holes) = self.get_active_player().map(|p| p.holes) else {
|
let Some(holes) = self.get_active_player().map(|p| p.holes) else {
|
||||||
return Err("No active player".into());
|
return Err("No active player".into());
|
||||||
};
|
};
|
||||||
if holes > 12 {
|
if holes >= 12 {
|
||||||
self.stage = Stage::Ended;
|
self.stage = Stage::Ended;
|
||||||
} else {
|
} else {
|
||||||
self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints {
|
self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints {
|
||||||
|
|
@ -946,7 +946,7 @@ impl GameState {
|
||||||
} else {
|
} else {
|
||||||
// The player has moved, we can mark its opponent's points (which is now the current player)
|
// The player has moved, we can mark its opponent's points (which is now the current player)
|
||||||
let new_hole = self.mark_points(self.active_player_id, self.dice_points.1);
|
let new_hole = self.mark_points(self.active_player_id, self.dice_points.1);
|
||||||
if new_hole && self.get_active_player().map(|p| p.holes).unwrap_or(0) > 12 {
|
if new_hole && self.get_active_player().map(|p| p.holes).unwrap_or(0) >= 12 {
|
||||||
self.stage = Stage::Ended;
|
self.stage = Stage::Ended;
|
||||||
}
|
}
|
||||||
TurnStage::RollDice
|
TurnStage::RollDice
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue