Compare commits

..

2 Commits

Author SHA1 Message Date
a9f29e24fe Show popup message while connecting to server 2022-10-15 11:54:57 +02:00
19993c560a Ready to implement game screen 2022-10-15 11:45:45 +02:00
6 changed files with 167 additions and 14 deletions

View File

@@ -1,20 +1,26 @@
use crate::cli_args::cli_args;
use crate::server;
use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use sea_battle_backend::data::GameRules;
use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
use sea_battle_backend::server::BotPlayQuery;
use sea_battle_backend::utils::{boxed_error, Res};
use std::sync::mpsc;
use std::sync::mpsc::TryRecvError;
use tokio::net::TcpStream;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
/// Connection client
///
/// This structure acts as a wrapper around websocket connection that handles automatically parsing
/// of incoming messages and encoding of outgoing messages
pub struct Client {
socket: WebSocketStream<MaybeTlsStream<TcpStream>>,
sink: SplitSink<WsStream, Message>,
receiver: mpsc::Receiver<ServerMessage>,
}
impl Client {
@@ -44,15 +50,36 @@ impl Client {
url.push_str(uri);
log::debug!("Connecting to {}", url);
// Connect to websocket & split streams
let (socket, _) = tokio_tungstenite::connect_async(url).await?;
let (sink, mut stream) = socket.split();
Ok(Self { socket })
// Receive server message on a separate task
let (sender, receiver) = mpsc::channel();
tokio::task::spawn(async move {
loop {
match Self::recv_next_msg(&mut stream).await {
Ok(msg) => {
if let Err(e) = sender.send(msg.clone()) {
log::error!("Failed to forward ws message! {} (msg={:?})", e, msg);
break;
}
}
Err(e) => {
log::error!("Failed receive next message from websocket! {}", e);
break;
}
}
}
});
Ok(Self { sink, receiver })
}
/// Receive next message from stream
async fn recv_next_msg(&mut self) -> Res<ServerMessage> {
async fn recv_next_msg(stream: &mut SplitStream<WsStream>) -> Res<ServerMessage> {
loop {
let chunk = match self.socket.next().await {
let chunk = match stream.next().await {
None => return Err(boxed_error("No more message in queue!")),
Some(d) => d,
};
@@ -87,9 +114,23 @@ impl Client {
/// Send a message through the stream
pub async fn send_message(&mut self, msg: &ClientMessage) -> Res {
self.socket
self.sink
.send(Message::Text(serde_json::to_string(&msg)?))
.await?;
Ok(())
}
/// Try to receive next message from websocket, in a non-blocking way
pub async fn try_recv_next_message(&self) -> Res<Option<ServerMessage>> {
match self.receiver.try_recv() {
Ok(msg) => Ok(Some(msg)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => Err(boxed_error("Receiver channel disconnected!")),
}
}
/// Block until the next message from websocket is availabl
pub async fn recv_next_message(&self) -> Res<ServerMessage> {
Ok(self.receiver.recv()?)
}
}

View File

@@ -1,3 +1,5 @@
extern crate core;
pub mod cli_args;
pub mod client;
pub mod constants;

View File

@@ -2,7 +2,6 @@ use std::error::Error;
use std::io;
use std::io::ErrorKind;
use cli_player::cli_args::{cli_args, TestDevScreen};
use crossterm::event::DisableMouseCapture;
use crossterm::event::EnableMouseCapture;
use crossterm::execute;
@@ -13,9 +12,16 @@ use env_logger::Env;
use tui::backend::{Backend, CrosstermBackend};
use tui::Terminal;
use cli_player::cli_args::{cli_args, TestDevScreen};
use cli_player::client::Client;
use cli_player::server::start_server_if_missing;
use cli_player::ui_screens::configure_game_rules::GameRulesConfigurationScreen;
use cli_player::ui_screens::game_screen::GameScreen;
use cli_player::ui_screens::popup_screen::PopupScreen;
use cli_player::ui_screens::select_play_mode::{SelectPlayModeResult, SelectPlayModeScreen};
use cli_player::ui_screens::*;
use sea_battle_backend::data::GameRules;
use sea_battle_backend::human_player_ws::ServerMessage;
/// Test code screens
async fn run_dev<B: Backend>(
@@ -66,10 +72,40 @@ async fn run_dev<B: Backend>(
async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn Error>> {
if let Some(d) = cli_args().dev_screen {
run_dev(terminal, d).await
} else {
// TODO : run app
Ok(())
return run_dev(terminal, d).await;
}
let mut rules = GameRules::default();
loop {
match SelectPlayModeScreen::default().show(terminal)? {
// TODO : Play against random player
ScreenResult::Ok(SelectPlayModeResult::PlayRandom) => todo!(),
// Play against bot
ScreenResult::Ok(SelectPlayModeResult::PlayAgainstBot) => {
// First, ask for custom rules
rules = match GameRulesConfigurationScreen::new(rules.clone()).show(terminal)? {
ScreenResult::Ok(r) => r,
ScreenResult::Canceled => continue,
};
// Then connect to server
PopupScreen::new("Connecting...").show_once(terminal)?;
let client = Client::start_bot_play(&rules).await?;
// Wait for the server to become ready
while !matches!(
client.recv_next_message().await?,
ServerMessage::OpponentConnected
) {}
// Display game screen
GameScreen::new(client).show(terminal).await?;
}
ScreenResult::Canceled | ScreenResult::Ok(SelectPlayModeResult::Exit) => return Ok(()),
}
}
}

View File

@@ -0,0 +1,49 @@
use std::io;
use std::time::{Duration, Instant};
use crossterm::event;
use crossterm::event::{Event, KeyCode};
use tui::backend::Backend;
use tui::{Frame, Terminal};
use crate::client::Client;
use crate::constants::*;
use crate::ui_screens::ScreenResult;
pub struct GameScreen {
client: Client,
}
impl GameScreen {
pub fn new(client: Client) -> Self {
Self { client }
}
pub async fn show<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| self.ui(f))?;
let timeout = TICK_RATE
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
_ => {}
}
}
}
if last_tick.elapsed() >= TICK_RATE {
last_tick = Instant::now();
}
}
}
fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {}
}

View File

@@ -2,6 +2,7 @@ use std::fmt::Debug;
pub mod configure_game_rules;
pub mod confirm_dialog;
pub mod game_screen;
pub mod input_screen;
pub mod popup_screen;
pub mod select_bot_type;
@@ -10,7 +11,7 @@ pub mod set_boats_layout;
pub mod utils;
#[derive(Debug)]
pub enum ScreenResult<E> {
pub enum ScreenResult<E = ()> {
Ok(E),
Canceled,
}

View File

@@ -17,6 +17,7 @@ use crate::ui_widgets::button_widget::ButtonWidget;
pub struct PopupScreen<'a> {
title: &'a str,
msg: &'a str,
can_close: bool,
}
impl<'a> PopupScreen<'a> {
@@ -24,9 +25,15 @@ impl<'a> PopupScreen<'a> {
Self {
title: "Message",
msg,
can_close: true,
}
}
pub fn set_can_close(mut self, can_close: bool) -> Self {
self.can_close = can_close;
self
}
pub fn show<B: Backend>(mut self, terminal: &mut Terminal<B>) -> io::Result<ScreenResult<()>> {
let mut last_tick = Instant::now();
loop {
@@ -53,12 +60,27 @@ impl<'a> PopupScreen<'a> {
}
}
/// Show message once message, without polling messages
pub fn show_once<B: Backend>(mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
self.can_close = false;
terminal.draw(|f| self.ui(f))?;
Ok(())
}
fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
// Preprocess message
let lines = textwrap::wrap(self.msg, f.size().width as usize - 20);
let line_max_len = lines.iter().map(|l| l.len()).max().unwrap();
let area = centered_rect_size(line_max_len as u16 + 4, 5 + lines.len() as u16, &f.size());
let area = centered_rect_size(
line_max_len as u16 + 4,
match self.can_close {
// reserve space for button
true => 5,
false => 2,
} + lines.len() as u16,
&f.size(),
);
let block = Block::default().borders(Borders::ALL).title(self.title);
f.render_widget(block, area);
@@ -85,7 +107,9 @@ impl<'a> PopupScreen<'a> {
let paragraph = Paragraph::new(text);
f.render_widget(paragraph, chunks[0]);
let ok_button = ButtonWidget::new("OK", true);
f.render_widget(ok_button, chunks[1]);
if self.can_close {
let ok_button = ButtonWidget::new("OK", true);
f.render_widget(ok_button, chunks[1]);
}
}
}