commit 86dfecb4b39ce43bf49c6ce32829a6418a815e03 Author: Alexander Bass Date: Sat Apr 22 18:10:47 2023 -0400 init diff --git a/.cargo/cargo.toml b/.cargo/cargo.toml new file mode 100644 index 0000000..0a30fd8 --- /dev/null +++ b/.cargo/cargo.toml @@ -0,0 +1,3 @@ + +[target.x86_64-unknown-linux-gnu] +rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c6cb93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +.rustc_info.json +progress \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7b734d3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,411 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "audir-sles" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5" + +[[package]] +name = "audrey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8" +dependencies = [ + "dasp_frame", + "dasp_sample", + "hound", + "lewton", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide 0.6.2", +] + +[[package]] +name = "fontdue" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0793f5137567643cf65ea42043a538804ff0fbf288649e2141442b602d81f9bc" +dependencies = [ + "hashbrown", + "ttf-parser", +] + +[[package]] +name = "glam" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "lewton" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0" +dependencies = [ + "byteorder", + "ogg", + "smallvec", +] + +[[package]] +name = "libc" +version = "0.2.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" + +[[package]] +name = "macroquad" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3790f7fd2e4c480108cbfc86488f023b72e1e0bb6ffd5c6cba38049c7e2fbfc" +dependencies = [ + "bumpalo", + "fontdue", + "glam", + "image", + "macroquad_macro", + "miniquad", + "quad-rand", + "quad-snd", +] + +[[package]] +name = "macroquad_macro" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5cecfede1e530599c8686f7f2d609489101d3d63741a6dc423afc997ce3fcc8" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "minesweeper" +version = "0.1.0" +dependencies = [ + "image", + "macroquad", +] + +[[package]] +name = "miniquad" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46381fe09fbf91bfa402a3e4fc26a104c9130562d51f89964c46adbc00591496" +dependencies = [ + "libc", + "ndk-sys", + "objc", + "winapi", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "ogg" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "png" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "quad-alsa-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5" +dependencies = [ + "libc", +] + +[[package]] +name = "quad-rand" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658fa1faf7a4cc5f057c9ee5ef560f717ad9d8dc66d975267f709624d6e1ab88" + +[[package]] +name = "quad-snd" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c954bb70493a2872775b74b663a767686e6d96d242e789d6a92cc4ebd2a64e" +dependencies = [ + "audir-sles", + "audrey", + "libc", + "quad-alsa-sys", + "winapi", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + +[[package]] +name = "ttf-parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c9c39e8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "minesweeper" +authors = ["Alexander Bass"] +version = "0.1.0" +edition = "2021" + +[dependencies] +image = { version = "0.24.6", default-features = false, features = ["png"] } + +macroquad = { version = "0.3.25", default-features = false, features = [ + # Audio is not needed for this, but a bug in Macroquad requires that the audio feature is present, else crash. + "audio", +] } + +[profile.release] +lto = true +strip = true +# codegen-units = 1 # Reduce number of codegen units to increase optimizations diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..cb92e9f --- /dev/null +++ b/README.MD @@ -0,0 +1,15 @@ +# Rust Minesweeper + + + +# Running +To run the desktop version: +``` +cargo run +``` + +To compile to WASM: +``` +cargo build --target wasm32-unknown-unknown --release +``` +To run that WASM, copy the resulting `target/wasm32-unknown-unknown/release/wasm` into the same directory as `minesweeper.html` and serve both files through a webserver. \ No newline at end of file diff --git a/assets/button.png b/assets/button.png new file mode 100644 index 0000000..00122d7 Binary files /dev/null and b/assets/button.png differ diff --git a/assets/button_clicked.png b/assets/button_clicked.png new file mode 100644 index 0000000..d085aad Binary files /dev/null and b/assets/button_clicked.png differ diff --git a/assets/cog.png b/assets/cog.png new file mode 100644 index 0000000..d605f81 Binary files /dev/null and b/assets/cog.png differ diff --git a/assets/english_32x.png b/assets/english_32x.png new file mode 100644 index 0000000..30e0046 Binary files /dev/null and b/assets/english_32x.png differ diff --git a/assets/exit_button.png b/assets/exit_button.png new file mode 100644 index 0000000..3f9595b Binary files /dev/null and b/assets/exit_button.png differ diff --git a/assets/exit_button_hover.png b/assets/exit_button_hover.png new file mode 100644 index 0000000..86fae89 Binary files /dev/null and b/assets/exit_button_hover.png differ diff --git a/assets/faces.png b/assets/faces.png new file mode 100644 index 0000000..968fdbd Binary files /dev/null and b/assets/faces.png differ diff --git a/assets/icons.png b/assets/icons.png new file mode 100644 index 0000000..b558061 Binary files /dev/null and b/assets/icons.png differ diff --git a/assets/japanese_32x.png b/assets/japanese_32x.png new file mode 100644 index 0000000..8d5b4d9 Binary files /dev/null and b/assets/japanese_32x.png differ diff --git a/assets/numbers.png b/assets/numbers.png new file mode 100644 index 0000000..3b990a6 Binary files /dev/null and b/assets/numbers.png differ diff --git a/minesweeper.html b/minesweeper.html new file mode 100644 index 0000000..7c72608 --- /dev/null +++ b/minesweeper.html @@ -0,0 +1,42 @@ + + + + + + + Minesweeper + + + + +
+ + + + + + +
+ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c2e5f01 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +max_width = 130 +attr_fn_like_width = 100 +chain_width = 100 +hard_tabs = true diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..75e1d8c --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,133 @@ +mod board_render; +mod highlighter; +pub mod settings_menu; +pub mod texture_store; +mod tile_render; +pub mod top_menu; +pub mod ui_event; + +use self::{ + highlighter::Highlighter, settings_menu::SettingsMenu, texture_store::TextureStore, top_menu::GUITop, ui_event::GUIEvents, +}; +use macroquad::prelude::*; +#[derive(Default, Copy, Clone, Debug)] +pub enum Language { + #[default] + English, + Japanese, +} +#[derive(Debug, Default)] +pub struct UIState { + pub width: usize, + pub height: usize, + pub tile_size: usize, + pub mouse_in_minefield: bool, + pub top_offset: usize, // Space above board to be used for other ui + pub reveal_all: bool, + pub letterbox: (f32, f32), + pub scale: f32, + pub frozen: bool, + pub cursor: (usize, usize), + pub settings_open: bool, + pub language: Language, +} +impl UIState { + pub fn new(width: usize, height: usize, tile_size: usize, top_offset: usize) -> Self { + return Self { + width, + height, + tile_size, + top_offset, + ..Default::default() + }; + } + pub fn update_dimensions(&mut self, width: usize, height: usize) { + self.width = width; + self.height = height; + } + pub fn update_letterbox(&mut self, screen_width: f32, screen_height: f32) { + let game_aspect_ratio = self.width as f32 / (self.height as f32 + self.top_offset as f32 / self.tile_size as f32); + + let screen_aspect_ratio = screen_width / screen_height; + if game_aspect_ratio > screen_aspect_ratio { + self.scale = screen_width / (self.width * self.tile_size) as f32; + } else { + self.scale = screen_height / ((self.height * self.tile_size) + self.top_offset) as f32; + } + + let total_height = (self.height * self.tile_size + self.top_offset) as f32 * self.scale; + + let total_width = (self.width * self.tile_size) as f32 * self.scale; + + if total_height < screen_height { + self.letterbox.0 = 0f32; + self.letterbox.1 = (screen_height - total_height) * 0.5; + } else { + self.letterbox.0 = (screen_width - total_width) * 0.5; + self.letterbox.1 = 0f32; + } + } +} + +impl UIState { + pub fn pixel_screen_offset(&self, x: usize, y: usize) -> (f32, f32) { + let (x, y) = self.pixel_screen_scale(x, y); + let x = x + self.letterbox.0; + let y = y + self.letterbox.1; + return (x, y); + } + pub fn pixel_screen_scale(&self, x: usize, y: usize) -> (f32, f32) { + let x = x as f32; + let y = y as f32; + return (x * self.scale, y * self.scale); + } +} + +#[derive(Default)] +pub struct GameUI { + pub event_handler: GUIEvents, + pub highlighter: Highlighter, + pub state: UIState, + pub settings_menu: SettingsMenu, + pub texture_store: TextureStore, + pub top_menu: GUITop, +} + +impl GameUI { + pub fn new(settings: UIState) -> Self { + let set = settings; + return Self { + state: set, + ..Default::default() + }; + } + pub fn is_valid_position(&self, x: usize, y: usize) -> bool { + if x < self.state.width && y < self.state.height { + return true; + } + false + } + + pub fn set_cursor(&mut self, x: usize, y: usize) { + self.state.cursor = (x, y); + } + + pub fn clear(&mut self) { + self.state.frozen = false; + self.state.reveal_all = false; + self.event_handler.clear(); + } + + pub fn to_coordinate_system(&self, x: f32, y: f32) -> Option<(usize, usize)> { + let y = y - self.state.top_offset as f32; + if x < 0.0 || y < 0.0 { + return None; + } + let y = (y / self.state.tile_size as f32) as usize; + let x = (x / self.state.tile_size as f32) as usize; + if !self.is_valid_position(x, y) { + return None; + } + return Some((x, y)); + } +} diff --git a/src/gui/board_render.rs b/src/gui/board_render.rs new file mode 100644 index 0000000..7cfd2e3 --- /dev/null +++ b/src/gui/board_render.rs @@ -0,0 +1,45 @@ +use crate::logic::game_board::GameBoard; +use macroquad::prelude::*; + +use super::{texture_store::TextureStore, ui_event::GUIEvents, UIState}; +impl GameBoard { + pub fn render(&self, textures: &TextureStore, settings: &UIState) { + // dbg!(&settings.top_offset, &settings.render_scale); + let tile_size = settings.tile_size; + let (scaled_tile, _) = settings.pixel_screen_scale(tile_size, 0); + for (x, col) in self.tiles.iter().enumerate() { + for (y, tile) in col.iter().enumerate() { + let (x, y) = settings.pixel_screen_offset(x * tile_size, y * tile_size + settings.top_offset); + draw_texture_ex( + textures.get_tiles()[tile.render(settings.reveal_all) as usize], + x, + y, + WHITE, + DrawTextureParams { + dest_size: Some(vec2(scaled_tile, scaled_tile)), + source: Some(Rect { + x: 0.0, + y: 0.0, + w: 32.0, + h: 32.0, + }), + rotation: 0.0, + flip_x: false, + flip_y: false, + pivot: None, + }, + ); + } + } + } + pub fn events(&self, settings: &UIState, event_handler: &mut GUIEvents) { + if settings.mouse_in_minefield && !settings.frozen { + if is_mouse_button_released(MouseButton::Left) { + event_handler.add(super::ui_event::GUIEvent::ClickTile(settings.cursor.0, settings.cursor.1)) + } + if is_mouse_button_released(MouseButton::Right) { + event_handler.add(super::ui_event::GUIEvent::ModifyTile(settings.cursor.0, settings.cursor.1)) + } + } + } +} diff --git a/src/gui/highlighter.rs b/src/gui/highlighter.rs new file mode 100644 index 0000000..8e5ed13 --- /dev/null +++ b/src/gui/highlighter.rs @@ -0,0 +1,194 @@ +#![allow(unused)] + +use crate::{ + logic::{ + game_board::GameBoard, + tile::{TileModifier, TileState}, + }, + util::{ADJACENT_WITHOUT_CENTER, ADJACENT_WITH_CENTER}, +}; + +use super::{top_menu::smile::SmileyState, ui_event::GUIEvent, ui_event::GUIEvents, UIState}; +use macroquad::prelude::*; +use std::default; + +#[derive(Default)] +pub struct Highlighter { + cursor_old: Option<(usize, usize)>, + pub highlight: Highlight, +} +#[derive(Clone, Copy, Default)] +pub enum Highlight { + #[default] + None, + Normal, + Wide, +} + +impl Highlighter { + pub fn events(&mut self, ui_state: &UIState, event_handler: &mut GUIEvents, game_board: &mut GameBoard) { + if !ui_state.frozen && ui_state.mouse_in_minefield { + if is_mouse_button_pressed(MouseButton::Left) { + self.highlight = Highlight::Normal; + } + if is_mouse_button_pressed(MouseButton::Middle) { + self.highlight = Highlight::Wide; + self.check_reveal(event_handler, ui_state, game_board) + } + } + if is_mouse_button_released(MouseButton::Left) { + self.reset_highlight(ui_state, event_handler); + if !ui_state.frozen { + event_handler.add(GUIEvent::SetSmileyState(SmileyState::Chillin)); + } + } + if is_mouse_button_released(MouseButton::Middle) { + self.reset_highlight(ui_state, event_handler); + if !ui_state.frozen { + event_handler.add(GUIEvent::SetSmileyState(SmileyState::Chillin)); + } + } + } + fn check_reveal(&self, event_handler: &mut GUIEvents, interface: &UIState, game_board: &mut GameBoard) { + let (x, y) = interface.cursor; + if let Some(tile) = game_board.get_tile_mut(x, y) { + let adjacent_mines = tile.adjacent; + if !tile.swept { + return; + } + if adjacent_mines == 0 { + return; + } + let mut adjacent_flags = 0; + let mut near = ADJACENT_WITHOUT_CENTER.to_vec(); + near.retain_mut(|pos| { + pos.0 += x as isize; + pos.1 += y as isize; + if pos.0 < 0 || pos.1 < 0 { + return false; + } + let x = pos.0 as usize; + let y = pos.1 as usize; + if let Some(tile) = game_board.get_tile(x, y) { + if let Some(TileModifier::Flagged) = tile.modifier { + adjacent_flags += 1; + return false; + } + return true; + } + false + }); + if adjacent_flags == adjacent_mines { + for empty_tile in near.iter() { + let x = empty_tile.0 as usize; + let y = empty_tile.1 as usize; + event_handler.add(GUIEvent::ClickTile(x, y)); + } + } + } + } + pub fn highlight(&mut self, interface: &UIState, event_handler: &mut GUIEvents) { + if interface.frozen { + return; + } + let (x, y) = interface.cursor; + match self.highlight { + Highlight::None => {} + Highlight::Normal => { + event_handler.add(GUIEvent::SetSmileyState(SmileyState::Suspense)); + + event_handler.add(GUIEvent::HighlightTile(x, y)); + } + Highlight::Wide => { + event_handler.add(GUIEvent::HighlightTile(x, y)); + event_handler.add(GUIEvent::SetSmileyState(SmileyState::Suspense)); + + for pos in ADJACENT_WITHOUT_CENTER.iter() { + let x = pos.0 + x as isize; + let y = pos.1 + y as isize; + if x < 0 || y < 0 || (interface.width as isize) <= x || (interface.height as isize) <= y { + continue; + } + let x = x as usize; + let y = y as usize; + event_handler.add(GUIEvent::HighlightTile(x, y)); + } + } + } + + self.move_highlight(&interface, event_handler); + } + + fn move_highlight(&mut self, interface: &UIState, event_handler: &mut GUIEvents) { + if let Some((old_x, old_y)) = self.cursor_old { + match self.highlight { + Highlight::None => (), + Highlight::Normal => { + event_handler.add(GUIEvent::UnHighlightTile(old_x, old_y)); + } + Highlight::Wide => { + let (new_x, new_y) = interface.cursor; + let mut old_highlighted_non_overlap = ADJACENT_WITH_CENTER.to_vec(); + // Retain all old highlighted points which do not overlap with new highlighted points + old_highlighted_non_overlap.retain_mut(|pos: &mut (isize, isize)| { + let x = pos.0 + old_x as isize; + let y = pos.1 + old_y as isize; + // Loop through old highlighted points to check if overlapping + for p in ADJACENT_WITH_CENTER.iter() { + let nx = p.0 + new_x as isize; + let ny = p.1 + new_y as isize; + if x == nx && y == ny { + // Do not retain point if point at same location + // found within new highlighted area + return false; + }; + } + // Update x and y value of `old_highlighted_non_overlap` as they currently are the + // initial values from the SCAN constant + pos.0 = x; + pos.1 = y; + true + }); + for pos in old_highlighted_non_overlap.iter() { + let x = pos.0; + let y = pos.1; + if x >= 0 && y >= 0 && x < interface.width as isize && y < interface.height as isize { + let x = x as usize; + let y = y as usize; + event_handler.add(GUIEvent::UnHighlightTile(x, y)); + } + } + } + } + } + + self.cursor_old = Some(interface.cursor); + } + + fn reset_highlight(&mut self, interface: &UIState, event_handler: &mut GUIEvents) { + if let Some((x, y)) = self.cursor_old { + match self.highlight { + Highlight::None => (), + Highlight::Normal => { + event_handler.add(GUIEvent::UnHighlightTile(x, y)); + } + Highlight::Wide => { + event_handler.add(GUIEvent::UnHighlightTile(x, y)); + + for pos in ADJACENT_WITHOUT_CENTER.iter() { + let x = pos.0 + x as isize; + let y = pos.1 + y as isize; + if x >= 0 && y >= 0 && x < interface.width as isize && y < interface.height as isize { + let x = x as usize; + let y = y as usize; + event_handler.add(GUIEvent::UnHighlightTile(x, y)); + } + } + } + } + } + + self.highlight = Highlight::None; + self.cursor_old = None; + } +} diff --git a/src/gui/settings_menu.rs b/src/gui/settings_menu.rs new file mode 100644 index 0000000..a99cdb0 --- /dev/null +++ b/src/gui/settings_menu.rs @@ -0,0 +1,229 @@ +use crate::logic::game_board::ModifyMode; + +use super::{ + texture_store::TextureStore, + ui_event::{GUIEvent, GUIEvents}, + Language, UIState, +}; +use macroquad::{ + hash, + prelude::*, + ui::{root_ui, widgets, Skin, Ui}, +}; + +pub struct SettingsMenu { + mines: usize, + width: usize, + height: usize, + board_modify_mode: ModifyMode, +} +impl Default for SettingsMenu { + fn default() -> Self { + Self { + mines: 99, + width: 30, + height: 16, + board_modify_mode: ModifyMode::Flag, + } + } +} + +impl SettingsMenu { + pub fn render( + &mut self, + ui_state: &UIState, + event_handler: &mut GUIEvents, + textures: &TextureStore, + skin: &Skin, + exit_button_skin: &Skin, + ) { + let screen_width = screen_width(); + let screen_height = screen_height(); + let background_color = Color::from_rgba(192, 192, 192, 255); + + root_ui().window(hash!(), vec2(0., 0.), vec2(screen_width, screen_height), |ui| { + draw_rectangle(0f32, 0f32, screen_width, screen_height, background_color); + ui.push_skin(&exit_button_skin); + if widgets::Button::new("").size(vec2(50.0, 50.0)).position(vec2(0f32, 0f32)).ui(ui) { + event_handler.add(GUIEvent::CloseSettings) + } + ui.pop_skin(); + ui.push_skin(&skin); + let half_screen_width = screen_width * 0.5; + const MIN_MINEFIELD_WIDTH: usize = 5; + const MAX_MINEFIELD_WIDTH: usize = 100; + const MIN_MINEFIELD_HEIGHT: usize = 5; + const MAX_MINEFIELD_HEIGHT: usize = 100; + render_counter( + &mut self.width, + ui, + &textures, + vec2(half_screen_width, 100f32), + String::from("Minefield Width"), + MIN_MINEFIELD_WIDTH, + MAX_MINEFIELD_WIDTH, + ); + render_counter( + &mut self.height, + ui, + &textures, + vec2(half_screen_width, 200f32), + String::from("Minefield Height"), + MIN_MINEFIELD_WIDTH, + MIN_MINEFIELD_HEIGHT, + ); + render_counter( + &mut self.mines, + ui, + &textures, + vec2(half_screen_width, 300f32), + String::from("Mines"), + 1, + self.width * self.height - 10, + ); + const NEW_GAME_HEIGHT: f32 = 40f32; + const NEW_GAME_WIDTH: f32 = 250f32; + if widgets::Button::new("New Game") + .size(vec2(NEW_GAME_WIDTH, NEW_GAME_HEIGHT)) + .position(vec2((screen_width - NEW_GAME_WIDTH) * 0.5, 0.0)) + .ui(ui) + { + event_handler.add(GUIEvent::CreateNewGame(self.width, self.height, self.mines)); + event_handler.add(GUIEvent::CloseSettings); + } + const BUTTON_MENU_WIDTH: f32 = 250f32; + let language_button_x = (screen_width - BUTTON_MENU_WIDTH) * 0.5; + const BUTTON_SIZE: f32 = 100f32; + const BUTTON_MENU_Y: f32 = 400f32; + let question_button_x = (screen_width - BUTTON_MENU_WIDTH) * 0.5 + (BUTTON_MENU_WIDTH - BUTTON_SIZE); + const BUTTON_MENU_LABEL_HEIGHT: f32 = 20f32; + widgets::Label::new("Language") + .position(vec2(language_button_x, BUTTON_MENU_Y - BUTTON_MENU_LABEL_HEIGHT)) + .size(vec2(BUTTON_SIZE, BUTTON_MENU_LABEL_HEIGHT)) + .ui(ui); + widgets::Label::new("Question Marking") + .position(vec2(question_button_x, BUTTON_MENU_Y - BUTTON_MENU_LABEL_HEIGHT)) + .size(vec2(BUTTON_SIZE, BUTTON_MENU_LABEL_HEIGHT)) + .ui(ui); + if let Language::English = ui_state.language { + if widgets::Button::new("English") + .size(vec2(BUTTON_SIZE, BUTTON_SIZE)) + .position(vec2(language_button_x, BUTTON_MENU_Y)) + .ui(ui) + { + event_handler.add(GUIEvent::SwitchLanguage(Language::Japanese)); + } + } else { + if widgets::Button::new("Japanese") + .size(vec2(BUTTON_SIZE, BUTTON_SIZE)) + .position(vec2(language_button_x, BUTTON_MENU_Y)) + .ui(ui) + { + event_handler.add(GUIEvent::SwitchLanguage(Language::English)); + } + } + if let ModifyMode::Question = self.board_modify_mode { + if widgets::Button::new("ON") + .size(vec2(BUTTON_SIZE, BUTTON_SIZE)) + .position(vec2(question_button_x, BUTTON_MENU_Y)) + .ui(ui) + { + self.board_modify_mode = ModifyMode::Flag; + event_handler.add(GUIEvent::SetQuestionMode(ModifyMode::Flag)); + } + } else { + if widgets::Button::new("OFF") + .size(vec2(BUTTON_SIZE, BUTTON_SIZE)) + .position(vec2(question_button_x, BUTTON_MENU_Y)) + .ui(ui) + { + self.board_modify_mode = ModifyMode::Question; + event_handler.add(GUIEvent::SetQuestionMode(ModifyMode::Question)); + } + } + }); + } +} + +fn render_counter( + count: &mut usize, + ui: &mut Ui, + textures: &TextureStore, + position: Vec2, + title: String, + min: usize, + max: usize, +) { + let digits: Vec = { + let digits = count.to_string(); + let digits = format!("{:0>3}", digits); + digits.chars().map(|i| (i.to_digit(10u32).unwrap_or(0)) as usize).collect() + }; + + const COUNTER_DIGIT_WIDTH: f32 = 13f32 * 2.0; + const COUNTER_DIGIT_HEIGHT: f32 = 23f32 * 2.0; + const COUNTER_BUTTON_HEIGHT: f32 = 30f32; + const COUNTER_BUTTON_MARGIN: f32 = 10f32; + const BUTTON_OFFSET_HEIGHT: f32 = (COUNTER_DIGIT_HEIGHT - COUNTER_BUTTON_HEIGHT) * 0.5; + + let counter_width = digits.len() as f32 * COUNTER_DIGIT_WIDTH; + let position = position - vec2(counter_width * 0.5, 0.0); + for (x, digit) in digits.iter().enumerate() { + let position = vec2(COUNTER_DIGIT_WIDTH * x as f32, 0f32) + position; + widgets::Texture::new(textures.numbers[*digit]) + .size(COUNTER_DIGIT_WIDTH, COUNTER_DIGIT_HEIGHT) + .position(position) + .ui(ui); + } + if widgets::Button::new("+") + .size(vec2(COUNTER_BUTTON_HEIGHT, COUNTER_BUTTON_HEIGHT)) + .position(position + vec2(counter_width + COUNTER_BUTTON_MARGIN, BUTTON_OFFSET_HEIGHT)) + .ui(ui) + { + *count += 1; + } + if widgets::Button::new("-") + .size(vec2(COUNTER_BUTTON_HEIGHT, COUNTER_BUTTON_HEIGHT)) + .position(position - vec2(COUNTER_BUTTON_HEIGHT + COUNTER_BUTTON_MARGIN, -BUTTON_OFFSET_HEIGHT)) + .ui(ui) + { + if *count > min { + *count -= 1; + } + } + if widgets::Button::new("++") + .size(vec2(COUNTER_BUTTON_HEIGHT, COUNTER_BUTTON_HEIGHT)) + .position( + position + + vec2( + counter_width + COUNTER_BUTTON_MARGIN * 2.0 + COUNTER_BUTTON_HEIGHT, + BUTTON_OFFSET_HEIGHT, + ), + ) + .ui(ui) + { + *count += 10; + } + if widgets::Button::new("--") + .size(vec2(COUNTER_BUTTON_HEIGHT, COUNTER_BUTTON_HEIGHT)) + .position(position - vec2((COUNTER_BUTTON_HEIGHT + COUNTER_BUTTON_MARGIN) * 2.0, -BUTTON_OFFSET_HEIGHT)) + .ui(ui) + { + if *count as isize - 10 > min as isize { + *count -= 10; + } else { + *count = min; + } + } + if *count > max { + *count = max; + } + if *count < min { + *count = min; + } + let label_height = 20f32; + widgets::Label::new(title) + .position(position - vec2(0f32, label_height)) + .size(vec2(counter_width, label_height)) + .ui(ui); +} diff --git a/src/gui/texture_store.rs b/src/gui/texture_store.rs new file mode 100644 index 0000000..b005274 --- /dev/null +++ b/src/gui/texture_store.rs @@ -0,0 +1,41 @@ +use image::ImageFormat; +use macroquad::texture::Texture2D; + +use crate::sprite_loader::load_sprites; + +use super::Language; + +pub struct TextureStore { + english_tiles: Vec, + japanese_tiles: Vec, + pub numbers: Vec, + pub smilies: Vec, + pub cog: Texture2D, + pub lang: Language, +} +impl Default for TextureStore { + fn default() -> Self { + Self::new() + } +} + +impl TextureStore { + pub fn new() -> Self { + Self { + numbers: load_sprites(include_bytes!("../../assets/numbers.png"), [26, 46], 1, 10).expect("Could not load sprites"), + english_tiles: load_sprites(include_bytes!("../../assets/english_32x.png"), [32, 32], 2, 8) + .expect("Could not load Tile Sprites"), + japanese_tiles: load_sprites(include_bytes!("../../assets/japanese_32x.png"), [32, 32], 2, 8) + .expect("Could not load Tile Sprites"), + smilies: load_sprites(include_bytes!("../../assets/faces.png"), [48, 48], 1, 5).expect("Could not load face sprites"), + cog: Texture2D::from_file_with_format(include_bytes!("../../assets/cog.png"), Some(ImageFormat::Png)), + lang: Language::English, + } + } + pub fn get_tiles(&self) -> &Vec { + return match self.lang { + Language::English => &self.english_tiles, + Language::Japanese => &self.japanese_tiles, + }; + } +} diff --git a/src/gui/tile_render.rs b/src/gui/tile_render.rs new file mode 100644 index 0000000..b7a20e1 --- /dev/null +++ b/src/gui/tile_render.rs @@ -0,0 +1,71 @@ +use crate::logic::tile::{Tile, TileModifier, TileState}; + +pub enum TileIndex { + Hidden, + Revealed, + Flag, + Question, + RevealedQuestion, //UNUSED. I'm not sure how this can come to be given that the win condition is for all empty tiles to be revealed. + RevealedMine, + Explosion, + FalseFlagMine, + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, +} + +impl Tile { + pub fn render(self, show_all: bool) -> TileIndex { + if self.swept && self.state == TileState::Mine { + return TileIndex::Explosion; + } + if show_all { + if let Some(modifier) = self.modifier { + if modifier == TileModifier::Flagged { + if self.state == TileState::Mine { + return TileIndex::Flag; + } else { + return TileIndex::FalseFlagMine; + } + } + } + if self.state == TileState::Mine { + return TileIndex::RevealedMine; + } + } + if self.swept { + if self.state == TileState::Mine { + TileIndex::Explosion + } else { + match self.adjacent { + 0 => TileIndex::Revealed, + 1 => TileIndex::One, + 2 => TileIndex::Two, + 3 => TileIndex::Three, + 4 => TileIndex::Four, + 5 => TileIndex::Five, + 6 => TileIndex::Six, + 7 => TileIndex::Seven, + 8 => TileIndex::Eight, + _ => TileIndex::RevealedQuestion, + } + } + } else { + if let Some(modif) = self.modifier { + match modif { + TileModifier::Flagged => TileIndex::Flag, + TileModifier::Unsure => TileIndex::Question, + } + } else if self.highlighted { + TileIndex::Revealed + } else { + TileIndex::Hidden + } + } + } +} diff --git a/src/gui/top_menu.rs b/src/gui/top_menu.rs new file mode 100644 index 0000000..3499d9c --- /dev/null +++ b/src/gui/top_menu.rs @@ -0,0 +1,81 @@ +use macroquad::{ + hash, + ui::{root_ui, widgets}, +}; + +pub mod flag_counter; +pub mod smile; +pub mod timer; + +use crate::logic::Minesweeper; + +use self::{flag_counter::GUIFlagCounter, smile::GUISmile, timer::GUITimer}; + +use super::{ + texture_store::TextureStore, + ui_event::{GUIEvent, GUIEvents}, + UIState, +}; +use macroquad::prelude::*; +#[derive(Default)] +pub struct GUITop { + pub flag_counter: GUIFlagCounter, + pub timer: GUITimer, + pub smile: GUISmile, +} + +impl GUITop { + pub fn render( + &mut self, + ui_state: &UIState, + game_logic: &Minesweeper, + event_handler: &mut GUIEvents, + textures: &TextureStore, + ) { + let background_color = Color::from_rgba(192, 192, 192, 255); + let border_color = Color::from_rgba(123, 123, 123, 255); + const BORDER_MARGIN: f32 = 3.0; + let top_offset = ui_state.top_offset as f32 * ui_state.scale + ui_state.letterbox.1; + let (x, y) = ui_state.pixel_screen_offset(0, 0); + let board_width = ui_state.width * ui_state.tile_size; + let (scaled_top_width, scaled_top_offset) = ui_state.pixel_screen_scale(board_width, ui_state.top_offset); + + root_ui().window(hash!(), vec2(0., 0.), vec2(screen_width(), top_offset), |ui| { + // Macroquad does not support window border colors so I hacked it in. + draw_rectangle(x, y, scaled_top_width, scaled_top_offset, border_color); + draw_rectangle( + BORDER_MARGIN + x, + BORDER_MARGIN + y, + scaled_top_width - BORDER_MARGIN * 2.0, + scaled_top_offset - BORDER_MARGIN * 2.0, + background_color, + ); + // done with the hackey bits. + + //Settings button. (didn't have enough logic to be broken into it's own file.) + { + const WIDTH: usize = 35; + const HEIGHT: usize = 35; + let pos_y = (ui_state.top_offset - HEIGHT) / 2; + let pos_x = (13 * 2 * 2 - WIDTH) / 2; + let (scaled_width, scaled_height) = ui_state.pixel_screen_scale(WIDTH as usize, HEIGHT); + let (pos_x, pos_y) = ui_state.pixel_screen_offset(pos_x, pos_y); + if widgets::Button::new(textures.cog) + .size(vec2(scaled_width, scaled_height)) + .position(vec2(pos_x, pos_y)) + .ui(ui) + { + if ui_state.settings_open { + event_handler.add(GUIEvent::CloseSettings) + } else { + event_handler.add(GUIEvent::OpenSettings) + } + } + } + + self.timer.render(&ui_state, game_logic.get_time(), ui, &textures); + self.smile.render(&ui_state, ui, event_handler, &textures); + self.flag_counter.render(&ui_state, game_logic.board.remaining_flags(), ui, &textures); + }); + } +} diff --git a/src/gui/top_menu/flag_counter.rs b/src/gui/top_menu/flag_counter.rs new file mode 100644 index 0000000..8d22719 --- /dev/null +++ b/src/gui/top_menu/flag_counter.rs @@ -0,0 +1,50 @@ +use macroquad::{ + prelude::*, + ui::{widgets, Ui}, +}; + +use crate::gui::texture_store::TextureStore; + +use super::UIState; + +pub struct GUIFlagCounter { + old_count: isize, + digits: Vec, +} +impl Default for GUIFlagCounter { + fn default() -> Self { + Self { + old_count: 0, + digits: vec![0, 0, 0, 0], + } + } +} + +impl GUIFlagCounter { + pub fn render(&mut self, ui_state: &UIState, remaining: isize, ui: &mut Ui, textures: &TextureStore) { + if self.old_count != remaining { + let remaining_string = remaining.abs().to_string(); + let remaining_string = format!("{:0>2}", remaining_string); + let digits: Vec = remaining_string.chars().map(|i| (i.to_digit(10u32).unwrap_or(0)) as usize).collect(); + let sign: usize = if remaining.signum() == -1 { 10 } else { 0 }; + let mut sign = vec![sign]; + sign.extend(digits); + self.digits = sign; + self.old_count = remaining; + } + + let top = ui_state.top_offset; + const WIDTH: usize = 13 * 2; + const HEIGHT: usize = 23 * 2; + let (scaled_width, scaled_height) = ui_state.pixel_screen_scale(WIDTH, HEIGHT); + + // let length = self.digits.len() as f32; + for (x, digit) in self.digits.iter().enumerate() { + let (pos_x, pos_y) = ui_state.pixel_screen_offset((x + 2) * WIDTH, (top - HEIGHT) / 2); + widgets::Texture::new(textures.numbers[*digit]) + .size(scaled_width, scaled_height) + .position(vec2(pos_x, pos_y)) + .ui(ui); + } + } +} diff --git a/src/gui/top_menu/smile.rs b/src/gui/top_menu/smile.rs new file mode 100644 index 0000000..21a76ed --- /dev/null +++ b/src/gui/top_menu/smile.rs @@ -0,0 +1,65 @@ +use macroquad::{ + prelude::*, + ui::{widgets, Ui}, +}; + +use crate::gui::{ + texture_store::TextureStore, + ui_event::{GUIEvent, GUIEvents}, +}; + +use super::UIState; +#[derive(Clone, Copy)] +pub enum SmileyState { + Chillin, + PressedChillin, + Suspense, + Victory, + Dead, +} +pub struct GUISmile { + reset_pressed: bool, + face: SmileyState, +} +impl Default for GUISmile { + fn default() -> Self { + Self { + reset_pressed: false, + face: SmileyState::Chillin, + } + } +} + +const WIDTH: usize = 70; +const HEIGHT: usize = 70; + +impl GUISmile { + pub fn render(&mut self, ui_state: &UIState, ui: &mut Ui, event_handler: &mut GUIEvents, textures: &TextureStore) { + let top_height = ui_state.top_offset; + let top_width = ui_state.width * ui_state.tile_size; + let pos_x = (top_width - HEIGHT) / 2; + let pos_y = (top_height - HEIGHT) / 2; + let (pos_x, pos_y) = ui_state.pixel_screen_offset(pos_x, pos_y); + let (scaled_width, scaled_height) = ui_state.pixel_screen_scale(WIDTH, HEIGHT); + if widgets::Button::new(textures.smilies[self.face as usize]) + .size(vec2(scaled_width, scaled_height)) + .position(vec2(pos_x, pos_y)) + .ui(ui) + { + self.reset_pressed = true; + } + if self.reset_pressed { + self.set_smile(SmileyState::PressedChillin); + + if is_mouse_button_released(MouseButton::Left) { + self.reset_pressed = false; + self.set_smile(SmileyState::Chillin); + + event_handler.add(GUIEvent::ClickReset); + } + } + } + pub fn set_smile(&mut self, face: SmileyState) { + self.face = face; + } +} diff --git a/src/gui/top_menu/timer.rs b/src/gui/top_menu/timer.rs new file mode 100644 index 0000000..312a397 --- /dev/null +++ b/src/gui/top_menu/timer.rs @@ -0,0 +1,48 @@ +use macroquad::{ + prelude::*, + ui::{widgets, Ui}, +}; + +use crate::gui::texture_store::TextureStore; + +use super::UIState; + +pub struct GUITimer { + old_time: u64, + digits: Vec, +} +impl Default for GUITimer { + fn default() -> Self { + Self { + old_time: 0u64, + digits: vec![0, 0, 0], + } + } +} + +impl GUITimer { + pub fn render(&mut self, ui_state: &UIState, time: Option, ui: &mut Ui, textures: &TextureStore) { + let time = time.unwrap_or_default() as u64; + // Only update digits if time is different + if self.old_time != time { + self.old_time = time; + let time_1 = time.to_string(); + let time_1 = format!("{:0>3}", time_1); + let digits: Vec = time_1.chars().map(|i| (i.to_digit(10u32).unwrap_or(0)) as usize).collect(); + self.digits = digits; + } + let top = ui_state.top_offset; + const WIDTH: usize = 13 * 2; + const HEIGHT: usize = 23 * 2; + let (scaled_width, scaled_height) = ui_state.pixel_screen_scale(WIDTH as usize, HEIGHT); + let board_width = ui_state.width * ui_state.tile_size; + let length = self.digits.len(); + for (x, digit) in self.digits.iter().enumerate() { + let (pos_x, pos_y) = ui_state.pixel_screen_offset(WIDTH * x + board_width - WIDTH * (length + 2), (top - HEIGHT) / 2); + widgets::Texture::new(textures.numbers[*digit]) + .size(scaled_width, scaled_height) + .position(vec2(pos_x, pos_y)) + .ui(ui); + } + } +} diff --git a/src/gui/ui_event.rs b/src/gui/ui_event.rs new file mode 100644 index 0000000..e70b229 --- /dev/null +++ b/src/gui/ui_event.rs @@ -0,0 +1,37 @@ +use crate::logic::game_board::ModifyMode; + +use super::{top_menu::smile::SmileyState, Language}; + +pub enum GUIEvent { + ClickReset, + OpenSettings, + CloseSettings, + SwitchLanguage(Language), + ClickTile(usize, usize), + ModifyTile(usize, usize), + HighlightTile(usize, usize), + UnHighlightTile(usize, usize), + CreateNewGame(usize, usize, usize), + SetQuestionMode(ModifyMode), + SetSmileyState(SmileyState), +} +#[derive(Default)] +pub struct GUIEvents { + events: Vec, +} + +impl GUIEvents { + pub fn add(&mut self, event: GUIEvent) { + self.events.push(event); + } + pub fn next(&mut self) -> Option { + if self.events.len() > 0 { + self.events.pop() + } else { + None + } + } + pub fn clear(&mut self) { + self.events.clear(); + } +} diff --git a/src/logic.rs b/src/logic.rs new file mode 100644 index 0000000..5ad32f6 --- /dev/null +++ b/src/logic.rs @@ -0,0 +1,88 @@ +pub mod events; +pub mod game_board; +pub mod tile; +mod timer; +use self::{ + events::{Events, GameEvent}, + timer::Timer, +}; +use game_board::GameBoard; +use std::error::Error; + +#[derive(PartialEq, Default)] +pub enum GameState { + #[default] + Empty, + Playing, + GameOver, + Victory, +} + +#[derive(Default)] +pub struct Minesweeper { + pub board: GameBoard, + pub state: GameState, + timer: Timer, + pub events: Events, +} + +impl Minesweeper { + pub fn new(width: usize, height: usize, mines: usize) -> Result> { + let game = Self { + board: GameBoard::new(width, height, mines)?, + ..Default::default() + }; + + Ok(game) + } + pub fn reset(&mut self) { + self.board.reset(); + self.state = GameState::Empty; + self.events.clear(); + self.events.add(GameEvent::Reset); + self.timer.clear(); + } + pub fn update_and_reset(&mut self, width: usize, height: usize, mines: usize) { + self.board.update(width, height, mines); + self.reset(); + } + + pub fn reveal(&mut self, x: usize, y: usize) { + if GameState::Empty == self.state { + self.timer.start(); + self.state = GameState::Playing; + self.events.add(GameEvent::InitDone); + } + if self.state != GameState::Playing { + return; + } + if let Some(state) = self.board.sweep(x, y, &mut self.events) { + if state == GameState::GameOver || state == GameState::Victory { + self.timer.stop() + } + self.state = state; + }; + self.events.add(GameEvent::SweepDone); + } + pub fn modify(&mut self, x: usize, y: usize) { + if self.state != GameState::Playing { + return; + } + self.board.modify(x, y, &mut self.events) + } + pub fn get_time(&self) -> Option { + return self.timer.elapsed(); + } + pub fn highlight(&mut self, x: usize, y: usize) { + if self.state == GameState::Playing || self.state == GameState::Empty { + if let Some(tile) = self.board.get_tile_mut(x, y) { + tile.highlight(); + } + } + } + pub fn remove_highlight(&mut self, x: usize, y: usize) { + if let Some(tile) = self.board.get_tile_mut(x, y) { + tile.remove_highlight(); + } + } +} diff --git a/src/logic/events.rs b/src/logic/events.rs new file mode 100644 index 0000000..ce081ab --- /dev/null +++ b/src/logic/events.rs @@ -0,0 +1,34 @@ +use super::game_board::GameBoard; +use super::tile::Tile; +pub enum GameEvent { + Lose(usize, usize, Tile), + RevealTile(usize, usize, Tile), + FlagTile(usize, usize, Tile), + QuestionTile(usize, usize, Tile), + SweepDone, + SweepBegin, + InitDone, + Win, + Reset, + GameEnd(GameBoard), +} +#[derive(Default)] +pub struct Events { + events: Vec, +} + +impl Events { + pub fn add(&mut self, event: GameEvent) { + self.events.push(event); + } + pub fn next(&mut self) -> Option { + if self.events.len() > 0 { + self.events.pop() + } else { + None + } + } + pub fn clear(&mut self) { + self.events.clear(); + } +} diff --git a/src/logic/game_board.rs b/src/logic/game_board.rs new file mode 100644 index 0000000..addab90 --- /dev/null +++ b/src/logic/game_board.rs @@ -0,0 +1,258 @@ +use std::error::Error; + +use std::collections::VecDeque; + +use macroquad::time; + +use super::tile::{TileModifier, TileState}; +use super::{Events, GameEvent, GameState}; +use crate::logic::tile::Tile; + +use crate::util::{ADJACENT_WITHOUT_CENTER, ADJACENT_WITH_CENTER}; + +#[derive(Clone, Default)] +pub struct GameBoard { + pub tiles: Vec>, + width: usize, + height: usize, + + state: BoardState, + non_mine_tiles: usize, + pub revealed_tiles: usize, + flags: usize, + mines: usize, + pub modify_mode: ModifyMode, +} +#[derive(Default, Clone)] +enum BoardState { + #[default] + Ungenerated, + Generated, +} +#[derive(Default, Clone, Debug)] +pub enum ModifyMode { + #[default] + Flag, + Question, +} + +impl GameBoard { + pub fn new(width: usize, height: usize, mines: usize) -> Result> { + if width == 0 || height == 0 { + return Err("Can't make game board with zero length dimension".into()); + }; + if (width * height) as isize - 9 < mines as isize { + // Can't have more mines than tiles on the game board. Else, the + // loop that places mines on board will never complete. + return Err("Not enough space for mines".into()); + } + + let board = Self { + tiles: vec![vec![Tile { ..Default::default() }; height]; width], + width, + height, + mines, + non_mine_tiles: width * height - mines, + ..Default::default() + }; + Ok(board) + } + + pub fn get_tile(&self, x: usize, y: usize) -> Option<&Tile> { + if self.is_valid_coord(x, y) { + return Some(&self.tiles[x][y]); + } + None + } + + pub fn get_tile_mut(&mut self, x: usize, y: usize) -> Option<&mut Tile> { + if self.is_valid_coord(x, y) { + return Some(&mut self.tiles[x][y]); + } + None + } + + pub fn is_valid_coord(&self, x: usize, y: usize) -> bool { + if x < self.width && y < self.height { + return true; + } + false + } + + pub fn reset(&mut self) { + self.state = BoardState::Ungenerated; + + self.revealed_tiles = 0; + self.flags = 0; + self.tiles = vec![vec![Tile { ..Default::default() }; self.height]; self.width] + } + pub fn update(&mut self, width: usize, height: usize, mines: usize) { + self.mines = mines; + self.height = height; + self.width = width; + self.non_mine_tiles = width * height - mines; + self.state = BoardState::Ungenerated; + } + + pub fn remaining_flags(&self) -> isize { + self.mines as isize - self.flags as isize + } + + pub fn modify(&mut self, x: usize, y: usize, event_handler: &mut Events) { + if let Some(&tile) = &self.get_tile(x, y) { + if tile.swept { + return; + } + let modifier = if let Some(modifier) = tile.modifier { + match modifier { + TileModifier::Flagged => { + self.flags -= 1; + match self.modify_mode { + ModifyMode::Flag => { + event_handler.add(GameEvent::FlagTile(x, y, tile.clone())); + None + } + ModifyMode::Question => { + event_handler.add(GameEvent::QuestionTile(x, y, tile.clone())); + + Some(TileModifier::Unsure) + } + } + } + TileModifier::Unsure => None, + } + } else { + self.flags += 1; + event_handler.add(GameEvent::FlagTile(x, y, tile.clone())); + Some(TileModifier::Flagged) + }; + if let Some(tile) = self.get_tile_mut(x, y) { + tile.modifier = modifier; + } + } + } + + pub fn sweep(&mut self, x: usize, y: usize, event_handler: &mut Events) -> Option { + if let BoardState::Ungenerated = self.state { + self.generate(x, y); + } + let &tile = &self.tiles[x][y]; + if let Some(_) = tile.modifier { + return None; + } + if tile.swept { + return None; + } + self.tiles[x][y].swept = true; + self.revealed_tiles += 1; + event_handler.add(GameEvent::RevealTile(x, y, self.tiles[x][y].clone())); + + if tile.state == TileState::Mine { + event_handler.add(GameEvent::Lose(x, y, tile.clone())); + + event_handler.add(GameEvent::GameEnd(self.clone())); + return Some(GameState::GameOver); + }; + event_handler.add(GameEvent::SweepBegin); + + let mut scan_list = VecDeque::from([(x, y)]); + let mut revealed: usize = 0; + while scan_list.len() > 0 { + for &scan_location in ADJACENT_WITHOUT_CENTER.iter() { + if let Some(old_tile) = self.get_tile(scan_list[0].0, scan_list[0].1) { + if old_tile.adjacent > 0 { + continue; + } + let x = scan_list[0].0 as isize + scan_location.0; + let y = scan_list[0].1 as isize + scan_location.1; + + if x < 0 || y < 0 { + continue; + } + let y = y as usize; + let x = x as usize; + if let Some(tile) = self.get_tile_mut(x, y) { + if tile.swept { + continue; + } + scan_list.push_back((x, y)); + tile.swept = true; + revealed += 1; + event_handler.add(GameEvent::RevealTile(x, y, tile.clone())); + } + } + } + scan_list.pop_front(); + } + self.revealed_tiles += revealed; + if self.revealed_tiles == self.non_mine_tiles { + event_handler.add(GameEvent::Win); + event_handler.add(GameEvent::GameEnd(self.clone())); + return Some(GameState::Victory); + } + None + } + + fn generate(&mut self, avoid_x: usize, avoid_y: usize) { + let width = self.width; + let height = self.height; + // Make list of all safe positions which are actually on board, + //removing all which are before, or past the bounds of the board. + let mut valid_safezone: Vec<(isize, isize)> = ADJACENT_WITH_CENTER.to_vec(); + valid_safezone.retain_mut(|pos: &mut (isize, isize)| { + let adjusted_x = pos.0 + avoid_x as isize; + let adjusted_y = pos.1 + avoid_y as isize; + if adjusted_x >= 0 && adjusted_y >= 0 && adjusted_x < width as isize && adjusted_y < height as isize { + pos.0 = adjusted_x; + pos.1 = adjusted_y; + return true; + } + false + }); + for safe_pos in valid_safezone.iter() { + let safe_x = safe_pos.0 as usize; + let safe_y = safe_pos.1 as usize; + self.tiles[safe_x][safe_y].safe = true; + } + + let mut i = 0; + + let seed = (time::get_time() * 1000000.0) as u64; + macroquad::rand::srand(seed); + while i != self.mines { + let x = macroquad::rand::gen_range(0, width); + let y = macroquad::rand::gen_range(0, height); + let mut tile = &mut self.tiles[x][y]; + + if tile.state == TileState::Mine || tile.safe == true { + continue; + } + + tile.state = TileState::Mine; + + i += 1; + } + + for x in 0..width { + for y in 0..height { + if let Some(tile) = self.get_tile(x, y) { + if tile.state == TileState::Mine { + for &scan_location in ADJACENT_WITH_CENTER.iter() { + let new_x = x as isize + scan_location.0; + let new_y = y as isize + scan_location.1; + if new_x < 0 || new_y < 0 { + continue; + } + let new_x = new_x as usize; + let new_y = new_y as usize; + if let Some(tile) = self.get_tile_mut(new_x, new_y) { + tile.increment_adjacent(); + } + } + } + } + } + } + self.state = BoardState::Generated; + } +} diff --git a/src/logic/tile.rs b/src/logic/tile.rs new file mode 100644 index 0000000..1f5a4ce --- /dev/null +++ b/src/logic/tile.rs @@ -0,0 +1,37 @@ +#[derive(Copy, Clone, PartialEq, Default)] +pub enum TileState { + #[default] + Empty, + Mine, +} +#[derive(Copy, Clone, PartialEq)] +pub enum TileModifier { + Flagged, + Unsure, +} + +#[derive(Copy, Clone, Default)] +pub struct Tile { + pub state: TileState, + pub modifier: Option, + pub swept: bool, + pub adjacent: u8, + pub safe: bool, + pub highlighted: bool, +} + +impl Tile { + pub fn increment_adjacent(&mut self) { + if self.state == TileState::Empty { + self.adjacent += 1; + } + } + pub fn highlight(&mut self) { + if self.swept == false { + self.highlighted = true; + } + } + pub fn remove_highlight(&mut self) { + self.highlighted = false; + } +} diff --git a/src/logic/timer.rs b/src/logic/timer.rs new file mode 100644 index 0000000..7f618ce --- /dev/null +++ b/src/logic/timer.rs @@ -0,0 +1,39 @@ +use macroquad::time::get_time; +#[derive(Default)] +pub struct Timer { + start_time: Option, + state: TimerState, + old: f64, +} +#[derive(Default)] +enum TimerState { + #[default] + Stopped, + Running, + Frozen, +} + +impl Timer { + pub fn clear(&mut self) { + self.start_time = None; + self.state = TimerState::Stopped; + } + pub fn start(&mut self) { + self.start_time = Some(get_time()); + self.state = TimerState::Running; + } + pub fn elapsed(&self) -> Option { + if let TimerState::Frozen = self.state { + return Some(self.old); + } + if let Some(time) = self.start_time { + Some(get_time() - time) + } else { + None + } + } + pub fn stop(&mut self) { + self.old = self.elapsed().unwrap_or(0f64); + self.state = TimerState::Frozen; + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2260c8d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,196 @@ +use gui::top_menu::smile::SmileyState; +use gui::ui_event::*; +use gui::{GameUI, UIState}; +use logic::{events::GameEvent, Minesweeper}; +use macroquad::{ + prelude::*, + ui::{root_ui, Skin}, + Window, +}; +mod gui; +mod logic; +mod sprite_loader; +mod util; + +fn main() { + let width = (30 * 32) as i32; + let height = (16 * 32) as i32 + 100; + Window::from_config( + Conf { + sample_count: 2, + window_title: String::from("Minesweeper"), + high_dpi: false, + window_width: width, + window_height: height, + ..Default::default() + }, + run(), + ); +} + +async fn run() { + let mut game_logic = Minesweeper::new(30, 16, 99).unwrap(); + + let top_buffer = 100; //px + let mut interface = GameUI::new(UIState::new(30, 16, 32, top_buffer)); + + let skin = { + let button_style = root_ui().style_builder().build(); + let scrollbar_handle_style = root_ui().style_builder().build(); + let window_style = root_ui().style_builder().color(Color::from_rgba(0, 0, 0, 0)).build(); + Skin { + scroll_width: 0f32, + scrollbar_handle_style, + button_style, + window_style, + ..root_ui().default_skin() + } + }; + + let settings_skin = { + let button_texture = Image::from_file_with_format(include_bytes!("../assets/button.png"), Some(ImageFormat::Png)); + let button_clicked_texture = + Image::from_file_with_format(include_bytes!("../assets/button_clicked.png"), Some(ImageFormat::Png)); + let label_style = root_ui().style_builder().font_size(20).build(); + let button_style = root_ui() + .style_builder() + .font_size(20) + .background(button_texture) + .background_clicked(button_clicked_texture) + .build(); + Skin { + scroll_width: 0f32, + button_style, + label_style, + ..root_ui().default_skin() + } + }; + let settings_skin_exit = { + let button_texture = Image::from_file_with_format(include_bytes!("../assets/exit_button.png"), Some(ImageFormat::Png)); + let button_hover_texture = + Image::from_file_with_format(include_bytes!("../assets/exit_button_hover.png"), Some(ImageFormat::Png)); + let button_style = root_ui() + .style_builder() + .background(button_texture) + .background_hovered(button_hover_texture) + .build(); + Skin { + scroll_width: 0f32, + button_style, + ..root_ui().default_skin() + } + }; + + let mut old_screen_size = (0.0, 0.0); + let background_color = Color::from_rgba(123, 123, 123, 255); + loop { + root_ui().push_skin(&skin); + clear_background(background_color); + + while let Some(ge) = game_logic.events.next() { + match ge { + GameEvent::Lose(_, _, _) => { + interface.state.frozen = true; + interface.event_handler.add(GUIEvent::SetSmileyState(SmileyState::Dead)); + interface.state.reveal_all = true; + } + GameEvent::Win => { + interface.state.frozen = true; + interface.event_handler.add(GUIEvent::SetSmileyState(SmileyState::Victory)); + interface.state.reveal_all = true; + } + GameEvent::Reset => { + interface.clear(); + } + _ => (), + } + } + { + let screen_width = screen_width(); + let screen_height = screen_height(); + // Only update letterboxing calculations when screen size changes + if (screen_width, screen_height) != old_screen_size { + interface.state.update_letterbox(screen_width, screen_height); + old_screen_size = (screen_width, screen_height); + } + + let (mouse_x, mouse_y) = mouse_position(); + let (min_x, min_y) = interface.state.pixel_screen_offset(10, 10 + interface.state.top_offset); + let tile_size = interface.state.tile_size; + let (max_x, max_y) = interface.state.pixel_screen_offset( + interface.state.width * tile_size - 10, + interface.state.height * tile_size + interface.state.top_offset - 10, + ); + + if mouse_x < min_x || mouse_y < min_y || mouse_x > max_x || mouse_y > max_y { + interface.state.mouse_in_minefield = false; + } else { + interface.state.mouse_in_minefield = true; + if let Some((x, y)) = interface.to_coordinate_system( + (mouse_x - interface.state.letterbox.0) / interface.state.scale, + (mouse_y - interface.state.letterbox.1) / interface.state.scale, + ) { + interface.set_cursor(x, y); + } + } + if interface.state.settings_open { + interface.settings_menu.render( + &interface.state, + &mut interface.event_handler, + &interface.texture_store, + &settings_skin, + &settings_skin_exit, + ); + } else { + interface.highlighter.events(&interface.state, &mut interface.event_handler, &mut game_logic.board); + interface.highlighter.highlight(&interface.state, &mut interface.event_handler); + game_logic.board.render(&interface.texture_store, &interface.state); + game_logic.board.events(&interface.state, &mut interface.event_handler); + interface.top_menu.render( + &interface.state, + &game_logic, + &mut interface.event_handler, + &interface.texture_store, + ); + } + } + + while let Some(ue) = interface.event_handler.next() { + match ue { + GUIEvent::ClickReset => { + game_logic.reset(); + interface.state.mouse_in_minefield = false + } + GUIEvent::ClickTile(x, y) => { + game_logic.reveal(x, y); + } + GUIEvent::ModifyTile(x, y) => game_logic.modify(x, y), + GUIEvent::HighlightTile(x, y) => game_logic.highlight(x, y), + GUIEvent::UnHighlightTile(x, y) => game_logic.remove_highlight(x, y), + GUIEvent::OpenSettings => { + interface.state.mouse_in_minefield = false; + interface.state.frozen = true; + interface.state.settings_open = true; + } + GUIEvent::CloseSettings => { + interface.state.frozen = false; + interface.state.settings_open = false; + } + GUIEvent::SwitchLanguage(lang) => { + interface.state.language = lang; + interface.texture_store.lang = lang; + } + GUIEvent::CreateNewGame(width, height, mines) => { + interface.state.frozen = false; + interface.state.update_dimensions(width, height); + game_logic.update_and_reset(width, height, mines); + interface.state.update_letterbox(screen_width(), screen_height()) + } + GUIEvent::SetQuestionMode(mode) => game_logic.board.modify_mode = mode, + GUIEvent::SetSmileyState(smiley_state) => interface.top_menu.smile.set_smile(smiley_state), + } + } + + next_frame().await; + } +} diff --git a/src/sprite_loader.rs b/src/sprite_loader.rs new file mode 100644 index 0000000..3ab86f2 --- /dev/null +++ b/src/sprite_loader.rs @@ -0,0 +1,27 @@ +use std::error::Error; + +use image::{load_from_memory, EncodableLayout}; +use macroquad::texture::{FilterMode, Texture2D}; +pub fn load_sprites(bytes: &[u8], tile_size: [usize; 2], rows: usize, columns: usize) -> Result, Box> { + let sprite_sheet = load_from_memory(bytes)?.to_rgba8(); + + let mut sprite_list: Vec = vec![]; + let [tile_width, tile_height] = tile_size; + let tile_width = tile_width as u32; + let tile_height = tile_height as u32; + + for i in 0..(rows * columns) { + let x = (i % columns) as u32; + let y = (i / columns) as u32; + let tile = image::imageops::crop_imm(&sprite_sheet, x * tile_width, y * tile_height, tile_width, tile_height).to_image(); + + tile.as_bytes(); + let tile_width = tile_width as u16; + let tile_height = tile_height as u16; + let tex = Texture2D::from_rgba8(tile_width, tile_height, &tile); + tex.set_filter(FilterMode::Nearest); + + sprite_list.push(tex); + } + Ok(sprite_list) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6548bc9 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,4 @@ +pub const ADJACENT_WITH_CENTER: [(isize, isize); 9] = + [(-1, -1), (0, -1), (1, -1), (-1, 0), (0, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]; + +pub const ADJACENT_WITHOUT_CENTER: [(isize, isize); 8] = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)];