paiagram/interface/tabs/
minesweeper.rs

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        // Simple random mine generation (not optimized)
120        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; // Boom
148        }
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 this cell is empty, reveal all neighbors
156        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                    // Reveal all mines on game over
214                    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}