Query boats layout

This commit is contained in:
Pierre HUBERT 2022-10-15 13:19:33 +02:00
parent a9f29e24fe
commit 375127eeee
5 changed files with 155 additions and 9 deletions

View File

@ -14,6 +14,16 @@ use crate::ui_screens::utils::centered_rect_size;
use crate::ui_screens::ScreenResult; use crate::ui_screens::ScreenResult;
use crate::ui_widgets::button_widget::ButtonWidget; use crate::ui_widgets::button_widget::ButtonWidget;
/// Convenience function to ask for user confirmation
pub fn confirm<B: Backend>(terminal: &mut Terminal<B>, msg: &str) -> bool {
matches!(
ConfirmDialogScreen::new(msg)
.show(terminal)
.unwrap_or(ScreenResult::Canceled),
ScreenResult::Ok(true)
)
}
pub struct ConfirmDialogScreen<'a> { pub struct ConfirmDialogScreen<'a> {
title: &'a str, title: &'a str,
msg: &'a str, msg: &'a str,

View File

@ -1,49 +1,159 @@
use std::io;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crossterm::event; use crossterm::event;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
use sea_battle_backend::utils::Res;
use tui::backend::Backend; use tui::backend::Backend;
use tui::{Frame, Terminal}; use tui::{Frame, Terminal};
use crate::client::Client; use crate::client::Client;
use crate::constants::*; 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::ScreenResult; use crate::ui_screens::ScreenResult;
enum GameStatus {
Pending,
WaitingForOpponentBoatsConfig,
OpponentReady,
Starting,
}
pub struct GameScreen { pub struct GameScreen {
client: Client, client: Client,
status: GameStatus,
opponent_name: Option<String>,
} }
impl GameScreen { impl GameScreen {
pub fn new(client: Client) -> Self { pub fn new(client: Client) -> Self {
Self { client } Self {
client,
status: GameStatus::Pending,
opponent_name: None,
}
} }
pub async fn show<B: Backend>( pub async fn show<B: Backend>(mut self, terminal: &mut Terminal<B>) -> Res<ScreenResult> {
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
// Update UI
terminal.draw(|f| self.ui(f))?; terminal.draw(|f| self.ui(f))?;
let timeout = TICK_RATE let timeout = TICK_RATE
.checked_sub(last_tick.elapsed()) .checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0)); .unwrap_or_else(|| Duration::from_secs(0));
// Handle terminal events
if crossterm::event::poll(timeout)? { if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
KeyCode::Char('q') => return Ok(ScreenResult::Canceled), // Leave game
KeyCode::Char('q')
if confirm(terminal, "Do you really want to leave game?") =>
{
return Ok(ScreenResult::Canceled);
}
_ => {} _ => {}
} }
} }
} }
// Handle incoming messages
while let Some(msg) = self.client.try_recv_next_message().await? {
match msg {
ServerMessage::SetInviteCode { .. } => unimplemented!(),
ServerMessage::InvalidInviteCode => unimplemented!(),
ServerMessage::WaitingForAnotherPlayer => unimplemented!(),
ServerMessage::OpponentConnected => unimplemented!(),
ServerMessage::SetOpponentName { name } => self.opponent_name = Some(name),
ServerMessage::QueryBoatsLayout { rules } => {
match SetBoatsLayoutScreen::new(&rules)
.set_confirm_on_cancel(true)
.show(terminal)?
{
ScreenResult::Ok(layout) => {
self.client
.send_message(&ClientMessage::BoatsLayout { layout })
.await?
}
ScreenResult::Canceled => {
return Ok(ScreenResult::Canceled);
}
};
}
ServerMessage::RejectedBoatsLayout { .. } => {
PopupScreen::new("Server rejected boats layout!! (is your version of SeaBattle up to date?)")
.show(terminal)?;
}
ServerMessage::WaitingForOtherPlayerConfiguration => {
self.status = GameStatus::WaitingForOpponentBoatsConfig;
}
ServerMessage::OpponentReady => {
self.status = GameStatus::OpponentReady;
}
ServerMessage::GameStarting => {
self.status = GameStatus::Starting;
}
ServerMessage::OpponentMustFire { .. } => {}
ServerMessage::RequestFire { .. } => {}
ServerMessage::FireResult { .. } => {}
ServerMessage::OpponentFireResult { .. } => {}
ServerMessage::LostGame { .. } => {}
ServerMessage::WonGame { .. } => {}
ServerMessage::OpponentRequestedRematch => {}
ServerMessage::OpponentAcceptedRematch => {}
ServerMessage::OpponentRejectedRematch => {}
ServerMessage::OpponentLeftGame => {}
ServerMessage::OpponentReplacedByBot => {}
}
}
if last_tick.elapsed() >= TICK_RATE { if last_tick.elapsed() >= TICK_RATE {
last_tick = Instant::now(); last_tick = Instant::now();
} }
} }
} }
fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {} fn opponent_name(&self) -> &str {
self.opponent_name.as_deref().unwrap_or("opponent")
}
fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
// If game is still starting
if matches!(self.status, GameStatus::Pending) {
PopupScreen::new("Game is pending...").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);
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;
}
// If game is still starting
if matches!(self.status, GameStatus::Starting) {
PopupScreen::new("Game is starting...").show_in_frame(f);
return;
}
}
} }

View File

@ -67,6 +67,12 @@ impl<'a> PopupScreen<'a> {
Ok(()) Ok(())
} }
/// Show message once message in a frame, without polling messages
pub fn show_in_frame<B: Backend>(mut self, frame: &mut Frame<B>) {
self.can_close = false;
self.ui(frame)
}
fn ui<B: Backend>(&mut self, f: &mut Frame<B>) { fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
// Preprocess message // Preprocess message
let lines = textwrap::wrap(self.msg, f.size().width as usize - 20); let lines = textwrap::wrap(self.msg, f.size().width as usize - 20);

View File

@ -13,6 +13,7 @@ use tui::{Frame, Terminal};
use sea_battle_backend::data::*; use sea_battle_backend::data::*;
use crate::constants::*; use crate::constants::*;
use crate::ui_screens::confirm_dialog::confirm;
use crate::ui_screens::utils::{centered_rect_size, centered_text}; use crate::ui_screens::utils::{centered_rect_size, centered_text};
use crate::ui_screens::ScreenResult; use crate::ui_screens::ScreenResult;
use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget}; use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget};
@ -23,6 +24,7 @@ pub struct SetBoatsLayoutScreen<'a> {
curr_boat: usize, curr_boat: usize,
layout: BoatsLayout, layout: BoatsLayout,
rules: &'a GameRules, rules: &'a GameRules,
confirm_on_cancel: bool,
} }
impl<'a> SetBoatsLayoutScreen<'a> { impl<'a> SetBoatsLayoutScreen<'a> {
@ -32,9 +34,16 @@ impl<'a> SetBoatsLayoutScreen<'a> {
layout: BoatsLayout::gen_random_for_rules(rules) layout: BoatsLayout::gen_random_for_rules(rules)
.expect("Failed to generate initial boats layout"), .expect("Failed to generate initial boats layout"),
rules, rules,
confirm_on_cancel: false,
} }
} }
/// Specify whether user should confirm his choice to cancel screen or not
pub fn set_confirm_on_cancel(mut self, confirm: bool) -> Self {
self.confirm_on_cancel = confirm;
self
}
pub fn show<B: Backend>( pub fn show<B: Backend>(
mut self, mut self,
terminal: &mut Terminal<B>, terminal: &mut Terminal<B>,
@ -56,7 +65,13 @@ impl<'a> SetBoatsLayoutScreen<'a> {
let event = event::read()?; let event = event::read()?;
if let Event::Key(key) = &event { if let Event::Key(key) = &event {
match key.code { match key.code {
KeyCode::Char('q') => return Ok(ScreenResult::Canceled), KeyCode::Char('q') => {
if !self.confirm_on_cancel
|| confirm(terminal, "Do you really want to quit?")
{
return Ok(ScreenResult::Canceled);
}
}
// Select next boat // Select next boat
KeyCode::Char('n') => self.curr_boat += self.layout.number_of_boats() - 1, KeyCode::Char('n') => self.curr_boat += self.layout.number_of_boats() - 1,

View File

@ -26,6 +26,7 @@ pub enum StartMode {
PlayRandom, PlayRandom,
} }
/// The messages a client could send to the server
#[derive(serde::Deserialize, serde::Serialize, Debug)] #[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ClientMessage { pub enum ClientMessage {
@ -37,6 +38,10 @@ pub enum ClientMessage {
RejectRematch, RejectRematch,
} }
/// The list of messages that can be sent from the server to the client
///
/// Messages types are ordered in the enum in a "kind of" chronogical order: most messages should be
/// sent only if the messages type below it have not already been sent.
#[derive(Message)] #[derive(Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]