1use super::Tab;
2use bevy::prelude::*;
3use egui::{Rect, Ui, Vec2};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(PartialEq, Debug, Clone, Copy, Serialize, Deserialize)]
8pub struct MinesweeperTab;
9
10impl Tab for MinesweeperTab {
11 const NAME: &'static str = "Minesweeper";
12 fn main_display(&mut self, world: &mut bevy::ecs::world::World, ui: &mut Ui) {
13 if let Err(e) = world.run_system_cached_with(show_minesweeper, ui) {
14 bevy::log::error!("UI Error while displaying minesweeper page: {}", e)
15 }
16 }
17 fn edit_display(&mut self, world: &mut bevy::ecs::world::World, ui: &mut Ui) {
18 ui.heading("Minesweeper");
19 ui.label("Tired of trains? Here's a minesweeper!");
20 ui.add_space(10.0);
21 let time = world.resource::<Time>().elapsed();
22 let mut data = world.resource_mut::<MinesweeperData>();
23 const SEGMENT_SPACING: f32 = 5.0;
24 let width = (ui.available_width() - SEGMENT_SPACING - SEGMENT_SPACING) / 3.0;
25 ui.horizontal(|ui| {
26 ui.spacing_mut().item_spacing.x = SEGMENT_SPACING;
27 if ui
28 .add_sized([width, 30.0], egui::Button::new("Easy"))
29 .clicked()
30 {
31 data.map.initialize(MinesweeperDifficulty::Easy);
32 data.started = true;
33 data.game_over = false;
34 data.won = false;
35 data.start_time = time;
36 data.elapsed = Duration::ZERO;
37 }
38 if ui
39 .add_sized([width, 30.0], egui::Button::new("Medium"))
40 .clicked()
41 {
42 data.map.initialize(MinesweeperDifficulty::Medium);
43 data.started = true;
44 data.game_over = false;
45 data.won = false;
46 data.start_time = time;
47 data.elapsed = Duration::ZERO;
48 }
49 if ui
50 .add_sized([width, 30.0], egui::Button::new("Hard"))
51 .clicked()
52 {
53 data.map.initialize(MinesweeperDifficulty::Hard);
54 data.started = true;
55 data.game_over = false;
56 data.won = false;
57 data.start_time = time;
58 data.elapsed = Duration::ZERO;
59 }
60 });
61 ui.label(format!("Elapsed: {:.0}s", data.elapsed.as_secs_f32()));
62 ui.ctx().request_repaint_after(Duration::from_millis(100));
63 ui.heading("High scores");
64 for (i, record) in data.record.iter().enumerate() {
65 ui.label(format!("{}. {:.2}s", i + 1, record.as_secs_f32()));
66 }
67 }
68}
69
70const MINE_STR: &str = "💣";
71const FLAG_STR: &str = "🚩";
72
73enum MinesweeperDifficulty {
74 Easy,
75 Medium,
76 Hard,
77}
78
79impl MinesweeperDifficulty {
80 fn parameters(&self) -> (u8, u8, u8) {
81 match self {
82 MinesweeperDifficulty::Easy => (9, 9, 10),
83 MinesweeperDifficulty::Medium => (16, 16, 40),
84 MinesweeperDifficulty::Hard => (30, 16, 99),
85 }
86 }
87}
88
89#[derive(Resource, Default)]
90pub struct MinesweeperData {
91 map: MinesweeperMap,
92 started: bool,
93 game_over: bool,
94 won: bool,
95 record: Vec<Duration>,
96 start_time: Duration,
97 elapsed: Duration,
98}
99
100#[derive(Default)]
101struct MinesweeperMap {
102 width: u8,
103 height: u8,
104 mines: Vec<(u8, u8)>,
105 revealed: Vec<(u8, u8)>,
106 flagged: Vec<(u8, u8)>,
107}
108
109impl MinesweeperMap {
110 fn initialize(&mut self, difficulty: MinesweeperDifficulty) -> bool {
111 let (width, height, mines) = difficulty.parameters();
112 self.width = width;
113 self.height = height;
114 self.mines.clear();
115 self.revealed.clear();
116 self.flagged.clear();
117 self.generate_mines(mines)
118 }
119 fn generate_mines(&mut self, mine_count: u8) -> bool {
120 use rand::Rng;
122 let mut rng = rand::rng();
123 if self.width as u16 * self.height as u16 <= mine_count as u16 {
124 return false;
125 }
126 while self.mines.len() < mine_count as usize {
127 let x = rng.random_range(0..self.width);
128 let y = rng.random_range(0..self.height);
129 if !self.mines.contains(&(x, y)) {
130 self.mines.push((x, y));
131 }
132 }
133 true
134 }
135 fn nearby_mines(&self, x: u8, y: u8) -> u8 {
136 let mut count = 0;
137 for xi in x.saturating_sub(1)..=(x + 1).min(self.width - 1) {
138 for yi in y.saturating_sub(1)..=(y + 1).min(self.height - 1) {
139 if self.mines.contains(&(xi, yi)) {
140 count += 1;
141 }
142 }
143 }
144 count
145 }
146 fn reveal(&mut self, x: u8, y: u8) -> bool {
147 if self.mines.contains(&(x, y)) {
148 return true; }
150 if self.revealed.contains(&(x, y)) || self.flagged.contains(&(x, y)) {
151 return false;
152 }
153
154 self.revealed.push((x, y));
155
156 if self.nearby_mines(x, y) == 0 {
158 for xi in x.saturating_sub(1)..=(x + 1).min(self.width - 1) {
159 for yi in y.saturating_sub(1)..=(y + 1).min(self.height - 1) {
160 self.reveal(xi, yi);
161 }
162 }
163 }
164 false
165 }
166 fn flag(&mut self, x: u8, y: u8) {
167 if self.revealed.contains(&(x, y)) {
168 return;
169 }
170 let i = self.flagged.iter().position(|&p| p == (x, y));
171 if let Some(i) = i {
172 self.flagged.remove(i);
173 } else {
174 self.flagged.push((x, y));
175 }
176 }
177}
178
179fn show_minesweeper(InMut(ui): InMut<Ui>, mut data: ResMut<MinesweeperData>, time: Res<Time>) {
180 if !data.started {
181 ui.centered_and_justified(|ui| ui.heading("Start from assistance panel"));
182 return;
183 }
184
185 if !data.game_over {
186 data.elapsed = time.elapsed().saturating_sub(data.start_time);
187 }
188
189 fn show_mine(x: u8, y: u8, ui: &mut Ui, data: &mut MinesweeperData) {
190 let is_revealed = data.map.revealed.contains(&(x, y));
191 let is_flagged = data.map.flagged.contains(&(x, y));
192 let is_mine = data.map.mines.contains(&(x, y));
193
194 let mut text = egui::WidgetText::from(" ");
195 if is_revealed {
196 if is_mine {
197 text = MINE_STR.into();
198 } else {
199 let count = data.map.nearby_mines(x, y);
200 if count > 0 {
201 text = count.to_string().into();
202 }
203 }
204 } else if is_flagged {
205 text = FLAG_STR.into();
206 }
207
208 let button = ui.add_sized([25.0, 25.0], egui::Button::new(text).selected(is_revealed));
209
210 if !data.game_over {
211 if button.clicked() && !is_flagged {
212 if data.map.reveal(x, y) {
213 data.game_over = true;
214 let mines = data.map.mines.clone();
216 data.map.revealed.extend_from_slice(&mines);
217 data.map.revealed.sort_unstable();
218 data.map.revealed.dedup();
219 }
220 }
221 if button.secondary_clicked() {
222 data.map.flag(x, y);
223 }
224 }
225 }
226
227 ui.spacing_mut().item_spacing = Vec2 { x: 2.0, y: 2.0 };
228 ui.spacing_mut().interact_size = Vec2::ZERO;
229 let h_space = (ui.available_width() - data.map.width as f32 * 30.0 + 5.0) / 2.0;
230 let v_space = (ui.available_height() - data.map.height as f32 * 30.0 + 5.0) / 2.0;
231 let max_rect = ui.max_rect();
232 ui.add_space(v_space.max(0.0));
233 ui.horizontal(|ui| {
234 ui.add_space(h_space.max(0.0));
235 egui::Grid::new("minesweeper_grid")
236 .spacing(Vec2 { x: 5.0, y: 5.0 })
237 .show(ui, |ui| {
238 for y in 0..data.map.height {
239 for x in 0..data.map.width {
240 show_mine(x, y, ui, &mut data);
241 }
242 ui.end_row();
243 }
244 });
245 let total_cells = (data.map.width as usize) * (data.map.height as usize);
246 let revealed_cells = data.map.revealed.len();
247 let mine_count = data.map.mines.len();
248 if !data.won && revealed_cells + mine_count == total_cells {
249 data.won = true;
250 data.game_over = true;
251 let elapsed = data.elapsed;
252 data.record.push(elapsed);
253 data.record.sort();
254 }
255 });
256 if data.won {
257 ui.place(
258 Rect::from_pos(max_rect.center()).expand2(Vec2 { x: 80.0, y: 20.0 }),
259 |ui: &mut Ui| {
260 ui.painter().rect(
261 ui.max_rect(),
262 5.0,
263 ui.visuals().widgets.active.bg_fill,
264 ui.visuals().widgets.active.bg_stroke,
265 egui::StrokeKind::Middle,
266 );
267 ui.centered_and_justified(|ui: &mut Ui| ui.heading("You won!"))
268 .response
269 },
270 );
271 }
272}