From f2ec85b46f8f40e691747c3ec9a3973cbaecf12b Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 15 Oct 2022 17:46:14 +0200 Subject: [PATCH] Can request & respond to rematch --- rust/cli_player/src/ui_screens/game_screen.rs | 166 ++++++++++++++++-- .../src/ui_widgets/button_widget.rs | 8 +- .../src/data/current_game_status.rs | 6 + 3 files changed, 168 insertions(+), 12 deletions(-) diff --git a/rust/cli_player/src/ui_screens/game_screen.rs b/rust/cli_player/src/ui_screens/game_screen.rs index 60280de..453568b 100644 --- a/rust/cli_player/src/ui_screens/game_screen.rs +++ b/rust/cli_player/src/ui_screens/game_screen.rs @@ -21,6 +21,7 @@ use crate::ui_screens::popup_screen::PopupScreen; use crate::ui_screens::set_boats_layout_screen::SetBoatsLayoutScreen; use crate::ui_screens::utils::{centered_rect_size, centered_text}; use crate::ui_screens::ScreenResult; +use crate::ui_widgets::button_widget::ButtonWidget; use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget}; type CoordinatesMapper = HashMap; @@ -33,6 +34,13 @@ enum GameStatus { Starting, MustFire, OpponentMustFire, + WonGame, + LostGame, + RematchRequestedByOpponent, + RematchRequestedByPlayer, + RematchAccepted, + RematchRejected, + OpponentLeftGame, } impl GameStatus { @@ -50,6 +58,32 @@ impl GameStatus { GameStatus::Starting => "Game is starting...", GameStatus::MustFire => "You must fire!", GameStatus::OpponentMustFire => "### must fire!", + GameStatus::WonGame => "You won the game!", + GameStatus::LostGame => "### won the game. You loose.", + GameStatus::RematchRequestedByOpponent => "Rematch requested by ###", + GameStatus::RematchRequestedByPlayer => "Rematch requested by you", + GameStatus::RematchAccepted => "Rematch accepted!", + GameStatus::RematchRejected => "Rematch rejected!", + GameStatus::OpponentLeftGame => "Opponent left game!", + } + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum Buttons { + RequestRematch, + AcceptRematch, + RejectRematch, + QuitGame, +} + +impl Buttons { + pub fn text(&self) -> &str { + match self { + Buttons::RequestRematch => "Request rematch", + Buttons::AcceptRematch => "Accept rematch", + Buttons::RejectRematch => "Reject rematch", + Buttons::QuitGame => "Quit game", } } } @@ -61,6 +95,7 @@ pub struct GameScreen { game: CurrentGameStatus, curr_shoot_position: Coordinates, last_opponent_fire_position: Coordinates, + curr_button: usize, } impl GameScreen { @@ -72,6 +107,7 @@ impl GameScreen { game: Default::default(), curr_shoot_position: Coordinates::new(0, 0), last_opponent_fire_position: Coordinates::invalid(), + curr_button: 0, } } @@ -81,6 +117,10 @@ impl GameScreen { let mut coordinates_mapper = CoordinatesMapper::new(); loop { + if !self.visible_buttons().is_empty() { + self.curr_button %= self.visible_buttons().len(); + } + // Update UI terminal.draw(|f| coordinates_mapper = self.ui(f))?; @@ -121,6 +161,36 @@ impl GameScreen { } } + // Change buttons + KeyCode::Left if self.game_over() => { + self.curr_button += self.visible_buttons().len() - 1 + } + KeyCode::Right if self.game_over() => self.curr_button += 1, + KeyCode::Tab if self.game_over() => self.curr_button += 1, + + // Submit button + KeyCode::Enter if self.game_over() => match self.curr_button() { + Buttons::RequestRematch => { + self.client + .send_message(&ClientMessage::RequestRematch) + .await?; + self.status = GameStatus::RematchRequestedByPlayer; + } + Buttons::AcceptRematch => { + self.client + .send_message(&ClientMessage::AcceptRematch) + .await?; + self.status = GameStatus::RematchAccepted; + } + Buttons::RejectRematch => { + self.client + .send_message(&ClientMessage::RejectRematch) + .await?; + self.status = GameStatus::RematchRejected; + } + Buttons::QuitGame => return Ok(ScreenResult::Ok(())), + }, + _ => {} } @@ -194,19 +264,41 @@ impl GameScreen { self.game = status; } - ServerMessage::FireResult { .. } => {} + ServerMessage::FireResult { .. } => { /* not used */ } ServerMessage::OpponentFireResult { pos, .. } => { self.last_opponent_fire_position = pos; } - ServerMessage::LostGame { .. } => {} - ServerMessage::WonGame { .. } => {} - ServerMessage::OpponentRequestedRematch => {} - ServerMessage::OpponentAcceptedRematch => {} - ServerMessage::OpponentRejectedRematch => {} - ServerMessage::OpponentLeftGame => {} - ServerMessage::OpponentReplacedByBot => {} + ServerMessage::LostGame { status } => { + self.game = status; + self.status = GameStatus::LostGame; + } + + ServerMessage::WonGame { status } => { + self.game = status; + self.status = GameStatus::WonGame; + } + + ServerMessage::OpponentRequestedRematch => { + self.status = GameStatus::RematchRequestedByOpponent; + } + + ServerMessage::OpponentAcceptedRematch => { + self.status = GameStatus::RematchAccepted; + } + + ServerMessage::OpponentRejectedRematch => { + self.status = GameStatus::RematchRejected; + } + + ServerMessage::OpponentLeftGame => { + self.status = GameStatus::OpponentLeftGame; + } + + ServerMessage::OpponentReplacedByBot => { + PopupScreen::new("Opponent was replaced by a bot.").show(terminal)?; + } } } @@ -220,10 +312,37 @@ impl GameScreen { matches!(self.status, GameStatus::MustFire) } + fn game_over(&self) -> bool { + self.game.is_game_over() + } + + fn visible_buttons(&self) -> Vec { + let mut buttons = vec![]; + if self.game_over() && self.status != GameStatus::RematchAccepted { + // Respond to rematch request / quit + if self.status == GameStatus::RematchRequestedByOpponent { + buttons.push(Buttons::AcceptRematch); + buttons.push(Buttons::RejectRematch); + } else if self.status != GameStatus::OpponentLeftGame + && self.status != GameStatus::RematchRejected + { + buttons.push(Buttons::RequestRematch); + } + + buttons.push(Buttons::QuitGame); + } + + buttons + } + fn opponent_name(&self) -> &str { self.opponent_name.as_deref().unwrap_or("opponent") } + fn curr_button(&self) -> Buttons { + self.visible_buttons()[self.curr_button] + } + fn player_map(&self, map: &CurrentGameMapStatus, opponent_map: bool) -> GameMapWidget { let mut map_widget = GameMapWidget::new(&self.game.rules).set_default_empty_char(' '); @@ -341,6 +460,13 @@ impl GameScreen { .set_legend("Use arrows + Enter\nor click on the place\nwhere you want\nto shoot"); } + // Prepare buttons + let buttons = self + .visible_buttons() + .iter() + .map(|b| ButtonWidget::new(b.text(), self.curr_button() == *b)) + .collect::>(); + // Show both maps if there is enough room on the screen let player_map_size = player_map.estimated_size(); let opponent_map_size = opponent_map.estimated_size(); @@ -353,7 +479,9 @@ impl GameScreen { false => max(player_map_size.0, opponent_map_size.0), }; - let max_width = max(maps_width, status_text.len() as u16); + let buttons_width = buttons.iter().fold(0, |a, b| a + b.estimated_size().0 + 4); + + let max_width = max(maps_width, status_text.len() as u16).max(buttons_width); let total_height = 3 + maps_height + 3; // Check if frame is too small @@ -399,7 +527,25 @@ impl GameScreen { } // Render buttons - // TODO : at the end of the game + if !buttons.is_empty() { + let buttons_area = Layout::default() + .direction(Direction::Horizontal) + .constraints( + (0..buttons.len()) + .map(|_| Constraint::Percentage(100 / buttons.len() as u16)) + .collect::>(), + ) + .split(chunks[2]); + + for (idx, b) in buttons.into_iter().enumerate() { + let target = centered_rect_size( + b.estimated_size().0, + b.estimated_size().1, + &buttons_area[idx], + ); + f.render_widget(b, target); + } + } coordinates_mapper } diff --git a/rust/cli_player/src/ui_widgets/button_widget.rs b/rust/cli_player/src/ui_widgets/button_widget.rs index 4fa10d7..0e4ff3b 100644 --- a/rust/cli_player/src/ui_widgets/button_widget.rs +++ b/rust/cli_player/src/ui_widgets/button_widget.rs @@ -34,14 +34,18 @@ impl ButtonWidget { self.min_width = min_width; self } + + pub fn estimated_size(&self) -> (u16, u16) { + ((self.label.len() + 2).max(self.min_width) as u16, 1) + } } impl Widget for ButtonWidget { fn render(self, area: Rect, buf: &mut Buffer) { - let expected_len = (self.label.len() + 2).max(self.min_width); + let expected_len = self.estimated_size().0; let mut label = self.label.clone(); - while label.len() < expected_len { + while label.len() < expected_len as usize { label.insert(0, ' '); label.push(' '); } diff --git a/rust/sea_battle_backend/src/data/current_game_status.rs b/rust/sea_battle_backend/src/data/current_game_status.rs index f424b7f..d8b8d8d 100644 --- a/rust/sea_battle_backend/src/data/current_game_status.rs +++ b/rust/sea_battle_backend/src/data/current_game_status.rs @@ -300,6 +300,12 @@ impl CurrentGameStatus { BotType::Smart => self.find_smart_bot_fire_location(), } } + + /// Check out whether game is over or not + pub fn is_game_over(&self) -> bool { + self.opponent_map.sunk_boats.len() == self.rules.boats_list().len() + || self.your_map.sunk_boats.len() == self.rules.boats_list().len() + } } #[cfg(test)]