init
3
.cargo/cargo.toml
Normal 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
|
@ -0,0 +1,3 @@
|
|||
target
|
||||
.rustc_info.json
|
||||
progress
|
411
Cargo.lock
generated
Normal 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
|
@ -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
|
@ -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
After Width: | Height: | Size: 185 B |
BIN
assets/button_clicked.png
Normal file
After Width: | Height: | Size: 160 B |
BIN
assets/cog.png
Normal file
After Width: | Height: | Size: 417 B |
BIN
assets/english_32x.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/exit_button.png
Normal file
After Width: | Height: | Size: 374 B |
BIN
assets/exit_button_hover.png
Normal file
After Width: | Height: | Size: 408 B |
BIN
assets/faces.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/icons.png
Normal file
After Width: | Height: | Size: 902 B |
BIN
assets/japanese_32x.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
assets/numbers.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
42
minesweeper.html
Normal 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
|
@ -0,0 +1,4 @@
|
|||
max_width = 130
|
||||
attr_fn_like_width = 100
|
||||
chain_width = 100
|
||||
hard_tabs = true
|
133
src/gui.rs
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
50
src/gui/top_menu/flag_counter.rs
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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)];
|