Merge pull request #4 from szymon-jozef/feat/bot

feat/bot
This commit is contained in:
Szymon
2026-08-19 02:44:10 -07:00
committed by GitHub
7 changed files with 236 additions and 37 deletions

View File

@@ -6,6 +6,7 @@ use std::{
use checkers::{ use checkers::{
logic::board::pawn::{CapturePath, MovePath}, logic::board::pawn::{CapturePath, MovePath},
network::{client::Client, message::ServerMessage, network_identity::NetworkIdentity}, network::{client::Client, message::ServerMessage, network_identity::NetworkIdentity},
super_advanced_ai::{Bot, BotDificulty},
}; };
use log::{error, warn}; use log::{error, warn};
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -14,6 +15,7 @@ const QUIT_COMMAND: &str = "/quit";
const HELP_COMMAND: &str = "/help"; const HELP_COMMAND: &str = "/help";
const SEND_COMMAND: &str = "/send"; const SEND_COMMAND: &str = "/send";
const READY_COMMAND: &str = "/ready"; const READY_COMMAND: &str = "/ready";
const SINGLEPLAYER_COMMAND: &str = "/single";
const CAPTURE_COMMAND: &str = "/capture"; const CAPTURE_COMMAND: &str = "/capture";
const MOVE_COMMAND: &str = "/move"; const MOVE_COMMAND: &str = "/move";
@@ -22,7 +24,9 @@ enum CliCommands {
Quit, Quit,
Help, Help,
Send, Send,
Ready, Ready,
Single,
Capture, Capture,
Move, Move,
@@ -46,6 +50,7 @@ impl TryFrom<&str> for CliCommands {
HELP_COMMAND => Ok(CliCommands::Help), HELP_COMMAND => Ok(CliCommands::Help),
SEND_COMMAND => Ok(CliCommands::Send), SEND_COMMAND => Ok(CliCommands::Send),
READY_COMMAND => Ok(CliCommands::Ready), READY_COMMAND => Ok(CliCommands::Ready),
SINGLEPLAYER_COMMAND => Ok(CliCommands::Single),
CAPTURE_COMMAND => Ok(CliCommands::Capture), CAPTURE_COMMAND => Ok(CliCommands::Capture),
MOVE_COMMAND => Ok(CliCommands::Move), 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] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init(); env_logger::init();
let Some(mut client) = Client::new().await else { let Some(mut client) = Client::new(None).await else {
println!("Not worky, ending"); println!("Not worky, ending");
return Ok(()); return Ok(());
}; };
client.update();
let (stdin_sender, mut stdin_receiver) = mpsc::channel(1024); let (stdin_sender, mut stdin_receiver) = mpsc::channel(1024);
let mut context: ClientContext = ClientContext::default(); let mut context: ClientContext = ClientContext::default();
@@ -112,12 +120,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
│\t {} - show this message │\t {} - show this message
│\t {} - message the server you're ready │\t {} - message the server you're ready
│\t {} - send text message to the server │\t {} - send text message to the server
│\t {} - play alone
│ │
│\t {} <index>- choose a capture path from the list │\t {} <index>- choose a capture path from the list
│\t {} <index> <index>- choose move from the list. First number is pawn you want to move and second is position it should take │\t {} <index> <index>- 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───────"); println!("───────Type /help for help───────");
@@ -199,6 +208,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
CliCommands::Ready => { CliCommands::Ready => {
client.signal_readiness().await; client.signal_readiness().await;
} }
CliCommands::Single => {
println!("Tried connecting bot to the server! Make sure to be /ready");
tokio::spawn(run_bot());
}
CliCommands::Send => { CliCommands::Send => {
let mut args: String = words[1..].join(" "); let mut args: String = words[1..].join(" ");
args = args.trim().to_string(); args = args.trim().to_string();

View File

@@ -1,3 +1,4 @@
pub mod logic; pub mod logic;
pub mod network; pub mod network;
pub mod settings; pub mod settings;
pub mod super_advanced_ai;

View File

@@ -23,22 +23,24 @@ pub enum ClientCommands {
} }
pub struct Client { pub struct Client {
conn_reciever_incoming: Option<Receiver<(SocketAddr, Message<ServerMessage>)>>,
conn_sender_outgoing: Sender<Message<ClientMessage>>, conn_sender_outgoing: Sender<Message<ClientMessage>>,
settings: ClientSettings,
update_sender: Sender<ServerMessage>,
update_receiver: Option<Receiver<ServerMessage>>, update_receiver: Option<Receiver<ServerMessage>>,
} }
impl Client { impl Client {
pub async fn new() -> Option<Client> { /// Creates a new instance of clients, connects it to the server and starts update loop
let settings = ClientSettings::new(); pub async fn new(settings: Option<ClientSettings>) -> Option<Client> {
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<ServerMessage>)>(1024); mpsc::channel::<(SocketAddr, Message<ServerMessage>)>(1024);
let mut conn: Connection<ServerMessage, ClientMessage> = let mut conn: Connection<ServerMessage, ClientMessage> =
Connection::new(ConnectionType::Server, conn_sender); Connection::new(ConnectionType::Server, conn_sender);
let conn_sender_outgoing = conn.get_sender(); let conn_sender_outgoing = conn.get_sender();
let (update_sender, update_receiver) = mpsc::channel(1024); let (update_sender, update_receiver) = mpsc::channel(1024);
@@ -52,32 +54,18 @@ impl Client {
conn.start_listening().await; conn.start_listening().await;
}); });
Some(Client { let conn_sender_outgoing_clone = conn_sender_outgoing.clone();
conn_reciever_incoming: Some(conn_reciever), let settings_clone = settings.clone();
conn_sender_outgoing, let update_sender_clone = update_sender.clone();
settings, let (hanshake_tx, handshake_rx) = tokio::sync::oneshot::channel::<bool>();
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();
tokio::spawn(async move { tokio::spawn(async move {
let mut hanshake_tx = Some(hanshake_tx);
loop { loop {
tokio::select! { tokio::select! {
oldest_msg = conn_reciever_incoming.recv() => { oldest_msg = conn_reciever.recv() => {
if let Some(oldest_msg) = oldest_msg { if let Some(oldest_msg) = oldest_msg {
let (addr, msg) = oldest_msg; let (addr, msg) = oldest_msg;
debug!("Got message from: {:?}", addr); debug!("Got message from: {:?}", addr);
@@ -86,24 +74,30 @@ impl Client {
ServerMessage::RequestHandshake => { ServerMessage::RequestHandshake => {
info!("Server requested handshake!"); info!("Server requested handshake!");
let msg = Message::new(ClientMessage::AnswerHandshake { 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 => { ServerMessage::AcceptHandshake => {
info!("Server accepted us! Yay :D"); info!("Server accepted us! Yay :D");
if let Some(hanshake_tx) = hanshake_tx.take() {
let _ = hanshake_tx.send(true);
}
} }
ServerMessage::DeclineHandshake { reason } => { ServerMessage::DeclineHandshake { reason } => {
error!("Server declined connection: {}", reason); error!("Server declined connection: {}", reason);
if let Some(hanshake_tx) = hanshake_tx.take() {
let _ = hanshake_tx.send(false);
}
break; break;
} }
_ => { // Pass all messages that require player intervention to the _ => { // Pass all messages that require player intervention to the
// local client // local client
let _ = update_sender.send(msg.content).await; let _ = update_sender_clone.send(msg.content).await;
} }
} }
} else { } 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<Message<ClientMessage>, postcard::Error>) { async fn send_message(&mut self, msg: Result<Message<ClientMessage>, postcard::Error>) {

View File

@@ -33,6 +33,7 @@ pub struct NetworkServerIdentity {
// writing this since 10 a.m. and it's 9 p.m. already) // writing this since 10 a.m. and it's 9 p.m. already)
pub identity: NetworkIdentity, pub identity: NetworkIdentity,
pub sender: Sender<Message<ServerMessage>>, pub sender: Sender<Message<ServerMessage>>,
pub is_handshaken: bool,
} }
impl NetworkServerIdentity { impl NetworkServerIdentity {
@@ -40,6 +41,7 @@ impl NetworkServerIdentity {
Self { Self {
identity: NetworkIdentity::default(), identity: NetworkIdentity::default(),
sender, sender,
is_handshaken: false,
} }
} }
} }

View File

@@ -186,7 +186,7 @@ impl Server {
ClientMessage::SignalReadiness => { ClientMessage::SignalReadiness => {
self.handle_readiness(addr).await self.handle_readiness(addr).await
}, }
ClientMessage::RequestCapture { capture_path } => { ClientMessage::RequestCapture { capture_path } => {
self.process_capture(capture_path).await; self.process_capture(capture_path).await;
@@ -282,7 +282,8 @@ impl Server {
return; return;
}; };
identity.identity.name = fixed_name identity.identity.name = fixed_name;
identity.is_handshaken = true;
} }
async fn process_capture(&mut self, capture_path: CapturePath) { async fn process_capture(&mut self, capture_path: CapturePath) {
@@ -313,6 +314,14 @@ impl Server {
} }
async fn handle_readiness(&mut self, addr: SocketAddr) { 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; self.state.ready_count += 1;
info!( info!(
"{} signaled readiness. Currently ready: {}/{}", "{} signaled readiness. Currently ready: {}/{}",

117
src/super_advanced_ai.rs Normal file
View File

@@ -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<Uuid> {
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;
}
}
}
}
}

44
tests/integration.rs Normal file
View File

@@ -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");
}
}
}