diff --git a/rust/cli_player/src/client.rs b/rust/cli_player/src/client.rs index 4c79094..e39db11 100644 --- a/rust/cli_player/src/client.rs +++ b/rust/cli_player/src/client.rs @@ -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>; + /// 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>, + sink: SplitSink, + receiver: mpsc::Receiver, } 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 { + async fn recv_next_msg(stream: &mut SplitStream) -> Res { 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> { + 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 { + Ok(self.receiver.recv()?) + } } diff --git a/rust/cli_player/src/main.rs b/rust/cli_player/src/main.rs index 80e5307..4a7651a 100644 --- a/rust/cli_player/src/main.rs +++ b/rust/cli_player/src/main.rs @@ -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,15 @@ 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::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( @@ -66,10 +71,39 @@ async fn run_dev( async fn run_app(terminal: &mut Terminal) -> Result<(), Box> { 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 + 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(()), + } } } diff --git a/rust/cli_player/src/ui_screens/game_screen.rs b/rust/cli_player/src/ui_screens/game_screen.rs new file mode 100644 index 0000000..0f3d116 --- /dev/null +++ b/rust/cli_player/src/ui_screens/game_screen.rs @@ -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( + mut self, + terminal: &mut Terminal, + ) -> io::Result { + 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(&mut self, f: &mut Frame) {} +} diff --git a/rust/cli_player/src/ui_screens/mod.rs b/rust/cli_player/src/ui_screens/mod.rs index dd0f2df..88e6690 100644 --- a/rust/cli_player/src/ui_screens/mod.rs +++ b/rust/cli_player/src/ui_screens/mod.rs @@ -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 { +pub enum ScreenResult { Ok(E), Canceled, }