From 97d7e70642f727f2e8d1fb8e7f5e23c2a9e04e14 Mon Sep 17 00:00:00 2001 From: szymon Date: Wed, 19 Aug 2026 09:56:51 +0200 Subject: [PATCH 1/6] feat: add super advanced ai --- src/lib.rs | 1 + src/super_advanced_ai.rs | 115 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/super_advanced_ai.rs 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/super_advanced_ai.rs b/src/super_advanced_ai.rs new file mode 100644 index 0000000..d4aebb6 --- /dev/null +++ b/src/super_advanced_ai.rs @@ -0,0 +1,115 @@ +use log::{error, info}; +use rand::seq::IndexedRandom; + +use crate::{logic::game_master::GameResult, network::client::Client}; + +#[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) { + let mut client = Client::new() + .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; + + tokio::spawn(async move { + 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); + client + .send_text_message( + "Why are you messaging bot? Are you stupid?".to_string(), + ) + .await; + } + crate::network::message::ServerMessage::GameEnd { result } => { + info!("Game has ended!"); + match result { + GameResult::Draw => { + info!("Draw!"); + } + GameResult::Lost(loser) => { + info!("ID: {} lost!", loser); + } + } + } + + _ => {} + }, + + None => { + error!("Connection broken! Super advanced ai logging out!"); + break; + } + } + } + }); + } +} From bd97584adbfe41627a86c1e8d6e226d797051578 Mon Sep 17 00:00:00 2001 From: szymon Date: Wed, 19 Aug 2026 10:13:43 +0200 Subject: [PATCH 2/6] feat: implement single player for cli client --- src/bin/client.rs | 19 ++++++++++++++++++- src/super_advanced_ai.rs | 7 ++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/bin/client.rs b/src/bin/client.rs index 44ed9cf..05be414 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,6 +79,11 @@ 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(); @@ -112,12 +122,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 +210,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/super_advanced_ai.rs b/src/super_advanced_ai.rs index d4aebb6..a97aee9 100644 --- a/src/super_advanced_ai.rs +++ b/src/super_advanced_ai.rs @@ -28,6 +28,7 @@ impl Bot { .await .expect("Couldn't connect to the server. Is it online?"); + client.update(); client.signal_readiness().await; // bot is always ready let mut receiver = client @@ -83,12 +84,8 @@ impl Bot { content, } => { info!("Bot got message from [{}] - {}", sender, content); - client - .send_text_message( - "Why are you messaging bot? Are you stupid?".to_string(), - ) - .await; } + crate::network::message::ServerMessage::GameEnd { result } => { info!("Game has ended!"); match result { From 6b565e4f0ee20075e65bcc1797236a73fc0119b3 Mon Sep 17 00:00:00 2001 From: szymon Date: Wed, 19 Aug 2026 10:29:06 +0200 Subject: [PATCH 3/6] test: integration test for server and bot --- tests/integration.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/integration.rs diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..07ac708 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,33 @@ +#[cfg(test)] +mod tests { + use tokio::time::Duration; + + use checkers::{ + network::server::Server, + super_advanced_ai::{Bot, BotDificulty}, + }; + + #[tokio::test] + async fn game_integration() { + tokio::spawn(async move { + let mut server = Server::new().await; + server.start().await; + server.update().await; + }); + + tokio::time::sleep(Duration::from_secs(2)).await; // wait for the server to run + + 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; + }); + + bot1_handle.await.unwrap(); + bot2_handle.await.unwrap(); + } +} From bcb757b904b26857d7d66500a1e76e717746862e Mon Sep 17 00:00:00 2001 From: szymon Date: Wed, 19 Aug 2026 11:09:46 +0200 Subject: [PATCH 4/6] fix: server allowing sending ready while hands not shaken --- src/network/client.rs | 7 +++++-- src/network/network_identity.rs | 2 ++ src/network/server.rs | 13 +++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/network/client.rs b/src/network/client.rs index a397473..4d6eaea 100644 --- a/src/network/client.rs +++ b/src/network/client.rs @@ -32,8 +32,11 @@ pub struct Client { } impl Client { - pub async fn new() -> Option { - let settings = ClientSettings::new(); + 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) = mpsc::channel::<(SocketAddr, Message)>(1024); 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: {}/{}", From f2829efd6c94e5ec91ad6a6fe6434b4f159360dc Mon Sep 17 00:00:00 2001 From: szymon Date: Wed, 19 Aug 2026 11:29:47 +0200 Subject: [PATCH 5/6] refactor: move update function of the network client to the constructor --- src/bin/client.rs | 4 +- src/network/client.rs | 46 ++++-------- src/super_advanced_ai.rs | 153 ++++++++++++++++++++------------------- 3 files changed, 96 insertions(+), 107 deletions(-) diff --git a/src/bin/client.rs b/src/bin/client.rs index 05be414..c96a384 100644 --- a/src/bin/client.rs +++ b/src/bin/client.rs @@ -88,13 +88,11 @@ async fn run_bot() { 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(); diff --git a/src/network/client.rs b/src/network/client.rs index 4d6eaea..7feb5d7 100644 --- a/src/network/client.rs +++ b/src/network/client.rs @@ -23,25 +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 { + /// 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); @@ -55,32 +54,14 @@ impl Client { conn.start_listening().await; }); - Some(Client { - conn_reciever_incoming: Some(conn_reciever), - conn_sender_outgoing, - - 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 conn_sender_outgoing_clone = conn_sender_outgoing.clone(); + let settings_clone = settings.clone(); + let update_sender_clone = update_sender.clone(); tokio::spawn(async move { 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); @@ -89,10 +70,10 @@ 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 => { @@ -106,7 +87,7 @@ impl Client { _ => { // 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 { @@ -117,6 +98,11 @@ impl Client { } } }); + + Some(Client { + conn_sender_outgoing, + update_receiver: Some(update_receiver), + }) } async fn send_message(&mut self, msg: Result, postcard::Error>) { diff --git a/src/super_advanced_ai.rs b/src/super_advanced_ai.rs index a97aee9..e36a5b0 100644 --- a/src/super_advanced_ai.rs +++ b/src/super_advanced_ai.rs @@ -1,7 +1,12 @@ use log::{error, info}; use rand::seq::IndexedRandom; +use uuid::Uuid; -use crate::{logic::game_master::GameResult, network::client::Client}; +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 { @@ -23,12 +28,14 @@ impl Bot { Self { dificulty } } - pub async fn game_loop(&self) { - let mut client = Client::new() - .await - .expect("Couldn't connect to the server. Is it online?"); + 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.update(); client.signal_readiness().await; // bot is always ready let mut receiver = client @@ -37,76 +44,74 @@ impl Bot { let dificulty = self.dificulty; - tokio::spawn(async move { - 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!"); - } - GameResult::Lost(loser) => { - info!("ID: {} lost!", loser); - } - } - } - - _ => {} - }, - - None => { - error!("Connection broken! Super advanced ai logging out!"); - break; + 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; } } - }); + } } } From 43d438a44fe00b3851393f8a6cdbbbf834d1a3ba Mon Sep 17 00:00:00 2001 From: szymon Date: Wed, 19 Aug 2026 11:40:53 +0200 Subject: [PATCH 6/6] fix: client playing when hands werent shaken yet --- src/network/client.rs | 30 ++++++++++++++++++++++++++---- tests/integration.rs | 21 ++++++++++++++++----- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/network/client.rs b/src/network/client.rs index 7feb5d7..1136f5c 100644 --- a/src/network/client.rs +++ b/src/network/client.rs @@ -58,7 +58,11 @@ impl Client { let settings_clone = settings.clone(); let update_sender_clone = 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.recv() => { @@ -78,10 +82,16 @@ impl Client { 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; } @@ -99,10 +109,22 @@ impl Client { } }); - Some(Client { - conn_sender_outgoing, - update_receiver: Some(update_receiver), - }) + 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/tests/integration.rs b/tests/integration.rs index 07ac708..f98df00 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use tokio::time::Duration; use checkers::{ @@ -9,25 +10,35 @@ mod tests { #[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; // wait for the server to run + 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; + bot1.game_loop().await }); let bot2_handle = tokio::spawn(async move { let bot2 = Bot::new(BotDificulty::Easy).await; - bot2.game_loop().await; + bot2.game_loop().await }); - bot1_handle.await.unwrap(); - bot2_handle.await.unwrap(); + 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"); + } } }