diff --git a/rust/Cargo.lock b/rust/Cargo.lock index cef9a3b..a89f3d5 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -465,14 +465,18 @@ dependencies = [ "clap", "crossterm", "env_logger", + "futures", "lazy_static", "log", "num", "num-derive", "num-traits", "sea_battle_backend", + "serde_json", + "serde_urlencoded", "textwrap", "tokio", + "tokio-tungstenite", "tui", ] diff --git a/rust/cli_player/Cargo.toml b/rust/cli_player/Cargo.toml index 22cb182..d989c78 100644 --- a/rust/cli_player/Cargo.toml +++ b/rust/cli_player/Cargo.toml @@ -17,4 +17,8 @@ tokio = "1.21.2" num = "0.4.0" num-traits = "0.2.15" num-derive = "0.3.3" -textwrap = "0.15.1" \ No newline at end of file +textwrap = "0.15.1" +tokio-tungstenite = "0.17.2" +serde_urlencoded = "0.7.1" +futures = "0.3.23" +serde_json = "1.0.85" \ No newline at end of file diff --git a/rust/cli_player/src/cli_args.rs b/rust/cli_player/src/cli_args.rs index 95fb4cb..ec84993 100644 --- a/rust/cli_player/src/cli_args.rs +++ b/rust/cli_player/src/cli_args.rs @@ -41,6 +41,11 @@ impl CliArgs { .parse::() .expect("Failed to parse listen port!") } + + /// Get local server address + pub fn local_server_address(&self) -> String { + format!("http://localhost:{}", self.listen_port()) + } } lazy_static::lazy_static! { diff --git a/rust/cli_player/src/client.rs b/rust/cli_player/src/client.rs new file mode 100644 index 0000000..4c79094 --- /dev/null +++ b/rust/cli_player/src/client.rs @@ -0,0 +1,95 @@ +use crate::cli_args::cli_args; +use crate::server; +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 tokio::net::TcpStream; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::{MaybeTlsStream, 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>, +} + +impl Client { + /// Start to play against a bot + /// + /// When playing against a bot, local server is always used + pub async fn start_bot_play(rules: &GameRules) -> Res { + server::start_server_if_missing().await; + + Self::connect_url( + &cli_args().local_server_address(), + &format!( + "/play/bot?{}", + serde_urlencoded::to_string(&BotPlayQuery { + rules: rules.clone(), + player_name: "Human".to_string() + }) + .unwrap() + ), + ) + .await + } + + /// Do connect to a server, returning + async fn connect_url(server: &str, uri: &str) -> Res { + let mut url = server.replace("http", "ws"); + url.push_str(uri); + log::debug!("Connecting to {}", url); + + let (socket, _) = tokio_tungstenite::connect_async(url).await?; + + Ok(Self { socket }) + } + + /// Receive next message from stream + async fn recv_next_msg(&mut self) -> Res { + loop { + let chunk = match self.socket.next().await { + None => return Err(boxed_error("No more message in queue!")), + Some(d) => d, + }; + + match chunk? { + Message::Text(t) => { + log::debug!("TEXT Got a text message from server!"); + let msg: ServerMessage = serde_json::from_str(&t)?; + return Ok(msg); + } + Message::Binary(_) => { + log::debug!("BINARY Got an unexpected binary message"); + return Err(boxed_error("Received an unexpected binary message!")); + } + Message::Ping(_) => { + log::debug!("PING Got a ping message from server"); + } + Message::Pong(_) => { + log::debug!("PONG Got a pong message"); + } + Message::Close(_) => { + log::debug!("CLOSE Got a close websocket message"); + return Err(boxed_error("Server requested to close connection!")); + } + Message::Frame(_) => { + log::debug!("FRAME Got an unexpected frame from server!"); + return Err(boxed_error("Got an unexpected frame!")); + } + } + } + } + + /// Send a message through the stream + pub async fn send_message(&mut self, msg: &ClientMessage) -> Res { + self.socket + .send(Message::Text(serde_json::to_string(&msg)?)) + .await?; + Ok(()) + } +} diff --git a/rust/cli_player/src/lib.rs b/rust/cli_player/src/lib.rs index 849b5ab..b9fa78c 100644 --- a/rust/cli_player/src/lib.rs +++ b/rust/cli_player/src/lib.rs @@ -1,4 +1,5 @@ pub mod cli_args; +pub mod client; pub mod constants; pub mod server; pub mod ui_screens; diff --git a/rust/sea_battle_backend/src/bot_player.rs b/rust/sea_battle_backend/src/bot_player.rs index b5afb48..3dd550d 100644 --- a/rust/sea_battle_backend/src/bot_player.rs +++ b/rust/sea_battle_backend/src/bot_player.rs @@ -39,6 +39,8 @@ impl Player for BotPlayer { true } + fn opponent_connected(&self) {} + fn set_other_player_name(&self, _name: &str) {} fn query_boats_layout(&self, rules: &GameRules) { diff --git a/rust/sea_battle_backend/src/game.rs b/rust/sea_battle_backend/src/game.rs index 15f2519..6380d36 100644 --- a/rust/sea_battle_backend/src/game.rs +++ b/rust/sea_battle_backend/src/game.rs @@ -14,6 +14,8 @@ pub trait Player { fn is_bot(&self) -> bool; + fn opponent_connected(&self); + fn set_other_player_name(&self, name: &str); fn query_boats_layout(&self, rules: &GameRules); @@ -240,6 +242,9 @@ where self.players.push(msg.0); if self.players.len() == 2 { + self.players[0].opponent_connected(); + self.players[1].opponent_connected(); + self.query_boats_disposition(); } } diff --git a/rust/sea_battle_backend/src/human_player.rs b/rust/sea_battle_backend/src/human_player.rs index e299bff..418337c 100644 --- a/rust/sea_battle_backend/src/human_player.rs +++ b/rust/sea_battle_backend/src/human_player.rs @@ -25,6 +25,10 @@ impl Player for HumanPlayer { false } + fn opponent_connected(&self) { + self.player.do_send(ServerMessage::OpponentConnected); + } + fn set_other_player_name(&self, name: &str) { self.player.do_send(ServerMessage::SetOpponentName { name: name.to_string(), diff --git a/rust/sea_battle_backend/src/human_player_ws.rs b/rust/sea_battle_backend/src/human_player_ws.rs index 898e3b4..625272c 100644 --- a/rust/sea_battle_backend/src/human_player_ws.rs +++ b/rust/sea_battle_backend/src/human_player_ws.rs @@ -47,6 +47,7 @@ pub enum ServerMessage { }, InvalidInviteCode, WaitingForAnotherPlayer, + OpponentConnected, SetOpponentName { name: String, }, diff --git a/rust/sea_battle_backend/src/test/bot_client.rs b/rust/sea_battle_backend/src/test/bot_client.rs index e01402f..2c286b0 100644 --- a/rust/sea_battle_backend/src/test/bot_client.rs +++ b/rust/sea_battle_backend/src/test/bot_client.rs @@ -196,6 +196,9 @@ impl BotClient { log::debug!("Got invalid invite code!"); return Ok(ClientEndResult::InvalidInviteCode); } + ServerMessage::OpponentConnected => { + log::debug!("Opponent connected"); + } ServerMessage::QueryBoatsLayout { rules } => { assert_eq!(&rules, &self.requested_rules); log::debug!("Server requested boats layout"); diff --git a/rust/sea_battle_backend/src/utils/mod.rs b/rust/sea_battle_backend/src/utils/mod.rs index 3e5d5a4..0fb3fea 100644 --- a/rust/sea_battle_backend/src/utils/mod.rs +++ b/rust/sea_battle_backend/src/utils/mod.rs @@ -1,2 +1,12 @@ +use std::error::Error; +use std::fmt::Display; +use std::io::ErrorKind; + pub mod network_utils; pub mod string_utils; + +pub type Res = Result>; + +pub fn boxed_error(msg: D) -> Box { + Box::new(std::io::Error::new(ErrorKind::Other, msg.to_string())) +}