From 70ca8aca2d13f97801709181831587352795bad9 Mon Sep 17 00:00:00 2001
From: sdasda7777 <17746796+sdasda7777@users.noreply.github.com>
Date: Thu, 7 Dec 2023 15:45:11 +0100
Subject: [PATCH] Another attempt at fixing WASM version
---
Cargo.toml | 1 +
index.html | 2 +-
src/main.rs | 2403 ++++++++++++++++++++++++++-------------------------
3 files changed, 1204 insertions(+), 1202 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index bd51bd9..04101ec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ rand = "0.8"
rand_chacha = "0.3"
toml = "0.8"
log = "0.4"
+web-time = "0.2"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
diff --git a/index.html b/index.html
index 872b4f4..f30616f 100644
--- a/index.html
+++ b/index.html
@@ -23,7 +23,7 @@
-
+
diff --git a/src/main.rs b/src/main.rs
index 84c2a97..f1319b5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,1201 +1,1202 @@
-// hide console window on Windows in release (also disables console output)
-#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
-#![feature(step_trait)]
-
-extern crate eframe;
-extern crate hhmmss;
-extern crate itertools;
-extern crate toml;
-extern crate rand_chacha;
-
-use itertools::Itertools;
-use hhmmss::Hhmmss;
-
-pub mod bwi;
-use bwi::BWI;
-
-pub mod minesweeper_model;
-use minesweeper_model::{CellState, DIMENSIONS_COUNT, GameBoard, GameState, InitialGameSettings};
-
-use eframe::{egui, emath::Align2};
-use eframe::egui::{Button, containers::panel::TopBottomPanel, Key, KeyboardShortcut,
- menu, Modifiers, PointerButton, Response, RichText, Sense};
-use eframe::epaint::{Color32, FontId, Pos2, Rect, Rounding, Shadow, Shape, Stroke};
-use std::{cmp::min, fs, time::SystemTime};
-use toml::Table;
-
-#[derive(PartialEq)]
-enum CursorMode {
- ProbeAndMark,
- Highlighter,
-}
-
-pub struct Shortcuts {
- probe_mark_shortcut: KeyboardShortcut,
- highlighter_shortcut: KeyboardShortcut,
- highlight_group_shortcuts: [KeyboardShortcut; 8],
-
- reset_view_shortcut: KeyboardShortcut,
- zoom_to_fit_shortcut: KeyboardShortcut,
-}
-
-impl Shortcuts {
- pub fn new() -> Self {
- let mod_none = Modifiers{
- alt: false,
- ctrl: false,
- shift: false,
- mac_cmd: false,
- command: false,
- };
- Self {
- probe_mark_shortcut: KeyboardShortcut::new(mod_none, Key::Q),
- highlighter_shortcut: KeyboardShortcut::new(mod_none, Key::W),
- highlight_group_shortcuts: [KeyboardShortcut::new(mod_none, Key::Num1),
- KeyboardShortcut::new(mod_none, Key::Num2),
- KeyboardShortcut::new(mod_none, Key::Num3),
- KeyboardShortcut::new(mod_none, Key::Num4),
- KeyboardShortcut::new(mod_none, Key::Num5),
- KeyboardShortcut::new(mod_none, Key::Num6),
- KeyboardShortcut::new(mod_none, Key::Num7),
- KeyboardShortcut::new(mod_none, Key::Num8)],
-
- reset_view_shortcut: KeyboardShortcut::new(mod_none, Key::D),
- zoom_to_fit_shortcut: KeyboardShortcut::new(mod_none, Key::F),
- }
- }
-}
-
-// When compiling natively:
-#[cfg(not(target_arch = "wasm32"))]
-fn main() -> Result<(), eframe::Error> {
- let options = eframe::NativeOptions {
- ..Default::default()
- };
- eframe::run_native(
- "Minesweeper6D",
- options,
- Box::new(|cc| {
- cc.egui_ctx.style_mut(|style| {
- style.visuals.menu_rounding = Rounding::ZERO;
- style.visuals.popup_shadow = Shadow::NONE;
- style.visuals.window_rounding = Rounding::ZERO;
- style.visuals.window_shadow = Shadow::NONE;
- });
- Box::::default()
- }),
- )
-}
-
-// When compiling to web using trunk:
-#[cfg(target_arch = "wasm32")]
-fn main() {
- // Redirect `log` message to `console.log` and friends:
- eframe::WebLogger::init(log::LevelFilter::Debug).ok();
-
- let web_options = eframe::WebOptions::default();
-
- wasm_bindgen_futures::spawn_local(async {
- eframe::WebRunner::new()
- .start(
- "the_canvas_id", // hardcode it
- web_options,
- Box::new(|cc| Box::::default()),
- )
- .await
- .expect("failed to start eframe");
- });
-}
-
-struct MinesweeperViewController {
- current_initial_settings: InitialGameSettings,
- next_initial_settings: InitialGameSettings,
-
- next_selected_preset: Option,
- presets: Vec,
-
- game: Option,
- start_time: Option,
- end_time: Option,
-
- cursor_mode: CursorMode,
- selected_highlighters: u8,
-
- view_origin: Pos2,
- zoom_factor: f32,
- cell_edge: f32,
- tile_spacings: [f32; DIMENSIONS_COUNT],
-
- show_timer_miliseconds: bool,
- show_delta: bool,
- show_neighbors: bool,
- unlimited_zoom: bool,
- probe_marked: bool,
- neighbor_coords: Option<[usize; DIMENSIONS_COUNT]>,
-
- new_game_window_enabled: bool,
- rules_window_enabled: bool,
- controls_window_enabled: bool,
- about_window_enabled: bool,
-
- selection_color: Color32,
- center_color: Color32,
- neighbor_color: Color32,
- highlight_colors: [Color32; 8],
-
- shortcuts: Shortcuts,
-}
-
-impl MinesweeperViewController {
- fn reset(&mut self) {
- self.game = None;
- self.cursor_mode = CursorMode::ProbeAndMark;
- }
-
- fn start(&mut self, initial: [usize; DIMENSIONS_COUNT]) {
- self.start_time = Some(SystemTime::now());
- self.end_time = None;
- if let Some(seed) = &self.current_initial_settings.seed {
- self.game = Some(GameBoard::new(self.current_initial_settings.size,
- self.current_initial_settings.wrap,
- self.current_initial_settings.mines,
- None,
- u64::from_str_radix(&seed, 16).ok()));
- self.game.as_mut().unwrap().probe_at(initial, true);
- } else {
- self.game = Some(GameBoard::new(self.current_initial_settings.size,
- self.current_initial_settings.wrap,
- self.current_initial_settings.mines,
- Some(initial),
- None));
- }
- }
-
- // Translate and Scale from screen coordinates to cell coordinates
- // Uses modular cutoff to decide in constant time whether mouse is over any cell:
- //
- // If current position modulo (x*cell+(x-1)*space_1+space_2)
- // is larger than (x*cell+(x-1)*space_1), it must be outside a cell, otherwise repeat:
- // | | | | | | | | |
- // | cell | space_1 | cell | space_1 | cell | space_1 | cell | space_2 |
- // | | | | | | | | |
- fn get_coords(&self, pos: Pos2) -> Option<[usize; DIMENSIONS_COUNT]> {
- if pos.x < self.view_origin.x || pos.y < self.view_origin.y {
- return None;
- }
-
- let [c_xx, c_yy, c_zz, c_uu, c_vv, c_ww] = self.current_initial_settings.size;
- let [sp_xx, sp_yy, sp_zz, sp_uu, sp_vv, sp_ww] = self.tile_spacings;
-
- // Sizes of blocks (cells + inner spacing)
- let x_block_size = c_xx as f32 * self.cell_edge + (c_xx - 1) as f32 * sp_xx;
- let y_block_size = c_yy as f32 * self.cell_edge + (c_yy - 1) as f32 * sp_yy;
- let z_block_size = c_zz as f32 * x_block_size + (c_zz - 1) as f32 * sp_zz;
- let u_block_size = c_uu as f32 * y_block_size + (c_uu - 1) as f32 * sp_uu;
- // let v_block_size = c_vv as f32 * z_block_size + (c_vv - 1) as f32 * sp_vv;
- // let w_block_size = c_ww as f32 * u_block_size + (c_ww - 1) as f32 * sp_ww;
-
- // Get logical points
- let (dx, dy) = ((pos.x - self.view_origin.x) / self.zoom_factor,
- (pos.y - self.view_origin.y) / self.zoom_factor);
-
- // Modulo largest period
- let (dx_m1, dy_m1) = (dx as f32 % (z_block_size + sp_vv),
- dy as f32 % (u_block_size + sp_ww));
- if dx_m1 > z_block_size || dy_m1 > u_block_size {
- return None;
- }
- // Modulo middle period
- let (dx_m2, dy_m2) = (dx_m1 % (x_block_size + sp_zz),
- dy_m1 % (y_block_size + sp_uu));
- if dx_m2 > x_block_size || dy_m2 > y_block_size {
- return None;
- }
- // Modulo smallest period
- let (dx_m3, dy_m3) = (dx_m2 % (self.cell_edge + sp_xx),
- dy_m2 % (self.cell_edge + sp_yy));
- if dx_m3 > self.cell_edge || dy_m3 > self.cell_edge {
- return None;
- }
-
- // Is cell-like, get coords
- let (xx, yy) = ((dx_m2 / (self.cell_edge + sp_xx)) as usize,
- (dy_m2 / (self.cell_edge + sp_yy)) as usize);
- let (zz, uu) = ((dx_m1 / (x_block_size + sp_zz)) as usize,
- (dy_m1 / (y_block_size + sp_uu)) as usize);
- let (vv, ww) = ((dx / (z_block_size + sp_vv)) as usize,
- (dy / (u_block_size + sp_ww)) as usize);
-
- // Check if actual cell within grid bounds
- if xx >= c_xx || yy >= c_yy || zz >= c_zz || uu >= c_uu || vv >= c_vv || ww >= c_ww {
- return None;
- }
-
- return Some([xx, yy, zz, uu, vv, ww]);
- }
-
- // Scale and Translate from logical points to screen coordinates
- fn sc_tr(&self, xx: f32, yy: f32) -> Pos2 {
- Pos2::new(xx * self.zoom_factor, yy * self.zoom_factor) + self.view_origin.to_vec2()
- }
-
- // Cumulative spacing in screen axis directions
- fn cum_spc_x(&self, xx: usize, zz: usize, vv: usize) -> f32 {
- let [c_xx, _, _, _, _, _] = self.current_initial_settings.size;
- let [sp_xx, _, sp_zz, _, sp_vv, _] = self.tile_spacings;
- return (xx+zz*(c_xx-1)+vv*(c_xx-1)) as f32*sp_xx + (zz+vv*(c_xx-1)) as f32*sp_zz + vv as f32*sp_vv;
- }
- fn cum_spc_y(&self, yy: usize, uu: usize, ww: usize) -> f32 {
- let [_, c_yy, _, _, _, _] = self.current_initial_settings.size;
- let [_, sp_yy, _, sp_uu, _, sp_ww] = self.tile_spacings;
- return (yy+uu*(c_yy-1)+ww*(c_yy-1)) as f32*sp_yy + (uu+ww*(c_yy-1)) as f32*sp_uu + ww as f32*sp_ww;
- }
-
- fn try_set_cursor(&mut self, mode: CursorMode) {
- match mode {
- CursorMode::ProbeAndMark => {
- self.cursor_mode = CursorMode::ProbeAndMark;
- },
- CursorMode::Highlighter => {
- if self.game != None {
- self.cursor_mode = CursorMode::Highlighter;
- }
- }
- }
- }
-
- fn reset_view(&mut self) {
- self.view_origin = Pos2::new(0.0, 20.0);
- self.zoom_factor = 1.0;
- }
-
- fn zoom_to_fit(&mut self, screen_size: Pos2) {
- let [c_xx, c_yy, c_zz, c_uu, c_vv, c_ww] = self.current_initial_settings.size;
- let [sp_xx, sp_yy, sp_zz, sp_uu, sp_vv, sp_ww] = self.tile_spacings;
-
- // TODO: allow user to set the padding
- let (padding_x, padding_y) = (5.0, 5.0);
-
- let x_block_size = c_xx as f32 * self.cell_edge + (c_xx - 1) as f32 * sp_xx;
- let y_block_size = c_yy as f32 * self.cell_edge + (c_yy - 1) as f32 * sp_yy;
- let z_block_size = c_zz as f32 * x_block_size + (c_zz - 1) as f32 * sp_zz;
- let u_block_size = c_uu as f32 * y_block_size + (c_uu - 1) as f32 * sp_uu;
- let v_block_size = c_vv as f32 * z_block_size + (c_vv - 1) as f32 * sp_vv;
- let w_block_size = c_ww as f32 * u_block_size + (c_ww - 1) as f32 * sp_ww;
-
- let x_factor = (screen_size.x - 2.0*padding_x) / v_block_size;
- let y_factor = (screen_size.y - 40.0 - 2.0*padding_y) / w_block_size;
-
- // Zoom to fit the larger side
- if (x_factor > y_factor && w_block_size * x_factor <= screen_size.y - 40.0 - 2.0*padding_y)
- || v_block_size * y_factor > screen_size.x - 2.0*padding_x {
- self.zoom_factor = if self.unlimited_zoom {x_factor} else {x_factor.clamp(0.01, 5.0)};
- } else {
- self.zoom_factor = if self.unlimited_zoom {y_factor} else {y_factor.clamp(0.01, 5.0)};
- }
-
- // Translate to center
- self.view_origin.x = (screen_size.x - 10.0 - v_block_size*self.zoom_factor) / 2.0 + padding_x;
- self.view_origin.y = (screen_size.y - 50.0 - w_block_size*self.zoom_factor) / 2.0 + 20.0 + padding_y;
- }
-}
-
-impl Default for MinesweeperViewController {
- fn default() -> Self {
- // Sanity check
- //println!("{}", std::mem::size_of::());
-
- let settings = InitialGameSettings {
- name: "Custom".into(),
- size: [4, 4, 4, 4, 1, 1],
- wrap: [false, false, false, false, false, false],
- mines: 20,
- seed: None,
- };
-
- let mut ret = Self {
- current_initial_settings: settings.clone(),
- next_initial_settings: settings,
-
- next_selected_preset: None,
- presets: vec![],
-
- game: None,
- start_time: None,
- end_time: None,
-
- cursor_mode: CursorMode::ProbeAndMark,
- selected_highlighters: 1,
-
- view_origin: Pos2::new(0.0, 20.0),
- zoom_factor: 1.0,
- cell_edge: 30.0,
- tile_spacings: [0.0, 0.0, 10.0, 10.0, 20.0, 20.0],
-
- show_timer_miliseconds: false,
- show_delta: true,
- show_neighbors: true,
- unlimited_zoom: false,
- probe_marked: false,
- neighbor_coords: None,
-
- new_game_window_enabled: false,
- rules_window_enabled: false,
- controls_window_enabled: false,
- about_window_enabled: false,
-
- selection_color: Color32::RED,
- center_color: Color32::LIGHT_RED,
- neighbor_color: Color32::LIGHT_BLUE,
- highlight_colors: [Color32::YELLOW, Color32::BROWN, Color32::LIGHT_GREEN, Color32::WHITE,
- Color32::KHAKI, Color32::DARK_BLUE, Color32::DARK_GREEN, Color32::GOLD],
-
- shortcuts: Shortcuts::new(),
- };
-
- let config_text = fs::read_to_string("config.toml").unwrap_or_else(|_| "".into());
- let config_table: Table = config_text.parse::().expect("Invalid configuration file");
-
- // Load in presets
- if let Some(val) = config_table.get("preset"){
- for e in val.as_array().unwrap() {
- let mut igs: InitialGameSettings = Default::default();
- if let Some(n) = e.get("name") {
- igs.name = n.as_str().unwrap().into();
- }
- if let Some(size_value) = e.get("size") {
- if let Some(a) = size_value.as_array() {
- if a.len() != DIMENSIONS_COUNT {
- println!("Warning: `size` should be array of {} elements, {} found", DIMENSIONS_COUNT, a.len());
- }
- for ii in 0..min(a.len(), DIMENSIONS_COUNT) {
- if let Some(i) = a[ii].as_integer().map(|e| e.clamp(1, 100) as usize) {
- igs.size[ii] = i;
- } else {
- println!("Warning: value at index {} of `size` is invalid", ii);
- }
- }
- } else {
- println!("Warning: value of `size` is invalid");
- }
- }
- if let Some(wrap_value) = e.get("wrap") {
- if let Some(a) = wrap_value.as_array() {
- if a.len() != DIMENSIONS_COUNT {
- println!("Warning: `wrap` should be array of {} elements, {} found", DIMENSIONS_COUNT, a.len());
- }
- for ii in 0..min(a.len(), DIMENSIONS_COUNT) {
- if let Some(b) = a[ii].as_bool() {
- igs.wrap[ii] = b;
- } else {
- println!("Warning: value at index {} of `wrap` is invalid", ii);
- }
- }
- } else {
- println!("Warning: value of `wrap` is invalid");
- }
- }
- if let Some(mines_value) = e.get("mines") {
- if let Some(i) = mines_value.as_integer()
- .map(|e| (e as u32)
- .clamp(1, (igs.size.iter().fold(1, |p, v| p*v) - 1) as u32)) {
- igs.mines = i;
- } else {
- println!("Warning: value of `mines` is invalid");
- }
- }
- if let Some(seed_value) = e.get("seed") {
- if let Some(s) = seed_value.as_str() {
- igs.seed = Some(s.into());
- } else {
- println!("Warning: value of `seed` is invalid");
- }
- }
- ret.presets.push(igs);
- }
- }
-
- // Load in other settings
- if let Some(val) = config_table.get("default_preset") {
- if let Some(i) = val.as_integer().map(|e| e as usize) {
- if i < ret.presets.len() {
- ret.current_initial_settings = ret.presets[i].clone();
- ret.next_initial_settings = ret.presets[i].clone();
- ret.next_selected_preset = Some(i as u32);
- } else {
- println!("Warning: value of `default_preset` is outside presets range");
- }
- } else {
- println!("Warning: value of `default_preset` is invalid");
- }
- }
-
- if let Some(val) = config_table.get("highlight_colors") {
- // Snippet by YgorSouza at https://github.com/emilk/egui/issues/3466#issuecomment-1762923933
- fn color_from_hex(hex: &str) -> Option {
- let hex = hex.trim_start_matches('#');
- let alpha = match hex.len() {
- 6 => false,
- 8 => true,
- _ => None?,
- };
- u32::from_str_radix(hex, 16)
- .ok()
- .map(|u| if alpha { u } else { u << 8 | 0xff })
- .map(u32::to_be_bytes)
- .map(|[r, g, b, a]| Color32::from_rgba_unmultiplied(r, g, b, a))
- }
- let a = val.as_array().unwrap();
- for ii in 0..8 {
- ret.highlight_colors[ii] = color_from_hex(a[ii].as_str().unwrap()).unwrap();
- }
- };
-
- if let Some(val) = config_table.get("show_timer_miliseconds") {
- ret.show_timer_miliseconds = val.as_bool().unwrap();
- }
- if let Some(val) = config_table.get("show_delta") {
- ret.show_delta = val.as_bool().unwrap();
- }
- if let Some(val) = config_table.get("show_neighbors") {
- ret.show_neighbors = val.as_bool().unwrap();
- }
- if let Some(val) = config_table.get("unlimited_zoom") {
- ret.unlimited_zoom = val.as_bool().unwrap();
- }
- if let Some(val) = config_table.get("probe_marked") {
- ret.probe_marked = val.as_bool().unwrap();
- }
-
- if let Some(val) = config_table.get("tile_spacings") {
- let a = val.as_array().unwrap();
- for ii in 0..4 {
- let valf = a[ii].as_float().unwrap() as f32;
- if valf >= 0.0 {
- ret.tile_spacings[ii] = valf;
- }
- }
- }
-
- ret
- }
-}
-
-impl eframe::App for MinesweeperViewController {
- fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
- if self.show_timer_miliseconds {
- ctx.request_repaint();
- } else {
- // TODO: This egui function is bugged, uncomment next line when fixed
- //ctx.request_repaint_after(Duration::new(1,0));
- }
-
- let mut new_game_window_enabled = self.new_game_window_enabled;
- if new_game_window_enabled {
- egui::Window::new("New Custom Game")
- .open(&mut new_game_window_enabled).show(ctx, |ui| {
-
- let resp = egui::ComboBox::from_id_source("preset_combobox")
- .width(300.0)
- .selected_text(if let Some(pno) = self.next_selected_preset {
- self.presets[pno as usize].name.as_str()
- } else {
- "Custom game"
- })
- .show_ui(ui, |ui| {
- (0..self.presets.len()).map(|ii| {
- ui.selectable_value(&mut self.next_selected_preset, Some(ii as u32),
- self.presets[ii].name.clone())
- }).collect::>()
- });
- if let Some(col) = resp.inner {
- if col.iter().any(|o: &Response| o.clicked()) {
- if let Some(pno) = self.next_selected_preset {
- self.next_initial_settings = self.presets[pno as usize].clone();
- }
- }
- }
-
- egui::Grid::new("dim_and_wrap_grid").show(ui, |ui| {
- ui.label("Dimensions: ");
- let resps = (0..DIMENSIONS_COUNT).map(
- |e| ui.add(egui::DragValue::new(&mut self.next_initial_settings.size[e])
- .speed(1).clamp_range(1..=100))
- ).collect::>();
- if resps.iter().any(|e| e.changed()) {
- self.next_selected_preset = None;
- };
- ui.end_row();
-
- ui.label("Wrapping: ");
- let resps = (0..DIMENSIONS_COUNT).map(
- |e| ui.add(egui::Checkbox::without_text(&mut self.next_initial_settings.wrap[e]))
- ).collect::>();
- if resps.iter().any(|e| e.changed()) {
- self.next_selected_preset = None;
- };
- ui.end_row();
- });
-
- ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
- ui.label("Mines: ");
- ui.add(egui::DragValue::new(&mut self.next_initial_settings.mines).speed(1)
- .clamp_range(1..=(self.next_initial_settings.size.iter().fold(1, |p, v| p*v)-1)));
- });
-
- let mut checkbox_state = self.next_initial_settings.seed != None;
- let checkbox = egui::Checkbox::new(&mut checkbox_state, "Generate the board based on the first click");
- if ui.add(checkbox).clicked() {
- if self.next_initial_settings.seed == None {
- self.next_initial_settings.seed = Some("".into());
- } else {
- self.next_initial_settings.seed = None;
- }
- }
-
- ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
- ui.label("Seed: ");
-
- if let Some(ref mut s) = self.next_initial_settings.seed {
- ui.add_enabled(true, egui::TextEdit::singleline(&mut *s));
- } else {
- ui.add_enabled(false, egui::TextEdit::singleline(&mut ""));
- };
- });
-
- ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
- if ui.button("Reset").clicked() {
- self.next_initial_settings = self.current_initial_settings.clone();
- }
- if ui.button("Start").clicked() {
- self.current_initial_settings = self.next_initial_settings.clone();
- self.new_game_window_enabled = false;
- self.reset();
- }
- });
- });
- }
- self.new_game_window_enabled = self.new_game_window_enabled && new_game_window_enabled;
-
- let mut rules_window_enabled = self.rules_window_enabled;
- if rules_window_enabled {
- egui::Window::new("Rules")
- .open(&mut rules_window_enabled).show(ctx, |ui| {
- ui.label(
-r"Rules are basically the same as with old-school minsweeper.
-
-Cell's number signifies how many mines are in its neighborhood, 0/empty meaning none. Probing a cell containing mine results in game over, goal of the game is to uncover (through probing) all cells not containing mines. Fields suspected of being mines may be marked with a flag, however it is not necessary.
-
-The twist is that in n dimensions, every cell has up to 3^n-1 neighbors (e.g. 8 for 2 dimensions, 26 for 3 dimensions, 80 for 4 dimensions)");
- });
- }
- self.rules_window_enabled = rules_window_enabled;
- let mut controls_window_enabled = self.controls_window_enabled;
- if controls_window_enabled {
- egui::Window::new("Controls")
- .open(&mut controls_window_enabled).show(ctx, |ui| {
- ui.label(
-r"Currently there are two tools: Probe/Mark and Highlighter.
-
-Probe/Mark probes a cell with primary button (usually Left Mouse Button) and marks a cell as a mine with secondary button (usually Right Mouse Button)
-
-Highlighter highlights with primary button and unhighlights with secondary button.
-
-Camera may be panned through dragging with middle mouse button, zoomed/unzoomed using scroll wheel.
-
-If neighbor hints are enabled, holding Shift freezes them in place, whereas holding Alt temporarily disables them.");
- });
- }
- self.controls_window_enabled = controls_window_enabled;
- let mut about_window_enabled = self.about_window_enabled;
- if about_window_enabled {
- egui::Window::new("About")
- .open(&mut about_window_enabled).show(ctx, |ui| {
- ui.label(format!(
-r"Minesweeper4D (version {})
-
-Code written by sdasda7777 (github.com/sdasda7777) (except where noted otherwise) with a lot of help from amazing members of the egui Discord server", option_env!("CARGO_PKG_VERSION").unwrap()));
- });
- }
- self.about_window_enabled = about_window_enabled;
-
- TopBottomPanel::top("menubar_panel")
- .frame(egui::Frame::none().fill(egui::Color32::LIGHT_BLUE))
- .show(ctx, |ui| {
- ui.visuals_mut().override_text_color = Some(egui::Color32::BLACK);
- menu::bar(ui, |ui| {
- ui.menu_button("Game", |ui| {
- let new_game_window_button = Button::new("New Custom Game")
- .selected(self.new_game_window_enabled);
- if ui.add(new_game_window_button).clicked() {
- self.new_game_window_enabled = !self.new_game_window_enabled;
- ui.close_menu();
- }
- if ui.button("Quick restart").clicked() {
- self.reset();
- ui.close_menu();
- }
- });
- ui.menu_button("View", |ui| {
- let _ = ui.button(format!("Current zoom: {:.3} %", self.zoom_factor*100.0));
- let reset_view_button = Button::new("Reset to 0x0 @ 100%")
- .shortcut_text(
- RichText::new(ctx.format_shortcut(&self.shortcuts.reset_view_shortcut))
- .color(Color32::WHITE));
- if ui.add(reset_view_button).clicked() {
- self.reset_view();
- ui.close_menu();
- }
- let zoom_to_fit_button = Button::new("Zoom to fit")
- .shortcut_text(
- RichText::new(ctx.format_shortcut(&self.shortcuts.zoom_to_fit_shortcut))
- .color(Color32::WHITE));
- if ui.add(zoom_to_fit_button).clicked() {
- self.zoom_to_fit(ctx.screen_rect().max);
- ui.close_menu();
- }
- let show_neighbors_button = Button::new("Show neighbors")
- .selected(self.show_neighbors);
- if ui.add(show_neighbors_button).clicked() {
- self.show_neighbors = !self.show_neighbors;
- ui.close_menu();
- }
- let unlimited_zoom_button = Button::new("Unlimited zoom")
- .selected(self.unlimited_zoom);
- if ui.add(unlimited_zoom_button).clicked() {
- self.unlimited_zoom = !self.unlimited_zoom;
- ui.close_menu();
- }
- let show_timer_miliseconds_button = Button::new("Show timer miliseconds")
- .selected(self.show_timer_miliseconds);
- if ui.add(show_timer_miliseconds_button).clicked() {
- self.show_timer_miliseconds = !self.show_timer_miliseconds;
- ui.close_menu();
- }
- });
- ui.menu_button("Tools", |ui| {
- ui.visuals_mut().widgets.noninteractive.weak_bg_fill = Color32::DARK_GRAY;
-
- let probe_and_mark_button = Button::new("Probe/Mark")
- .selected(self.cursor_mode == CursorMode::ProbeAndMark)
- .shortcut_text(
- RichText::new(ctx.format_shortcut(&self.shortcuts.probe_mark_shortcut))
- .color(Color32::WHITE));
-
- let highlight_button = Button::new("Highlighter")
- .selected(if self.cursor_mode == CursorMode::Highlighter {true} else {false})
- .shortcut_text(
- RichText::new(ctx.format_shortcut(&self.shortcuts.highlighter_shortcut))
- .color(Color32::WHITE));
-
- if ui.add(probe_and_mark_button).clicked() {
- self.try_set_cursor(CursorMode::ProbeAndMark);
- ui.close_menu();
- }
- ui.menu_button("Probe/Mark options", |ui| {
- let probe_marked = Button::new("Allow probing marked cells").selected(self.probe_marked);
- if ui.add(probe_marked).clicked() {
- self.probe_marked = !self.probe_marked;
- }
- });
-
- // The highligh tool is disabled when game is NotRunning
- if ui.add_enabled(self.game != None, highlight_button).clicked() {
- self.try_set_cursor(CursorMode::Highlighter);
- ui.close_menu();
- }
- ui.menu_button("Highlight groups", |ui| {
- for ii in 0..8 {
- let highlight_group_button
- = Button::new(format!("Group {} ({})", ii+1,
- if (self.selected_highlighters & (1 << ii)) > 0 {"on"} else {"off"}))
- .selected((self.selected_highlighters & (1 << ii)) > 0)
- .stroke(Stroke::new(2.0, self.highlight_colors[ii]))
- .shortcut_text(ctx.format_shortcut(&self.shortcuts.highlight_group_shortcuts[ii]));
-
- if ui.add(highlight_group_button).clicked() {
- self.selected_highlighters ^= 1 << ii;
- }
- }
- });
- });
- ui.menu_button("Help", |ui| {
- let rules_button = Button::new("Rules").selected(self.rules_window_enabled);
- let controls_button = Button::new("Controls").selected(self.controls_window_enabled);
- let about_button = Button::new("About").selected(self.about_window_enabled);
- if ui.add(rules_button).clicked() {
- self.rules_window_enabled = !self.rules_window_enabled;
- ui.close_menu();
- }
- if ui.add(controls_button).clicked() {
- self.controls_window_enabled = !self.controls_window_enabled;
- ui.close_menu();
- }
- if ui.add(about_button).clicked() {
- self.about_window_enabled = !self.about_window_enabled;
- ui.close_menu();
- }
- });
- if ui.button(format!("Δ: {}", if self.show_delta {"yes"} else {"no"})).clicked() {
- self.show_delta = !self.show_delta;
- }
- if let Some(game) = &self.game {
- let seed = format!("seed: {:016x}", game.seed());
- ui.add(egui::TextEdit::singleline(&mut seed.as_str()));
- }
- ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
- ui.button(format!("({}/{}) {}",
- if let Some(game) = &self.game {game.marked_as_mine()} else {0},
- self.current_initial_settings.mines,
- (0..DIMENSIONS_COUNT).map(
- |i| format!("{}{}",
- self.current_initial_settings.size[i],
- if self.current_initial_settings.wrap[i] {"w"} else {""})
- ).join(" x ")))
- });
- });
- });
-
- TopBottomPanel::bottom("bottom_panel")
- .frame(egui::Frame::none().fill(egui::Color32::LIGHT_BLUE))
- .show(ctx, |ui| {
- ui.visuals_mut().override_text_color = Some(egui::Color32::BLACK);
- menu::bar(ui, |ui| {
- match self.cursor_mode {
- CursorMode::ProbeAndMark => {
- let _ = ui.button("Probe/Mark: primary to probe a cell, secondary to mark as a mine");
- },
- CursorMode::Highlighter => {
- let _ = ui.button(
- format!("Highlighter ({}): primary to highlight a cell, secondary to unhighlight",
- self.selected_highlighters)
- );
- }
- }
-
- ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
- if let Some(game) = &self.game {
- if let Some(start_time) = self.start_time {
- let dur = if let Some(end_time) = self.end_time
- { end_time } else { SystemTime::now() }
- .duration_since(start_time).unwrap();
- let fdur = if self.show_timer_miliseconds
- { dur.hhmmssxxx() } else { dur.hhmmss() };
- let _ = ui.button(format!("{} {}",match game.state() {
- GameState::Victory => "You won!",
- GameState::Loss => "You lost!",
- _ => ""
- }, fdur
- ));
- }
- }
- });
- });
- });
-
- egui::CentralPanel::default()
- .frame(egui::Frame::none().fill(egui::Color32::GRAY))
- .show(ctx, |ui| {
-
- let basic_stroke = Stroke::new(2.0 * self.zoom_factor, egui::Color32::BLACK);
- let harder_stroke = Stroke::new(4.0 * self.zoom_factor, egui::Color32::BLACK);
- let neighbor_stroke = Stroke::new(3.0 * self.zoom_factor, self.neighbor_color);
- let center_stroke = Stroke::new(3.0 * self.zoom_factor, self.center_color);
- let selection_stroke = Stroke::new(3.0 * self.zoom_factor, self.selection_color);
- let highlight_strokes = self.highlight_colors.map(|x| Stroke::new(2.0 * self.zoom_factor, x));
-
- let screen_size = ctx.screen_rect().max;
- let [c_xx, c_yy, c_zz, c_uu, c_vv, c_ww] = self.current_initial_settings.size;
-
- let (painter_response, painter) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag());
-
- // Paint cell contents
- let background_color = Color32::GRAY;
- if self.zoom_factor > 0.05 {
- if let Some(game) = &self.game {
- for iw in 0..c_ww {
- for iv in 0..c_vv {
- for iu in 0..c_uu {
- for iz in 0..c_zz {
- for iy in 0..c_yy {
- for ix in 0..c_xx {
- let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
- let ulc = self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x,
- (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y);
- // Only draw symbols reasonably close to the viewport
- if ulc.x >= -self.cell_edge*self.zoom_factor && ulc.x <= screen_size.x
- && ulc.y >= -self.cell_edge*self.zoom_factor && ulc.y <= screen_size.y {
- let (symbol, color) = match game.cell_at([ix, iy, iz, iu, iv, iw]) {
- CellState::UndiscoveredMine(_)
- => if game.state() == GameState::Victory {
- ("💣".into(), Color32::GREEN)
- } else if game.state() == GameState::Loss {
- ("💣".into(), Color32::RED)
- } else {
- ("".into(), Color32::GRAY)
- },
- CellState::MarkedMine(_)
- => if game.state() == GameState::Victory || game.state() == GameState::Loss {
- ("🚩".into(), Color32::GREEN)
- } else {
- ("🚩".into(), Color32::GRAY)
- },
- CellState::ExplodedMine(_) => ("💥".into(), Color32::RED),
- CellState::UndiscoveredEmpty(..) => ("".into(), Color32::GRAY),
- CellState::MarkedEmpty(..)
- => if game.state() == GameState::Victory || game.state() == GameState::Loss {
- ("🚩".into(), Color32::RED)
- } else {
- ("🚩".into(), Color32::GRAY)
- },
- CellState::DiscoveredEmpty(mc, delta, _)
- => (if mc == 0 && delta == 0 {"".into()}
- else {format!("{}", if self.show_delta {delta} else {mc as i32})},
- Color32::LIGHT_GRAY),
- };
-
- // Only paint squares with different color than the current background
- if color != background_color {
- painter.add(
- Shape::rect_filled(
- Rect::from_min_max(
- ulc,
- self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz+1) as f32 * self.cell_edge + spc_x,
- (iy+iu*c_yy+iw*c_yy*c_uu+1) as f32 * self.cell_edge + spc_y)),
- Rounding::ZERO, color
- )
- );
- }
-
- if symbol != "" {
- // Since drawing text is somewhat expensive, only draw text that can most definitely be read
- if self.zoom_factor >= 0.10 {
- painter.text(
- self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_x,
- (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_y),
- Align2::CENTER_CENTER,
- symbol,
- FontId::proportional(25.0 * self.zoom_factor),
- Color32::BLACK
- );
- } else {
- painter.add(
- Shape::circle_filled(
- self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_x,
- (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_y),
- 10.0 * self.zoom_factor,
- Color32::GRAY)
- );
- }
- }
- }
- }}}}}}
- }
- }
-
- // Paint lines
- for iw in 0..c_ww {
- for iu in 0..c_uu {
- for iy in 0..c_yy {
- let spc_y = self.cum_spc_y(iy,iu,iw);
- let pos_y = (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y;
- for iv in 0..c_vv {
- for iz in 0..c_zz {
- for ix in 0..c_xx {
- for ix2 in 0..=1 {
- let spc_x = self.cum_spc_x(ix,iz,iv);
- let ulc = self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz+ix2) as f32 * self.cell_edge + spc_x, pos_y);
- if ulc.x >= -self.cell_edge*self.zoom_factor && ulc.x <= screen_size.x
- && ulc.y >= -self.cell_edge*self.zoom_factor && ulc.y <= screen_size.y+self.cell_edge {
- painter.add(
- Shape::line_segment(
- [ulc,
- self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz+ix2) as f32 * self.cell_edge + spc_x,
- pos_y + self.cell_edge)],
- if (ix == 0 && ix2 == 0)
- || (ix+1 == c_xx && ix2 == 1)
- {harder_stroke} else {basic_stroke}));
- }
- }}}}
- }}}
- for iv in 0..c_vv {
- for iz in 0..c_zz {
- for ix in 0..c_xx {
- let spc_x = self.cum_spc_x(ix,iz,iv);
- let pos_x = (ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x;
- for iw in 0..c_ww {
- for iu in 0..c_uu {
- for iy in 0..c_yy {
- for iy2 in 0..=1 {
- let spc_y = self.cum_spc_y(iy,iu,iw);
- let ulc = self.sc_tr(pos_x, (iy+iu*c_yy+iw*c_yy*c_uu+iy2) as f32*self.cell_edge + spc_y);
- if ulc.x >= -self.cell_edge*self.zoom_factor && ulc.x <= screen_size.x
- && ulc.y >= -self.cell_edge*self.zoom_factor && ulc.y <= screen_size.y {
- painter.add(
- Shape::line_segment(
- [ulc,
- self.sc_tr(pos_x + self.cell_edge,
- (iy+iu*c_yy+iw*c_yy*c_uu+iy2) as f32*self.cell_edge + spc_y)],
- if (iy == 0 && iy2 == 0)
- || (iy+1 == c_yy && iy2 == 1)
- {harder_stroke} else {basic_stroke}));
- }
- }}}}
- }}}
-
- // Paint cursor, neighbor hints and their center
- if let Some(pos) = painter_response.hover_pos() {
- let mut neighbor_coords = None;
- let mut mouse_coords = None;
-
- if self.show_neighbors && !ui.input(|i| i.modifiers.matches(Modifiers::ALT)) {
- if ui.input(|i| i.modifiers.matches(Modifiers::SHIFT)) && self.neighbor_coords != None {
- neighbor_coords = self.neighbor_coords;
- }
- }
- if let Some(coords) = self.get_coords(pos) {
- if neighbor_coords == None && self.show_neighbors
- && !ui.input(|i| i.modifiers.matches(Modifiers::ALT)) {
- self.neighbor_coords = Some(coords);
- neighbor_coords = Some(coords);
- }
- mouse_coords = Some(coords);
- }
-
- if let Some([ix, iy, iz, iu, iv, iw]) = neighbor_coords {
- let [cw_xx, cw_yy, cw_zz, cw_uu, cw_vv, cw_ww] = self.current_initial_settings.wrap;
- for iwsupp in BWI::new(iw as i32-1,iw as i32+1,0,c_ww as i32-1,cw_ww) {
- for ivsupp in BWI::new(iv as i32-1,iv as i32+1,0,c_vv as i32-1,cw_vv) {
- for iusupp in BWI::new(iu as i32-1,iu as i32+1,0,c_uu as i32-1,cw_uu) {
- for izsupp in BWI::new(iz as i32-1,iz as i32+1,0,c_zz as i32-1,cw_zz) {
- for iysupp in BWI::new(iy as i32-1,iy as i32+1,0,c_yy as i32-1,cw_yy) {
- for ixsupp in BWI::new(ix as i32-1,ix as i32+1,0,c_xx as i32-1,cw_xx) {
- let (ixb, iyb, izb, iub, ivb, iwb)
- = (ixsupp as usize, iysupp as usize, izsupp as usize,
- iusupp as usize, ivsupp as usize, iwsupp as usize);
- let (spc_x, spc_y) = (self.cum_spc_x(ixb,izb,ivb), self.cum_spc_y(iyb,iub,iwb));
- let (ulc_x, ulc_y) = (ixb+izb*c_xx+ivb*c_xx*c_zz, iyb+iub*c_yy+iwb*c_yy*c_uu);
- painter.add(
- Shape::rect_stroke(
- Rect::from_min_max(
- self.sc_tr(ulc_x as f32 * self.cell_edge + spc_x,
- ulc_y as f32 * self.cell_edge + spc_y),
- self.sc_tr((ulc_x+1) as f32 * self.cell_edge + spc_x,
- (ulc_y+1) as f32 * self.cell_edge + spc_y)),
- Rounding::ZERO, neighbor_stroke));
- }}}}}}
-
- let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
- let (ulc_x, ulc_y) = (ix+iz*c_xx+iv*c_xx*c_zz, iy+iu*c_yy+iw*c_yy*c_uu);
- painter.add(
- Shape::rect_stroke(
- Rect::from_min_max(
- self.sc_tr(ulc_x as f32 * self.cell_edge + spc_x,
- ulc_y as f32 * self.cell_edge + spc_y),
- self.sc_tr((ulc_x+1) as f32 * self.cell_edge + spc_x,
- (ulc_y+1) as f32 * self.cell_edge + spc_y)),
- Rounding::ZERO, center_stroke));
- }
- if let Some([ix, iy, iz, iu, iv, iw]) = mouse_coords {
- let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
- let (ulc_x, ulc_y) = (ix+iz*c_xx+iv*c_xx*c_zz, iy+iu*c_yy+iw*c_yy*c_uu);
- painter.add(
- Shape::rect_stroke(
- Rect::from_min_max(
- self.sc_tr(ulc_x as f32 * self.cell_edge + spc_x,
- ulc_y as f32 * self.cell_edge + spc_y),
- self.sc_tr((ulc_x+1) as f32 * self.cell_edge + spc_x,
- (ulc_y+1) as f32 * self.cell_edge + spc_y)),
- Rounding::ZERO, selection_stroke));
- }
- }
-
- // Paint highlights
- const HIGHLIGHT_SPACING: f32 = 2.5;
- for iw in 0..c_ww {
- for iv in 0..c_vv {
- for iu in 0..c_uu {
- for iz in 0..c_zz {
- for iy in 0..c_yy {
- for ix in 0..c_xx {
- if let Some(game) = &self.game {
- match game.cell_at([ix, iy, iz, iu, iv, iw]) {
- CellState::UndiscoveredMine(g) | CellState::MarkedMine(g)
- | CellState::ExplodedMine(g) | CellState::UndiscoveredEmpty(.., g)
- | CellState::MarkedEmpty(.., g) | CellState::DiscoveredEmpty(.., g)
- => {
- let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
- let mut next_start_group = 0;
- if g > 0 { for current_side in 0..8 {
- for highlight_group in (next_start_group..8).chain(0..next_start_group) {
- if (g & (1 << highlight_group)) > 0 {
- let mut p1x = (ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x;
- let mut p1y = (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y;
- let mut p2x = (ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x;
- let mut p2y = (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y;
- match current_side {
- 0 | 6 | 7 => {p1x += HIGHLIGHT_SPACING;},
- 1 | 5 => {p1x += self.cell_edge/2.0;},
- 2 | 3 | 4 => {p1x += self.cell_edge-HIGHLIGHT_SPACING;},
- _ => {}
- };
- match current_side {
- 0 | 1 | 2 => {p1y += HIGHLIGHT_SPACING;},
- 3 | 7 => {p1y += self.cell_edge/2.0;},
- 4 | 5 | 6 => {p1y += self.cell_edge-HIGHLIGHT_SPACING;},
- _ => {}
- };
- match current_side {
- 0 | 4 => {p2x += self.cell_edge/2.0;},
- 1 | 2 | 3 => {p2x += self.cell_edge-HIGHLIGHT_SPACING;},
- 5 | 6 | 7 => {p2x += HIGHLIGHT_SPACING;},
- _ => {}
- };
- match current_side {
- 0 | 1 | 7 => {p2y += HIGHLIGHT_SPACING;},
- 2 | 6 => {p2y += self.cell_edge/2.0;},
- 3 | 4 | 5 => {p2y += self.cell_edge-HIGHLIGHT_SPACING;},
- _ => {}
- };
-
- painter.add(
- Shape::line_segment([self.sc_tr(p1x, p1y),
- self.sc_tr(p2x, p2y)],
- highlight_strokes[highlight_group]));
- next_start_group = (highlight_group + 1) % 8;
- break;
- }
- }
- }}
- }
- };
- }
- }}}}}}
-
- // React to clicks
- // TODO: Maybe polymorphism/enum impl wouldn't be a bad idea here
- if painter_response.clicked_by(PointerButton::Primary) {
- // println!("primary click");
- if CursorMode::ProbeAndMark == self.cursor_mode {
- if let Some(pos) = ctx.pointer_interact_pos() {
- if let Some(coords) = self.get_coords(pos) {
- if let Some(game) = &mut self.game {
- if game.state() != GameState::Victory && game.state() != GameState::Loss {
- match game.probe_at(coords, self.probe_marked) {
- GameState::Victory | GameState::Loss => {
- self.end_time = Some(SystemTime::now());
- },
- GameState::Running => {}
- }
- }
- } else {
- self.start(coords);
- }
- }
- }
- } else if self.cursor_mode == CursorMode::Highlighter {
- if let Some(pos) = ctx.pointer_interact_pos() {
- if let Some(coords) = self.get_coords(pos) {
- if let Some(game) = &mut self.game {
- game.highlight_at(coords, self.selected_highlighters, true);
- }
- }
- }
- }
- }
- if painter_response.clicked_by(PointerButton::Secondary) {
- // println!("secondary click");
- if CursorMode::ProbeAndMark == self.cursor_mode {
- if let Some(pos) = ctx.pointer_interact_pos() {
- if let Some(coords) = self.get_coords(pos) {
- if let Some(game) = &mut self.game {
- if game.state() != GameState::Victory && game.state() != GameState::Loss {
- game.mark_at(coords);
- }
- }
- }
- }
- } else if self.cursor_mode == CursorMode::Highlighter {
- if let Some(pos) = ctx.pointer_interact_pos() {
- if let Some(coords) = self.get_coords(pos) {
- if let Some(game) = &mut self.game {
- game.highlight_at(coords, self.selected_highlighters, false);
- }
- }
- }
- }
- }
- if painter_response.dragged() {
- if ui.input(|i| i.pointer.button_down(PointerButton::Middle)) {
- //println!("dragged");
- self.view_origin += painter_response.drag_delta();
- } else if ui.input(|i| i.pointer.button_down(PointerButton::Primary)) {
- if self.cursor_mode == CursorMode::Highlighter {
- if let Some(pos) = ctx.pointer_interact_pos() {
- if let Some(coords) = self.get_coords(pos) {
- if let Some(game) = &mut self.game {
- game.highlight_at(coords, self.selected_highlighters, true);
- }
- }
- }
- }
- } else if ui.input(|i| i.pointer.button_down(PointerButton::Secondary)) {
- if self.cursor_mode == CursorMode::Highlighter {
- if let Some(pos) = ctx.pointer_interact_pos() {
- if let Some(coords) = self.get_coords(pos) {
- if let Some(game) = &mut self.game {
- game.highlight_at(coords, self.selected_highlighters, false);
- }
- }
- }
- }
- }
- }
- // Zoom/unzoom
- if painter_response.hovered() {
- let delta = ctx.input(|i| i.scroll_delta);
- //println!("{:?}", delta.y);
- if delta.y > 0.0 && (self.zoom_factor < 5.0 || self.unlimited_zoom) {
- if let Some(pos) = ctx.pointer_interact_pos() {
- let old_factor = self.zoom_factor;
- self.zoom_factor *= 1.5;
- self.view_origin.x -= ((pos.x - self.view_origin.x) / old_factor) * (self.zoom_factor - old_factor);
- self.view_origin.y -= ((pos.y - self.view_origin.y) / old_factor) * (self.zoom_factor - old_factor);
- }
- } else if delta.y < 0.0 && (self.zoom_factor > 0.01 || self.unlimited_zoom) {
- if let Some(pos) = ctx.pointer_interact_pos() {
- let old_factor = self.zoom_factor;
- self.zoom_factor /= 1.5;
- self.view_origin.x -= ((pos.x - self.view_origin.x) / old_factor) * (self.zoom_factor - old_factor);
- self.view_origin.y -= ((pos.y - self.view_origin.y) / old_factor) * (self.zoom_factor - old_factor);
- }
- }
- }
- // Keyboard Shortcuts
- // The check below is to prevent triggering when trying to type
- // the seed in the new game window. It's a bit crude, but it works.
- if !self.new_game_window_enabled {
- // TODO: `consume_shortcut` instead of `key_pressed` would allow for more flexibility,
- // but `consume_shortcut` doesn't allow indeterminate states for modifiers (at least currently)
- if ui.input_mut(|i| i.key_pressed(self.shortcuts.probe_mark_shortcut.key)) {
- self.try_set_cursor(CursorMode::ProbeAndMark);
- }
- if ui.input_mut(|i| i.key_pressed(self.shortcuts.highlighter_shortcut.key)) {
- self.try_set_cursor(CursorMode::Highlighter);
- }
- for ii in 0..8 {
- if ui.input_mut(|i| i.key_pressed(self.shortcuts.highlight_group_shortcuts[ii].key)) {
- self.selected_highlighters ^= 1 << ii;
- }
- }
-
- if ui.input_mut(|i| i.key_pressed(self.shortcuts.reset_view_shortcut.key)) {
- self.reset_view();
- }
- if ui.input_mut(|i| i.key_pressed(self.shortcuts.zoom_to_fit_shortcut.key)) {
- self.zoom_to_fit(ctx.screen_rect().max);
- }
- }
- });
- }
-}
+// hide console window on Windows in release (also disables console output)
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+#![feature(step_trait)]
+
+extern crate eframe;
+extern crate hhmmss;
+extern crate itertools;
+extern crate toml;
+extern crate rand_chacha;
+
+use itertools::Itertools;
+use hhmmss::Hhmmss;
+
+pub mod bwi;
+use bwi::BWI;
+
+pub mod minesweeper_model;
+use minesweeper_model::{CellState, DIMENSIONS_COUNT, GameBoard, GameState, InitialGameSettings};
+
+use eframe::{egui, emath::Align2};
+use eframe::egui::{Button, containers::panel::TopBottomPanel, Key, KeyboardShortcut,
+ menu, Modifiers, PointerButton, Response, RichText, Sense};
+use eframe::epaint::{Color32, FontId, Pos2, Rect, Rounding, Shadow, Shape, Stroke};
+use std::{cmp::min, fs};
+use web_time::SystemTime;
+use toml::Table;
+
+#[derive(PartialEq)]
+enum CursorMode {
+ ProbeAndMark,
+ Highlighter,
+}
+
+pub struct Shortcuts {
+ probe_mark_shortcut: KeyboardShortcut,
+ highlighter_shortcut: KeyboardShortcut,
+ highlight_group_shortcuts: [KeyboardShortcut; 8],
+
+ reset_view_shortcut: KeyboardShortcut,
+ zoom_to_fit_shortcut: KeyboardShortcut,
+}
+
+impl Shortcuts {
+ pub fn new() -> Self {
+ let mod_none = Modifiers{
+ alt: false,
+ ctrl: false,
+ shift: false,
+ mac_cmd: false,
+ command: false,
+ };
+ Self {
+ probe_mark_shortcut: KeyboardShortcut::new(mod_none, Key::Q),
+ highlighter_shortcut: KeyboardShortcut::new(mod_none, Key::W),
+ highlight_group_shortcuts: [KeyboardShortcut::new(mod_none, Key::Num1),
+ KeyboardShortcut::new(mod_none, Key::Num2),
+ KeyboardShortcut::new(mod_none, Key::Num3),
+ KeyboardShortcut::new(mod_none, Key::Num4),
+ KeyboardShortcut::new(mod_none, Key::Num5),
+ KeyboardShortcut::new(mod_none, Key::Num6),
+ KeyboardShortcut::new(mod_none, Key::Num7),
+ KeyboardShortcut::new(mod_none, Key::Num8)],
+
+ reset_view_shortcut: KeyboardShortcut::new(mod_none, Key::D),
+ zoom_to_fit_shortcut: KeyboardShortcut::new(mod_none, Key::F),
+ }
+ }
+}
+
+// When compiling natively:
+#[cfg(not(target_arch = "wasm32"))]
+fn main() -> Result<(), eframe::Error> {
+ let options = eframe::NativeOptions {
+ ..Default::default()
+ };
+ eframe::run_native(
+ "Minesweeper6D",
+ options,
+ Box::new(|cc| {
+ cc.egui_ctx.style_mut(|style| {
+ style.visuals.menu_rounding = Rounding::ZERO;
+ style.visuals.popup_shadow = Shadow::NONE;
+ style.visuals.window_rounding = Rounding::ZERO;
+ style.visuals.window_shadow = Shadow::NONE;
+ });
+ Box::::default()
+ }),
+ )
+}
+
+// When compiling to web using trunk:
+#[cfg(target_arch = "wasm32")]
+fn main() {
+ // Redirect `log` message to `console.log` and friends:
+ eframe::WebLogger::init(log::LevelFilter::Debug).ok();
+
+ let web_options = eframe::WebOptions::default();
+
+ wasm_bindgen_futures::spawn_local(async {
+ eframe::WebRunner::new()
+ .start(
+ "the_canvas_id", // hardcode it
+ web_options,
+ Box::new(|cc| Box::::default()),
+ )
+ .await
+ .expect("failed to start eframe");
+ });
+}
+
+struct MinesweeperViewController {
+ current_initial_settings: InitialGameSettings,
+ next_initial_settings: InitialGameSettings,
+
+ next_selected_preset: Option,
+ presets: Vec,
+
+ game: Option,
+ start_time: Option,
+ end_time: Option,
+
+ cursor_mode: CursorMode,
+ selected_highlighters: u8,
+
+ view_origin: Pos2,
+ zoom_factor: f32,
+ cell_edge: f32,
+ tile_spacings: [f32; DIMENSIONS_COUNT],
+
+ show_timer_miliseconds: bool,
+ show_delta: bool,
+ show_neighbors: bool,
+ unlimited_zoom: bool,
+ probe_marked: bool,
+ neighbor_coords: Option<[usize; DIMENSIONS_COUNT]>,
+
+ new_game_window_enabled: bool,
+ rules_window_enabled: bool,
+ controls_window_enabled: bool,
+ about_window_enabled: bool,
+
+ selection_color: Color32,
+ center_color: Color32,
+ neighbor_color: Color32,
+ highlight_colors: [Color32; 8],
+
+ shortcuts: Shortcuts,
+}
+
+impl MinesweeperViewController {
+ fn reset(&mut self) {
+ self.game = None;
+ self.cursor_mode = CursorMode::ProbeAndMark;
+ }
+
+ fn start(&mut self, initial: [usize; DIMENSIONS_COUNT]) {
+ self.start_time = Some(SystemTime::now());
+ self.end_time = None;
+ if let Some(seed) = &self.current_initial_settings.seed {
+ self.game = Some(GameBoard::new(self.current_initial_settings.size,
+ self.current_initial_settings.wrap,
+ self.current_initial_settings.mines,
+ None,
+ u64::from_str_radix(&seed, 16).ok()));
+ self.game.as_mut().unwrap().probe_at(initial, true);
+ } else {
+ self.game = Some(GameBoard::new(self.current_initial_settings.size,
+ self.current_initial_settings.wrap,
+ self.current_initial_settings.mines,
+ Some(initial),
+ None));
+ }
+ }
+
+ // Translate and Scale from screen coordinates to cell coordinates
+ // Uses modular cutoff to decide in constant time whether mouse is over any cell:
+ //
+ // If current position modulo (x*cell+(x-1)*space_1+space_2)
+ // is larger than (x*cell+(x-1)*space_1), it must be outside a cell, otherwise repeat:
+ // | | | | | | | | |
+ // | cell | space_1 | cell | space_1 | cell | space_1 | cell | space_2 |
+ // | | | | | | | | |
+ fn get_coords(&self, pos: Pos2) -> Option<[usize; DIMENSIONS_COUNT]> {
+ if pos.x < self.view_origin.x || pos.y < self.view_origin.y {
+ return None;
+ }
+
+ let [c_xx, c_yy, c_zz, c_uu, c_vv, c_ww] = self.current_initial_settings.size;
+ let [sp_xx, sp_yy, sp_zz, sp_uu, sp_vv, sp_ww] = self.tile_spacings;
+
+ // Sizes of blocks (cells + inner spacing)
+ let x_block_size = c_xx as f32 * self.cell_edge + (c_xx - 1) as f32 * sp_xx;
+ let y_block_size = c_yy as f32 * self.cell_edge + (c_yy - 1) as f32 * sp_yy;
+ let z_block_size = c_zz as f32 * x_block_size + (c_zz - 1) as f32 * sp_zz;
+ let u_block_size = c_uu as f32 * y_block_size + (c_uu - 1) as f32 * sp_uu;
+ // let v_block_size = c_vv as f32 * z_block_size + (c_vv - 1) as f32 * sp_vv;
+ // let w_block_size = c_ww as f32 * u_block_size + (c_ww - 1) as f32 * sp_ww;
+
+ // Get logical points
+ let (dx, dy) = ((pos.x - self.view_origin.x) / self.zoom_factor,
+ (pos.y - self.view_origin.y) / self.zoom_factor);
+
+ // Modulo largest period
+ let (dx_m1, dy_m1) = (dx as f32 % (z_block_size + sp_vv),
+ dy as f32 % (u_block_size + sp_ww));
+ if dx_m1 > z_block_size || dy_m1 > u_block_size {
+ return None;
+ }
+ // Modulo middle period
+ let (dx_m2, dy_m2) = (dx_m1 % (x_block_size + sp_zz),
+ dy_m1 % (y_block_size + sp_uu));
+ if dx_m2 > x_block_size || dy_m2 > y_block_size {
+ return None;
+ }
+ // Modulo smallest period
+ let (dx_m3, dy_m3) = (dx_m2 % (self.cell_edge + sp_xx),
+ dy_m2 % (self.cell_edge + sp_yy));
+ if dx_m3 > self.cell_edge || dy_m3 > self.cell_edge {
+ return None;
+ }
+
+ // Is cell-like, get coords
+ let (xx, yy) = ((dx_m2 / (self.cell_edge + sp_xx)) as usize,
+ (dy_m2 / (self.cell_edge + sp_yy)) as usize);
+ let (zz, uu) = ((dx_m1 / (x_block_size + sp_zz)) as usize,
+ (dy_m1 / (y_block_size + sp_uu)) as usize);
+ let (vv, ww) = ((dx / (z_block_size + sp_vv)) as usize,
+ (dy / (u_block_size + sp_ww)) as usize);
+
+ // Check if actual cell within grid bounds
+ if xx >= c_xx || yy >= c_yy || zz >= c_zz || uu >= c_uu || vv >= c_vv || ww >= c_ww {
+ return None;
+ }
+
+ return Some([xx, yy, zz, uu, vv, ww]);
+ }
+
+ // Scale and Translate from logical points to screen coordinates
+ fn sc_tr(&self, xx: f32, yy: f32) -> Pos2 {
+ Pos2::new(xx * self.zoom_factor, yy * self.zoom_factor) + self.view_origin.to_vec2()
+ }
+
+ // Cumulative spacing in screen axis directions
+ fn cum_spc_x(&self, xx: usize, zz: usize, vv: usize) -> f32 {
+ let [c_xx, _, _, _, _, _] = self.current_initial_settings.size;
+ let [sp_xx, _, sp_zz, _, sp_vv, _] = self.tile_spacings;
+ return (xx+zz*(c_xx-1)+vv*(c_xx-1)) as f32*sp_xx + (zz+vv*(c_xx-1)) as f32*sp_zz + vv as f32*sp_vv;
+ }
+ fn cum_spc_y(&self, yy: usize, uu: usize, ww: usize) -> f32 {
+ let [_, c_yy, _, _, _, _] = self.current_initial_settings.size;
+ let [_, sp_yy, _, sp_uu, _, sp_ww] = self.tile_spacings;
+ return (yy+uu*(c_yy-1)+ww*(c_yy-1)) as f32*sp_yy + (uu+ww*(c_yy-1)) as f32*sp_uu + ww as f32*sp_ww;
+ }
+
+ fn try_set_cursor(&mut self, mode: CursorMode) {
+ match mode {
+ CursorMode::ProbeAndMark => {
+ self.cursor_mode = CursorMode::ProbeAndMark;
+ },
+ CursorMode::Highlighter => {
+ if self.game != None {
+ self.cursor_mode = CursorMode::Highlighter;
+ }
+ }
+ }
+ }
+
+ fn reset_view(&mut self) {
+ self.view_origin = Pos2::new(0.0, 20.0);
+ self.zoom_factor = 1.0;
+ }
+
+ fn zoom_to_fit(&mut self, screen_size: Pos2) {
+ let [c_xx, c_yy, c_zz, c_uu, c_vv, c_ww] = self.current_initial_settings.size;
+ let [sp_xx, sp_yy, sp_zz, sp_uu, sp_vv, sp_ww] = self.tile_spacings;
+
+ // TODO: allow user to set the padding
+ let (padding_x, padding_y) = (5.0, 5.0);
+
+ let x_block_size = c_xx as f32 * self.cell_edge + (c_xx - 1) as f32 * sp_xx;
+ let y_block_size = c_yy as f32 * self.cell_edge + (c_yy - 1) as f32 * sp_yy;
+ let z_block_size = c_zz as f32 * x_block_size + (c_zz - 1) as f32 * sp_zz;
+ let u_block_size = c_uu as f32 * y_block_size + (c_uu - 1) as f32 * sp_uu;
+ let v_block_size = c_vv as f32 * z_block_size + (c_vv - 1) as f32 * sp_vv;
+ let w_block_size = c_ww as f32 * u_block_size + (c_ww - 1) as f32 * sp_ww;
+
+ let x_factor = (screen_size.x - 2.0*padding_x) / v_block_size;
+ let y_factor = (screen_size.y - 40.0 - 2.0*padding_y) / w_block_size;
+
+ // Zoom to fit the larger side
+ if (x_factor > y_factor && w_block_size * x_factor <= screen_size.y - 40.0 - 2.0*padding_y)
+ || v_block_size * y_factor > screen_size.x - 2.0*padding_x {
+ self.zoom_factor = if self.unlimited_zoom {x_factor} else {x_factor.clamp(0.01, 5.0)};
+ } else {
+ self.zoom_factor = if self.unlimited_zoom {y_factor} else {y_factor.clamp(0.01, 5.0)};
+ }
+
+ // Translate to center
+ self.view_origin.x = (screen_size.x - 10.0 - v_block_size*self.zoom_factor) / 2.0 + padding_x;
+ self.view_origin.y = (screen_size.y - 50.0 - w_block_size*self.zoom_factor) / 2.0 + 20.0 + padding_y;
+ }
+}
+
+impl Default for MinesweeperViewController {
+ fn default() -> Self {
+ // Sanity check
+ //println!("{}", std::mem::size_of::());
+
+ let settings = InitialGameSettings {
+ name: "Custom".into(),
+ size: [4, 4, 4, 4, 1, 1],
+ wrap: [false, false, false, false, false, false],
+ mines: 20,
+ seed: None,
+ };
+
+ let mut ret = Self {
+ current_initial_settings: settings.clone(),
+ next_initial_settings: settings,
+
+ next_selected_preset: None,
+ presets: vec![],
+
+ game: None,
+ start_time: None,
+ end_time: None,
+
+ cursor_mode: CursorMode::ProbeAndMark,
+ selected_highlighters: 1,
+
+ view_origin: Pos2::new(0.0, 20.0),
+ zoom_factor: 1.0,
+ cell_edge: 30.0,
+ tile_spacings: [0.0, 0.0, 10.0, 10.0, 20.0, 20.0],
+
+ show_timer_miliseconds: false,
+ show_delta: true,
+ show_neighbors: true,
+ unlimited_zoom: false,
+ probe_marked: false,
+ neighbor_coords: None,
+
+ new_game_window_enabled: false,
+ rules_window_enabled: false,
+ controls_window_enabled: false,
+ about_window_enabled: false,
+
+ selection_color: Color32::RED,
+ center_color: Color32::LIGHT_RED,
+ neighbor_color: Color32::LIGHT_BLUE,
+ highlight_colors: [Color32::YELLOW, Color32::BROWN, Color32::LIGHT_GREEN, Color32::WHITE,
+ Color32::KHAKI, Color32::DARK_BLUE, Color32::DARK_GREEN, Color32::GOLD],
+
+ shortcuts: Shortcuts::new(),
+ };
+
+ let config_text = fs::read_to_string("config.toml").unwrap_or_else(|_| "".into());
+ let config_table: Table = config_text.parse::().expect("Invalid configuration file");
+
+ // Load in presets
+ if let Some(val) = config_table.get("preset"){
+ for e in val.as_array().unwrap() {
+ let mut igs: InitialGameSettings = Default::default();
+ if let Some(n) = e.get("name") {
+ igs.name = n.as_str().unwrap().into();
+ }
+ if let Some(size_value) = e.get("size") {
+ if let Some(a) = size_value.as_array() {
+ if a.len() != DIMENSIONS_COUNT {
+ println!("Warning: `size` should be array of {} elements, {} found", DIMENSIONS_COUNT, a.len());
+ }
+ for ii in 0..min(a.len(), DIMENSIONS_COUNT) {
+ if let Some(i) = a[ii].as_integer().map(|e| e.clamp(1, 100) as usize) {
+ igs.size[ii] = i;
+ } else {
+ println!("Warning: value at index {} of `size` is invalid", ii);
+ }
+ }
+ } else {
+ println!("Warning: value of `size` is invalid");
+ }
+ }
+ if let Some(wrap_value) = e.get("wrap") {
+ if let Some(a) = wrap_value.as_array() {
+ if a.len() != DIMENSIONS_COUNT {
+ println!("Warning: `wrap` should be array of {} elements, {} found", DIMENSIONS_COUNT, a.len());
+ }
+ for ii in 0..min(a.len(), DIMENSIONS_COUNT) {
+ if let Some(b) = a[ii].as_bool() {
+ igs.wrap[ii] = b;
+ } else {
+ println!("Warning: value at index {} of `wrap` is invalid", ii);
+ }
+ }
+ } else {
+ println!("Warning: value of `wrap` is invalid");
+ }
+ }
+ if let Some(mines_value) = e.get("mines") {
+ if let Some(i) = mines_value.as_integer()
+ .map(|e| (e as u32)
+ .clamp(1, (igs.size.iter().fold(1, |p, v| p*v) - 1) as u32)) {
+ igs.mines = i;
+ } else {
+ println!("Warning: value of `mines` is invalid");
+ }
+ }
+ if let Some(seed_value) = e.get("seed") {
+ if let Some(s) = seed_value.as_str() {
+ igs.seed = Some(s.into());
+ } else {
+ println!("Warning: value of `seed` is invalid");
+ }
+ }
+ ret.presets.push(igs);
+ }
+ }
+
+ // Load in other settings
+ if let Some(val) = config_table.get("default_preset") {
+ if let Some(i) = val.as_integer().map(|e| e as usize) {
+ if i < ret.presets.len() {
+ ret.current_initial_settings = ret.presets[i].clone();
+ ret.next_initial_settings = ret.presets[i].clone();
+ ret.next_selected_preset = Some(i as u32);
+ } else {
+ println!("Warning: value of `default_preset` is outside presets range");
+ }
+ } else {
+ println!("Warning: value of `default_preset` is invalid");
+ }
+ }
+
+ if let Some(val) = config_table.get("highlight_colors") {
+ // Snippet by YgorSouza at https://github.com/emilk/egui/issues/3466#issuecomment-1762923933
+ fn color_from_hex(hex: &str) -> Option {
+ let hex = hex.trim_start_matches('#');
+ let alpha = match hex.len() {
+ 6 => false,
+ 8 => true,
+ _ => None?,
+ };
+ u32::from_str_radix(hex, 16)
+ .ok()
+ .map(|u| if alpha { u } else { u << 8 | 0xff })
+ .map(u32::to_be_bytes)
+ .map(|[r, g, b, a]| Color32::from_rgba_unmultiplied(r, g, b, a))
+ }
+ let a = val.as_array().unwrap();
+ for ii in 0..8 {
+ ret.highlight_colors[ii] = color_from_hex(a[ii].as_str().unwrap()).unwrap();
+ }
+ };
+
+ if let Some(val) = config_table.get("show_timer_miliseconds") {
+ ret.show_timer_miliseconds = val.as_bool().unwrap();
+ }
+ if let Some(val) = config_table.get("show_delta") {
+ ret.show_delta = val.as_bool().unwrap();
+ }
+ if let Some(val) = config_table.get("show_neighbors") {
+ ret.show_neighbors = val.as_bool().unwrap();
+ }
+ if let Some(val) = config_table.get("unlimited_zoom") {
+ ret.unlimited_zoom = val.as_bool().unwrap();
+ }
+ if let Some(val) = config_table.get("probe_marked") {
+ ret.probe_marked = val.as_bool().unwrap();
+ }
+
+ if let Some(val) = config_table.get("tile_spacings") {
+ let a = val.as_array().unwrap();
+ for ii in 0..4 {
+ let valf = a[ii].as_float().unwrap() as f32;
+ if valf >= 0.0 {
+ ret.tile_spacings[ii] = valf;
+ }
+ }
+ }
+
+ ret
+ }
+}
+
+impl eframe::App for MinesweeperViewController {
+ fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
+ if self.show_timer_miliseconds {
+ ctx.request_repaint();
+ } else {
+ // TODO: This egui function is bugged, uncomment next line when fixed
+ //ctx.request_repaint_after(Duration::new(1,0));
+ }
+
+ let mut new_game_window_enabled = self.new_game_window_enabled;
+ if new_game_window_enabled {
+ egui::Window::new("New Custom Game")
+ .open(&mut new_game_window_enabled).show(ctx, |ui| {
+
+ let resp = egui::ComboBox::from_id_source("preset_combobox")
+ .width(300.0)
+ .selected_text(if let Some(pno) = self.next_selected_preset {
+ self.presets[pno as usize].name.as_str()
+ } else {
+ "Custom game"
+ })
+ .show_ui(ui, |ui| {
+ (0..self.presets.len()).map(|ii| {
+ ui.selectable_value(&mut self.next_selected_preset, Some(ii as u32),
+ self.presets[ii].name.clone())
+ }).collect::>()
+ });
+ if let Some(col) = resp.inner {
+ if col.iter().any(|o: &Response| o.clicked()) {
+ if let Some(pno) = self.next_selected_preset {
+ self.next_initial_settings = self.presets[pno as usize].clone();
+ }
+ }
+ }
+
+ egui::Grid::new("dim_and_wrap_grid").show(ui, |ui| {
+ ui.label("Dimensions: ");
+ let resps = (0..DIMENSIONS_COUNT).map(
+ |e| ui.add(egui::DragValue::new(&mut self.next_initial_settings.size[e])
+ .speed(1).clamp_range(1..=100))
+ ).collect::>();
+ if resps.iter().any(|e| e.changed()) {
+ self.next_selected_preset = None;
+ };
+ ui.end_row();
+
+ ui.label("Wrapping: ");
+ let resps = (0..DIMENSIONS_COUNT).map(
+ |e| ui.add(egui::Checkbox::without_text(&mut self.next_initial_settings.wrap[e]))
+ ).collect::>();
+ if resps.iter().any(|e| e.changed()) {
+ self.next_selected_preset = None;
+ };
+ ui.end_row();
+ });
+
+ ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
+ ui.label("Mines: ");
+ ui.add(egui::DragValue::new(&mut self.next_initial_settings.mines).speed(1)
+ .clamp_range(1..=(self.next_initial_settings.size.iter().fold(1, |p, v| p*v)-1)));
+ });
+
+ let mut checkbox_state = self.next_initial_settings.seed != None;
+ let checkbox = egui::Checkbox::new(&mut checkbox_state, "Generate the board based on the first click");
+ if ui.add(checkbox).clicked() {
+ if self.next_initial_settings.seed == None {
+ self.next_initial_settings.seed = Some("".into());
+ } else {
+ self.next_initial_settings.seed = None;
+ }
+ }
+
+ ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
+ ui.label("Seed: ");
+
+ if let Some(ref mut s) = self.next_initial_settings.seed {
+ ui.add_enabled(true, egui::TextEdit::singleline(&mut *s));
+ } else {
+ ui.add_enabled(false, egui::TextEdit::singleline(&mut ""));
+ };
+ });
+
+ ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
+ if ui.button("Reset").clicked() {
+ self.next_initial_settings = self.current_initial_settings.clone();
+ }
+ if ui.button("Start").clicked() {
+ self.current_initial_settings = self.next_initial_settings.clone();
+ self.new_game_window_enabled = false;
+ self.reset();
+ }
+ });
+ });
+ }
+ self.new_game_window_enabled = self.new_game_window_enabled && new_game_window_enabled;
+
+ let mut rules_window_enabled = self.rules_window_enabled;
+ if rules_window_enabled {
+ egui::Window::new("Rules")
+ .open(&mut rules_window_enabled).show(ctx, |ui| {
+ ui.label(
+r"Rules are basically the same as with old-school minsweeper.
+
+Cell's number signifies how many mines are in its neighborhood, 0/empty meaning none. Probing a cell containing mine results in game over, goal of the game is to uncover (through probing) all cells not containing mines. Fields suspected of being mines may be marked with a flag, however it is not necessary.
+
+The twist is that in n dimensions, every cell has up to 3^n-1 neighbors (e.g. 8 for 2 dimensions, 26 for 3 dimensions, 80 for 4 dimensions)");
+ });
+ }
+ self.rules_window_enabled = rules_window_enabled;
+ let mut controls_window_enabled = self.controls_window_enabled;
+ if controls_window_enabled {
+ egui::Window::new("Controls")
+ .open(&mut controls_window_enabled).show(ctx, |ui| {
+ ui.label(
+r"Currently there are two tools: Probe/Mark and Highlighter.
+
+Probe/Mark probes a cell with primary button (usually Left Mouse Button) and marks a cell as a mine with secondary button (usually Right Mouse Button)
+
+Highlighter highlights with primary button and unhighlights with secondary button.
+
+Camera may be panned through dragging with middle mouse button, zoomed/unzoomed using scroll wheel.
+
+If neighbor hints are enabled, holding Shift freezes them in place, whereas holding Alt temporarily disables them.");
+ });
+ }
+ self.controls_window_enabled = controls_window_enabled;
+ let mut about_window_enabled = self.about_window_enabled;
+ if about_window_enabled {
+ egui::Window::new("About")
+ .open(&mut about_window_enabled).show(ctx, |ui| {
+ ui.label(format!(
+r"Minesweeper4D (version {})
+
+Code written by sdasda7777 (github.com/sdasda7777) (except where noted otherwise) with a lot of help from amazing members of the egui Discord server", option_env!("CARGO_PKG_VERSION").unwrap()));
+ });
+ }
+ self.about_window_enabled = about_window_enabled;
+
+ TopBottomPanel::top("menubar_panel")
+ .frame(egui::Frame::none().fill(egui::Color32::LIGHT_BLUE))
+ .show(ctx, |ui| {
+ ui.visuals_mut().override_text_color = Some(egui::Color32::BLACK);
+ menu::bar(ui, |ui| {
+ ui.menu_button("Game", |ui| {
+ let new_game_window_button = Button::new("New Custom Game")
+ .selected(self.new_game_window_enabled);
+ if ui.add(new_game_window_button).clicked() {
+ self.new_game_window_enabled = !self.new_game_window_enabled;
+ ui.close_menu();
+ }
+ if ui.button("Quick restart").clicked() {
+ self.reset();
+ ui.close_menu();
+ }
+ });
+ ui.menu_button("View", |ui| {
+ let _ = ui.button(format!("Current zoom: {:.3} %", self.zoom_factor*100.0));
+ let reset_view_button = Button::new("Reset to 0x0 @ 100%")
+ .shortcut_text(
+ RichText::new(ctx.format_shortcut(&self.shortcuts.reset_view_shortcut))
+ .color(Color32::WHITE));
+ if ui.add(reset_view_button).clicked() {
+ self.reset_view();
+ ui.close_menu();
+ }
+ let zoom_to_fit_button = Button::new("Zoom to fit")
+ .shortcut_text(
+ RichText::new(ctx.format_shortcut(&self.shortcuts.zoom_to_fit_shortcut))
+ .color(Color32::WHITE));
+ if ui.add(zoom_to_fit_button).clicked() {
+ self.zoom_to_fit(ctx.screen_rect().max);
+ ui.close_menu();
+ }
+ let show_neighbors_button = Button::new("Show neighbors")
+ .selected(self.show_neighbors);
+ if ui.add(show_neighbors_button).clicked() {
+ self.show_neighbors = !self.show_neighbors;
+ ui.close_menu();
+ }
+ let unlimited_zoom_button = Button::new("Unlimited zoom")
+ .selected(self.unlimited_zoom);
+ if ui.add(unlimited_zoom_button).clicked() {
+ self.unlimited_zoom = !self.unlimited_zoom;
+ ui.close_menu();
+ }
+ let show_timer_miliseconds_button = Button::new("Show timer miliseconds")
+ .selected(self.show_timer_miliseconds);
+ if ui.add(show_timer_miliseconds_button).clicked() {
+ self.show_timer_miliseconds = !self.show_timer_miliseconds;
+ ui.close_menu();
+ }
+ });
+ ui.menu_button("Tools", |ui| {
+ ui.visuals_mut().widgets.noninteractive.weak_bg_fill = Color32::DARK_GRAY;
+
+ let probe_and_mark_button = Button::new("Probe/Mark")
+ .selected(self.cursor_mode == CursorMode::ProbeAndMark)
+ .shortcut_text(
+ RichText::new(ctx.format_shortcut(&self.shortcuts.probe_mark_shortcut))
+ .color(Color32::WHITE));
+
+ let highlight_button = Button::new("Highlighter")
+ .selected(if self.cursor_mode == CursorMode::Highlighter {true} else {false})
+ .shortcut_text(
+ RichText::new(ctx.format_shortcut(&self.shortcuts.highlighter_shortcut))
+ .color(Color32::WHITE));
+
+ if ui.add(probe_and_mark_button).clicked() {
+ self.try_set_cursor(CursorMode::ProbeAndMark);
+ ui.close_menu();
+ }
+ ui.menu_button("Probe/Mark options", |ui| {
+ let probe_marked = Button::new("Allow probing marked cells").selected(self.probe_marked);
+ if ui.add(probe_marked).clicked() {
+ self.probe_marked = !self.probe_marked;
+ }
+ });
+
+ // The highligh tool is disabled when game is NotRunning
+ if ui.add_enabled(self.game != None, highlight_button).clicked() {
+ self.try_set_cursor(CursorMode::Highlighter);
+ ui.close_menu();
+ }
+ ui.menu_button("Highlight groups", |ui| {
+ for ii in 0..8 {
+ let highlight_group_button
+ = Button::new(format!("Group {} ({})", ii+1,
+ if (self.selected_highlighters & (1 << ii)) > 0 {"on"} else {"off"}))
+ .selected((self.selected_highlighters & (1 << ii)) > 0)
+ .stroke(Stroke::new(2.0, self.highlight_colors[ii]))
+ .shortcut_text(ctx.format_shortcut(&self.shortcuts.highlight_group_shortcuts[ii]));
+
+ if ui.add(highlight_group_button).clicked() {
+ self.selected_highlighters ^= 1 << ii;
+ }
+ }
+ });
+ });
+ ui.menu_button("Help", |ui| {
+ let rules_button = Button::new("Rules").selected(self.rules_window_enabled);
+ let controls_button = Button::new("Controls").selected(self.controls_window_enabled);
+ let about_button = Button::new("About").selected(self.about_window_enabled);
+ if ui.add(rules_button).clicked() {
+ self.rules_window_enabled = !self.rules_window_enabled;
+ ui.close_menu();
+ }
+ if ui.add(controls_button).clicked() {
+ self.controls_window_enabled = !self.controls_window_enabled;
+ ui.close_menu();
+ }
+ if ui.add(about_button).clicked() {
+ self.about_window_enabled = !self.about_window_enabled;
+ ui.close_menu();
+ }
+ });
+ if ui.button(format!("Δ: {}", if self.show_delta {"yes"} else {"no"})).clicked() {
+ self.show_delta = !self.show_delta;
+ }
+ if let Some(game) = &self.game {
+ let seed = format!("seed: {:016x}", game.seed());
+ ui.add(egui::TextEdit::singleline(&mut seed.as_str()));
+ }
+ ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
+ ui.button(format!("({}/{}) {}",
+ if let Some(game) = &self.game {game.marked_as_mine()} else {0},
+ self.current_initial_settings.mines,
+ (0..DIMENSIONS_COUNT).map(
+ |i| format!("{}{}",
+ self.current_initial_settings.size[i],
+ if self.current_initial_settings.wrap[i] {"w"} else {""})
+ ).join(" x ")))
+ });
+ });
+ });
+
+ TopBottomPanel::bottom("bottom_panel")
+ .frame(egui::Frame::none().fill(egui::Color32::LIGHT_BLUE))
+ .show(ctx, |ui| {
+ ui.visuals_mut().override_text_color = Some(egui::Color32::BLACK);
+ menu::bar(ui, |ui| {
+ match self.cursor_mode {
+ CursorMode::ProbeAndMark => {
+ let _ = ui.button("Probe/Mark: primary to probe a cell, secondary to mark as a mine");
+ },
+ CursorMode::Highlighter => {
+ let _ = ui.button(
+ format!("Highlighter ({}): primary to highlight a cell, secondary to unhighlight",
+ self.selected_highlighters)
+ );
+ }
+ }
+
+ ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
+ if let Some(game) = &self.game {
+ if let Some(start_time) = self.start_time {
+ let dur = if let Some(end_time) = self.end_time
+ { end_time } else { SystemTime::now() }
+ .duration_since(start_time).unwrap();
+ let fdur = if self.show_timer_miliseconds
+ { dur.hhmmssxxx() } else { dur.hhmmss() };
+ let _ = ui.button(format!("{} {}",match game.state() {
+ GameState::Victory => "You won!",
+ GameState::Loss => "You lost!",
+ _ => ""
+ }, fdur
+ ));
+ }
+ }
+ });
+ });
+ });
+
+ egui::CentralPanel::default()
+ .frame(egui::Frame::none().fill(egui::Color32::GRAY))
+ .show(ctx, |ui| {
+
+ let basic_stroke = Stroke::new(2.0 * self.zoom_factor, egui::Color32::BLACK);
+ let harder_stroke = Stroke::new(4.0 * self.zoom_factor, egui::Color32::BLACK);
+ let neighbor_stroke = Stroke::new(3.0 * self.zoom_factor, self.neighbor_color);
+ let center_stroke = Stroke::new(3.0 * self.zoom_factor, self.center_color);
+ let selection_stroke = Stroke::new(3.0 * self.zoom_factor, self.selection_color);
+ let highlight_strokes = self.highlight_colors.map(|x| Stroke::new(2.0 * self.zoom_factor, x));
+
+ let screen_size = ctx.screen_rect().max;
+ let [c_xx, c_yy, c_zz, c_uu, c_vv, c_ww] = self.current_initial_settings.size;
+
+ let (painter_response, painter) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag());
+
+ // Paint cell contents
+ let background_color = Color32::GRAY;
+ if self.zoom_factor > 0.05 {
+ if let Some(game) = &self.game {
+ for iw in 0..c_ww {
+ for iv in 0..c_vv {
+ for iu in 0..c_uu {
+ for iz in 0..c_zz {
+ for iy in 0..c_yy {
+ for ix in 0..c_xx {
+ let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
+ let ulc = self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x,
+ (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y);
+ // Only draw symbols reasonably close to the viewport
+ if ulc.x >= -self.cell_edge*self.zoom_factor && ulc.x <= screen_size.x
+ && ulc.y >= -self.cell_edge*self.zoom_factor && ulc.y <= screen_size.y {
+ let (symbol, color) = match game.cell_at([ix, iy, iz, iu, iv, iw]) {
+ CellState::UndiscoveredMine(_)
+ => if game.state() == GameState::Victory {
+ ("💣".into(), Color32::GREEN)
+ } else if game.state() == GameState::Loss {
+ ("💣".into(), Color32::RED)
+ } else {
+ ("".into(), Color32::GRAY)
+ },
+ CellState::MarkedMine(_)
+ => if game.state() == GameState::Victory || game.state() == GameState::Loss {
+ ("🚩".into(), Color32::GREEN)
+ } else {
+ ("🚩".into(), Color32::GRAY)
+ },
+ CellState::ExplodedMine(_) => ("💥".into(), Color32::RED),
+ CellState::UndiscoveredEmpty(..) => ("".into(), Color32::GRAY),
+ CellState::MarkedEmpty(..)
+ => if game.state() == GameState::Victory || game.state() == GameState::Loss {
+ ("🚩".into(), Color32::RED)
+ } else {
+ ("🚩".into(), Color32::GRAY)
+ },
+ CellState::DiscoveredEmpty(mc, delta, _)
+ => (if mc == 0 && delta == 0 {"".into()}
+ else {format!("{}", if self.show_delta {delta} else {mc as i32})},
+ Color32::LIGHT_GRAY),
+ };
+
+ // Only paint squares with different color than the current background
+ if color != background_color {
+ painter.add(
+ Shape::rect_filled(
+ Rect::from_min_max(
+ ulc,
+ self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz+1) as f32 * self.cell_edge + spc_x,
+ (iy+iu*c_yy+iw*c_yy*c_uu+1) as f32 * self.cell_edge + spc_y)),
+ Rounding::ZERO, color
+ )
+ );
+ }
+
+ if symbol != "" {
+ // Since drawing text is somewhat expensive, only draw text that can most definitely be read
+ if self.zoom_factor >= 0.10 {
+ painter.text(
+ self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_x,
+ (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_y),
+ Align2::CENTER_CENTER,
+ symbol,
+ FontId::proportional(25.0 * self.zoom_factor),
+ Color32::BLACK
+ );
+ } else {
+ painter.add(
+ Shape::circle_filled(
+ self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_x,
+ (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge+self.cell_edge/2.0 + spc_y),
+ 10.0 * self.zoom_factor,
+ Color32::GRAY)
+ );
+ }
+ }
+ }
+ }}}}}}
+ }
+ }
+
+ // Paint lines
+ for iw in 0..c_ww {
+ for iu in 0..c_uu {
+ for iy in 0..c_yy {
+ let spc_y = self.cum_spc_y(iy,iu,iw);
+ let pos_y = (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y;
+ for iv in 0..c_vv {
+ for iz in 0..c_zz {
+ for ix in 0..c_xx {
+ for ix2 in 0..=1 {
+ let spc_x = self.cum_spc_x(ix,iz,iv);
+ let ulc = self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz+ix2) as f32 * self.cell_edge + spc_x, pos_y);
+ if ulc.x >= -self.cell_edge*self.zoom_factor && ulc.x <= screen_size.x
+ && ulc.y >= -self.cell_edge*self.zoom_factor && ulc.y <= screen_size.y+self.cell_edge {
+ painter.add(
+ Shape::line_segment(
+ [ulc,
+ self.sc_tr((ix+iz*c_xx+iv*c_xx*c_zz+ix2) as f32 * self.cell_edge + spc_x,
+ pos_y + self.cell_edge)],
+ if (ix == 0 && ix2 == 0)
+ || (ix+1 == c_xx && ix2 == 1)
+ {harder_stroke} else {basic_stroke}));
+ }
+ }}}}
+ }}}
+ for iv in 0..c_vv {
+ for iz in 0..c_zz {
+ for ix in 0..c_xx {
+ let spc_x = self.cum_spc_x(ix,iz,iv);
+ let pos_x = (ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x;
+ for iw in 0..c_ww {
+ for iu in 0..c_uu {
+ for iy in 0..c_yy {
+ for iy2 in 0..=1 {
+ let spc_y = self.cum_spc_y(iy,iu,iw);
+ let ulc = self.sc_tr(pos_x, (iy+iu*c_yy+iw*c_yy*c_uu+iy2) as f32*self.cell_edge + spc_y);
+ if ulc.x >= -self.cell_edge*self.zoom_factor && ulc.x <= screen_size.x
+ && ulc.y >= -self.cell_edge*self.zoom_factor && ulc.y <= screen_size.y {
+ painter.add(
+ Shape::line_segment(
+ [ulc,
+ self.sc_tr(pos_x + self.cell_edge,
+ (iy+iu*c_yy+iw*c_yy*c_uu+iy2) as f32*self.cell_edge + spc_y)],
+ if (iy == 0 && iy2 == 0)
+ || (iy+1 == c_yy && iy2 == 1)
+ {harder_stroke} else {basic_stroke}));
+ }
+ }}}}
+ }}}
+
+ // Paint cursor, neighbor hints and their center
+ if let Some(pos) = painter_response.hover_pos() {
+ let mut neighbor_coords = None;
+ let mut mouse_coords = None;
+
+ if self.show_neighbors && !ui.input(|i| i.modifiers.matches(Modifiers::ALT)) {
+ if ui.input(|i| i.modifiers.matches(Modifiers::SHIFT)) && self.neighbor_coords != None {
+ neighbor_coords = self.neighbor_coords;
+ }
+ }
+ if let Some(coords) = self.get_coords(pos) {
+ if neighbor_coords == None && self.show_neighbors
+ && !ui.input(|i| i.modifiers.matches(Modifiers::ALT)) {
+ self.neighbor_coords = Some(coords);
+ neighbor_coords = Some(coords);
+ }
+ mouse_coords = Some(coords);
+ }
+
+ if let Some([ix, iy, iz, iu, iv, iw]) = neighbor_coords {
+ let [cw_xx, cw_yy, cw_zz, cw_uu, cw_vv, cw_ww] = self.current_initial_settings.wrap;
+ for iwsupp in BWI::new(iw as i32-1,iw as i32+1,0,c_ww as i32-1,cw_ww) {
+ for ivsupp in BWI::new(iv as i32-1,iv as i32+1,0,c_vv as i32-1,cw_vv) {
+ for iusupp in BWI::new(iu as i32-1,iu as i32+1,0,c_uu as i32-1,cw_uu) {
+ for izsupp in BWI::new(iz as i32-1,iz as i32+1,0,c_zz as i32-1,cw_zz) {
+ for iysupp in BWI::new(iy as i32-1,iy as i32+1,0,c_yy as i32-1,cw_yy) {
+ for ixsupp in BWI::new(ix as i32-1,ix as i32+1,0,c_xx as i32-1,cw_xx) {
+ let (ixb, iyb, izb, iub, ivb, iwb)
+ = (ixsupp as usize, iysupp as usize, izsupp as usize,
+ iusupp as usize, ivsupp as usize, iwsupp as usize);
+ let (spc_x, spc_y) = (self.cum_spc_x(ixb,izb,ivb), self.cum_spc_y(iyb,iub,iwb));
+ let (ulc_x, ulc_y) = (ixb+izb*c_xx+ivb*c_xx*c_zz, iyb+iub*c_yy+iwb*c_yy*c_uu);
+ painter.add(
+ Shape::rect_stroke(
+ Rect::from_min_max(
+ self.sc_tr(ulc_x as f32 * self.cell_edge + spc_x,
+ ulc_y as f32 * self.cell_edge + spc_y),
+ self.sc_tr((ulc_x+1) as f32 * self.cell_edge + spc_x,
+ (ulc_y+1) as f32 * self.cell_edge + spc_y)),
+ Rounding::ZERO, neighbor_stroke));
+ }}}}}}
+
+ let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
+ let (ulc_x, ulc_y) = (ix+iz*c_xx+iv*c_xx*c_zz, iy+iu*c_yy+iw*c_yy*c_uu);
+ painter.add(
+ Shape::rect_stroke(
+ Rect::from_min_max(
+ self.sc_tr(ulc_x as f32 * self.cell_edge + spc_x,
+ ulc_y as f32 * self.cell_edge + spc_y),
+ self.sc_tr((ulc_x+1) as f32 * self.cell_edge + spc_x,
+ (ulc_y+1) as f32 * self.cell_edge + spc_y)),
+ Rounding::ZERO, center_stroke));
+ }
+ if let Some([ix, iy, iz, iu, iv, iw]) = mouse_coords {
+ let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
+ let (ulc_x, ulc_y) = (ix+iz*c_xx+iv*c_xx*c_zz, iy+iu*c_yy+iw*c_yy*c_uu);
+ painter.add(
+ Shape::rect_stroke(
+ Rect::from_min_max(
+ self.sc_tr(ulc_x as f32 * self.cell_edge + spc_x,
+ ulc_y as f32 * self.cell_edge + spc_y),
+ self.sc_tr((ulc_x+1) as f32 * self.cell_edge + spc_x,
+ (ulc_y+1) as f32 * self.cell_edge + spc_y)),
+ Rounding::ZERO, selection_stroke));
+ }
+ }
+
+ // Paint highlights
+ const HIGHLIGHT_SPACING: f32 = 2.5;
+ for iw in 0..c_ww {
+ for iv in 0..c_vv {
+ for iu in 0..c_uu {
+ for iz in 0..c_zz {
+ for iy in 0..c_yy {
+ for ix in 0..c_xx {
+ if let Some(game) = &self.game {
+ match game.cell_at([ix, iy, iz, iu, iv, iw]) {
+ CellState::UndiscoveredMine(g) | CellState::MarkedMine(g)
+ | CellState::ExplodedMine(g) | CellState::UndiscoveredEmpty(.., g)
+ | CellState::MarkedEmpty(.., g) | CellState::DiscoveredEmpty(.., g)
+ => {
+ let (spc_x, spc_y) = (self.cum_spc_x(ix,iz,iv), self.cum_spc_y(iy,iu,iw));
+ let mut next_start_group = 0;
+ if g > 0 { for current_side in 0..8 {
+ for highlight_group in (next_start_group..8).chain(0..next_start_group) {
+ if (g & (1 << highlight_group)) > 0 {
+ let mut p1x = (ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x;
+ let mut p1y = (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y;
+ let mut p2x = (ix+iz*c_xx+iv*c_xx*c_zz) as f32 * self.cell_edge + spc_x;
+ let mut p2y = (iy+iu*c_yy+iw*c_yy*c_uu) as f32 * self.cell_edge + spc_y;
+ match current_side {
+ 0 | 6 | 7 => {p1x += HIGHLIGHT_SPACING;},
+ 1 | 5 => {p1x += self.cell_edge/2.0;},
+ 2 | 3 | 4 => {p1x += self.cell_edge-HIGHLIGHT_SPACING;},
+ _ => {}
+ };
+ match current_side {
+ 0 | 1 | 2 => {p1y += HIGHLIGHT_SPACING;},
+ 3 | 7 => {p1y += self.cell_edge/2.0;},
+ 4 | 5 | 6 => {p1y += self.cell_edge-HIGHLIGHT_SPACING;},
+ _ => {}
+ };
+ match current_side {
+ 0 | 4 => {p2x += self.cell_edge/2.0;},
+ 1 | 2 | 3 => {p2x += self.cell_edge-HIGHLIGHT_SPACING;},
+ 5 | 6 | 7 => {p2x += HIGHLIGHT_SPACING;},
+ _ => {}
+ };
+ match current_side {
+ 0 | 1 | 7 => {p2y += HIGHLIGHT_SPACING;},
+ 2 | 6 => {p2y += self.cell_edge/2.0;},
+ 3 | 4 | 5 => {p2y += self.cell_edge-HIGHLIGHT_SPACING;},
+ _ => {}
+ };
+
+ painter.add(
+ Shape::line_segment([self.sc_tr(p1x, p1y),
+ self.sc_tr(p2x, p2y)],
+ highlight_strokes[highlight_group]));
+ next_start_group = (highlight_group + 1) % 8;
+ break;
+ }
+ }
+ }}
+ }
+ };
+ }
+ }}}}}}
+
+ // React to clicks
+ // TODO: Maybe polymorphism/enum impl wouldn't be a bad idea here
+ if painter_response.clicked_by(PointerButton::Primary) {
+ // println!("primary click");
+ if CursorMode::ProbeAndMark == self.cursor_mode {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ if let Some(coords) = self.get_coords(pos) {
+ if let Some(game) = &mut self.game {
+ if game.state() != GameState::Victory && game.state() != GameState::Loss {
+ match game.probe_at(coords, self.probe_marked) {
+ GameState::Victory | GameState::Loss => {
+ self.end_time = Some(SystemTime::now());
+ },
+ GameState::Running => {}
+ }
+ }
+ } else {
+ self.start(coords);
+ }
+ }
+ }
+ } else if self.cursor_mode == CursorMode::Highlighter {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ if let Some(coords) = self.get_coords(pos) {
+ if let Some(game) = &mut self.game {
+ game.highlight_at(coords, self.selected_highlighters, true);
+ }
+ }
+ }
+ }
+ }
+ if painter_response.clicked_by(PointerButton::Secondary) {
+ // println!("secondary click");
+ if CursorMode::ProbeAndMark == self.cursor_mode {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ if let Some(coords) = self.get_coords(pos) {
+ if let Some(game) = &mut self.game {
+ if game.state() != GameState::Victory && game.state() != GameState::Loss {
+ game.mark_at(coords);
+ }
+ }
+ }
+ }
+ } else if self.cursor_mode == CursorMode::Highlighter {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ if let Some(coords) = self.get_coords(pos) {
+ if let Some(game) = &mut self.game {
+ game.highlight_at(coords, self.selected_highlighters, false);
+ }
+ }
+ }
+ }
+ }
+ if painter_response.dragged() {
+ if ui.input(|i| i.pointer.button_down(PointerButton::Middle)) {
+ //println!("dragged");
+ self.view_origin += painter_response.drag_delta();
+ } else if ui.input(|i| i.pointer.button_down(PointerButton::Primary)) {
+ if self.cursor_mode == CursorMode::Highlighter {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ if let Some(coords) = self.get_coords(pos) {
+ if let Some(game) = &mut self.game {
+ game.highlight_at(coords, self.selected_highlighters, true);
+ }
+ }
+ }
+ }
+ } else if ui.input(|i| i.pointer.button_down(PointerButton::Secondary)) {
+ if self.cursor_mode == CursorMode::Highlighter {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ if let Some(coords) = self.get_coords(pos) {
+ if let Some(game) = &mut self.game {
+ game.highlight_at(coords, self.selected_highlighters, false);
+ }
+ }
+ }
+ }
+ }
+ }
+ // Zoom/unzoom
+ if painter_response.hovered() {
+ let delta = ctx.input(|i| i.scroll_delta);
+ //println!("{:?}", delta.y);
+ if delta.y > 0.0 && (self.zoom_factor < 5.0 || self.unlimited_zoom) {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ let old_factor = self.zoom_factor;
+ self.zoom_factor *= 1.5;
+ self.view_origin.x -= ((pos.x - self.view_origin.x) / old_factor) * (self.zoom_factor - old_factor);
+ self.view_origin.y -= ((pos.y - self.view_origin.y) / old_factor) * (self.zoom_factor - old_factor);
+ }
+ } else if delta.y < 0.0 && (self.zoom_factor > 0.01 || self.unlimited_zoom) {
+ if let Some(pos) = ctx.pointer_interact_pos() {
+ let old_factor = self.zoom_factor;
+ self.zoom_factor /= 1.5;
+ self.view_origin.x -= ((pos.x - self.view_origin.x) / old_factor) * (self.zoom_factor - old_factor);
+ self.view_origin.y -= ((pos.y - self.view_origin.y) / old_factor) * (self.zoom_factor - old_factor);
+ }
+ }
+ }
+ // Keyboard Shortcuts
+ // The check below is to prevent triggering when trying to type
+ // the seed in the new game window. It's a bit crude, but it works.
+ if !self.new_game_window_enabled {
+ // TODO: `consume_shortcut` instead of `key_pressed` would allow for more flexibility,
+ // but `consume_shortcut` doesn't allow indeterminate states for modifiers (at least currently)
+ if ui.input_mut(|i| i.key_pressed(self.shortcuts.probe_mark_shortcut.key)) {
+ self.try_set_cursor(CursorMode::ProbeAndMark);
+ }
+ if ui.input_mut(|i| i.key_pressed(self.shortcuts.highlighter_shortcut.key)) {
+ self.try_set_cursor(CursorMode::Highlighter);
+ }
+ for ii in 0..8 {
+ if ui.input_mut(|i| i.key_pressed(self.shortcuts.highlight_group_shortcuts[ii].key)) {
+ self.selected_highlighters ^= 1 << ii;
+ }
+ }
+
+ if ui.input_mut(|i| i.key_pressed(self.shortcuts.reset_view_shortcut.key)) {
+ self.reset_view();
+ }
+ if ui.input_mut(|i| i.key_pressed(self.shortcuts.zoom_to_fit_shortcut.key)) {
+ self.zoom_to_fit(ctx.screen_rect().max);
+ }
+ }
+ });
+ }
+}