Merge pull request #1 from szymon-jozef/feat/networking

feat/networking
This commit is contained in:
Szymon
2026-08-17 14:52:53 -07:00
committed by GitHub
22 changed files with 1925 additions and 55 deletions

279
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"]

View File

@@ -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)

295
src/bin/client.rs Normal file
View File

@@ -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<Self, Self::Error> {
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<NetworkIdentity>,
pub is_my_turn: bool,
pub available_captures: Option<Vec<CapturePath>>,
pub available_moves: Option<Vec<MovePath>>,
}
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<dyn std::error::Error>> {
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 {} <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
└───────────────────────────────────────
",
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::<usize>() {
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 <index>, 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::<usize>() {
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::<usize>() {
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 <index> <index>, 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 <index> <index>, 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()?;
}
}
}
}
}

13
src/bin/server.rs Normal file
View File

@@ -0,0 +1,13 @@
use checkers::network::server::Server;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut server: Server = Server::new().await;
env_logger::init();
server.start().await;
server.update().await;
Ok(())
}

2
src/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod logic;
pub mod network;

View File

@@ -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<Field>,
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<Field> {
self.board.clone()
}
/// For testing
fn new_empty(player1: &mut Player, player2: &mut Player, size: Option<usize>) -> 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<Position> = (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);

View File

@@ -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<Field>,
pub size: usize,
}
impl From<Board> for BoardView {
fn from(value: Board) -> Self {
Self {
board: value.get_board(),
size: value.size,
}
}
}
impl Index<usize> 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::<String>()
})
.collect::<Vec<String>>()
.join("\n")
}
}

View File

@@ -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<Pawn>,

View File

@@ -1,3 +1,4 @@
pub mod board;
pub mod board_view;
mod field;
pub mod pawn;

View File

@@ -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<Position>,
@@ -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<Position>,

View File

@@ -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<CapturePath> {
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;

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
mod board;
pub mod board;
pub mod game_master;
mod math;
pub mod math;
mod player;

View File

@@ -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,

View File

@@ -1,4 +1,3 @@
mod logic;
fn main() {
println!("Hello, world!");

292
src/network/client.rs Normal file
View File

@@ -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<CapturePath>),
AvailableMoves(Vec<MovePath>),
BoardView(BoardView),
CurrentTurn(Uuid),
TextMessage { sender: String, content: String },
}
pub struct Client {
conn_reciever_incoming: Option<Receiver<(SocketAddr, Message<ServerMessage>)>>,
conn_sender_outgoing: Sender<Message<ClientMessage>>,
settings: ClientSettings,
commands_sender: Option<Sender<ClientCommands>>,
commands_receiver: Option<Receiver<ClientCommands>>,
update_sender: Option<Sender<ClientData>>,
}
impl Client {
pub async fn new() -> Option<Client> {
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<ServerMessage>)>(1024);
let mut conn: Connection<ServerMessage, ClientMessage> =
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<Sender<ClientCommands>> {
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<ClientData>) {
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<Message<ClientMessage>, postcard::Error>,
sender: &Sender<Message<ClientMessage>>,
) {
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;
}
}
};
}

222
src/network/connection.rs Normal file
View File

@@ -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<Inbound, Outbound>
where
Inbound: MessageLike,
Outbound: MessageLike,
{
conn_type: ConnectionType,
sender: Sender<(SocketAddr, Message<Inbound>)>,
reciever: Option<Receiver<Message<Outbound>>>,
tcp: Option<TcpStream>,
peer: Option<SocketAddr>,
outbound_type: PhantomData<Outbound>,
}
impl<Inbound, Outbound> Connection<Inbound, Outbound>
where
Inbound: MessageLike,
Outbound: MessageLike,
{
pub fn new(
conn_type: ConnectionType,
sender: Sender<(SocketAddr, Message<Inbound>)>,
) -> Connection<Inbound, Outbound> {
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<Message<Outbound>> {
let (sender, reciever) = tokio::sync::mpsc::channel(1024);
self.reciever = Some(reciever);
sender
}
async fn send(&mut self, msg: Message<Outbound>) {
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<u8> = 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<Inbound, postcard::Error> = 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(());
}
}
}
}

140
src/network/message.rs Normal file
View File

@@ -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<Vec<u8>, postcard::Error>;
fn from_bits(bits: &[u8]) -> Result<Self, postcard::Error>
where
Self: Sized;
}
impl<T> MessageLike for T
where
T: DeserializeOwned + Serialize,
{
fn to_bits(&self) -> Result<Vec<u8>, postcard::Error> {
to_allocvec(self)
}
fn from_bits(bits: &[u8]) -> Result<Self, postcard::Error> {
from_bytes(bits)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub enum ServerMessage {
RequestHandshake,
AcceptHandshake,
DeclineHandshake { reason: String },
GameStart { identity: NetworkIdentity },
AvailableCaptures { captures: Vec<CapturePath> },
AvailableMoves { moves: Vec<MovePath> },
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<T>
where
T: MessageLike,
{
pub size: u32,
pub content: T,
}
impl<T> Message<T>
where
T: MessageLike,
{
pub fn new(content: T) -> Result<Message<T>, 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<ServerMessage> =
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);
}
}

5
src/network/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod client;
pub mod connection;
pub mod message;
pub mod network_identity;
pub mod server;

View File

@@ -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<Message<ServerMessage>>,
}
impl NetworkServerIdentity {
pub fn new(sender: Sender<Message<ServerMessage>>) -> Self {
Self {
identity: NetworkIdentity::default(),
sender,
}
}
}
impl AsMut<NetworkIdentity> for NetworkServerIdentity {
fn as_mut(&mut self) -> &mut NetworkIdentity {
&mut self.identity
}
}

490
src/network/server.rs Normal file
View File

@@ -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<TcpListener>,
connections: HashMap<SocketAddr, NetworkServerIdentity>,
reciever: Receiver<(SocketAddr, Message<ClientMessage>)>,
sender: Sender<(SocketAddr, Message<ClientMessage>)>,
welcoming_reciever: Option<Receiver<(SocketAddr, Sender<Message<ServerMessage>>)>>,
game_master: Option<GameMaster>,
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<ClientMessage>)>(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<Message<ServerMessage>>)>(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<ClientMessage, ServerMessage> =
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<String> = 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<Message<ServerMessage>, 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<CapturePath>) {
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<crate::logic::board::pawn::MovePath>,
) {
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<Message<ServerMessage>, 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<Message<ServerMessage>, 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);
}
}