This commit is contained in:
Alexander Bass 2023-04-22 18:10:47 -04:00
commit 86dfecb4b3
36 changed files with 2173 additions and 0 deletions

3
.cargo/cargo.toml Normal file
View file

@ -0,0 +1,3 @@
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
target
.rustc_info.json
progress

411
Cargo.lock generated Normal file
View file

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

18
Cargo.toml Normal file
View file

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

15
README.MD Normal file
View file

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

BIN
assets/button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

BIN
assets/button_clicked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

BIN
assets/cog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

BIN
assets/english_32x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/exit_button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

BIN
assets/faces.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

BIN
assets/japanese_32x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

BIN
assets/numbers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

42
minesweeper.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Minesweeper</title>
<style>
html,
body,
canvas {
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
overflow: hidden;
position: absolute;
background: black;
z-index: 0;
/* user-select: none; */
/* pointer-events: none; */
}
</style>
</head>
<body oncontextmenu="return false;">
<div oncontextmenu="return false">
<canvas id="glcanvas" tabindex='1'></canvas>
<!-- Minified and statically hosted version of https://github.com/not-fl3/macroquad/blob/master/js/mq_js_bundle.js -->
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
<script>
load("./minesweeper.wasm");
</script> <!-- Your compiled wasm file -->
<script>
document.addEventListener("contextmenu", (e) => {
e.preventDefault();
});
</script>
</div>
</body>

4
rustfmt.toml Normal file
View file

@ -0,0 +1,4 @@
max_width = 130
attr_fn_like_width = 100
chain_width = 100
hard_tabs = true

133
src/gui.rs Normal file
View file

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

45
src/gui/board_render.rs Normal file
View file

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

194
src/gui/highlighter.rs Normal file
View file

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

229
src/gui/settings_menu.rs Normal file
View file

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

41
src/gui/texture_store.rs Normal file
View file

@ -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<Texture2D>,
japanese_tiles: Vec<Texture2D>,
pub numbers: Vec<Texture2D>,
pub smilies: Vec<Texture2D>,
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<Texture2D> {
return match self.lang {
Language::English => &self.english_tiles,
Language::Japanese => &self.japanese_tiles,
};
}
}

71
src/gui/tile_render.rs Normal file
View file

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

81
src/gui/top_menu.rs Normal file
View file

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

View file

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

65
src/gui/top_menu/smile.rs Normal file
View file

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

48
src/gui/top_menu/timer.rs Normal file
View file

@ -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<usize>,
}
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<f64>, 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<usize> = 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);
}
}
}

37
src/gui/ui_event.rs Normal file
View file

@ -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<GUIEvent>,
}
impl GUIEvents {
pub fn add(&mut self, event: GUIEvent) {
self.events.push(event);
}
pub fn next(&mut self) -> Option<GUIEvent> {
if self.events.len() > 0 {
self.events.pop()
} else {
None
}
}
pub fn clear(&mut self) {
self.events.clear();
}
}

88
src/logic.rs Normal file
View file

@ -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<Minesweeper, Box<dyn Error>> {
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<f64> {
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();
}
}
}

34
src/logic/events.rs Normal file
View file

@ -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<GameEvent>,
}
impl Events {
pub fn add(&mut self, event: GameEvent) {
self.events.push(event);
}
pub fn next(&mut self) -> Option<GameEvent> {
if self.events.len() > 0 {
self.events.pop()
} else {
None
}
}
pub fn clear(&mut self) {
self.events.clear();
}
}

258
src/logic/game_board.rs Normal file
View file

@ -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<Vec<Tile>>,
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<Self, Box<dyn Error>> {
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<GameState> {
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;
}
}

37
src/logic/tile.rs Normal file
View file

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

39
src/logic/timer.rs Normal file
View file

@ -0,0 +1,39 @@
use macroquad::time::get_time;
#[derive(Default)]
pub struct Timer {
start_time: Option<f64>,
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<f64> {
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;
}
}

196
src/main.rs Normal file
View file

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

27
src/sprite_loader.rs Normal file
View file

@ -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<Vec<Texture2D>, Box<dyn Error>> {
let sprite_sheet = load_from_memory(bytes)?.to_rgba8();
let mut sprite_list: Vec<Texture2D> = 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)
}

4
src/util.rs Normal file
View file

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