From 26d5f85c3ce733bd8050d54dc01936ff196c2ce1 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 15 Oct 2022 14:46:10 +0200 Subject: [PATCH] Display maps on play --- rust/cli_player/src/ui_screens/game_screen.rs | 184 ++++++++++++++++-- 1 file changed, 163 insertions(+), 21 deletions(-) diff --git a/rust/cli_player/src/ui_screens/game_screen.rs b/rust/cli_player/src/ui_screens/game_screen.rs index f17ff2f..ea355bf 100644 --- a/rust/cli_player/src/ui_screens/game_screen.rs +++ b/rust/cli_player/src/ui_screens/game_screen.rs @@ -1,10 +1,15 @@ +use std::cmp::max; use std::time::{Duration, Instant}; use crossterm::event; use crossterm::event::{Event, KeyCode}; +use sea_battle_backend::data::{CurrentGameMapStatus, CurrentGameStatus}; use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage}; use sea_battle_backend::utils::Res; use tui::backend::Backend; +use tui::layout::{Constraint, Direction, Layout}; +use tui::style::Color; +use tui::widgets::Paragraph; use tui::{Frame, Terminal}; use crate::client::Client; @@ -12,19 +17,44 @@ use crate::constants::*; use crate::ui_screens::confirm_dialog::confirm; use crate::ui_screens::popup_screen::PopupScreen; use crate::ui_screens::set_boats_layout::SetBoatsLayoutScreen; +use crate::ui_screens::utils::{centered_rect_size, centered_text}; use crate::ui_screens::ScreenResult; +use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget}; +#[derive(Eq, PartialEq)] enum GameStatus { Pending, WaitingForOpponentBoatsConfig, OpponentReady, Starting, + MustFire, + OpponentMustFire, +} + +impl GameStatus { + pub fn can_show_game_maps(&self) -> bool { + self != &GameStatus::Pending + && self != &GameStatus::WaitingForOpponentBoatsConfig + && self != &GameStatus::OpponentReady + } + + pub fn status_text(&self) -> &str { + match self { + GameStatus::Pending => "Game is pending...", + GameStatus::WaitingForOpponentBoatsConfig => "Waiting for ### boats configuration", + GameStatus::OpponentReady => "### is ready!", + GameStatus::Starting => "Game is starting...", + GameStatus::MustFire => "You must fire!", + GameStatus::OpponentMustFire => "### must fire!", + } + } } pub struct GameScreen { client: Client, status: GameStatus, opponent_name: Option, + game_status: CurrentGameStatus, } impl GameScreen { @@ -33,6 +63,7 @@ impl GameScreen { client, status: GameStatus::Pending, opponent_name: None, + game_status: Default::default(), } } @@ -68,6 +99,7 @@ impl GameScreen { ServerMessage::InvalidInviteCode => unimplemented!(), ServerMessage::WaitingForAnotherPlayer => unimplemented!(), ServerMessage::OpponentConnected => unimplemented!(), + ServerMessage::SetOpponentName { name } => self.opponent_name = Some(name), ServerMessage::QueryBoatsLayout { rules } => { @@ -103,10 +135,19 @@ impl GameScreen { self.status = GameStatus::Starting; } - ServerMessage::OpponentMustFire { .. } => {} - ServerMessage::RequestFire { .. } => {} + ServerMessage::OpponentMustFire { status } => { + self.status = GameStatus::OpponentMustFire; + self.game_status = status; + } + + ServerMessage::RequestFire { status } => { + self.status = GameStatus::MustFire; + self.game_status = status; + } + ServerMessage::FireResult { .. } => {} ServerMessage::OpponentFireResult { .. } => {} + ServerMessage::LostGame { .. } => {} ServerMessage::WonGame { .. } => {} ServerMessage::OpponentRequestedRematch => {} @@ -123,37 +164,138 @@ impl GameScreen { } } + fn can_fire(&self) -> bool { + matches!(self.status, GameStatus::MustFire) + } + fn opponent_name(&self) -> &str { self.opponent_name.as_deref().unwrap_or("opponent") } + fn player_map(&self, map: &CurrentGameMapStatus) -> GameMapWidget { + // Sunk boats + let sunk_boats = ColoredCells { + color: Color::Red, + cells: map + .sunk_boats + .iter() + .flat_map(|b| b.all_coordinates()) + .collect::>(), + }; + + // Touched boats + let touched_areas = ColoredCells { + color: Color::Rgb(245, 45, 7), + cells: map.successful_strikes.clone(), + }; + + // Failed strikes + let failed_strikes = ColoredCells { + color: Color::Gray, + cells: map.failed_strikes.clone(), + }; + + // Boats + let boats = ColoredCells { + color: Color::Blue, + cells: map + .boats + .0 + .iter() + .flat_map(|b| b.all_coordinates()) + .collect::>(), + }; + + GameMapWidget::new(&self.game_status.rules) + .add_colored_cells(sunk_boats) + .add_colored_cells(touched_areas) + .add_colored_cells(failed_strikes) + .add_colored_cells(boats) + } + fn ui(&mut self, f: &mut Frame) { - // If game is still starting - if matches!(self.status, GameStatus::Pending) { - PopupScreen::new("Game is pending...").show_in_frame(f); + let status_text = self + .status + .status_text() + .replace("###", self.opponent_name()); + + // If the game is in a state where game maps can not be shown + if !self.status.can_show_game_maps() { + PopupScreen::new(&status_text).show_in_frame(f); return; } - // If game is still starting - if matches!(self.status, GameStatus::WaitingForOpponentBoatsConfig) { - PopupScreen::new(&format!( - "Waiting for boats configuration of {}...", - self.opponent_name() - )) - .show_in_frame(f); + // Draw main ui (default play UI) + let player_map = self + .player_map(&self.game_status.your_map) + .set_title("YOUR map"); + + let mut opponent_map = self + .player_map(&self.game_status.opponent_map) + .set_title(self.opponent_name()); + + if self.can_fire() { + opponent_map = opponent_map + .set_legend("Use arrows + enter\nor click on the place\nwhere you want\nto shoot"); + } + + // 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(); + let both_maps_width = player_map_size.0 + opponent_map_size.0 + 3; + let show_both_maps = both_maps_width <= f.size().width; + + let maps_height = max(player_map_size.1, opponent_map_size.1); + let maps_width = match show_both_maps { + true => both_maps_width, + false => max(player_map_size.0, opponent_map_size.0), + }; + + let max_width = max(maps_width, status_text.len() as u16); + let total_height = 3 + maps_height + 3; + + // Check if frame is too small + if max_width > f.size().width || total_height > f.size().height { + PopupScreen::new("Screen too small!").show_in_frame(f); return; } - // If game is still starting - if matches!(self.status, GameStatus::OpponentReady) { - PopupScreen::new(&format!("{} is ready!", self.opponent_name())).show_in_frame(f); - return; + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Length(maps_height), + Constraint::Length(3), + ]) + .split(centered_rect_size(max_width, total_height, &f.size())); + + // Render status + let paragraph = Paragraph::new(status_text.as_str()); + f.render_widget(paragraph, centered_text(&status_text, &chunks[0])); + + // Render maps + if show_both_maps { + let maps_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(player_map_size.0), + Constraint::Length(3), + Constraint::Length(opponent_map_size.0), + ]) + .split(chunks[1]); + + f.render_widget(player_map, maps_chunks[0]); + f.render_widget(opponent_map, maps_chunks[2]); + } else { + // Render a single map + if self.can_fire() { + f.render_widget(opponent_map, chunks[1]); + } else { + f.render_widget(player_map, chunks[1]); + } } - // If game is still starting - if matches!(self.status, GameStatus::Starting) { - PopupScreen::new("Game is starting...").show_in_frame(f); - return; - } + // Render buttons + // TODO : at the end of the game } }