diff --git a/Cargo.lock b/Cargo.lock index e8e6e05..7485743 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.5" @@ -11,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "1.0.0" @@ -76,6 +88,12 @@ dependencies = [ "critical-section", ] +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "bitflags" version = "1.3.2" @@ -94,6 +112,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + [[package]] name = "byteorder" version = "1.5.0" @@ -130,6 +154,7 @@ dependencies = [ "dirs", "env_logger", "log", + "macroquad", "postcard", "rand", "serde", @@ -147,12 +172,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -162,6 +202,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -271,12 +320,41 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fontdue" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7894823fa221401399e2598f8b63f81ac77ff5c63248b7656779bff1632d7d3d" +dependencies = [ + "hashbrown 0.15.5", + "ttf-parser", +] + [[package]] name = "futures-core" version = "0.3.34" @@ -326,6 +404,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "glam" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" + [[package]] name = "hash32" version = "0.2.1" @@ -341,6 +425,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -376,6 +462,19 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -459,6 +558,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.20" @@ -483,12 +588,63 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "macroquad" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cbd54d99d4d3ac59deed7ee4927bbbf4eb33a55635530af3be9395a55746090" +dependencies = [ + "fontdue", + "glam", + "image", + "macroquad_macro", + "miniquad", + "quad-rand", +] + +[[package]] +name = "macroquad_macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "miniquad" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbaea978d741f3b60ced666b06a2a1e4a2bc81cb8b9dd29f4a40965e1f59e65" +dependencies = [ + "libc", + "ndk-sys", + "objc-rs", + "winapi", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -500,6 +656,30 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc-rs" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8" +dependencies = [ + "malloc_buf", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -547,6 +727,19 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.15.0" @@ -594,6 +787,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quad-rand" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" + [[package]] name = "quote" version = "1.0.47" @@ -764,6 +963,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "slab" version = "0.4.12" @@ -910,6 +1115,15 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1043,6 +1257,28 @@ dependencies = [ "semver", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index a172223..3c9f58a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ edition = "2024" dirs = "6.0.0" env_logger = "0.11.11" log = "0.4.33" +macroquad = "0.4.16" postcard = { version = "1.1.3", features = ["alloc"] } rand = "0.10.2" serde = { version = "1.0.229", features = ["derive"] } diff --git a/assets/background.png b/assets/background.png new file mode 100644 index 0000000..d8aa8dc Binary files /dev/null and b/assets/background.png differ diff --git a/nix/package.nix b/nix/package.nix index 5aae574..ad6511f 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -16,11 +16,26 @@ # rustc # already provided by buildRustPackage :p pkg-config + makeWrapper ]; # runtime deps buildInputs = with pkgs; [ + wayland + wayland-protocols + libGL + alsa-lib + libX11.out + libXi.out + libXcursor.out + libXrandr.out + libxkbcommon.out + libxcb + libXinerama + libXxf86vm + + fontconfig ]; meta = { diff --git a/nix/shell.nix b/nix/shell.nix index 0e5364e..f0aceec 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,16 +1,29 @@ { - perSystem = { pkgs, config, ... }: { - devShells.default = pkgs.mkShell { - packages = with pkgs; [ - rustfmt - clippy - ]; + perSystem = + { + pkgs, + config, + ... + }: + { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + rustfmt + clippy + ]; - RUST_LOG = "debug"; + RUST_LOG = "debug"; - inputsFrom = [ - (config.packages.default) - ]; + LD_LIBRARY_PATH = builtins.concatStringsSep ":" [ + "${pkgs.libX11}/lib" + "${pkgs.libXi}/lib" + "${pkgs.libGL}/lib" + "${pkgs.libxkbcommon}/lib" + ]; + + inputsFrom = [ + (config.packages.default) + ]; + }; }; - }; } diff --git a/src/bin/client.rs b/src/bin/client.rs index c96a384..95e7585 100644 --- a/src/bin/client.rs +++ b/src/bin/client.rs @@ -15,6 +15,7 @@ const QUIT_COMMAND: &str = "/quit"; const HELP_COMMAND: &str = "/help"; const SEND_COMMAND: &str = "/send"; const READY_COMMAND: &str = "/ready"; +const UNREADY_COMMAND: &str = "/unready"; const SINGLEPLAYER_COMMAND: &str = "/single"; const CAPTURE_COMMAND: &str = "/capture"; @@ -26,6 +27,8 @@ enum CliCommands { Send, Ready, + Unready, + Single, Capture, @@ -50,6 +53,7 @@ impl TryFrom<&str> for CliCommands { HELP_COMMAND => Ok(CliCommands::Help), SEND_COMMAND => Ok(CliCommands::Send), READY_COMMAND => Ok(CliCommands::Ready), + UNREADY_COMMAND => Ok(CliCommands::Unready), SINGLEPLAYER_COMMAND => Ok(CliCommands::Single), CAPTURE_COMMAND => Ok(CliCommands::Capture), @@ -59,6 +63,7 @@ impl TryFrom<&str> for CliCommands { } } +#[derive(Default)] struct ClientContext { pub identity: Option, pub is_my_turn: bool, @@ -67,18 +72,6 @@ struct ClientContext { pub available_moves: Option>, } -impl Default for ClientContext { - fn default() -> Self { - Self { - identity: None, - is_my_turn: false, - - available_captures: None, - available_moves: None, - } - } -} - async fn run_bot() { let bot = Bot::new(BotDificulty::Easy).await; bot.game_loop().await; @@ -102,10 +95,8 @@ async fn main() -> Result<(), Box> { loop { let mut buffer = String::new(); - if stdin.read_line(&mut buffer).is_ok() { - if stdin_sender.blocking_send(buffer).is_err() { - break; - } + if stdin.read_line(&mut buffer).is_ok() && stdin_sender.blocking_send(buffer).is_err() { + break; } } }); @@ -119,6 +110,7 @@ async fn main() -> Result<(), Box> { │\t {} - quit │\t {} - show this message │\t {} - message the server you're ready +│\t {} - revoke readiness │\t {} - send text message to the server │\t {} - play alone │ @@ -126,7 +118,7 @@ async fn main() -> Result<(), Box> { │\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, SINGLEPLAYER_COMMAND , CAPTURE_COMMAND, MOVE_COMMAND + QUIT_COMMAND, HELP_COMMAND, READY_COMMAND, UNREADY_COMMAND, SEND_COMMAND, SINGLEPLAYER_COMMAND , CAPTURE_COMMAND, MOVE_COMMAND ); println!("───────Type /help for help───────"); @@ -209,6 +201,10 @@ async fn main() -> Result<(), Box> { client.signal_readiness().await; } + CliCommands::Unready => { + client.revoke_readiness().await; + } + CliCommands::Single => { println!("Tried connecting bot to the server! Make sure to be /ready"); tokio::spawn(run_bot()); diff --git a/src/bin/macroquad_client.rs b/src/bin/macroquad_client.rs new file mode 100644 index 0000000..9b21907 --- /dev/null +++ b/src/bin/macroquad_client.rs @@ -0,0 +1,169 @@ +use checkers::{ + settings::{ + client_settings::ClientSettings, general_settings::SettingsLike, + server_settings::ServerSettings, + }, + ui::{ + macroquad::{ + difficulty_selection::draw_dificulty_selection, game::game::GameClient, + main_menu::draw_main_menu, mode_selection::draw_mode_selection, + server_selection::draw_server_selection, settings::draw_settings, + }, + state::{GameContext, GuiState}, + }, +}; +use macroquad::{ + color::{BLACK, WHITE}, + math::vec2, + miniquad::{self, conf::LinuxBackend::WaylandWithX11Fallback}, + text::draw_text, + texture::{DrawTextureParams, draw_texture_ex, load_texture}, + ui::{Skin, root_ui}, + window::{Conf, next_frame, screen_height, screen_width}, +}; + +fn window_conf() -> Conf { + Conf { + window_title: "Checkers".to_owned(), + window_resizable: true, + platform: miniquad::conf::Platform { + linux_backend: WaylandWithX11Fallback, + ..Default::default() + }, + ..Default::default() + } +} + +pub async fn get_general_style() -> Skin { + let label_style = root_ui() + .style_builder() + .text_color(WHITE) + .font_size(64) + .build(); + + let button_style = root_ui() + .style_builder() + .text_color(BLACK) + .font_size(32) + .build(); + + let checkbox_style = root_ui() + .style_builder() + .color(BLACK) + .text_color(BLACK) + .build(); + + Skin { + label_style, + button_style, + checkbox_style, + ..root_ui().default_skin() + } +} + +#[macroquad::main(window_conf)] +pub async fn main() { + env_logger::init(); + + let mut state = Default::default(); + let mut context = GameContext::default(); + + let mut client_settings = ClientSettings::new(); + let mut server_settings = ServerSettings::new(); + + let background = load_texture("assets/background.png").await.unwrap(); + + let main_menu_style = get_general_style().await; + root_ui().push_skin(&main_menu_style); + + loop { + draw_texture_ex( + &background, + 0.0, + 0.0, + WHITE, + DrawTextureParams { + dest_size: Some(vec2(screen_width(), screen_height())), + ..DrawTextureParams::default() + }, + ); + + match &mut state { + GuiState::MainMenu => { + draw_main_menu(&mut state).await; + } + + GuiState::ModeSelection => { + draw_mode_selection(&mut state, &mut context).await; + } + + GuiState::DificultySelection => { + draw_dificulty_selection(&mut state, &mut context).await; + } + + GuiState::ServerSelection => { + draw_server_selection(&mut state, &mut context).await; + } + + GuiState::Connecting(receiver) => { + draw_text("Connecting...", 0.0, 0.0, 64.0, BLACK); + + match receiver.try_recv() { + Ok(client) => { + if let Some(client) = client { + draw_text("Connected!", 0.0, 0.0, 64.0, BLACK); + let client = GameClient::new(client, &context); + if let Some(client) = client { + state = GuiState::Game(client); + } else { + // ugly ass code + draw_text("Could not connect!", 0.0, 0.0, 64.0, BLACK); + } + } else { + draw_text("Could not connect!", 0.0, 0.0, 64.0, BLACK); + } + } + Err(_) => { + draw_text("Error", 0.0, 0.0, 64.0, BLACK); + } + } + } + + GuiState::Settings => { + draw_settings( + &mut state, + &mut context, + &mut client_settings, + &mut server_settings, + ) + .await; + } + + GuiState::Game(client) => { + while !client.should_close { + draw_texture_ex( + &background, + 0.0, + 0.0, + WHITE, + DrawTextureParams { + dest_size: Some(vec2(screen_width(), screen_height())), + ..DrawTextureParams::default() + }, + ); + + client.update(); + client.draw(); + next_frame().await; + } + + state = GuiState::MainMenu; + } + + GuiState::Exit => { + break; + } + } + next_frame().await; + } +} diff --git a/src/lib.rs b/src/lib.rs index f5d0540..bb474ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,4 @@ pub mod logic; pub mod network; pub mod settings; pub mod super_advanced_ai; +pub mod ui; diff --git a/src/logic/board/board.rs b/src/logic/board/board.rs index b5eda57..e60013a 100644 --- a/src/logic/board/board.rs +++ b/src/logic/board/board.rs @@ -79,6 +79,7 @@ impl Board { } /// For testing + #[allow(dead_code)] fn new_empty(player1: &mut Player, player2: &mut Player, size: Option) -> Self { debug!("Creating new empty board!"); let size = size.unwrap_or(8); @@ -104,6 +105,7 @@ impl Board { Board { board, size } } + #[allow(dead_code)] fn place_pawn(&mut self, target: Position, owner: &Player) { debug!("Placing new pawn by hand!"); self[target].pawn = Some(Pawn::new(owner)); @@ -131,6 +133,7 @@ impl Board { pos.is_in_range(self.size) && self[pos].pawn.is_none() } + #[allow(dead_code)] fn is_pawn_movable(&self, pos: Position, player: &Player) -> bool { self.get_available_moves(pos, player) .is_some_and(|moves| !moves.is_empty()) @@ -218,7 +221,7 @@ impl Board { from, to ); - if !self.is_player_owner_of_the_pawn(player, &pawn) { + if !self.is_player_owner_of_the_pawn(player, pawn) { debug!("Player wasn't owner of the pawn..."); return false; } @@ -279,33 +282,32 @@ impl Board { let target_option: Option = current_pos.checked_add(direction); if let Some(target) = target_option - && self.can_capture(current_pos, target, player, captured_enemies, &pawn) + && self.can_capture(current_pos, target, player, captured_enemies, pawn) + && let Some(path_after_capture) = target.checked_add(direction) { - if let Some(path_after_capture) = target.checked_add(direction) { - debug!( - "Found possible capture: {} -> {}", - current_pos, path_after_capture - ); + debug!( + "Found possible capture: {} -> {}", + current_pos, path_after_capture + ); - found_any_capture = true; + found_any_capture = true; - let mut new_path = current_path.clone(); - new_path.push(path_after_capture); + let mut new_path = current_path.clone(); + new_path.push(path_after_capture); - let mut new_enemies = captured_enemies.clone(); - new_enemies.push(target); + let mut new_enemies = captured_enemies.clone(); + new_enemies.push(target); - self.find_captures( - start_pos, - path_after_capture, - &mut new_path, - &new_enemies, - available_directions, - player, - all_paths, - &pawn, - ); - } + self.find_captures( + start_pos, + path_after_capture, + &mut new_path, + &new_enemies, + available_directions, + player, + all_paths, + pawn, + ); } } @@ -415,7 +417,7 @@ impl Board { return false; } - if !self.is_player_owner_of_the_pawn(&player, self[from].pawn.as_ref().unwrap()) { + if !self.is_player_owner_of_the_pawn(player, self[from].pawn.as_ref().unwrap()) { warn!("Player {} tried moving a pawn which he doesn own!", player); debug!("Tried moving from {} to {}", from, to); return false; @@ -462,7 +464,7 @@ impl Board { return false; } - if !self.is_player_owner_of_the_pawn(player, &pawn) { + if !self.is_player_owner_of_the_pawn(player, pawn) { warn!("Player {} tried moving a pawn which he doesn own!", player); return false; } @@ -491,9 +493,9 @@ impl Board { impl Index for Board { type Output = [Field]; - fn index<'a>(&'a self, i: usize) -> &'a [Field] { - let start = i * &self.size; - let end: usize = start + &self.size; + fn index(&self, i: usize) -> &[Field] { + let start = i * self.size; + let end: usize = start + self.size; &self.board[start..end] } } @@ -507,9 +509,9 @@ impl Index for Board { } impl IndexMut for Board { - fn index_mut<'a>(&'a mut self, i: usize) -> &'a mut [Field] { - let start = i * &self.size; - let end: usize = start + &self.size; + fn index_mut(&mut self, i: usize) -> &mut [Field] { + let start = i * self.size; + let end: usize = start + self.size; &mut self.board[start..end] } } diff --git a/src/logic/board/board_view.rs b/src/logic/board/board_view.rs index c1b4828..480ce14 100644 --- a/src/logic/board/board_view.rs +++ b/src/logic/board/board_view.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::logic::board::{board::Board, field::Field}; /// View of the board, without any methods. Meant for sending over network -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] pub struct BoardView { pub board: Vec, pub size: usize, @@ -24,13 +24,31 @@ impl From for BoardView { 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; + fn index(&self, i: usize) -> &[Field] { + let start = i * self.size; + let end: usize = start + self.size; &self.board[start..end] } } +impl IntoIterator for BoardView { + type Item = Field; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.board.into_iter() + } +} + +impl<'a> IntoIterator for &'a BoardView { + type Item = &'a Field; + type IntoIter = std::slice::Iter<'a, Field>; + + fn into_iter(self) -> Self::IntoIter { + self.board.iter() + } +} + impl BoardView { pub fn to_string(&self, owner: &Uuid) -> String { (0..self.size) diff --git a/src/logic/board/field.rs b/src/logic/board/field.rs index 7ee3a18..f95e00b 100644 --- a/src/logic/board/field.rs +++ b/src/logic/board/field.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::logic::{board::pawn::Pawn, math::position::Position}; -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Default)] pub struct Field { pub position: Position, pub pawn: Option, diff --git a/src/logic/board/mod.rs b/src/logic/board/mod.rs index 81d9a95..85d5161 100644 --- a/src/logic/board/mod.rs +++ b/src/logic/board/mod.rs @@ -1,4 +1,4 @@ pub mod board; pub mod board_view; -mod field; +pub mod field; pub mod pawn; diff --git a/src/logic/game_master.rs b/src/logic/game_master.rs index bbcd997..76741a1 100644 --- a/src/logic/game_master.rs +++ b/src/logic/game_master.rs @@ -105,7 +105,7 @@ impl GameMaster { fn is_player_lost(&self, player: &Player) -> bool { let player_pawns = self.board.get_player_pawns_positions(player); - player_pawns.len() == 0 + player_pawns.is_empty() || player_pawns.iter().all(|pos| { self.board.get_available_moves(*pos, player).is_none() && self.board.get_available_captures(*pos, player).is_none() diff --git a/src/logic/math/position.rs b/src/logic/math/position.rs index dbb7199..a5a904a 100644 --- a/src/logic/math/position.rs +++ b/src/logic/math/position.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::logic::math::vector::Vector2D; -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default)] /// Position on the board. Consists of row and column. Should be treated like a point in space on /// unsigned grid. /// diff --git a/src/network/client.rs b/src/network/client.rs index 1136f5c..e5dd638 100644 --- a/src/network/client.rs +++ b/src/network/client.rs @@ -129,7 +129,7 @@ impl Client { async fn send_message(&mut self, msg: Result, postcard::Error>) { debug!("Sending message to the server!"); - let _ = match msg { + match msg { Err(e) => { error!("There was an error while creating the message: {}", e); return; @@ -137,10 +137,9 @@ impl Client { Ok(msg) => { if let Err(e) = self.conn_sender_outgoing.send(msg).await { error!("Error while sending a message to connection thread: {}", e); - return; } } - }; + } } pub async fn send_capture(&mut self, capture_path: CapturePath) { @@ -168,6 +167,12 @@ impl Client { self.send_message(msg).await; } + pub async fn revoke_readiness(&mut self) { + debug!("Revoking readiness and sending it to the server..."); + let msg = Message::new(ClientMessage::SignalUnreadiness); + self.send_message(msg).await; + } + pub fn get_update_receiver(&mut self) -> Option> { self.update_receiver.take() } diff --git a/src/network/connection.rs b/src/network/connection.rs index 7d58775..514738c 100644 --- a/src/network/connection.rs +++ b/src/network/connection.rs @@ -110,8 +110,7 @@ where 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) { @@ -202,20 +201,20 @@ where { error!("Erro while sending message to main thread: {}", e); } - return Ok(()); + Ok(()) } Err(e) => { error!( "Error while preparing message in read_content for sender: {}", e ); - return Ok(()); + Ok(()) } } } Err(e) => { error!("Error while getting the message from bits: {}", e); - return Ok(()); + Ok(()) } } } diff --git a/src/network/message.rs b/src/network/message.rs index 3250e73..91b978a 100644 --- a/src/network/message.rs +++ b/src/network/message.rs @@ -60,6 +60,7 @@ pub enum ServerMessage { pub enum ClientMessage { AnswerHandshake { player_name: String }, SignalReadiness, + SignalUnreadiness, ConnectionDead { addr: SocketAddr }, diff --git a/src/network/server.rs b/src/network/server.rs index bf6f8dd..a3ea6cf 100644 --- a/src/network/server.rs +++ b/src/network/server.rs @@ -30,8 +30,9 @@ const MAX_PLAYABLE_CONNECTIONS: usize = 2; // DON'T EVER CHANGE THIS AS GAME OF // There will be a spectator mode, hence the name __PLAYABLE__ connection. You will be able to have // more connections, but they won't be able to play -#[derive(PartialEq)] -enum ServerStage { +#[derive(Debug, PartialEq, Default)] +pub enum ServerStage { + #[default] Lobby, Game, End, @@ -155,7 +156,7 @@ impl Server { 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 }); + let msg = Message::new(ServerMessage::DeclineHandshake { reason }); if let Ok(msg) = msg { let _ = sender.send(msg).await; // we can't use send_message because we @@ -188,6 +189,10 @@ impl Server { self.handle_readiness(addr).await } + ClientMessage::SignalUnreadiness => { + self.handle_unreadiness(addr).await; + } + ClientMessage::RequestCapture { capture_path } => { self.process_capture(capture_path).await; } @@ -336,6 +341,37 @@ impl Server { } } + async fn handle_unreadiness(&mut self, addr: SocketAddr) { + if !self.connections[&addr].is_handshaken { + warn!( + "Connection: {} tried revoking readiness, but hands were not shaken. Ignoring....", + addr + ); + return; + } + + if self.state.stage != ServerStage::Lobby { + warn!( + "Player {} tried revoking readiness, but game already started. Ignoring...", + addr + ); + return; + } + + self.state.ready_count -= 1; + + self.connections + .get_mut(&addr) + .expect("Could not get the indentity of the player that wanted to revoke readinessl") + .identity + .is_ready = false; + + info!( + "Player {} revoked his readiness. Currently ready: {}/{}", + addr, self.state.ready_count, MAX_PLAYABLE_CONNECTIONS + ); + } + /* === Sending messages === */ /// Should be used after every players move @@ -464,7 +500,6 @@ impl Server { } Err(e) => { error!("Error while broadcasting the message: {}", e); - return; } } } diff --git a/src/super_advanced_ai.rs b/src/super_advanced_ai.rs index e36a5b0..e824135 100644 --- a/src/super_advanced_ai.rs +++ b/src/super_advanced_ai.rs @@ -8,8 +8,9 @@ use crate::{ settings::{client_settings::ClientSettings, general_settings::DEFAULT_URL}, }; -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(PartialEq, Eq, Clone, Copy, Default)] pub enum BotDificulty { + #[default] Easy, Normal, Hard, @@ -65,12 +66,12 @@ impl Bot { .choose(&mut rand::rng()) .expect("Could not get random move"); - client.send_move(rand_pawn.from, rand_move.clone()).await; + client.send_move(rand_pawn.from, *rand_move).await; } crate::network::message::ServerMessage::BroadcastBoardState { board: _ } => { info!("Got server state!"); - match dificulty.clone() { + match dificulty { BotDificulty::Easy => {} // we do nothing with it because easy bot // just does random stuff. In future // board_view can be used to calculate the diff --git a/src/ui/macroquad/difficulty_selection.rs b/src/ui/macroquad/difficulty_selection.rs new file mode 100644 index 0000000..0718f3c --- /dev/null +++ b/src/ui/macroquad/difficulty_selection.rs @@ -0,0 +1,80 @@ +use std::sync::mpsc; + +use macroquad::{ + color::GRAY, + math::vec2, + shapes::draw_rectangle, + ui::{hash, root_ui, widgets::Group}, + window::{screen_height, screen_width}, +}; + +use crate::{ + settings::{ + client_settings::ClientSettings, + general_settings::{DEFAULT_URL, SettingsLike}, + }, + ui::{ + macroquad::menu_builder::MenuBuilder, + state::{ + GameContext, + GuiState::{self}, + connect_bot, connect_to_server, run_server, + }, + }, +}; + +pub async fn draw_dificulty_selection(state: &mut GuiState, context: &mut GameContext) { + let menu_size = vec2(screen_width() * 0.5, screen_height() * 0.5); + let menu_pos = vec2( + screen_width() / 2.0 - menu_size.x / 2.0, + screen_height() / 2.0 - menu_size.y / 2.0, + ); + + let mut menu_builder = MenuBuilder::new(menu_size.x, menu_size.y * 0.1); + + draw_rectangle(menu_pos.x, menu_pos.y, menu_size.x, menu_size.y, GRAY); + + Group::new(hash!(), menu_size) + .position(menu_pos) + .ui(&mut root_ui(), |ui| { + menu_builder.label(ui, "Difficulty Selection"); + + // TODO! refactor this ugly ass code + + if menu_builder.button(ui, "Easy") { + context.difficulty = crate::super_advanced_ai::BotDificulty::Easy; + start_single(state, context); + } + + if menu_builder.button(ui, "Medium") { + context.difficulty = crate::super_advanced_ai::BotDificulty::Normal; + start_single(state, context); + } + + if menu_builder.button(ui, "Hard") { + context.difficulty = crate::super_advanced_ai::BotDificulty::Hard; + start_single(state, context); + } + + if menu_builder.button(ui, "Go back") { + *state = GuiState::ModeSelection; + } + }); +} + +fn start_single(state: &mut GuiState, context: &GameContext) { + let mut client_settings = ClientSettings::new(); + client_settings.server_url = DEFAULT_URL; // we overwrite server_url since we connect locally + + let (tx, rx) = mpsc::channel(); + + run_server(tx); + + let result = rx.recv(); + + if let Ok(_result) = result { + connect_bot(context.difficulty); + } + + *state = GuiState::Connecting(connect_to_server()); +} diff --git a/src/ui/macroquad/game/board.rs b/src/ui/macroquad/game/board.rs new file mode 100644 index 0000000..35579a6 --- /dev/null +++ b/src/ui/macroquad/game/board.rs @@ -0,0 +1,472 @@ +use std::{ + ops::{Index, IndexMut}, + slice::IterMut, +}; + +use log::debug; +use macroquad::{ + color::{BLACK, Color, GREEN, RED, WHITE, YELLOW}, + input::{is_mouse_button_pressed, mouse_position}, + math::{Rect, vec2}, + shapes::{draw_circle, draw_circle_lines, draw_rectangle}, +}; +use uuid::Uuid; + +use crate::logic::{ + board::{ + board_view::BoardView, + field::Field, + pawn::{CapturePath, MovePath}, + }, + math::position::Position, +}; + +const MOVE_HIGHLITGHT_COLOR: Color = GREEN; +const CAPTURE_HIGHLITGHT_COLOR: Color = RED; + +#[derive(Default)] +struct GuiField { + rect: Rect, + field: Field, + pos: Position, + is_clickable: bool, + color: Color, + is_blac: bool, +} + +#[derive(Default)] +struct GuiFieldVec { + gui_fields: Vec, +} + +impl GuiFieldVec { + pub fn push(&mut self, input: GuiField) { + self.gui_fields.push(input); + } + + pub fn clear(&mut self) { + self.gui_fields.clear(); + } +} + +impl<'a> IntoIterator for &'a GuiFieldVec { + type Item = &'a GuiField; + type IntoIter = std::slice::Iter<'a, GuiField>; + + fn into_iter(self) -> Self::IntoIter { + self.gui_fields.iter() + } +} + +impl<'a> IntoIterator for &'a mut GuiFieldVec { + type Item = &'a mut GuiField; + type IntoIter = IterMut<'a, GuiField>; + + fn into_iter(self) -> Self::IntoIter { + self.gui_fields.iter_mut() + } +} + +impl Index for GuiFieldVec { + type Output = GuiField; + + fn index(&self, index: Position) -> &Self::Output { + self.gui_fields + .iter() + .find(|field| field.pos == index) + .unwrap() + } +} + +impl IndexMut for GuiFieldVec { + fn index_mut(&mut self, index: Position) -> &mut Self::Output { + self.gui_fields + .iter_mut() + .find(|field| field.pos == index) + .unwrap() + } +} + +#[derive(Default, Debug)] +enum BoardState { + #[default] + View, + ChoosePawn, + ChooseMove(Position), + ChooseCapture(Position), +} + +pub struct Board { + rect: Rect, + + board_view: BoardView, + fields: GuiFieldVec, + + state: BoardState, + + my_id: Uuid, + is_my_turn: bool, + + available_captures: Option>, + current_deepness: usize, + current_capture_path: Vec, + choosen_capture: Option, + + available_moves: Option>, + choosen_move: Option, + + field_size: f32, +} + +impl Default for Board { + fn default() -> Self { + Self::new(Rect::default(), Uuid::default()) + } +} + +impl Board { + pub fn new(rect: Rect, player_id: Uuid) -> Self { + Self { + rect, + + board_view: BoardView::default(), + fields: GuiFieldVec::default(), + + state: BoardState::default(), + + my_id: player_id, + is_my_turn: false, + + available_captures: None, + current_capture_path: vec![], + current_deepness: 0, + choosen_capture: None, + + available_moves: None, + choosen_move: None, + + field_size: 32.0, + } + } + + pub fn update(&mut self) { + self.update_dimensions(); + self.reset_color(); + + let clicked_field = self.get_clicked_field(); + + match self.state { + BoardState::View => { + if self.is_my_turn { + self.state = BoardState::ChoosePawn; + } + } + + BoardState::ChoosePawn => { + if let Some(clicked_field) = clicked_field { + self.delegate_move(clicked_field.pos); + } + + if self.is_back_move_wanted_by_player() { + self.state = BoardState::View; + } + + self.highlight_current_moves(); + } + + BoardState::ChooseMove(from) => { + if self.is_back_move_wanted_by_player() { + self.state = BoardState::ChoosePawn; + } + + self.highlight_move_paths(from); + + if let Some(field) = &self.get_clicked_field() + && let Some(available_moves) = &self.available_moves + && available_moves.iter().any(|available_move| { + available_move.from == from + && available_move.available_steps.contains(&field.pos) + }) + { + self.choosen_move = Some(MovePath { + from, + available_steps: vec![field.pos], + }); + self.state = BoardState::View; + } + } + + BoardState::ChooseCapture(from) => { + if self.is_back_move_wanted_by_player() { + self.reset_capture(); + return; + } + + let Some(available_captures) = &self.available_captures else { + return; + }; + + let possible_moves: Vec<_> = available_captures + .iter() + .filter_map(|capture| { + if capture.from == from + && capture.steps.starts_with(&self.current_capture_path) + { + Some(&capture.steps) + } else { + None + } + }) + .collect(); + + let current_highlight: Vec<_> = possible_moves + .iter() + .filter_map(|possible_move| possible_move.get(self.current_deepness)) + .copied() + .collect(); + + if let Some(clicked_field) = &self.get_clicked_field().map(|field| field.pos) + && possible_moves.iter().any(|possible_move| { + if let Some(valid_move) = possible_move.get(self.current_deepness) { + valid_move == clicked_field + } else { + false + } + }) + { + self.current_capture_path.push(*clicked_field); + self.current_deepness += 1; + } + + if current_highlight.is_empty() { + debug!("Capture path complete!"); + self.choosen_capture = available_captures + .iter() + .find(|capture| { + capture.from == from && capture.steps == self.current_capture_path + }) + .cloned(); + + self.reset_capture(); + } + + self.highlight_capture_path(current_highlight); + } + } + } + + fn reset_capture(&mut self) { + self.current_capture_path.clear(); + self.current_deepness = 0; + self.state = BoardState::View; + } + + fn delegate_move(&mut self, from: Position) { + if let Some(capture_path) = &self.available_captures + && capture_path.iter().any(|path| path.from == from) + { + self.state = BoardState::ChooseCapture(from); + return; + } + + if let Some(available_moves) = &self.available_moves + && available_moves.iter().any(|path| path.from == from) + { + self.state = BoardState::ChooseMove(from); + } + } + + pub fn get_choosen_capture(&mut self) -> Option { + self.choosen_capture.take() + } + + pub fn get_choosen_move(&mut self) -> Option { + self.choosen_move.take() + } + + pub fn update_board_view(&mut self, board_view: BoardView) { + self.board_view = board_view; + } + + pub fn update_available_captures(&mut self, captures: Vec) { + self.available_captures = Some(captures); + self.available_moves = None; + } + + pub fn update_available_moves(&mut self, moves: Vec) { + self.state = BoardState::ChoosePawn; + + self.available_moves = Some(moves); + self.available_captures = None; + } + + pub fn update_board_rect(&mut self, rect: Rect) -> bool { + if self.rect != rect { + self.rect = rect; + return true; + } + + false + } + + /// Recreate fields vec with new dimensions. Should be used when new board_view arrives + pub fn repopulate(&mut self) { + self.fields.clear(); + + self.field_size = self.rect.w / self.board_view.size as f32; + + for field in &self.board_view { + let pos: Position = field.position; + + let is_row_even: bool = pos.row.is_multiple_of(2); + let is_column_even: bool = pos.column.is_multiple_of(2); + let is_field_black: bool = is_row_even ^ is_column_even; + + let abs_x = self.rect.x + pos.column as f32 * self.field_size; + let abs_y = self.rect.y + pos.row as f32 * self.field_size; + + let rect = Rect { + x: abs_x, + y: abs_y, + w: self.field_size, + h: self.field_size, + }; + + self.fields.push(GuiField { + rect, + field: field.clone(), + is_clickable: false, + pos, + color: if is_field_black { BLACK } else { WHITE }, + is_blac: is_field_black, + }); + } + } + + /// Updates dimensions of graphical squares + pub fn update_dimensions(&mut self) { + self.field_size = self.rect.w / self.board_view.size as f32; + + for field in &mut self.fields { + let pos: Position = field.pos; + + let abs_x = self.rect.x + pos.column as f32 * self.field_size; + let abs_y = self.rect.y + pos.row as f32 * self.field_size; + + let new_rect = Rect { + x: abs_x, + y: abs_y, + w: self.field_size, + h: self.field_size, + }; + + field.rect = new_rect; + } + } + + fn reset_color(&mut self) { + for field in &mut self.fields { + field.color = if field.is_blac { BLACK } else { WHITE }; + } + } + + pub fn draw(&self) { + for field in &self.fields { + draw_rectangle( + field.rect.x, + field.rect.y, + field.rect.w, + field.rect.h, + field.color, + ); + + if field.is_clickable { + draw_rectangle( + field.rect.x, + field.rect.y, + field.rect.w, + field.rect.h, + GREEN, + ); + } + + if let Some(pawn) = &field.field.pawn { + let color = if pawn.owner == self.my_id { WHITE } else { RED }; // TODO! This should + // be customizable + + let center = field.rect.center(); + + let r = field.rect.w * 0.5; + let thickness = 0.5; + + draw_circle(center.x, center.y, r, color); + match pawn.state { + crate::logic::board::pawn::PawnState::Man => { + draw_circle_lines(center.x, center.y, r, thickness, BLACK); + } + crate::logic::board::pawn::PawnState::Dame => { + draw_circle_lines(center.x, center.y, r, thickness, YELLOW); + } + } + } + } + } + + fn get_clicked_field(&self) -> Option<&GuiField> { + let (mouse_x, mouse_y) = mouse_position(); + let mouse_point = vec2(mouse_x, mouse_y); + + if is_mouse_button_pressed(macroquad::input::MouseButton::Left) { + for field in &self.fields { + if field.rect.contains(mouse_point) { + return Some(field); + } + } + } + None + } + + fn is_back_move_wanted_by_player(&self) -> bool { + is_mouse_button_pressed(macroquad::input::MouseButton::Right) + } + + pub fn set_my_turn(&mut self, is_my_turn: bool) { + self.is_my_turn = is_my_turn; + } + + fn highlight_current_moves(&mut self) { + if let Some(available_captures) = &self.available_captures { + for capture_path in available_captures { + self.fields[capture_path.from].color = MOVE_HIGHLITGHT_COLOR; + } + + return; + } + + if let Some(available_moves) = &self.available_moves { + for available_move in available_moves { + self.fields[available_move.from].color = MOVE_HIGHLITGHT_COLOR; + } + } + } + + fn highlight_move_paths(&mut self, from: Position) { + let Some(available_moves) = &self.available_moves else { + return; + }; + + for available_move in available_moves { + if available_move.from == from { + for step in &available_move.available_steps { + self.fields[*step].color = MOVE_HIGHLITGHT_COLOR; + } + } + } + } + + fn highlight_capture_path(&mut self, fields: Vec) { + for field in fields { + self.fields[field].color = CAPTURE_HIGHLITGHT_COLOR; + } + } +} diff --git a/src/ui/macroquad/game/chat.rs b/src/ui/macroquad/game/chat.rs new file mode 100644 index 0000000..e775fd0 --- /dev/null +++ b/src/ui/macroquad/game/chat.rs @@ -0,0 +1,165 @@ +use macroquad::{ + color::{BLACK, WHITE}, + math::{Rect, vec2}, + shapes::draw_rectangle, + text::measure_text, + ui::{ + Skin, Ui, hash, root_ui, + widgets::{Group, InputText}, + }, + window::{screen_height, screen_width}, +}; +use tokio::sync::mpsc::Sender; + +use crate::ui::macroquad::game::game::GuiCommands; + +#[derive(Debug)] +struct Message { + sender: String, + content: String, +} + +#[derive(Default)] +pub struct Chat { + area: Rect, + messages: Vec, + message_buffer: String, +} + +impl Chat { + pub fn update(&mut self, game_area: &Rect) { + self.area = Rect { + x: game_area.w, + y: 0.0, + w: screen_width() * 0.2, + h: screen_height(), + }; + } + + pub fn push_message(&mut self, sender: String, content: String) { + self.messages.push(Message { sender, content }); + } + + pub fn send_message(&mut self, cmd_sender: Sender) { + if !self.message_buffer.is_empty() { + let _ = // TODO! Check the message before sending + cmd_sender + .try_send(GuiCommands::Send(self.message_buffer.clone())); + self.message_buffer.clear(); + } + } + + pub fn draw(&mut self) { + draw_rectangle(self.area.x, self.area.y, self.area.w, self.area.h, BLACK); + + let input_size = vec2(self.area.w, self.area.h * 0.1); + let input_pos = vec2(self.area.x, self.area.h - input_size.y); + + InputText::new(hash!()) + .position(input_pos) + .size(input_size) + .ui(&mut root_ui(), &mut self.message_buffer); + + let chat_size = vec2(self.area.w, self.area.h - input_size.y); + let chat_pos = vec2(self.area.x, self.area.y); + + let font_size = (self.area.w * 0.1) as u16; + + let label_style = root_ui() + .style_builder() + .font_size(font_size) + .text_color(WHITE) + .build(); + + let chat_skin: Skin = Skin { + label_style, + ..root_ui().default_skin() + }; + + root_ui().push_skin(&chat_skin); + + let mut prev_sender: &str = ""; + + Group::new(hash!(), chat_size) + .position(chat_pos) + .ui(&mut root_ui(), |ui| { + for message in &self.messages { + if prev_sender != message.sender { + if !prev_sender.is_empty() { + // make space if it's not the first message + ui.label(None, ""); + } + ui.label(None, &format!("[{}]", message.sender)); + } + + wrap_message(ui, &message.content, font_size, chat_size.x); + prev_sender = &message.sender; + } + }); + + root_ui().pop_skin(); + } +} + +fn wrap_message(ui: &mut Ui, msg: &String, font_size: u16, chat_width: f32) { + let chat_width = chat_width - 20.0; // we leave some room + + let mut buffer = String::new(); + + if measure_text(msg, None, font_size, 1.0).width < chat_width { + ui.label(None, msg); + return; + } + + for word in msg.split_whitespace() { + if measure_text(word, None, font_size, 1.0).width >= chat_width { + wrap_one_word(ui, word, font_size, chat_width); + continue; + } + + let test_string = if buffer.is_empty() { + word.to_string() + } else { + format!("{} {}", buffer, word) + }; + + let test_width = measure_text(&test_string, None, font_size, 1.0).width; + + if test_width < chat_width { + buffer = test_string; + } else { + if !buffer.is_empty() { + ui.label(None, &buffer); + } + + buffer = word.to_string(); + } + } + + if !buffer.is_empty() { + ui.label(None, &buffer); + } +} + +fn wrap_one_word(ui: &mut Ui, msg: &str, font_size: u16, chat_width: f32) { + if measure_text(msg, None, font_size, 1.0).width < chat_width { + return; + } + + let msg_half = msg.chars().count() / 2; + + let first_half: String = msg.chars().take(msg_half).collect(); + let second_half: String = msg.chars().skip(msg_half).collect(); + + if measure_text(&first_half, None, font_size, 1.0).width >= chat_width { + wrap_one_word(ui, &first_half, font_size, chat_width); + } else { + ui.label(None, &format!("{}-", first_half)); + } + + if measure_text(&second_half, None, font_size, 1.0).width >= chat_width { + wrap_one_word(ui, &second_half, font_size, chat_width); + } else { + ui.label(None, &format!("{}-", second_half)); // TODO! Last word always has - at the end. + } +} diff --git a/src/ui/macroquad/game/game.rs b/src/ui/macroquad/game/game.rs new file mode 100644 index 0000000..169a01a --- /dev/null +++ b/src/ui/macroquad/game/game.rs @@ -0,0 +1,373 @@ +use tokio::sync::mpsc::{self, Sender}; + +use log::{debug, error, info}; +use macroquad::{ + color::GRAY, + input::{KeyCode, get_keys_pressed}, + math::{Rect, Vec2, vec2}, + shapes::draw_rectangle, + ui::{ + hash, root_ui, + widgets::{Checkbox, Group}, + }, + window::{screen_height, screen_width}, +}; +use tokio::sync::mpsc::{Receiver, error::TryRecvError::Disconnected}; + +use crate::{ + logic::board::pawn::{CapturePath, MovePath}, + network::{ + client::Client, message::ServerMessage, network_identity::NetworkIdentity, + server::ServerStage, + }, + ui::{ + macroquad::{ + game::{board::Board, chat::Chat}, + menu_builder::MenuBuilder, + }, + state::GameContext, + }, +}; + +pub struct GameClient { + cmd_sender: Sender, + update_receiver: Receiver, + + identity: Option, + + board: Board, + is_my_turn: bool, + + game_state: ServerStage, + + game_area: Rect, + board_area: Rect, + + lobby: Lobby, + chat: Chat, + + game_result: Option, + pub should_close: bool, +} + +#[derive(Default)] +struct Lobby { + is_ready: bool, + checkbox_pos: Vec2, + checkbox_size: Vec2, +} + +pub enum GuiCommands { + Send(String), + + Ready, + Unready, + + Capture(CapturePath), + Move(MovePath), +} + +impl GameClient { + pub fn new(mut client: Client, _context: &GameContext) -> Option { + let Some(update_receiver) = client.get_update_receiver() else { + return None; + }; + + let (cmd_sender, mut cmd_recv) = mpsc::channel::(10); + + std::thread::spawn(|| { + let rt = tokio::runtime::Runtime::new().unwrap(); // i dont like this + + rt.block_on(async move { + loop { + match cmd_recv.recv().await { + Some(cmd) => match cmd { + GuiCommands::Send(content) => { + client.send_text_message(content).await; + } + + GuiCommands::Ready => { + client.signal_readiness().await; + } + + GuiCommands::Unready => { + client.revoke_readiness().await; + } + GuiCommands::Capture(capture_path) => { + client.send_capture(capture_path).await; + } + + GuiCommands::Move(move_path) => { + client + .send_move(move_path.from, move_path.available_steps[0]) + .await; // ugly + } + }, + + None => { + error!("Connection with cmd thread broken"); + return; + } + } + } + }); + }); + + let board = Board::default(); + + let game_stage = ServerStage::default(); + let mut lobby = Lobby::default(); + lobby.checkbox_size = vec2(32.0, 16.0); + + let game_area = Rect { + x: 0.0, + y: 0.0, + w: screen_width() * 0.8, + h: screen_height(), + }; + + let board_area = Rect::default(); + + let chat = Chat::default(); + + Some(Self { + cmd_sender, + update_receiver, + identity: None, + + board, + lobby, + + is_my_turn: false, + + game_state: game_stage, + + game_area, + board_area, + + chat, + + game_result: None, + should_close: false, + }) + } + + pub fn draw(&mut self) { + match self.game_state { + ServerStage::Lobby => { + self.draw_lobby(); + } + + ServerStage::Game => { + self.draw_game(); + } + + ServerStage::End => { + self.draw_summary_screen(); + } + } + self.chat.draw(); + } + + fn draw_lobby(&mut self) { + let lobby = &self.lobby; + + let background_x = lobby.checkbox_size.x * 10.0; + let background_y = lobby.checkbox_size.y * 10.0; + + draw_rectangle( + lobby.checkbox_pos.x - background_x * 0.5, + lobby.checkbox_pos.y - background_y * 0.5, + background_x, + background_y, + GRAY, + ); + + let before_click: bool = self.lobby.is_ready; + Checkbox::new(hash!()) + .pos(self.lobby.checkbox_pos) + .label("Ready") + .size(self.lobby.checkbox_size) + .ui(&mut root_ui(), &mut self.lobby.is_ready); + + if before_click != self.lobby.is_ready { + if self.lobby.is_ready { + let _ = self.cmd_sender.try_send(GuiCommands::Ready); // TODO! Maybe handle failure + // or something + } else { + let _ = self.cmd_sender.try_send(GuiCommands::Unready); + } + } + } + + fn draw_game(&mut self) { + self.board.draw(); + } + + fn draw_summary_screen(&mut self) { + // this function doesn't only draw but i'm to lazy to change + // this name everywhere + let mut menu_builder = MenuBuilder::new(self.game_area.w, self.game_area.h * 0.1); + let menu_pos = vec2(self.game_area.x, self.game_area.y); + + draw_rectangle( + menu_pos.x, + menu_pos.y, + self.game_area.w, + self.game_area.h, + GRAY, + ); + + Group::new(hash!(), vec2(self.game_area.w, self.game_area.h)) + .position(menu_pos) + .ui(&mut root_ui(), |ui| { + menu_builder.label(ui, "Game ended!"); + + if let Some(result) = &self.game_result { + menu_builder.label(ui, result); + } else { + menu_builder.label(ui, "Could not get result"); + } + + if menu_builder.button(ui, "Main menu") { + // TODO! Should also close the connection + // and cleanup + self.should_close = true; + } + }); + } + + /* === UPDATING STUFF ==== */ + + pub fn update(&mut self) { + self.update_rects(); + self.update_network(); + + match self.game_state { + ServerStage::Lobby => self.update_lobby(), + + ServerStage::Game => { + self.board.update(); + } + + ServerStage::End => {} + } + + for key in get_keys_pressed() { + if key == KeyCode::Enter { + self.chat.send_message(self.cmd_sender.clone()); // not sure about cloning this + // every message sent. Maybe + // Chat should have it's own + // clone? + } + } + + if let Some(capture_path) = self.board.get_choosen_capture() { + let _ = self.cmd_sender.try_send(GuiCommands::Capture(capture_path)); + } + + if let Some(move_path) = self.board.get_choosen_move() { + let _ = self.cmd_sender.try_send(GuiCommands::Move(move_path)); + } + } + + fn update_network(&mut self) { + match self.update_receiver.try_recv() { + Ok(msg) => match msg { + ServerMessage::GameStart { identity } => { + info!("We go identity!"); + self.identity = Some(identity.clone()); + self.board = Board::new(self.board_area, identity.id); + self.game_state = ServerStage::Game; + } + + ServerMessage::AvailableCaptures { captures } => { + self.board.update_available_captures(captures); + } + + ServerMessage::AvailableMoves { moves } => { + self.board.update_available_moves(moves); + } + + ServerMessage::BroadcastBoardState { board } => { + self.board.update_board_view(board); + self.board.repopulate(); + } + + ServerMessage::BroadcastCurrentTurn { active_player } => { + if let Some(identity) = &self.identity + && identity.id == active_player + { + info!("It's out turn!"); + self.is_my_turn = true; + } else { + self.is_my_turn = false; + } + + self.board.set_my_turn(self.is_my_turn); + } + + ServerMessage::BroadCastTextMessage { sender, content } => { + debug!("Got message from: {} with content: {}", sender, content); + self.chat.push_message(sender, content); + } + + ServerMessage::GameEnd { result } => { + self.game_result = match result { + crate::logic::game_master::GameResult::Lost(uuid) => { + if self.identity.clone().unwrap().id == uuid { + Some("You lost!".to_string()) + } else { + Some("You won!".to_string()) + } + } + crate::logic::game_master::GameResult::Draw => Some("Draw!".to_string()), + }; + + self.game_state = ServerStage::End; + } + + _ => {} // ignore things that network client handled by itself + }, + + Err(Disconnected) => { + error!("Connection broken!!!!"); + } + + Err(_) => {} // No messages - nothing wrong (why is this even an error) + } + } + + fn update_lobby(&mut self) { + self.lobby.checkbox_pos = vec2( + self.game_area.w / 2.0 - self.lobby.checkbox_size.x / 2.0, + self.game_area.h / 2.0, + ); + } + + fn update_rects(&mut self) { + self.game_area = Rect { + x: 0.0, + y: 0.0, + w: screen_width() * 0.8, + h: screen_height(), + }; + + self.chat.update(&self.game_area); + + let w = self.game_area.w * 0.75; + let h = self.game_area.h * 0.75; + let side = w.min(h); + + self.board_area = Rect { + x: self.game_area.w / 2.0 - (side / 2.0), + y: self.game_area.h / 2.0 - (side / 2.0), + w: side, + h: side, + }; + + if self.board.update_board_rect(self.board_area) { + self.board.update_dimensions(); + } + } +} diff --git a/src/ui/macroquad/game/mod.rs b/src/ui/macroquad/game/mod.rs new file mode 100644 index 0000000..1d4e5b3 --- /dev/null +++ b/src/ui/macroquad/game/mod.rs @@ -0,0 +1,3 @@ +mod board; +mod chat; +pub mod game; diff --git a/src/ui/macroquad/main_menu.rs b/src/ui/macroquad/main_menu.rs new file mode 100644 index 0000000..286134f --- /dev/null +++ b/src/ui/macroquad/main_menu.rs @@ -0,0 +1,39 @@ +use macroquad::{ + color::GRAY, + math::vec2, + shapes::draw_rectangle, + ui::{hash, root_ui, widgets::Group}, + window::{screen_height, screen_width}, +}; + +use crate::ui::{macroquad::menu_builder::MenuBuilder, state::GuiState}; + +pub async fn draw_main_menu(state: &mut GuiState) { + let menu_size = vec2(screen_width() * 0.5, screen_height() * 0.5); + let menu_pos = vec2( + screen_width() / 2.0 - menu_size.x / 2.0, + screen_height() / 2.0 - menu_size.y / 2.0, + ); + + let mut menu_builder = MenuBuilder::new(menu_size.x, menu_size.y * 0.1); + + draw_rectangle(menu_pos.x, menu_pos.y, menu_size.x, menu_size.y, GRAY); + + Group::new(hash!(), menu_size) + .position(menu_pos) + .ui(&mut root_ui(), |ui| { + menu_builder.label(ui, "Main menu"); + + if menu_builder.button(ui, "Play") { + *state = GuiState::ModeSelection; + } + + if menu_builder.button(ui, "Settings") { + *state = GuiState::Settings; + } + + if menu_builder.button(ui, "Quit") { + *state = GuiState::Exit; + } + }); +} diff --git a/src/ui/macroquad/menu_builder.rs b/src/ui/macroquad/menu_builder.rs new file mode 100644 index 0000000..cf88dce --- /dev/null +++ b/src/ui/macroquad/menu_builder.rs @@ -0,0 +1,58 @@ +use macroquad::{ + math::{Vec2, vec2}, + ui::{ + Ui, hash, + widgets::{Button, InputText, Label}, + }, +}; + +pub struct MenuBuilder { + center_x: f32, + current_y: f32, + button_size: Vec2, + spacing: f32, +} + +impl MenuBuilder { + pub fn new(window_width: f32, start_y: f32) -> Self { + let button_size = vec2(200.0, 50.0); + Self { + center_x: (window_width - button_size.x) / 2.0, + current_y: start_y, + button_size, + spacing: 15.0, + } + } + + pub fn text_input(&mut self, ui: &mut Ui, id: &str, buffer: &mut String) { + InputText::new(hash!(id)) + .position(vec2(self.center_x, self.current_y)) + .size(self.button_size) + .ui(ui, buffer); + + self.update_height(); + } + + pub fn label(&mut self, ui: &mut Ui, label: &str) { + Label::new(label) + .position(vec2(self.center_x, self.current_y)) + .size(self.button_size) + .ui(ui); + + self.update_height(); + } + + pub fn button(&mut self, ui: &mut Ui, label: &str) -> bool { + let clicked = Button::new(label) + .position(vec2(self.center_x, self.current_y)) + .size(self.button_size) + .ui(ui); + + self.update_height(); + clicked + } + + fn update_height(&mut self) { + self.current_y += self.button_size.y + self.spacing; + } +} diff --git a/src/ui/macroquad/mod.rs b/src/ui/macroquad/mod.rs new file mode 100644 index 0000000..fe4dfa0 --- /dev/null +++ b/src/ui/macroquad/mod.rs @@ -0,0 +1,7 @@ +pub mod difficulty_selection; +pub mod game; +pub mod main_menu; +mod menu_builder; +pub mod mode_selection; +pub mod server_selection; +pub mod settings; diff --git a/src/ui/macroquad/mode_selection.rs b/src/ui/macroquad/mode_selection.rs new file mode 100644 index 0000000..2cb6757 --- /dev/null +++ b/src/ui/macroquad/mode_selection.rs @@ -0,0 +1,46 @@ +use macroquad::{ + color::GRAY, + math::vec2, + shapes::draw_rectangle, + ui::{hash, root_ui, widgets::Group}, + window::{screen_height, screen_width}, +}; + +use crate::ui::{ + macroquad::menu_builder::MenuBuilder, + state::{ + GameContext, + GuiState::{self, ServerSelection}, + }, +}; + +pub async fn draw_mode_selection(state: &mut GuiState, context: &mut GameContext) { + let menu_size = vec2(screen_width() * 0.5, screen_height() * 0.5); + let menu_pos = vec2( + screen_width() / 2.0 - menu_size.x / 2.0, + screen_height() / 2.0 - menu_size.y / 2.0, + ); + + let mut menu_builder = MenuBuilder::new(menu_size.x, menu_size.y * 0.1); + + draw_rectangle(menu_pos.x, menu_pos.y, menu_size.x, menu_size.y, GRAY); + + Group::new(hash!(), menu_size) + .position(menu_pos) + .ui(&mut root_ui(), |ui| { + menu_builder.label(ui, "Mode selection"); + + if menu_builder.button(ui, "Singleplayer") { + context.gamemode = crate::ui::state::GameMode::Singleplayer; + *state = GuiState::DificultySelection; + } + + if menu_builder.button(ui, "Multiplayer") { + *state = ServerSelection; + } + + if menu_builder.button(ui, "Go back") { + *state = GuiState::MainMenu; + } + }); +} diff --git a/src/ui/macroquad/server_selection.rs b/src/ui/macroquad/server_selection.rs new file mode 100644 index 0000000..55a6a90 --- /dev/null +++ b/src/ui/macroquad/server_selection.rs @@ -0,0 +1,63 @@ +use std::sync::mpsc; + +use macroquad::{ + color::GRAY, + math::vec2, + shapes::draw_rectangle, + ui::{hash, root_ui, widgets::Group}, + window::{screen_height, screen_width}, +}; + +use crate::ui::{ + macroquad::menu_builder::MenuBuilder, + state::{ + GameContext, + GuiState::{self}, + connect_to_server, run_server, + }, +}; + +pub async fn draw_server_selection(state: &mut GuiState, context: &mut GameContext) { + let menu_size = vec2(screen_width() * 0.5, screen_height() * 0.5); + let menu_pos = vec2( + screen_width() / 2.0 - menu_size.x / 2.0, + screen_height() / 2.0 - menu_size.y / 2.0, + ); + + let mut menu_builder = MenuBuilder::new(menu_size.x, menu_size.y * 0.1); + + draw_rectangle(menu_pos.x, menu_pos.y, menu_size.x, menu_size.y, GRAY); + + Group::new(hash!(), menu_size) + .position(menu_pos) + .ui(&mut root_ui(), |ui| { + menu_builder.label(ui, "Host"); + if menu_builder.button(ui, "Start") { + context.gamemode = crate::ui::state::GameMode::Multiplayer { is_hosting: true }; + + let (tx, rx) = mpsc::channel(); + run_server(tx); + + let result = rx.recv(); + + if let Ok(_result) = result { + *state = GuiState::Connecting(connect_to_server()); + } + } + + menu_builder.label(ui, "Server selection"); + + menu_builder.text_input(ui, "server url", &mut context.server_url_buffer); + //context.normalise_url(); + + if menu_builder.button(ui, "Connect") { + context.gamemode = crate::ui::state::GameMode::Multiplayer { is_hosting: false }; + *state = GuiState::Connecting(connect_to_server()); + // TODO Validate url before connecting or something like that + } + + if menu_builder.button(ui, "Go back") { + *state = GuiState::ModeSelection; + } + }); +} diff --git a/src/ui/macroquad/settings.rs b/src/ui/macroquad/settings.rs new file mode 100644 index 0000000..7e336f1 --- /dev/null +++ b/src/ui/macroquad/settings.rs @@ -0,0 +1,85 @@ +use log::error; +use macroquad::{ + color::GRAY, + math::vec2, + shapes::draw_rectangle, + ui::{hash, root_ui, widgets::Group}, + window::{screen_height, screen_width}, +}; + +use crate::{ + settings::{ + client_settings::ClientSettings, general_settings::SettingsLike, + server_settings::ServerSettings, + }, + ui::{ + macroquad::menu_builder::MenuBuilder, + state::{GameContext, GuiState}, + }, +}; + +pub async fn draw_settings( + state: &mut GuiState, + context: &mut GameContext, + client_settings: &mut ClientSettings, + server_settings: &mut ServerSettings, +) { + let menu_size = vec2(screen_width() * 0.5, screen_height() * 0.5); + let menu_pos = vec2( + screen_width() / 2.0 - menu_size.x / 2.0, + screen_height() / 2.0 - menu_size.y / 2.0, + ); + + let mut menu_builder = MenuBuilder::new(menu_size.x, menu_size.y * 0.1); + draw_rectangle(menu_pos.x, menu_pos.y, menu_size.x, menu_size.y, GRAY); + + Group::new(hash!(), menu_size) + .position(menu_pos) + .ui(&mut root_ui(), |ui| { + // Client + menu_builder.label(ui, "Game settings"); + + menu_builder.label(ui, "Name"); + menu_builder.text_input(ui, "name input", &mut client_settings.name); + + menu_builder.label(ui, "Default server url"); + menu_builder.text_input(ui, "url input", &mut context.server_url_buffer); + + menu_builder.label(ui, ""); + + // Server + menu_builder.label(ui, "Server settings"); + menu_builder.label(ui, "TODO"); + + if let Some(error_msg) = &context.latest_error_message { + menu_builder.label(ui, error_msg); + } + + // Exit + if menu_builder.button(ui, "Save and back") { + client_settings.server_url = match context.server_url_buffer.parse() { + Ok(result) => result, + + Err(e) => { + context.latest_error_message = Some(e.to_string()); + return; + } + }; + + if let Err(e) = client_settings.save_to_file() { + context.latest_error_message = Some(e.to_string()); + error!("Error while saving client settings to file: {}", e); + return; + } + + if let Err(e) = server_settings.save_to_file() { + context.latest_error_message = Some(e.to_string()); + error!("Error while saving server settings to file: {}", e); + return; + } + + context.latest_error_message = None; + *state = GuiState::MainMenu; + } + }); +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..2b23ee7 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,2 @@ +pub mod macroquad; +pub mod state; diff --git a/src/ui/state.rs b/src/ui/state.rs new file mode 100644 index 0000000..827945a --- /dev/null +++ b/src/ui/state.rs @@ -0,0 +1,92 @@ +use std::sync::mpsc::{self, Receiver, Sender}; + +use crate::{ + network::{client::Client, server::Server}, + super_advanced_ai::{Bot, BotDificulty}, + ui::macroquad::game::game::GameClient, +}; + +#[derive(Default)] +pub enum GuiState { + #[default] + MainMenu, + + ModeSelection, + DificultySelection, + ServerSelection, + + Connecting(Receiver>), + + Settings, + Game(GameClient), + Exit, +} + +#[derive(Default, Clone, Copy)] +pub enum GameMode { + #[default] + Singleplayer, + Multiplayer { + is_hosting: bool, + }, +} + +#[derive(Clone)] +pub struct GameContext { + pub difficulty: BotDificulty, + pub gamemode: GameMode, + pub server_url_buffer: String, + + pub latest_error_message: Option, +} + +impl Default for GameContext { + fn default() -> Self { + Self { + difficulty: BotDificulty::default(), + gamemode: GameMode::default(), + server_url_buffer: "127.0.0.1:6767".to_string(), + latest_error_message: None, + } + } +} + +// TODO! Move this +pub fn connect_to_server() -> Receiver> { + let (tx, rc) = mpsc::channel::>(); + + let _ = std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + + let client_option = rt.block_on(async { Client::new(None).await }); + + let _ = tx.send(client_option); + + std::thread::park(); + }); + + rc +} + +pub fn run_server(sender: Sender<()>) { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + + let mut server = rt.block_on(Server::new()); + rt.block_on(server.start()); + + let _ = sender.send(()); + + rt.block_on(server.update()); + }); +} + +pub fn connect_bot(dificluty: BotDificulty) { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + + let bot = rt.block_on(Bot::new(dificluty)); + + rt.block_on(bot.game_loop()); + }); +}