diff --git a/Cargo.lock b/Cargo.lock index ac90b9e..9be20c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,16 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Checkers" -version = "0.0.1" -dependencies = [ - "env_logger", - "log", - "rand", - "uuid", -] - [[package]] name = "aho-corasick" version = "1.1.5" @@ -73,9 +63,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] [[package]] name = "bitflags" @@ -95,6 +94,18 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cfg-if" version = "1.0.4" @@ -112,6 +123,28 @@ dependencies = [ "rand_core", ] +[[package]] +name = "checkers" +version = "0.0.1" +dependencies = [ + "env_logger", + "log", + "postcard", + "rand", + "serde", + "tokio", + "uuid", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -127,6 +160,12 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "defmt" version = "1.1.1" @@ -158,6 +197,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "env_filter" version = "2.0.0" @@ -187,6 +238,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -231,6 +292,15 @@ dependencies = [ "wasip3", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -246,6 +316,20 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -341,6 +425,15 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.33" @@ -353,6 +446,17 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -365,6 +469,29 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -386,6 +513,19 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -437,6 +577,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "regex" version = "1.13.1" @@ -466,12 +615,27 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -480,31 +644,32 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", + "serde_derive", ] [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -520,12 +685,53 @@ dependencies = [ "zmij", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" version = "2.0.119" @@ -568,6 +774,34 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -594,9 +828,16 @@ checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom", "js-sys", + "serde_core", "wasm-bindgen", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" diff --git a/Cargo.toml b/Cargo.toml index 632801c..765094a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "Checkers" +name = "checkers" description = "Game of checkers" version = "0.0.1" readme = "README.md" @@ -12,10 +12,11 @@ edition = "2024" [dependencies] env_logger = "0.11.11" log = "0.4.33" +postcard = { version = "1.1.3", features = ["alloc"] } rand = "0.10.2" +serde = { version = "1.0.229", features = ["derive"] } +tokio = { version = "1.53.1", features = ["full"] } [dependencies.uuid] version = "1.24.0" -features = [ - "v4", -] +features = ["serde", "v4"] diff --git a/README.md b/README.md index 4cc69c8..0f64eb4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ -# Game of checkers +# Game of checkers online +Currently WIP + +# Architecture +## Game logic +Game logic is handled through board and game-master structs. It's self-contained + +## Network +This project has network server and client written with tokio. + +## Example clients +In `src/bin/client.rs` there is an example client in cli. It works for now, but in the end there will be also a gui client. Anyone can write their own clients, due to the way it's written, tho. + +# Thoughts +Server is self-contained, so frontend can be written in: +- ratatui? +- raylib bindings for rust +- egui +- macroquad +- cli lol (already kinda happening) diff --git a/src/bin/client.rs b/src/bin/client.rs new file mode 100644 index 0000000..90d9eec --- /dev/null +++ b/src/bin/client.rs @@ -0,0 +1,295 @@ +use std::{ + fmt::Display, + io::{self, Write}, +}; + +use checkers::{ + logic::board::pawn::{CapturePath, MovePath}, + network::{ + client::{Client, ClientData}, + network_identity::NetworkIdentity, + }, +}; +use log::{debug, error, info, warn}; +use tokio::sync::mpsc; +use uuid::Uuid; + +const QUIT_COMMAND: &str = "/quit"; +const HELP_COMMAND: &str = "/help"; +const SEND_COMMAND: &str = "/send"; +const READY_COMMAND: &str = "/ready"; + +const CAPTURE_COMMAND: &str = "/capture"; +const MOVE_COMMAND: &str = "/move"; + +enum CliCommands { + Quit, + Help, + Send, + Ready, + + Capture, + Move, +} + +#[derive(Debug, PartialEq)] +struct UnknownCommandError; + +impl Display for UnknownCommandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Unknown command!") + } +} + +impl TryFrom<&str> for CliCommands { + type Error = UnknownCommandError; + + fn try_from(value: &str) -> Result { + match value { + QUIT_COMMAND => Ok(CliCommands::Quit), + HELP_COMMAND => Ok(CliCommands::Help), + SEND_COMMAND => Ok(CliCommands::Send), + READY_COMMAND => Ok(CliCommands::Ready), + + CAPTURE_COMMAND => Ok(CliCommands::Capture), + MOVE_COMMAND => Ok(CliCommands::Move), + _ => Err(UnknownCommandError), + } + } +} + +struct ClientContext { + pub identity: Option, + pub is_my_turn: bool, + + pub available_captures: Option>, + pub available_moves: Option>, +} + +impl Default for ClientContext { + fn default() -> Self { + Self { + identity: None, + is_my_turn: false, + + available_captures: None, + available_moves: None, + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let Some(mut client) = Client::new().await else { + println!("Not worky, ending"); + return Ok(()); + }; + + let (client_sender, mut client_receiver) = mpsc::channel(1024); + + client.set_update_sender(client_sender); + client.update(); + + let (stdin_sender, mut stdin_receiver) = mpsc::channel(1024); + + let mut context: ClientContext = ClientContext::default(); + + tokio::task::spawn_blocking(move || { + let stdin = std::io::stdin(); + + loop { + let mut buffer = String::new(); + if stdin.read_line(&mut buffer).is_ok() { + if stdin_sender.blocking_send(buffer).is_err() { + break; + } + } + } + }); + + let help_msg: String = format!( + "\n┌────Help──────────────────────────── +│\t {} - quit +│\t {} - show this message +│\t {} - message the server you're ready +│\t {} - send text message to the server +│ +│\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 + ); + + info!("───────Type /help for help───────"); + loop { + tokio::select! { + msg = client_receiver.recv() => { + match msg { + Some(ClientData::GameStart { + identity, + }) => { + info!( + "Got new identity: {}", + identity + ); + context.identity = Some(identity); + } + + Some(ClientData::BoardView(view)) => { + info!("New board state:\n{}", view.to_string(&context.identity.as_ref().unwrap().id)); + + } + + Some(ClientData::CurrentTurn(current_turn)) => { + context.is_my_turn = current_turn == context.identity.as_ref().expect("Server sent us new current turn without telling us who we are!").id; + if context.is_my_turn { + info!("It's our turn!"); + } else { + info!("It's the turn of our enemy!"); + } + } + + Some(ClientData::AvailableCaptures(capture_paths)) => { + info!("Available captures paths: {:?}", capture_paths); + context.available_moves = None; + context.available_captures = Some(capture_paths); + } + + Some(ClientData::AvailableMoves(move_paths)) => { + info!("Available moves: {:?}", move_paths); + context.available_moves = Some(move_paths); + context.available_captures = None; + }, + + Some(ClientData::TextMessage {sender, content}) => { + info!("[{}] - {}", sender, content); + } + + Some(ClientData::GameEnd(game_result)) => { + info!("Game has ended with the result: {:?}", game_result); + } + + None => { + warn!("Connection broken! Stopping"); + break Ok(()); + } + } + } + + buffer_opt = stdin_receiver.recv() => { + if let Some(mut buffer) = buffer_opt { + buffer.pop(); // remove \n + + if !buffer.is_empty() { + let words: Vec<&str> = buffer.split_whitespace().collect(); + let cmd = words[0]; + + match CliCommands::try_from(cmd) { + Ok(cmd) => match cmd { + CliCommands::Quit => { + info!("Bye bye"); + std::process::exit(0); + } + CliCommands::Help => { + info!("{}", help_msg); + } + CliCommands::Ready => { + client.signal_readiness().await; + } + CliCommands::Send => { + let mut args: String = words[1..].join(" "); + args = args.trim().to_string(); + client.send_text_message(args).await; + } + + CliCommands::Capture => { + if words.len() < 2 { + error!("Too little arguments!"); + continue; + } + + let arg = words[1]; + + match arg.parse::() { + Ok(index) => { + if let Some(captures) = context.available_captures.as_ref() && context.is_my_turn { + if index >= captures.len() { + error!("Cannot capture path that doesn't exist!"); + continue; + } + + if !context.is_my_turn { + error!("We cannot capture when it's not our turn!"); + } + + client.send_capture(captures[index].clone()).await; + } + }, + Err(_) => { + error!("Invalid input! Use: /capture , where index is the number from capture_path"); + continue; + } + } + }, + CliCommands::Move => { + if words.len() < 3 { + error!("Too little arguments provided!"); + continue; + } + + let arg = words[1]; + let move_arg = words[2]; + + + match arg.parse::() { + Ok(index) => { + if let Some(moves) = context.available_moves.as_ref() && context.is_my_turn { + if index >= moves.len() { + error!("Cannot make a move that doesn't exist!"); + continue; + } + + if !context.is_my_turn { + error!("We cannot make a move when it's not our turn!"); + } + + let move_path = moves[index].clone(); + + match move_arg.parse::() { + Ok(move_index) => { + if move_index >= move_path.available_steps.len() { + error!("Second index out of range!"); + continue; + } + + client.send_move(move_path.from, move_path.available_steps[move_index]).await; + }, + Err(_) => { + error!("Invalid second index. Use: /move , where first index is the pawn you want to move and second index is position you want it to take"); + continue; + } + } + + } + }, + Err(_) => { + error!("Invalid input! Use: /move , where first index is the pawn you want to move and second index is position you want it to take"); + continue; + } + } + + }, + }, + Err(e) => error!("{}", e), + } + } + print!("> "); + io::stdout().flush()?; + } + } + } + } +} diff --git a/src/bin/server.rs b/src/bin/server.rs new file mode 100644 index 0000000..2c1ab1d --- /dev/null +++ b/src/bin/server.rs @@ -0,0 +1,13 @@ +use checkers::network::server::Server; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut server: Server = Server::new().await; + + env_logger::init(); + + server.start().await; + server.update().await; + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8b83056 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod logic; +pub mod network; diff --git a/src/logic/board/board.rs b/src/logic/board/board.rs index c6c8b65..b5eda57 100644 --- a/src/logic/board/board.rs +++ b/src/logic/board/board.rs @@ -11,6 +11,9 @@ use std::ops::{Index, IndexMut}; use log::{debug, error, info, trace, warn}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Board { board: Vec, pub size: usize, @@ -69,6 +72,12 @@ impl Board { Board { board, size } } + /// Returns the clone of the board inner vec. Meant for `BoardView`. This vec shouldn't be + /// manipulated by hand + pub fn get_board(&self) -> Vec { + self.board.clone() + } + /// For testing fn new_empty(player1: &mut Player, player2: &mut Player, size: Option) -> Self { debug!("Creating new empty board!"); @@ -524,8 +533,8 @@ mod tests { fn new_board_test() { init_logger(); - let mut player1: Player = Player::new("Morbius".to_string()); - let mut player2: Player = Player::new("Milo".to_string()); + let mut player1: Player = Player::new("Morbius"); + let mut player2: Player = Player::new("Milo"); let test_board: Board = Board::new(&mut player1, &mut player2, None); let size: usize = test_board.size; @@ -572,8 +581,8 @@ mod tests { fn test_overloaded_operators() { init_logger(); - let mut player1: Player = Player::new("Morbius".to_string()); - let mut player2: Player = Player::new("Milo".to_string()); + let mut player1: Player = Player::new("Morbius"); + let mut player2: Player = Player::new("Milo"); let test_board: Board = Board::new(&mut player1, &mut player2, None); @@ -604,8 +613,8 @@ mod tests { fn test_moving_pawn() { init_logger(); - let mut player1: Player = Player::new("Morbius".to_string()); - let mut player2: Player = Player::new("Milo".to_string()); + let mut player1: Player = Player::new("Morbius"); + let mut player2: Player = Player::new("Milo"); let mut test_board: Board = Board::new(&mut player1, &mut player2, None); @@ -639,8 +648,8 @@ mod tests { fn test_getting_pawn_moves() { init_logger(); - let mut p1: Player = Player::new("Morbius".to_string()); - let mut p2: Player = Player::new("Milo".to_string()); + let mut p1: Player = Player::new("Morbius"); + let mut p2: Player = Player::new("Milo"); let mut board: Board = Board::new_empty(&mut p1, &mut p2, None); @@ -673,8 +682,8 @@ mod tests { fn test_getting_player_pawns_positions() { init_logger(); - let mut p1: Player = Player::new("Morbius".to_string()); - let mut p2: Player = Player::new("Milo".to_string()); + let mut p1: Player = Player::new("Morbius"); + let mut p2: Player = Player::new("Milo"); let test_board: Board = Board::new(&mut p1, &mut p2, None); let expected_positions: Vec = (0..test_board.size) @@ -704,8 +713,8 @@ mod tests { fn test_is_movable() { init_logger(); - let mut p1: Player = Player::new("Morbius".to_string()); - let mut p2: Player = Player::new("Milo".to_string()); + let mut p1: Player = Player::new("Morbius"); + let mut p2: Player = Player::new("Milo"); let test_board: Board = Board::new(&mut p1, &mut p2, None); assert!(test_board.is_pawn_movable(Position { row: 2, column: 1 }, &p1)); @@ -723,8 +732,8 @@ mod tests { fn test_getting_captures_path() { init_logger(); - let mut p1 = Player::new("Morbius".to_string()); - let mut p2 = Player::new("Milo".to_string()); + let mut p1 = Player::new("Morbius"); + let mut p2 = Player::new("Milo"); let mut board = Board::new_empty(&mut p1, &mut p2, None); @@ -755,8 +764,8 @@ mod tests { fn test_capturing() { init_logger(); - let mut p1 = Player::new("Morbius".to_string()); - let mut p2 = Player::new("Milo".to_string()); + let mut p1 = Player::new("Morbius"); + let mut p2 = Player::new("Milo"); let mut board = Board::new_empty(&mut p1, &mut p2, None); diff --git a/src/logic/board/board_view.rs b/src/logic/board/board_view.rs new file mode 100644 index 0000000..8e0e14b --- /dev/null +++ b/src/logic/board/board_view.rs @@ -0,0 +1,63 @@ +use std::ops::Index; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::logic::board::{board::Board, field::Field, pawn::Pawn}; + +/// View of the board, without any methods. Meant for sending over network +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct BoardView { + pub board: Vec, + pub size: usize, +} + +impl From for BoardView { + fn from(value: Board) -> Self { + Self { + board: value.get_board(), + size: value.size, + } + } +} + +impl Index for BoardView { + type Output = [Field]; + + fn index<'a>(&'a self, i: usize) -> &'a [Field] { + let start = i * &self.size; + let end: usize = start + &self.size; + &self.board[start..end] + } +} + +impl BoardView { + pub fn to_string(&self, owner: &Uuid) -> String { + (0..self.size) + .map(move |row| { + (0..self.size) + .map(move |column| match &self[row][column].pawn { + Some(pawn) => match pawn.state { + super::pawn::PawnState::Man => { + if pawn.owner == *owner { + 'm' + } else { + 'M' + } + } + super::pawn::PawnState::Dame => { + if pawn.owner == *owner { + 'd' + } else { + 'D' + } + } + }, + None => ' ', + }) + .collect::() + }) + .collect::>() + .join("\n") + } +} diff --git a/src/logic/board/field.rs b/src/logic/board/field.rs index 592bfe4..7ee3a18 100644 --- a/src/logic/board/field.rs +++ b/src/logic/board/field.rs @@ -1,8 +1,10 @@ use std::fmt; +use serde::{Deserialize, Serialize}; + use crate::logic::{board::pawn::Pawn, math::position::Position}; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct Field { pub position: Position, pub pawn: Option, diff --git a/src/logic/board/mod.rs b/src/logic/board/mod.rs index 079d7f1..81d9a95 100644 --- a/src/logic/board/mod.rs +++ b/src/logic/board/mod.rs @@ -1,3 +1,4 @@ pub mod board; +pub mod board_view; mod field; pub mod pawn; diff --git a/src/logic/board/pawn.rs b/src/logic/board/pawn.rs index 4ff18a8..548bd78 100644 --- a/src/logic/board/pawn.rs +++ b/src/logic/board/pawn.rs @@ -1,16 +1,16 @@ +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::logic::{math::position::Position, player::Player}; -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub enum PawnState { Man, Dame, - Captured, } /// Basic pawn -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct Pawn { pub state: PawnState, pub owner: Uuid, @@ -25,6 +25,7 @@ impl Pawn { } } +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] pub struct CapturePath { pub from: Position, pub steps: Vec, @@ -39,7 +40,7 @@ impl CapturePath { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] pub struct MovePath { pub from: Position, pub available_steps: Vec, diff --git a/src/logic/game_master.rs b/src/logic/game_master.rs index ba5fb8a..bbcd997 100644 --- a/src/logic/game_master.rs +++ b/src/logic/game_master.rs @@ -1,14 +1,18 @@ use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::logic::{ board::{ board::Board, + board_view::BoardView, pawn::{CapturePath, MovePath}, }, math::position::Position, player::Player, }; +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] pub enum GameResult { Lost(uuid::Uuid), Draw, @@ -21,7 +25,7 @@ struct PlayerList { } impl PlayerList { - pub fn new(player1_name: String, player2_name: String) -> Self { + pub fn new(player1_name: &str, player2_name: &str) -> Self { PlayerList { players: [Player::new(player1_name), Player::new(player2_name)], current_turn: 0, @@ -48,7 +52,7 @@ pub struct GameMaster { } impl GameMaster { - pub fn new(player1_name: String, player2_name: String) -> Self { + pub fn new(player1_name: &str, player2_name: &str) -> Self { let mut players: PlayerList = PlayerList::new(player1_name, player2_name); let [player1, player2] = &mut players.players; @@ -57,6 +61,23 @@ impl GameMaster { GameMaster { players, board } } + pub fn get_players_names_and_ids(&self) -> Vec<(String, Uuid)> { + let mut result: Vec<(String, Uuid)> = vec![]; + for player in &self.players.players { + result.push((player.name.clone(), player.id)); + } + + result + } + + pub fn get_board_view(&self) -> BoardView { + BoardView::from(self.board.clone()) + } + + pub fn get_current_turn(&self) -> Uuid { + self.players.get_current_turn().id + } + pub fn get_current_player_captures(&self) -> Vec { self.board .get_player_pawns_positions(self.players.get_current_turn()) @@ -165,7 +186,7 @@ mod tests { use rand::seq::IndexedRandom; fn run_game() { - let mut master: GameMaster = GameMaster::new("Morbius".to_string(), "Milo".to_string()); + let mut master: GameMaster = GameMaster::new("Morbius", "Milo"); let mut turns = 0; let max_turns = 2000; diff --git a/src/logic/math/position.rs b/src/logic/math/position.rs index 6188ca9..5b12c47 100644 --- a/src/logic/math/position.rs +++ b/src/logic/math/position.rs @@ -2,10 +2,11 @@ use core::fmt; use std::ops::Sub; use log::{debug, trace}; +use serde::{Deserialize, Serialize}; use crate::logic::math::vector::Vector2D; -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] /// Position on the board. Consists of row and column. Should be treated like a point in space on /// unsigned grid. /// @@ -31,7 +32,9 @@ impl Position { trace!("Adding row: {} + {} = {}", self.row, delta.row, new_row); trace!( "Adding column: {} + {} = {}", - self.column, delta.column, new_column + self.column, + delta.column, + new_column ); Some(Position { diff --git a/src/logic/mod.rs b/src/logic/mod.rs index bb9dc31..c09da7c 100644 --- a/src/logic/mod.rs +++ b/src/logic/mod.rs @@ -1,4 +1,4 @@ -mod board; +pub mod board; pub mod game_master; -mod math; +pub mod math; mod player; diff --git a/src/logic/player.rs b/src/logic/player.rs index 6036a0f..307ef54 100644 --- a/src/logic/player.rs +++ b/src/logic/player.rs @@ -12,9 +12,9 @@ pub struct Player { } impl Player { - pub fn new(name: String) -> Self { + pub fn new(name: &str) -> Self { Player { - name, + name: name.to_string(), id: Uuid::new_v4(), end_row: None, vertical_direction: None, diff --git a/src/main.rs b/src/main.rs index 59c15eb..7da3a48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod logic; fn main() { println!("Hello, world!"); diff --git a/src/network/client.rs b/src/network/client.rs new file mode 100644 index 0000000..0ef12c5 --- /dev/null +++ b/src/network/client.rs @@ -0,0 +1,292 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use log::{debug, error, info, warn}; +use tokio::sync::mpsc::{self, Receiver, Sender}; +use uuid::Uuid; + +use crate::{ + logic::{ + board::{ + board_view::BoardView, + pawn::{CapturePath, MovePath}, + }, + game_master::GameResult, + math::position::Position, + }, + network::{ + connection::{Connection, ConnectionType}, + message::{ + ClientMessage::{self, SignalReadiness, TextMessage}, + Message, ServerMessage, + }, + network_identity::NetworkIdentity, + }, +}; + +#[derive(Clone)] +pub struct ClientSettings { + server_url: SocketAddr, + name: String, +} + +pub enum ClientCommands { + SendCapture(CapturePath), + SendMove { from: Position, to: Position }, + SendText(String), + SendReady, +} + +/// Data that network client gives to normal client. I have no better idea for this name, hence this +/// comment +pub enum ClientData { + GameStart { identity: NetworkIdentity }, + GameEnd(GameResult), + + AvailableCaptures(Vec), + AvailableMoves(Vec), + + BoardView(BoardView), + CurrentTurn(Uuid), + + TextMessage { sender: String, content: String }, +} + +pub struct Client { + conn_reciever_incoming: Option)>>, + conn_sender_outgoing: Sender>, + settings: ClientSettings, + + commands_sender: Option>, + commands_receiver: Option>, + + update_sender: Option>, +} + +impl Client { + pub async fn new() -> Option { + let settings = ClientSettings { + server_url: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6767), + name: "Morbius".into(), + }; + + let (conn_sender, 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 (commands_sender, commands_receiver) = mpsc::channel(1024); + + if !conn.connect_to_server(settings.server_url).await { + error!("Could not connect with the server: {}", settings.server_url); + return None; + } + + tokio::spawn(async move { + conn.start_listening().await; + }); + + Some(Client { + conn_reciever_incoming: Some(conn_reciever), + conn_sender_outgoing, + + settings, + + commands_receiver: Some(commands_receiver), + commands_sender: Some(commands_sender), + + update_sender: None, + }) + } + + pub fn get_commands_sender(&mut self) -> Option> { + if self.commands_sender.is_none() { + warn!("Tried taking commands sender a second time! This shouldn't happen"); + } + self.commands_sender.take() + } + + pub fn set_update_sender(&mut self, sender: Sender) { + if self.update_sender.is_some() { + warn!("Cannot set update sender a second time!"); + return; + } + + self.update_sender = Some(sender); + } + + 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 Some(update_sender) = self.update_sender.take() else { + error!("Update sender is missing. Cannot update!"); + return; + }; + + let Some(mut commands_receiver) = self.commands_receiver.take() else { + error!("Commands receiver is missing! Cannot update."); + return; + }; + + let settings = self.settings.clone(); + + tokio::spawn(async move { + loop { + tokio::select! { + oldest_msg = conn_reciever_incoming.recv() => { + if let Some(oldest_msg) = oldest_msg { + let (addr, msg) = oldest_msg; + debug!("Got message from: {:?}", addr); + + match msg.content { + ServerMessage::RequestHandshake => { + info!("Server requested handshake!"); + let msg = Message::new(ClientMessage::AnswerHandshake { + player_name: settings.name.clone(), + }); + + let _ = conn_sender.send(msg.unwrap()).await; + } + + ServerMessage::AcceptHandshake => { + info!("Server accepted us! Yay :D"); + } + + ServerMessage::DeclineHandshake { reason } => { + error!("Server declined connection: {}", reason); + break; + } + + ServerMessage::GameStart { + identity, + } => { + debug!("Got GameStart message! Identity: {}", identity); + let _ = update_sender.send(ClientData::GameStart { identity }).await; + } + + ServerMessage::BroadcastCurrentTurn { active_player } => { + let _ = update_sender.send(ClientData::CurrentTurn(active_player)).await; + }, + ServerMessage::BroadcastBoardState { board } => { + let _ = update_sender.send(ClientData::BoardView(board)).await; + }, + + ServerMessage::BroadCastTextMessage {sender, content} => { + let _ = update_sender.send(ClientData::TextMessage {sender, content}).await; + } + + + ServerMessage::AvailableCaptures { captures } => { + let _ = update_sender.send(ClientData::AvailableCaptures(captures)).await; + }, + + ServerMessage::AvailableMoves { moves } => { + let _ = update_sender.send(ClientData::AvailableMoves(moves)).await; + }, + + ServerMessage::GameEnd { result } => { + let _ = update_sender.send(ClientData::GameEnd(result)).await; + }, + } + } else { + error!("Connection broken!"); + break; + } + } + + oldest_cmd = commands_receiver.recv() => { + if let Some(oldest_cmd) = oldest_cmd { + match oldest_cmd { + ClientCommands::SendCapture(capture_path) => { + debug!("Sending capture path: {:?}", capture_path); + let msg = Message::new(ClientMessage::RequestCapture { capture_path }); + send_message(msg, &conn_sender).await; + }, + + ClientCommands::SendMove { from, to } => { + debug!("Sending move from: {} to: {}", from, to); + let msg = Message::new(ClientMessage::RequestMove { from, to}); + send_message(msg, &conn_sender).await; + }, + + ClientCommands::SendText(content) => { + debug!("Seding text message: {}", content); + let msg = Message::new(TextMessage(content)); + send_message(msg, &conn_sender).await; + }, + + ClientCommands::SendReady => { + debug!("Signaling readiness!"); + let msg = Message::new(SignalReadiness); + send_message(msg, &conn_sender).await; + }, + } + } else { + error!("Couldn't decipher the command!"); + break; + } + } + + } + } + }); + } + + pub async fn send_capture(&mut self, capture: CapturePath) { + debug!("Requesting sending capture: {:?}", capture); + if let Some(sender) = &self.commands_sender { + let _ = sender.send(ClientCommands::SendCapture(capture)).await; + } else { + error!("Command sender not set!"); + } + } + + pub async fn send_move(&mut self, from: Position, to: Position) { + debug!("Requested sending move from: {} to: {}", from, to); + if let Some(sender) = &self.commands_sender { + let _ = sender.send(ClientCommands::SendMove { from, to }).await; + } else { + error!("Command sender not set!"); + } + } + + pub async fn send_text_message(&mut self, content: String) { + if let Some(sender) = &self.commands_sender { + let _ = sender.send(ClientCommands::SendText(content)).await; + } else { + error!("Command sender not set!"); + } + } + + pub async fn signal_readiness(&mut self) { + if let Some(sender) = &self.commands_sender { + let _ = sender.send(ClientCommands::SendReady).await; + } else { + error!("Command sender not set!"); + } + } +} + +async fn send_message( + msg: Result, postcard::Error>, + sender: &Sender>, +) { + debug!("Sending message to the server!"); + let _ = match msg { + Err(e) => { + error!("There was an error while creating the message: {}", e); + return; + } + Ok(msg) => { + if let Err(e) = sender.send(msg).await { + error!("Error while sending a message to connection thread: {}", e); + return; + } + } + }; +} diff --git a/src/network/connection.rs b/src/network/connection.rs new file mode 100644 index 0000000..7d58775 --- /dev/null +++ b/src/network/connection.rs @@ -0,0 +1,222 @@ +use std::io; +use std::marker::PhantomData; +use std::net::SocketAddr; + +use log::{debug, error, info}; +use tokio::sync::mpsc::Receiver; +use tokio::{net::TcpStream, sync::mpsc::Sender}; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::network::message::{Message, MessageLike}; + +/// Enum defines __TO WHAT__ connection is connecting +#[derive(Debug)] +pub enum ConnectionType { + Server, + Client, +} + +#[derive(Debug)] +pub struct Connection +where + Inbound: MessageLike, + Outbound: MessageLike, +{ + conn_type: ConnectionType, + sender: Sender<(SocketAddr, Message)>, + reciever: Option>>, + tcp: Option, + peer: Option, + outbound_type: PhantomData, +} + +impl Connection +where + Inbound: MessageLike, + Outbound: MessageLike, +{ + pub fn new( + conn_type: ConnectionType, + sender: Sender<(SocketAddr, Message)>, + ) -> Connection { + Connection { + conn_type, + sender, + reciever: None, + tcp: None, + peer: None, + outbound_type: PhantomData, + } + } + + /// Meant for clients + pub async fn connect_to_server(&mut self, url: SocketAddr) -> bool { + match self.conn_type { + ConnectionType::Server => { + self.tcp = match TcpStream::connect(url).await { + Ok(conn) => { + self.peer = Some(url); + Some(conn) + } + Err(e) => { + error!("Error while connecting to the server: {}", e); + None + } + }; + + self.tcp.is_some() + } + ConnectionType::Client => false, // only client can connect to the server + } + } + + /// Meant for server + pub fn delegate(&mut self, tcp: TcpStream, client_addr: SocketAddr) -> bool { + match self.conn_type { + ConnectionType::Server => false, // only server can delegate + ConnectionType::Client => { + self.peer = Some(client_addr); + self.tcp = Some(tcp); + true + } + } + } + + pub fn get_sender(&mut self) -> Sender> { + let (sender, reciever) = tokio::sync::mpsc::channel(1024); + self.reciever = Some(reciever); + sender + } + + async fn send(&mut self, msg: Message) { + let size = msg.size; + let content = msg.content; + + let Some(tcp) = &mut self.tcp else { + error!("Tcp connection is not established! Cannot send"); + return; + }; + + if let Err(e) = tcp.write_all(&size.to_be_bytes()).await { + error!("Error while sending header to: {:?}\n{}", self.peer, e); + return; + } + + let Ok(msg) = content.to_bits() else { + error!("Cannot change message into bytes while sending!"); + return; + }; + + if let Err(e) = tcp.write_all(&msg).await { + error!("Error while sending content to: {:?}\n{}", self.peer, e); + return; + }; + } + + pub async fn start_listening(&mut self) { + let mut receiver = self + .reciever + .take() + .expect("Tried listening, but get_sender wasn't called"); + + loop { + tokio::select! { + result = self.read_header() => { + if let Err(e) = result { + error!( + "Connection broken with {:?}: {}. Disconnecting...", + self.peer, e + ); + self.tcp = None; + break; + } + }, + + msg_opt = receiver.recv() => { + match msg_opt { + Some(msg) => { + self.send(msg).await; + }, + None => { + info!("Remote closed the channel for {:?}. Disconnecting...", self.peer); + self.tcp = None; + break; + } + } + } + } + } + + self.reciever = Some(receiver); + } + + async fn read_header(&mut self) -> Result<(), io::Error> { + let mut buff = [0u8; 4]; + + let tcp = self.tcp.as_mut().ok_or(io::ErrorKind::NotConnected)?; + + tcp.read_exact(&mut buff).await?; + + let size_to_read: u32 = u32::from_be_bytes(buff); + + self.read_content(size_to_read).await + } + + async fn read_content(&mut self, size_to_read: u32) -> Result<(), io::Error> { + let max_size = 1024 * 1024 * 2; + + if size_to_read > max_size { + error!( + "Message sent exeeds the limit. Sent {}, max: {}", + size_to_read, max_size + ); + self.tcp = None; + return Err(io::ErrorKind::InvalidData.into()); + } + + let mut buff: Vec = vec![0; size_to_read as usize]; + + let Some(tcp) = &mut self.tcp else { + error!( + "Tried reading content, but tcp connection is not established! This should __never__ happen" + ); + panic!(); + }; + + let bytes_read = tcp.read_exact(&mut buff).await?; + debug!("Read {} bytes during read_content", bytes_read); + + let msg_result: Result = MessageLike::from_bits(&buff); + + match msg_result { + Ok(msg_content) => { + let new_msg = Message::new(msg_content); + + match new_msg { + Ok(msg) => { + if let Err(e) = self + .sender + .send((self.peer.expect("Couldn't get peer addr"), msg)) + .await + { + error!("Erro while sending message to main thread: {}", e); + } + return Ok(()); + } + Err(e) => { + error!( + "Error while preparing message in read_content for sender: {}", + e + ); + return Ok(()); + } + } + } + Err(e) => { + error!("Error while getting the message from bits: {}", e); + return Ok(()); + } + } + } +} diff --git a/src/network/message.rs b/src/network/message.rs new file mode 100644 index 0000000..61e6f12 --- /dev/null +++ b/src/network/message.rs @@ -0,0 +1,140 @@ +use std::net::SocketAddr; + +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use uuid::Uuid; + +use crate::{ + logic::{ + board::{ + board::Board, + board_view::BoardView, + pawn::{CapturePath, MovePath}, + }, + game_master::GameResult, + math::position::Position, + }, + network::network_identity::NetworkIdentity, +}; + +use postcard::{from_bytes, to_allocvec}; + +pub trait MessageLike { + fn to_bits(&self) -> Result, postcard::Error>; + fn from_bits(bits: &[u8]) -> Result + where + Self: Sized; +} + +impl MessageLike for T +where + T: DeserializeOwned + Serialize, +{ + fn to_bits(&self) -> Result, postcard::Error> { + to_allocvec(self) + } + + fn from_bits(bits: &[u8]) -> Result { + from_bytes(bits) + } +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +pub enum ServerMessage { + RequestHandshake, + AcceptHandshake, + DeclineHandshake { reason: String }, + + GameStart { identity: NetworkIdentity }, + + AvailableCaptures { captures: Vec }, + AvailableMoves { moves: Vec }, + + BroadcastBoardState { board: BoardView }, + BroadcastCurrentTurn { active_player: Uuid }, + + BroadCastTextMessage { sender: String, content: String }, + + GameEnd { result: GameResult }, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +pub enum ClientMessage { + AnswerHandshake { player_name: String }, + SignalReadiness, + + ConnectionDead { addr: SocketAddr }, + + RequestCapture { capture_path: CapturePath }, + RequestMove { from: Position, to: Position }, + + TextMessage(String), +} + +#[derive(Clone)] +pub struct Message +where + T: MessageLike, +{ + pub size: u32, + pub content: T, +} + +impl Message +where + T: MessageLike, +{ + pub fn new(content: T) -> Result, postcard::Error> { + let size: usize = content.to_bits()?.len(); + + Ok(Message { + size: size as u32, + content, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_server_message_serde() { + let server_msg_content: ServerMessage = ServerMessage::RequestHandshake; + let msg: Message = + Message::new(server_msg_content.clone()).expect("Couldn't serialize message"); + + assert_eq!(1, msg.size); + + let bits = msg + .content + .to_bits() + .expect("Could not convert message content to bits!"); + + let recovered_message: ServerMessage = + MessageLike::from_bits(&bits).expect("Could not recover the message"); + + assert_eq!(recovered_message, server_msg_content); + } + + #[test] + fn test_client_message_serde() { + let from: Position = Position { row: 0, column: 0 }; + let to: Position = Position { row: 1, column: 1 }; + + let client_msg_content: ClientMessage = ClientMessage::RequestMove { from, to }; + + let msg = Message::new(client_msg_content.clone()).expect("Could not serialize message"); + + assert_eq!(5, msg.size); + + let bits = msg + .content + .to_bits() + .expect("Could not convert the message into bits!"); + + let recovered_message: ClientMessage = + MessageLike::from_bits(&bits).expect("Could not recover the message!"); + + assert_eq!(recovered_message, client_msg_content); + } +} diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 0000000..47fa7b1 --- /dev/null +++ b/src/network/mod.rs @@ -0,0 +1,5 @@ +pub mod client; +pub mod connection; +pub mod message; +pub mod network_identity; +pub mod server; diff --git a/src/network/network_identity.rs b/src/network/network_identity.rs new file mode 100644 index 0000000..02ace06 --- /dev/null +++ b/src/network/network_identity.rs @@ -0,0 +1,51 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::Sender; + +use crate::network::message::{Message, ServerMessage}; + +#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)] +pub struct NetworkIdentity { + pub name: String, + pub id: uuid::Uuid, + pub is_ready: bool, +} + +impl Display for NetworkIdentity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "(Name: {}, id: {})", self.name, self.id) + } +} + +impl Default for NetworkIdentity { + fn default() -> Self { + Self { + name: String::new(), + id: uuid::Uuid::nil(), + is_ready: false, + } + } +} + +pub struct NetworkServerIdentity { + // this feels silly but i have no better idea rn (i've been + // writing this since 10 a.m. and it's 9 p.m. already) + pub identity: NetworkIdentity, + pub sender: Sender>, +} + +impl NetworkServerIdentity { + pub fn new(sender: Sender>) -> Self { + Self { + identity: NetworkIdentity::default(), + sender, + } + } +} + +impl AsMut for NetworkServerIdentity { + fn as_mut(&mut self) -> &mut NetworkIdentity { + &mut self.identity + } +} diff --git a/src/network/server.rs b/src/network/server.rs new file mode 100644 index 0000000..d9e8137 --- /dev/null +++ b/src/network/server.rs @@ -0,0 +1,490 @@ +use log::{debug, error, info, warn}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, SocketAddr}, +}; +use tokio::{ + net::TcpListener, + sync::mpsc::{Receiver, Sender}, +}; +use uuid::Uuid; + +use crate::{ + logic::{ + board::{board_view::BoardView, pawn::CapturePath}, + game_master::{GameMaster, GameResult}, + math::position::Position, + }, + network::{ + connection::{Connection, ConnectionType}, + message::{ + ClientMessage::{self}, + Message, ServerMessage, + }, + network_identity::{NetworkIdentity, NetworkServerIdentity}, + }, +}; + +// TODO! More settings! +pub struct ServerSettings { + pub addr: SocketAddr, + pub max_connections: usize, + pub allow_spectators: bool, +} + +#[derive(PartialEq)] +enum ServerStage { + Lobby, + Game, + End, +} + +struct ServerState { + ready_count: usize, + stage: ServerStage, +} + +impl Default for ServerState { + fn default() -> Self { + Self { + ready_count: 0, + stage: ServerStage::Lobby, + } + } +} + +pub struct Server { + listener: Option, + connections: HashMap, + + reciever: Receiver<(SocketAddr, Message)>, + sender: Sender<(SocketAddr, Message)>, + + welcoming_reciever: Option>)>>, + + game_master: Option, + settings: ServerSettings, + + state: ServerState, +} + +impl Server { + pub async fn new() -> Server { + // TODO! Should be read from a file + let settings = ServerSettings { + addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6767), + max_connections: 2, + allow_spectators: false, + }; + + let (sender, reciever) = + tokio::sync::mpsc::channel::<(SocketAddr, Message)>(1024); // random + // number + + Server { + listener: Some( + TcpListener::bind(settings.addr) + .await + .expect("Could not start the server"), + ), // TODO! Handle this nicely, as this will + // crash gui app + connections: HashMap::with_capacity(2), + sender, + reciever, + welcoming_reciever: None, + game_master: None, + settings, + state: ServerState::default(), + } + } + + pub async fn start(&mut self) { + let (welcoming_tx, welcoming_rx) = + tokio::sync::mpsc::channel::<(SocketAddr, Sender>)>(10); + + self.welcoming_reciever = Some(welcoming_rx); + + let listener = self + .listener + .take() + .expect("Cannot start the server without tcp listener."); + + info!("Starting the server at: {:?}", listener.local_addr()); + + let server_sender = self.sender.clone(); + + tokio::spawn(async move { + loop { + if let Ok((socket, addr)) = listener.accept().await { + info!("New connection from: {:?}", addr); + let mut conn: Connection = + Connection::new(ConnectionType::Client, server_sender.clone()); + + conn.delegate(socket, addr); + + let sender = conn.get_sender(); + let disconnect_sender = server_sender.clone(); + let _ = welcoming_tx.send((addr, sender)).await; + + tokio::spawn(async move { + conn.start_listening().await; + + // TODO! Add waiting for reconnect + + let disconnect_msg = Message::new(ClientMessage::ConnectionDead { addr }); + + let _ = disconnect_sender + .send(( + addr, + disconnect_msg.expect( + "Could not create disconnect msg. this shouldn't happen", + ), + )) + .await; + }); + } + } + }); + } + + /// Process incoming messages + pub async fn update(&mut self) { + loop { + tokio::select! { + new_client_opt = self.welcoming_reciever.as_mut().expect("Welcoming reciever was not set. Was the server started?").recv() => { + if let Some((addr, sender)) = new_client_opt { + if self.state.stage == ServerStage::Lobby && self.connections.keys().len() < self.settings.max_connections && !self.settings.allow_spectators { + let identity = NetworkServerIdentity::new(sender); + self.connections.insert(addr, identity); + self.request_handshake(addr).await; + } else { + let reason = "There is currently game going and it doesn't allow spectators".to_string(); + info!("Rejecting {}, because {}", addr, reason); + + let msg = Message::new(ServerMessage::DeclineHandshake { reason: reason }); + + if let Ok(msg) = msg { + let _ = sender.send(msg).await; // we can't use send_message because we + // don't add addr to connections + } else { + warn!("Could not send reason of rejection. Server is acting like my ex..."); + return; + } + } + } else { + warn!("Welcoming reciever closed!"); + break; + } + } + + msg_opt = self.reciever.recv() => { + if let Some(oldest_message) = msg_opt { + let (addr, msg) = oldest_message; + + match msg.content { + ClientMessage::AnswerHandshake { player_name } => { + self.process_handshake(addr, player_name).await; + } + + ClientMessage::ConnectionDead { addr } => { + self.remove_client(addr); + }, + + ClientMessage::SignalReadiness => { + self.handle_readiness(addr).await + }, + + ClientMessage::RequestCapture { capture_path } => { + self.process_capture(capture_path).await; + } + + ClientMessage::RequestMove { from, to } => { + self.process_move(from, to).await; + } + + ClientMessage::TextMessage (content) => { + info!("{} sent us a message: {}", addr, content); + let sender = self.connections[&addr].identity.name.clone(); + self.broadcast_text_message(sender, content).await; + } + } + } else { + warn!("Couldn't get latest message!"); + break; + } + } + } + } + } + + async fn start_game(&mut self) { + info!("Starting the game!"); + let mut names: Vec = vec![]; + for networkidentity in self.connections.values() { + if networkidentity.identity.is_ready { + names.push(networkidentity.identity.name.clone()); + } + } + + if names.len() != self.settings.max_connections { + error!("Tried starting the game, but could not gather enough ready player names"); + return; + } + + let game_master = GameMaster::new(&names[0], &names[1]); + + let names_and_ids: Vec<(String, Uuid)> = game_master.get_players_names_and_ids(); + + let mut msgs: Vec<(SocketAddr, Result, postcard::Error>)> = vec![]; + + for (name, id) in names_and_ids { + for (addr, networkidentity) in self.connections.iter_mut() { + if networkidentity.identity.name == name { + networkidentity.identity.id = id; + + msgs.push(( + *addr, + Message::new(ServerMessage::GameStart { + identity: networkidentity.identity.clone(), + }), + )); + } + } + } + + for (addr, msg) in msgs { + self.send_message(addr, msg).await; + } + + self.game_master = Some(game_master); + self.on_player_action().await; + } + + /* ======= Processing messages :D helper functions ======== */ + + async fn process_handshake(&mut self, addr: SocketAddr, player_name: String) { + info!("Client: {:?} sent us his name: {}", addr, player_name); + self.send_message(addr, Message::new(ServerMessage::AcceptHandshake)) + .await; + + let fixed_name: String; + + if self + .connections + .values() + .any(|v| v.identity.name == player_name) + { + fixed_name = format!("{}(2)", player_name); + warn!( + "Player has the same name as the other player so changing it to {}", + fixed_name + ); + } else { + fixed_name = player_name; + } + + let Some(identity) = self.connections.get_mut(&addr) else { + error!("Could not get network identity while processing the handshake!"); + return; + }; + + identity.identity.name = fixed_name + } + + async fn process_capture(&mut self, capture_path: CapturePath) { + if let Some(gm) = self.game_master.as_mut() { + if gm.capture(&capture_path) { + debug!("Capture valid!"); + self.on_player_action().await; + } else { + debug!("Invalid capture!"); + } + } + } + + async fn process_move(&mut self, from: Position, to: Position) { + if let Some(gm) = self.game_master.as_mut() { + if gm.move_pawn(from, to) { + debug!("Move was valid!"); + self.on_player_action().await; + } else { + debug!("Move was invalid!"); + } + } + } + + async fn request_handshake(&mut self, addr: SocketAddr) { + let msg = Message::new(ServerMessage::RequestHandshake); + self.send_message(addr, msg).await; + } + + async fn handle_readiness(&mut self, addr: SocketAddr) { + self.state.ready_count += 1; + info!( + "{} signaled readiness. Currently ready: {}/{}", + addr, self.state.ready_count, self.settings.max_connections + ); + self.connections.get_mut(&addr).expect("Couldn't get the identity of the ready player. I guess this should never happen so i'm panicking like a little bitch").identity.is_ready = true; // TODO! This sometimes happens for some reason (when there is a game going, both player leave and someone tries to join) + + if self.state.ready_count == self.settings.max_connections { + info!("All players are ready! Changing state to ServerStage::Game"); + self.state.stage = ServerStage::Game; + self.start_game().await; + } + } + + /* === Sending messages === */ + + /// Should be used after every players move + async fn on_player_action(&mut self) { + if self.state.stage == ServerStage::Game { + self.broadcast_board_view().await; + self.broadcast_current_turn().await; + + let Some(gm) = &self.game_master else { + error!("Game master not set while player made an action!"); + return; + }; + + let captures = gm.get_current_player_captures(); + let moves = gm.get_current_player_moves(); + let current_player = gm.get_current_turn(); + + if let Some(result) = gm.get_game_result() { + info!("Game ended with result: {:?}", result); + self.handle_game_end(result).await; + return; + } + + if !captures.is_empty() { + debug!("Current captures: {:?}", captures); + self.send_player_captures(current_player, captures).await; + return; + } + + if !moves.is_empty() { + debug!("Current moves: {:?}", moves); + self.send_player_moves(current_player, moves).await; + return; + } + + panic!("on_player_action should never get here"); + } + } + + async fn send_player_captures(&mut self, current_player: Uuid, captures: Vec) { + let Some(addr) = self.get_client_by_id(current_player) else { + error!("Could not get player connection, while sending him his captures!"); + return; + }; + + let msg = Message::new(ServerMessage::AvailableCaptures { captures }); + self.send_message(*addr, msg).await; + } + + async fn send_player_moves( + &mut self, + current_player: Uuid, + moves: Vec, + ) { + let Some(addr) = self.get_client_by_id(current_player) else { + error!("Could not get player connection, while sending him his moves!"); + return; + }; + + let msg = Message::new(ServerMessage::AvailableMoves { moves }); + self.send_message(*addr, msg).await; + } + + async fn handle_game_end(&mut self, result: GameResult) { + let msg = Message::new(ServerMessage::GameEnd { result }); + self.broadcast_message(msg).await; + self.state.stage = ServerStage::End; + } + + async fn broadcast_board_view(&mut self) { + if let Some(gm) = &self.game_master { + let msg = Message::new(ServerMessage::BroadcastBoardState { + board: gm.get_board_view(), + }); + self.broadcast_message(msg).await; + } else { + error!("Cannot broadcast board view as game manager is not set!"); + } + } + + async fn broadcast_current_turn(&mut self) { + if let Some(gm) = &self.game_master { + let msg = Message::new(ServerMessage::BroadcastCurrentTurn { + active_player: gm.get_current_turn(), + }); + self.broadcast_message(msg).await; + } else { + error!("Cannot broadcast current turn as game master doesn't exists!!!!!!!!!"); + } + } + + async fn broadcast_text_message(&mut self, sender: String, content: String) { + let msg = Message::new(ServerMessage::BroadCastTextMessage { sender, content }); + self.broadcast_message(msg).await; + } + + /* === Helper methods === */ + + async fn send_message( + &mut self, + addr: SocketAddr, + msg: Result, postcard::Error>, + ) { + let conn_opt = self.connections.get_mut(&addr); + + let Some(identity) = conn_opt else { + error!("Could not get identity from connections list"); + return; + }; + + let _ = match msg { + Err(e) => { + error!("There was an error while creating message: {}", e); + return; + } + Ok(msg) => identity.sender.send(msg).await, + }; + } + + async fn broadcast_message(&mut self, msg: Result, postcard::Error>) { + match msg { + Ok(msg) => { + for identity in self.connections.values_mut() { + let _ = identity.sender.send(msg.clone()).await; + } + } + Err(e) => { + error!("Error while broadcasting the message: {}", e); + return; + } + } + } + + fn get_client_by_id(&self, id: Uuid) -> Option<&SocketAddr> { + self.connections + .iter() + .find(|(_, indentity)| indentity.identity.id == id) + .map(|(addr, _)| addr) + } + + fn remove_client(&mut self, addr: SocketAddr) { + warn!("Removing: {:?} from connection list", addr); + let is_ready = &mut self.connections.get_mut(&addr).expect("Couldn't get the identity of disconnected player. I don't think i should panic but i don't care fuck you").identity.is_ready; + + if *is_ready { + *is_ready = false; + self.state.ready_count -= 1; + info!( + "Currently {}/{} players are ready", + self.state.ready_count, self.settings.max_connections + ); + } + self.connections.remove(&addr); + } +}