diff --git a/src/bin/client.rs b/src/bin/client.rs index 44ed9cf..c96a384 100644 --- a/src/bin/client.rs +++ b/src/bin/client.rs @@ -6,6 +6,7 @@ use std::{ use checkers::{ logic::board::pawn::{CapturePath, MovePath}, network::{client::Client, message::ServerMessage, network_identity::NetworkIdentity}, + super_advanced_ai::{Bot, BotDificulty}, }; use log::{error, warn}; use tokio::sync::mpsc; @@ -14,6 +15,7 @@ const QUIT_COMMAND: &str = "/quit"; const HELP_COMMAND: &str = "/help"; const SEND_COMMAND: &str = "/send"; const READY_COMMAND: &str = "/ready"; +const SINGLEPLAYER_COMMAND: &str = "/single"; const CAPTURE_COMMAND: &str = "/capture"; const MOVE_COMMAND: &str = "/move"; @@ -22,7 +24,9 @@ enum CliCommands { Quit, Help, Send, + Ready, + Single, Capture, Move, @@ -46,6 +50,7 @@ impl TryFrom<&str> for CliCommands { HELP_COMMAND => Ok(CliCommands::Help), SEND_COMMAND => Ok(CliCommands::Send), READY_COMMAND => Ok(CliCommands::Ready), + SINGLEPLAYER_COMMAND => Ok(CliCommands::Single), CAPTURE_COMMAND => Ok(CliCommands::Capture), MOVE_COMMAND => Ok(CliCommands::Move), @@ -74,17 +79,20 @@ impl Default for ClientContext { } } +async fn run_bot() { + let bot = Bot::new(BotDificulty::Easy).await; + bot.game_loop().await; +} + #[tokio::main] async fn main() -> Result<(), Box> { env_logger::init(); - let Some(mut client) = Client::new().await else { + let Some(mut client) = Client::new(None).await else { println!("Not worky, ending"); return Ok(()); }; - client.update(); - let (stdin_sender, mut stdin_receiver) = mpsc::channel(1024); let mut context: ClientContext = ClientContext::default(); @@ -112,12 +120,13 @@ async fn main() -> Result<(), Box> { │\t {} - show this message │\t {} - message the server you're ready │\t {} - send text message to the server +│\t {} - play alone │ │\t {} - choose a capture path from the list │\t {} - choose move from the list. First number is pawn you want to move and second is position it should take └─────────────────────────────────────── ", - QUIT_COMMAND, HELP_COMMAND, READY_COMMAND, SEND_COMMAND, CAPTURE_COMMAND, MOVE_COMMAND + QUIT_COMMAND, HELP_COMMAND, READY_COMMAND, SEND_COMMAND, SINGLEPLAYER_COMMAND , CAPTURE_COMMAND, MOVE_COMMAND ); println!("───────Type /help for help───────"); @@ -199,6 +208,12 @@ async fn main() -> Result<(), Box> { CliCommands::Ready => { client.signal_readiness().await; } + + CliCommands::Single => { + println!("Tried connecting bot to the server! Make sure to be /ready"); + tokio::spawn(run_bot()); + } + CliCommands::Send => { let mut args: String = words[1..].join(" "); args = args.trim().to_string(); diff --git a/src/lib.rs b/src/lib.rs index a15f61f..f5d0540 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod logic; pub mod network; pub mod settings; +pub mod super_advanced_ai; diff --git a/src/network/client.rs b/src/network/client.rs index a397473..1136f5c 100644 --- a/src/network/client.rs +++ b/src/network/client.rs @@ -23,22 +23,24 @@ pub enum ClientCommands { } pub struct Client { - conn_reciever_incoming: Option)>>, conn_sender_outgoing: Sender>, - settings: ClientSettings, - - update_sender: Sender, update_receiver: Option>, } impl Client { - pub async fn new() -> Option { - let settings = ClientSettings::new(); + /// Creates a new instance of clients, connects it to the server and starts update loop + pub async fn new(settings: Option) -> Option { + let Some(settings) = settings.or(Some(ClientSettings::new())) else { + error!("Could not load client settings"); + return None; + }; - let (conn_sender, conn_reciever) = + let (conn_sender, mut conn_reciever) = mpsc::channel::<(SocketAddr, Message)>(1024); + let mut conn: Connection = Connection::new(ConnectionType::Server, conn_sender); + let conn_sender_outgoing = conn.get_sender(); let (update_sender, update_receiver) = mpsc::channel(1024); @@ -52,32 +54,18 @@ impl Client { conn.start_listening().await; }); - Some(Client { - conn_reciever_incoming: Some(conn_reciever), - conn_sender_outgoing, + let conn_sender_outgoing_clone = conn_sender_outgoing.clone(); + let settings_clone = settings.clone(); + let update_sender_clone = update_sender.clone(); - settings, - - update_sender, - update_receiver: Some(update_receiver), - }) - } - - pub fn update(&mut self) { - let Some(mut conn_reciever_incoming) = self.conn_reciever_incoming.take() else { - error!("Tried updating, but there's no conn_reciever of incoming messages!"); - return; - }; - - let conn_sender = self.conn_sender_outgoing.clone(); - - let settings = self.settings.clone(); - let update_sender = self.update_sender.clone(); + let (hanshake_tx, handshake_rx) = tokio::sync::oneshot::channel::(); tokio::spawn(async move { + let mut hanshake_tx = Some(hanshake_tx); + loop { tokio::select! { - oldest_msg = conn_reciever_incoming.recv() => { + oldest_msg = conn_reciever.recv() => { if let Some(oldest_msg) = oldest_msg { let (addr, msg) = oldest_msg; debug!("Got message from: {:?}", addr); @@ -86,24 +74,30 @@ impl Client { ServerMessage::RequestHandshake => { info!("Server requested handshake!"); let msg = Message::new(ClientMessage::AnswerHandshake { - player_name: settings.name.clone(), + player_name: settings_clone.name.clone(), }); - let _ = conn_sender.send(msg.unwrap()).await; + let _ = conn_sender_outgoing_clone.send(msg.unwrap()).await; } ServerMessage::AcceptHandshake => { info!("Server accepted us! Yay :D"); + if let Some(hanshake_tx) = hanshake_tx.take() { + let _ = hanshake_tx.send(true); + } } ServerMessage::DeclineHandshake { reason } => { error!("Server declined connection: {}", reason); + if let Some(hanshake_tx) = hanshake_tx.take() { + let _ = hanshake_tx.send(false); + } break; } _ => { // Pass all messages that require player intervention to the // local client - let _ = update_sender.send(msg.content).await; + let _ = update_sender_clone.send(msg.content).await; } } } else { @@ -114,6 +108,23 @@ impl Client { } } }); + + let is_handshake_accepted: bool = match handshake_rx.await { + Ok(is_accepted) => is_accepted, + Err(e) => { + error!("Error while receiving handshake: {}", e); + return None; + } + }; + + if is_handshake_accepted { + Some(Client { + conn_sender_outgoing, + update_receiver: Some(update_receiver), + }) + } else { + None + } } async fn send_message(&mut self, msg: Result, postcard::Error>) { diff --git a/src/network/network_identity.rs b/src/network/network_identity.rs index 02ace06..3ef7dbe 100644 --- a/src/network/network_identity.rs +++ b/src/network/network_identity.rs @@ -33,6 +33,7 @@ pub struct NetworkServerIdentity { // writing this since 10 a.m. and it's 9 p.m. already) pub identity: NetworkIdentity, pub sender: Sender>, + pub is_handshaken: bool, } impl NetworkServerIdentity { @@ -40,6 +41,7 @@ impl NetworkServerIdentity { Self { identity: NetworkIdentity::default(), sender, + is_handshaken: false, } } } diff --git a/src/network/server.rs b/src/network/server.rs index 71519b3..bf6f8dd 100644 --- a/src/network/server.rs +++ b/src/network/server.rs @@ -186,7 +186,7 @@ impl Server { ClientMessage::SignalReadiness => { self.handle_readiness(addr).await - }, + } ClientMessage::RequestCapture { capture_path } => { self.process_capture(capture_path).await; @@ -282,7 +282,8 @@ impl Server { return; }; - identity.identity.name = fixed_name + identity.identity.name = fixed_name; + identity.is_handshaken = true; } async fn process_capture(&mut self, capture_path: CapturePath) { @@ -313,6 +314,14 @@ impl Server { } async fn handle_readiness(&mut self, addr: SocketAddr) { + if !self.connections[&addr].is_handshaken { + warn!( + "Connection: {} tried sending readiness, but it have not shaken hands. Ignoring...", + addr + ); + return; + } + self.state.ready_count += 1; info!( "{} signaled readiness. Currently ready: {}/{}", diff --git a/src/super_advanced_ai.rs b/src/super_advanced_ai.rs new file mode 100644 index 0000000..e36a5b0 --- /dev/null +++ b/src/super_advanced_ai.rs @@ -0,0 +1,117 @@ +use log::{error, info}; +use rand::seq::IndexedRandom; +use uuid::Uuid; + +use crate::{ + logic::game_master::GameResult, + network::client::Client, + settings::{client_settings::ClientSettings, general_settings::DEFAULT_URL}, +}; + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum BotDificulty { + Easy, + Normal, + Hard, +} + +pub struct Bot { + dificulty: BotDificulty, +} + +impl Bot { + pub async fn new(dificulty: BotDificulty) -> Self { + if dificulty == BotDificulty::Normal || dificulty == BotDificulty::Hard { + todo!("Only easy is implemented"); + } + + Self { dificulty } + } + + pub async fn game_loop(&self) -> Option { + let mut client = Client::new(Some(ClientSettings { + name: "Bot".to_string(), + server_url: DEFAULT_URL, + })) + .await + .expect("Couldn't connect to the server. Is it online?"); + + client.signal_readiness().await; // bot is always ready + + let mut receiver = client + .get_update_receiver() + .expect("Could not receive the receiver"); + + let dificulty = self.dificulty; + + loop { + match receiver.recv().await { + Some(message) => match message { + crate::network::message::ServerMessage::GameStart { identity } => { + info!("Bot got identity: {}", identity); + } + crate::network::message::ServerMessage::AvailableCaptures { captures } => { + let capture = captures + .choose(&mut rand::rng()) + .expect("Could not select random capture"); + client.send_capture(capture.clone()).await; + } + crate::network::message::ServerMessage::AvailableMoves { moves } => { + let rand_pawn = moves + .choose(&mut rand::rng()) + .expect("Couldn't select random move"); + let rand_move = rand_pawn + .available_steps + .choose(&mut rand::rng()) + .expect("Could not get random move"); + + client.send_move(rand_pawn.from, rand_move.clone()).await; + } + crate::network::message::ServerMessage::BroadcastBoardState { board: _ } => { + info!("Got server state!"); + + match dificulty.clone() { + BotDificulty::Easy => {} // we do nothing with it because easy bot + // just does random stuff. In future + // board_view can be used to calculate the + // best possible move atm + BotDificulty::Normal => todo!(), + BotDificulty::Hard => todo!(), + } + } + crate::network::message::ServerMessage::BroadcastCurrentTurn { + active_player: _, + } => {} // we do nothing with this - bot doesn't need to know if it's the + // active player, as it's only responding to messages + crate::network::message::ServerMessage::BroadCastTextMessage { + sender, + content, + } => { + info!("Bot got message from [{}] - {}", sender, content); + } + + crate::network::message::ServerMessage::GameEnd { result } => { + info!("Game has ended!"); + match result { + GameResult::Draw => { + info!("Draw!"); + return None; + } + GameResult::Lost(loser) => { + info!("ID: {} lost!", loser); + return Some(loser); + } + } + } + + _ => {} + }, + + None => { + error!("Connection broken! Super advanced ai logging out!"); + return None; + } + } + } + } +} diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..f98df00 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,44 @@ +#[cfg(test)] +mod tests { + + use tokio::time::Duration; + + use checkers::{ + network::server::Server, + super_advanced_ai::{Bot, BotDificulty}, + }; + + #[tokio::test] + async fn game_integration() { + let _ = env_logger::builder().is_test(true).try_init(); + + tokio::spawn(async move { + let mut server = Server::new().await; + server.start().await; + server.update().await; + }); + + tokio::time::sleep(Duration::from_secs(2)).await; + + let bot1_handle = tokio::spawn(async move { + let bot1 = Bot::new(BotDificulty::Easy).await; + bot1.game_loop().await + }); + + let bot2_handle = tokio::spawn(async move { + let bot2 = Bot::new(BotDificulty::Easy).await; + bot2.game_loop().await + }); + + let bot1_result = bot1_handle.await.unwrap(); + let bot2_result = bot2_handle.await.unwrap(); + + if bot1_result.is_some() && bot2_result.is_some() { + assert_eq!(bot1_result.unwrap(), bot2_result.unwrap()); + } else if bot1_result.is_none() && bot2_result.is_none() { + () + } else { + panic!("Bot result differanciate"); + } + } +}