Merge pull request #5 from szymon-jozef/feat/macroquad-ui

feat/macroquad ui
This commit is contained in:
Szymon
2026-09-05 16:48:17 +02:00
committed by GitHub
32 changed files with 2059 additions and 82 deletions

236
Cargo.lock generated
View File

@@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.5" version = "1.1.5"
@@ -11,6 +17,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "1.0.0" version = "1.0.0"
@@ -76,6 +88,12 @@ dependencies = [
"critical-section", "critical-section",
] ]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -94,6 +112,12 @@ version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytemuck"
version = "1.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@@ -130,6 +154,7 @@ dependencies = [
"dirs", "dirs",
"env_logger", "env_logger",
"log", "log",
"macroquad",
"postcard", "postcard",
"rand", "rand",
"serde", "serde",
@@ -147,12 +172,27 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.5" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" 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]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.3.0" version = "0.3.0"
@@ -162,6 +202,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "critical-section" name = "critical-section"
version = "1.2.0" version = "1.2.0"
@@ -271,12 +320,41 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.5" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 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]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.34" version = "0.3.34"
@@ -326,6 +404,12 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "glam"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
[[package]] [[package]]
name = "hash32" name = "hash32"
version = "0.2.1" version = "0.2.1"
@@ -341,6 +425,8 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash", "foldhash",
] ]
@@ -376,6 +462,19 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.14.0" version = "2.14.0"
@@ -459,6 +558,12 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.20" version = "0.1.20"
@@ -483,12 +588,63 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.3" version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" 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]] [[package]]
name = "mio" name = "mio"
version = "1.2.2" version = "1.2.2"
@@ -500,6 +656,30 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@@ -547,6 +727,19 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 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]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.15.0" version = "1.15.0"
@@ -594,6 +787,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quad-rand"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -764,6 +963,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" 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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@@ -1043,6 +1257,28 @@ dependencies = [
"semver", "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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"

View File

@@ -13,6 +13,7 @@ edition = "2024"
dirs = "6.0.0" dirs = "6.0.0"
env_logger = "0.11.11" env_logger = "0.11.11"
log = "0.4.33" log = "0.4.33"
macroquad = "0.4.16"
postcard = { version = "1.1.3", features = ["alloc"] } postcard = { version = "1.1.3", features = ["alloc"] }
rand = "0.10.2" rand = "0.10.2"
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }

BIN
assets/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

View File

@@ -16,11 +16,26 @@
# rustc # rustc
# already provided by buildRustPackage :p # already provided by buildRustPackage :p
pkg-config pkg-config
makeWrapper
]; ];
# runtime deps # runtime deps
buildInputs = with pkgs; [ buildInputs = with pkgs; [
wayland
wayland-protocols
libGL
alsa-lib
libX11.out
libXi.out
libXcursor.out
libXrandr.out
libxkbcommon.out
libxcb
libXinerama
libXxf86vm
fontconfig
]; ];
meta = { meta = {

View File

@@ -1,5 +1,11 @@
{ {
perSystem = { pkgs, config, ... }: { perSystem =
{
pkgs,
config,
...
}:
{
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
rustfmt rustfmt
@@ -8,6 +14,13 @@
RUST_LOG = "debug"; RUST_LOG = "debug";
LD_LIBRARY_PATH = builtins.concatStringsSep ":" [
"${pkgs.libX11}/lib"
"${pkgs.libXi}/lib"
"${pkgs.libGL}/lib"
"${pkgs.libxkbcommon}/lib"
];
inputsFrom = [ inputsFrom = [
(config.packages.default) (config.packages.default)
]; ];

View File

@@ -15,6 +15,7 @@ const QUIT_COMMAND: &str = "/quit";
const HELP_COMMAND: &str = "/help"; const HELP_COMMAND: &str = "/help";
const SEND_COMMAND: &str = "/send"; const SEND_COMMAND: &str = "/send";
const READY_COMMAND: &str = "/ready"; const READY_COMMAND: &str = "/ready";
const UNREADY_COMMAND: &str = "/unready";
const SINGLEPLAYER_COMMAND: &str = "/single"; const SINGLEPLAYER_COMMAND: &str = "/single";
const CAPTURE_COMMAND: &str = "/capture"; const CAPTURE_COMMAND: &str = "/capture";
@@ -26,6 +27,8 @@ enum CliCommands {
Send, Send,
Ready, Ready,
Unready,
Single, Single,
Capture, Capture,
@@ -50,6 +53,7 @@ impl TryFrom<&str> for CliCommands {
HELP_COMMAND => Ok(CliCommands::Help), HELP_COMMAND => Ok(CliCommands::Help),
SEND_COMMAND => Ok(CliCommands::Send), SEND_COMMAND => Ok(CliCommands::Send),
READY_COMMAND => Ok(CliCommands::Ready), READY_COMMAND => Ok(CliCommands::Ready),
UNREADY_COMMAND => Ok(CliCommands::Unready),
SINGLEPLAYER_COMMAND => Ok(CliCommands::Single), SINGLEPLAYER_COMMAND => Ok(CliCommands::Single),
CAPTURE_COMMAND => Ok(CliCommands::Capture), CAPTURE_COMMAND => Ok(CliCommands::Capture),
@@ -59,6 +63,7 @@ impl TryFrom<&str> for CliCommands {
} }
} }
#[derive(Default)]
struct ClientContext { struct ClientContext {
pub identity: Option<NetworkIdentity>, pub identity: Option<NetworkIdentity>,
pub is_my_turn: bool, pub is_my_turn: bool,
@@ -67,18 +72,6 @@ struct ClientContext {
pub available_moves: Option<Vec<MovePath>>, 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,
}
}
}
async fn run_bot() { async fn run_bot() {
let bot = Bot::new(BotDificulty::Easy).await; let bot = Bot::new(BotDificulty::Easy).await;
bot.game_loop().await; bot.game_loop().await;
@@ -102,12 +95,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
loop { loop {
let mut buffer = String::new(); let mut buffer = String::new();
if stdin.read_line(&mut buffer).is_ok() { if stdin.read_line(&mut buffer).is_ok() && stdin_sender.blocking_send(buffer).is_err() {
if stdin_sender.blocking_send(buffer).is_err() {
break; break;
} }
} }
}
}); });
let mut client_receiver = client let mut client_receiver = client
@@ -119,6 +110,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
│\t {} - quit │\t {} - quit
│\t {} - show this message │\t {} - show this message
│\t {} - message the server you're ready │\t {} - message the server you're ready
│\t {} - revoke readiness
│\t {} - send text message to the server │\t {} - send text message to the server
│\t {} - play alone │\t {} - play alone
│ │
@@ -126,7 +118,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
│\t {} <index> <index>- choose move from the list. First number is pawn you want to move and second is position it should take │\t {} <index> <index>- choose move from the list. First number is pawn you want to move and second is position it should take
└─────────────────────────────────────── └───────────────────────────────────────
", ",
QUIT_COMMAND, HELP_COMMAND, READY_COMMAND, SEND_COMMAND, 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───────"); println!("───────Type /help for help───────");
@@ -209,6 +201,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
client.signal_readiness().await; client.signal_readiness().await;
} }
CliCommands::Unready => {
client.revoke_readiness().await;
}
CliCommands::Single => { CliCommands::Single => {
println!("Tried connecting bot to the server! Make sure to be /ready"); println!("Tried connecting bot to the server! Make sure to be /ready");
tokio::spawn(run_bot()); tokio::spawn(run_bot());

169
src/bin/macroquad_client.rs Normal file
View File

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

View File

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

View File

@@ -79,6 +79,7 @@ impl Board {
} }
/// For testing /// For testing
#[allow(dead_code)]
fn new_empty(player1: &mut Player, player2: &mut Player, size: Option<usize>) -> Self { fn new_empty(player1: &mut Player, player2: &mut Player, size: Option<usize>) -> Self {
debug!("Creating new empty board!"); debug!("Creating new empty board!");
let size = size.unwrap_or(8); let size = size.unwrap_or(8);
@@ -104,6 +105,7 @@ impl Board {
Board { board, size } Board { board, size }
} }
#[allow(dead_code)]
fn place_pawn(&mut self, target: Position, owner: &Player) { fn place_pawn(&mut self, target: Position, owner: &Player) {
debug!("Placing new pawn by hand!"); debug!("Placing new pawn by hand!");
self[target].pawn = Some(Pawn::new(owner)); self[target].pawn = Some(Pawn::new(owner));
@@ -131,6 +133,7 @@ impl Board {
pos.is_in_range(self.size) && self[pos].pawn.is_none() pos.is_in_range(self.size) && self[pos].pawn.is_none()
} }
#[allow(dead_code)]
fn is_pawn_movable(&self, pos: Position, player: &Player) -> bool { fn is_pawn_movable(&self, pos: Position, player: &Player) -> bool {
self.get_available_moves(pos, player) self.get_available_moves(pos, player)
.is_some_and(|moves| !moves.is_empty()) .is_some_and(|moves| !moves.is_empty())
@@ -218,7 +221,7 @@ impl Board {
from, to 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..."); debug!("Player wasn't owner of the pawn...");
return false; return false;
} }
@@ -279,9 +282,9 @@ impl Board {
let target_option: Option<Position> = current_pos.checked_add(direction); let target_option: Option<Position> = current_pos.checked_add(direction);
if let Some(target) = target_option 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!( debug!(
"Found possible capture: {} -> {}", "Found possible capture: {} -> {}",
current_pos, path_after_capture current_pos, path_after_capture
@@ -303,11 +306,10 @@ impl Board {
available_directions, available_directions,
player, player,
all_paths, all_paths,
&pawn, pawn,
); );
} }
} }
}
if !found_any_capture && !current_path.is_empty() && !captured_enemies.is_empty() { if !found_any_capture && !current_path.is_empty() && !captured_enemies.is_empty() {
all_paths.push(CapturePath { all_paths.push(CapturePath {
@@ -415,7 +417,7 @@ impl Board {
return false; 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); warn!("Player {} tried moving a pawn which he doesn own!", player);
debug!("Tried moving from {} to {}", from, to); debug!("Tried moving from {} to {}", from, to);
return false; return false;
@@ -462,7 +464,7 @@ impl Board {
return false; 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); warn!("Player {} tried moving a pawn which he doesn own!", player);
return false; return false;
} }
@@ -491,9 +493,9 @@ impl Board {
impl Index<usize> for Board { impl Index<usize> for Board {
type Output = [Field]; type Output = [Field];
fn index<'a>(&'a self, i: usize) -> &'a [Field] { fn index(&self, i: usize) -> &[Field] {
let start = i * &self.size; let start = i * self.size;
let end: usize = start + &self.size; let end: usize = start + self.size;
&self.board[start..end] &self.board[start..end]
} }
} }
@@ -507,9 +509,9 @@ impl Index<Position> for Board {
} }
impl IndexMut<usize> for Board { impl IndexMut<usize> for Board {
fn index_mut<'a>(&'a mut self, i: usize) -> &'a mut [Field] { fn index_mut(&mut self, i: usize) -> &mut [Field] {
let start = i * &self.size; let start = i * self.size;
let end: usize = start + &self.size; let end: usize = start + self.size;
&mut self.board[start..end] &mut self.board[start..end]
} }
} }

View File

@@ -6,7 +6,7 @@ use uuid::Uuid;
use crate::logic::board::{board::Board, field::Field}; use crate::logic::board::{board::Board, field::Field};
/// View of the board, without any methods. Meant for sending over network /// 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 struct BoardView {
pub board: Vec<Field>, pub board: Vec<Field>,
pub size: usize, pub size: usize,
@@ -24,13 +24,31 @@ impl From<Board> for BoardView {
impl Index<usize> for BoardView { impl Index<usize> for BoardView {
type Output = [Field]; type Output = [Field];
fn index<'a>(&'a self, i: usize) -> &'a [Field] { fn index(&self, i: usize) -> &[Field] {
let start = i * &self.size; let start = i * self.size;
let end: usize = start + &self.size; let end: usize = start + self.size;
&self.board[start..end] &self.board[start..end]
} }
} }
impl IntoIterator for BoardView {
type Item = Field;
type IntoIter = std::vec::IntoIter<Field>;
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 { impl BoardView {
pub fn to_string(&self, owner: &Uuid) -> String { pub fn to_string(&self, owner: &Uuid) -> String {
(0..self.size) (0..self.size)

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::logic::{board::pawn::Pawn, math::position::Position}; 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 struct Field {
pub position: Position, pub position: Position,
pub pawn: Option<Pawn>, pub pawn: Option<Pawn>,

View File

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

View File

@@ -105,7 +105,7 @@ impl GameMaster {
fn is_player_lost(&self, player: &Player) -> bool { fn is_player_lost(&self, player: &Player) -> bool {
let player_pawns = self.board.get_player_pawns_positions(player); let player_pawns = self.board.get_player_pawns_positions(player);
player_pawns.len() == 0 player_pawns.is_empty()
|| player_pawns.iter().all(|pos| { || player_pawns.iter().all(|pos| {
self.board.get_available_moves(*pos, player).is_none() self.board.get_available_moves(*pos, player).is_none()
&& self.board.get_available_captures(*pos, player).is_none() && self.board.get_available_captures(*pos, player).is_none()

View File

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::logic::math::vector::Vector2D; 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 /// Position on the board. Consists of row and column. Should be treated like a point in space on
/// unsigned grid. /// unsigned grid.
/// ///

View File

@@ -129,7 +129,7 @@ impl Client {
async fn send_message(&mut self, msg: Result<Message<ClientMessage>, postcard::Error>) { async fn send_message(&mut self, msg: Result<Message<ClientMessage>, postcard::Error>) {
debug!("Sending message to the server!"); debug!("Sending message to the server!");
let _ = match msg { match msg {
Err(e) => { Err(e) => {
error!("There was an error while creating the message: {}", e); error!("There was an error while creating the message: {}", e);
return; return;
@@ -137,10 +137,9 @@ impl Client {
Ok(msg) => { Ok(msg) => {
if let Err(e) = self.conn_sender_outgoing.send(msg).await { if let Err(e) = self.conn_sender_outgoing.send(msg).await {
error!("Error while sending a message to connection thread: {}", e); error!("Error while sending a message to connection thread: {}", e);
return;
} }
} }
}; }
} }
pub async fn send_capture(&mut self, capture_path: CapturePath) { pub async fn send_capture(&mut self, capture_path: CapturePath) {
@@ -168,6 +167,12 @@ impl Client {
self.send_message(msg).await; 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<Receiver<ServerMessage>> { pub fn get_update_receiver(&mut self) -> Option<Receiver<ServerMessage>> {
self.update_receiver.take() self.update_receiver.take()
} }

View File

@@ -110,8 +110,7 @@ where
if let Err(e) = tcp.write_all(&msg).await { if let Err(e) = tcp.write_all(&msg).await {
error!("Error while sending content to: {:?}\n{}", self.peer, e); error!("Error while sending content to: {:?}\n{}", self.peer, e);
return; }
};
} }
pub async fn start_listening(&mut self) { pub async fn start_listening(&mut self) {
@@ -202,20 +201,20 @@ where
{ {
error!("Erro while sending message to main thread: {}", e); error!("Erro while sending message to main thread: {}", e);
} }
return Ok(()); Ok(())
} }
Err(e) => { Err(e) => {
error!( error!(
"Error while preparing message in read_content for sender: {}", "Error while preparing message in read_content for sender: {}",
e e
); );
return Ok(()); Ok(())
} }
} }
} }
Err(e) => { Err(e) => {
error!("Error while getting the message from bits: {}", e); error!("Error while getting the message from bits: {}", e);
return Ok(()); Ok(())
} }
} }
} }

View File

@@ -60,6 +60,7 @@ pub enum ServerMessage {
pub enum ClientMessage { pub enum ClientMessage {
AnswerHandshake { player_name: String }, AnswerHandshake { player_name: String },
SignalReadiness, SignalReadiness,
SignalUnreadiness,
ConnectionDead { addr: SocketAddr }, ConnectionDead { addr: SocketAddr },

View File

@@ -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 // 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 // more connections, but they won't be able to play
#[derive(PartialEq)] #[derive(Debug, PartialEq, Default)]
enum ServerStage { pub enum ServerStage {
#[default]
Lobby, Lobby,
Game, Game,
End, End,
@@ -155,7 +156,7 @@ impl Server {
let reason = "There is currently game going and it doesn't allow spectators".to_string(); let reason = "There is currently game going and it doesn't allow spectators".to_string();
info!("Rejecting {}, because {}", addr, reason); 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 { if let Ok(msg) = msg {
let _ = sender.send(msg).await; // we can't use send_message because we let _ = sender.send(msg).await; // we can't use send_message because we
@@ -188,6 +189,10 @@ impl Server {
self.handle_readiness(addr).await self.handle_readiness(addr).await
} }
ClientMessage::SignalUnreadiness => {
self.handle_unreadiness(addr).await;
}
ClientMessage::RequestCapture { capture_path } => { ClientMessage::RequestCapture { capture_path } => {
self.process_capture(capture_path).await; 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 === */ /* === Sending messages === */
/// Should be used after every players move /// Should be used after every players move
@@ -464,7 +500,6 @@ impl Server {
} }
Err(e) => { Err(e) => {
error!("Error while broadcasting the message: {}", e); error!("Error while broadcasting the message: {}", e);
return;
} }
} }
} }

View File

@@ -8,8 +8,9 @@ use crate::{
settings::{client_settings::ClientSettings, general_settings::DEFAULT_URL}, settings::{client_settings::ClientSettings, general_settings::DEFAULT_URL},
}; };
#[derive(PartialEq, Eq, Clone, Copy)] #[derive(PartialEq, Eq, Clone, Copy, Default)]
pub enum BotDificulty { pub enum BotDificulty {
#[default]
Easy, Easy,
Normal, Normal,
Hard, Hard,
@@ -65,12 +66,12 @@ impl Bot {
.choose(&mut rand::rng()) .choose(&mut rand::rng())
.expect("Could not get random move"); .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: _ } => { crate::network::message::ServerMessage::BroadcastBoardState { board: _ } => {
info!("Got server state!"); info!("Got server state!");
match dificulty.clone() { match dificulty {
BotDificulty::Easy => {} // we do nothing with it because easy bot BotDificulty::Easy => {} // we do nothing with it because easy bot
// just does random stuff. In future // just does random stuff. In future
// board_view can be used to calculate the // board_view can be used to calculate the

View File

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

View File

@@ -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<GuiField>,
}
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<Position> for GuiFieldVec {
type Output = GuiField;
fn index(&self, index: Position) -> &Self::Output {
self.gui_fields
.iter()
.find(|field| field.pos == index)
.unwrap()
}
}
impl IndexMut<Position> 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<Vec<CapturePath>>,
current_deepness: usize,
current_capture_path: Vec<Position>,
choosen_capture: Option<CapturePath>,
available_moves: Option<Vec<MovePath>>,
choosen_move: Option<MovePath>,
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<CapturePath> {
self.choosen_capture.take()
}
pub fn get_choosen_move(&mut self) -> Option<MovePath> {
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<CapturePath>) {
self.available_captures = Some(captures);
self.available_moves = None;
}
pub fn update_available_moves(&mut self, moves: Vec<MovePath>) {
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<Position>) {
for field in fields {
self.fields[field].color = CAPTURE_HIGHLITGHT_COLOR;
}
}
}

View File

@@ -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>,
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<GuiCommands>) {
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.
}
}

View File

@@ -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<GuiCommands>,
update_receiver: Receiver<ServerMessage>,
identity: Option<NetworkIdentity>,
board: Board,
is_my_turn: bool,
game_state: ServerStage,
game_area: Rect,
board_area: Rect,
lobby: Lobby,
chat: Chat,
game_result: Option<String>,
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<Self> {
let Some(update_receiver) = client.get_update_receiver() else {
return None;
};
let (cmd_sender, mut cmd_recv) = mpsc::channel::<GuiCommands>(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();
}
}
}

View File

@@ -0,0 +1,3 @@
mod board;
mod chat;
pub mod game;

View File

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

View File

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

7
src/ui/macroquad/mod.rs Normal file
View File

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

View File

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

View File

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

View File

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

2
src/ui/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod macroquad;
pub mod state;

92
src/ui/state.rs Normal file
View File

@@ -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<Option<Client>>),
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<String>,
}
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<Option<Client>> {
let (tx, rc) = mpsc::channel::<Option<Client>>();
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());
});
}