Start to create WS client for cli_player

This commit is contained in:
Pierre HUBERT 2022-10-15 10:12:02 +02:00
parent d90560d330
commit 4af2585a8b
11 changed files with 135 additions and 1 deletions

4
rust/Cargo.lock generated
View File

@ -465,14 +465,18 @@ dependencies = [
"clap", "clap",
"crossterm", "crossterm",
"env_logger", "env_logger",
"futures",
"lazy_static", "lazy_static",
"log", "log",
"num", "num",
"num-derive", "num-derive",
"num-traits", "num-traits",
"sea_battle_backend", "sea_battle_backend",
"serde_json",
"serde_urlencoded",
"textwrap", "textwrap",
"tokio", "tokio",
"tokio-tungstenite",
"tui", "tui",
] ]

View File

@ -17,4 +17,8 @@ tokio = "1.21.2"
num = "0.4.0" num = "0.4.0"
num-traits = "0.2.15" num-traits = "0.2.15"
num-derive = "0.3.3" num-derive = "0.3.3"
textwrap = "0.15.1" textwrap = "0.15.1"
tokio-tungstenite = "0.17.2"
serde_urlencoded = "0.7.1"
futures = "0.3.23"
serde_json = "1.0.85"

View File

@ -41,6 +41,11 @@ impl CliArgs {
.parse::<u16>() .parse::<u16>()
.expect("Failed to parse listen port!") .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! { lazy_static::lazy_static! {

View File

@ -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<MaybeTlsStream<TcpStream>>,
}
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<Self> {
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<Self> {
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<ServerMessage> {
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(())
}
}

View File

@ -1,4 +1,5 @@
pub mod cli_args; pub mod cli_args;
pub mod client;
pub mod constants; pub mod constants;
pub mod server; pub mod server;
pub mod ui_screens; pub mod ui_screens;

View File

@ -39,6 +39,8 @@ impl Player for BotPlayer {
true true
} }
fn opponent_connected(&self) {}
fn set_other_player_name(&self, _name: &str) {} fn set_other_player_name(&self, _name: &str) {}
fn query_boats_layout(&self, rules: &GameRules) { fn query_boats_layout(&self, rules: &GameRules) {

View File

@ -14,6 +14,8 @@ pub trait Player {
fn is_bot(&self) -> bool; fn is_bot(&self) -> bool;
fn opponent_connected(&self);
fn set_other_player_name(&self, name: &str); fn set_other_player_name(&self, name: &str);
fn query_boats_layout(&self, rules: &GameRules); fn query_boats_layout(&self, rules: &GameRules);
@ -240,6 +242,9 @@ where
self.players.push(msg.0); self.players.push(msg.0);
if self.players.len() == 2 { if self.players.len() == 2 {
self.players[0].opponent_connected();
self.players[1].opponent_connected();
self.query_boats_disposition(); self.query_boats_disposition();
} }
} }

View File

@ -25,6 +25,10 @@ impl Player for HumanPlayer {
false false
} }
fn opponent_connected(&self) {
self.player.do_send(ServerMessage::OpponentConnected);
}
fn set_other_player_name(&self, name: &str) { fn set_other_player_name(&self, name: &str) {
self.player.do_send(ServerMessage::SetOpponentName { self.player.do_send(ServerMessage::SetOpponentName {
name: name.to_string(), name: name.to_string(),

View File

@ -47,6 +47,7 @@ pub enum ServerMessage {
}, },
InvalidInviteCode, InvalidInviteCode,
WaitingForAnotherPlayer, WaitingForAnotherPlayer,
OpponentConnected,
SetOpponentName { SetOpponentName {
name: String, name: String,
}, },

View File

@ -196,6 +196,9 @@ impl BotClient {
log::debug!("Got invalid invite code!"); log::debug!("Got invalid invite code!");
return Ok(ClientEndResult::InvalidInviteCode); return Ok(ClientEndResult::InvalidInviteCode);
} }
ServerMessage::OpponentConnected => {
log::debug!("Opponent connected");
}
ServerMessage::QueryBoatsLayout { rules } => { ServerMessage::QueryBoatsLayout { rules } => {
assert_eq!(&rules, &self.requested_rules); assert_eq!(&rules, &self.requested_rules);
log::debug!("Server requested boats layout"); log::debug!("Server requested boats layout");

View File

@ -1,2 +1,12 @@
use std::error::Error;
use std::fmt::Display;
use std::io::ErrorKind;
pub mod network_utils; pub mod network_utils;
pub mod string_utils; pub mod string_utils;
pub type Res<E = ()> = Result<E, Box<dyn Error>>;
pub fn boxed_error<D: Display>(msg: D) -> Box<dyn Error> {
Box::new(std::io::Error::new(ErrorKind::Other, msg.to_string()))
}