Query boats layout
This commit is contained in:
parent
a9f29e24fe
commit
375127eeee
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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)]
|
||||||
|
Loading…
Reference in New Issue
Block a user