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