1#![doc = include_str!("../README.md")]
2
3use egui::{
4 Align2, Color32, CornerRadius, FontId, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
5 Vec2, Widget,
6};
7
8#[derive(Clone, Copy, PartialEq, Eq, Debug)]
12pub enum CellState {
13 Hidden,
15 Revealed,
17 Flagged,
19}
20
21#[derive(Clone, Debug)]
23pub struct Cell {
24 pub is_mine: bool,
26 pub state: CellState,
28 pub adjacent_mines: u8,
30}
31
32impl Default for Cell {
33 fn default() -> Self {
34 Self {
35 is_mine: false,
36 state: CellState::Hidden,
37 adjacent_mines: 0,
38 }
39 }
40}
41
42#[derive(Clone, Copy, PartialEq, Eq, Debug)]
44pub enum GameStatus {
45 Playing,
47 Won,
49 Lost,
51}
52
53pub struct MinesweeperGame {
61 pub width: usize,
63 pub height: usize,
65 pub mines: usize,
67 pub cells: Vec<Cell>,
69 pub status: GameStatus,
71 initialized: bool,
72}
73
74impl MinesweeperGame {
75 pub fn new(width: usize, height: usize, mines: usize) -> Self {
78 assert!(width > 0 && height > 0);
79 let mines = mines.min(width * height - 1);
80 Self {
81 width,
82 height,
83 mines,
84 cells: vec![Cell::default(); width * height],
85 status: GameStatus::Playing,
86 initialized: false,
87 }
88 }
89
90 pub fn reset(&mut self) {
92 *self = Self::new(self.width, self.height, self.mines);
93 }
94
95 pub fn flags_placed(&self) -> usize {
97 self.cells
98 .iter()
99 .filter(|c| c.state == CellState::Flagged)
100 .count()
101 }
102
103 #[inline]
106 fn idx(&self, x: usize, y: usize) -> usize {
107 y * self.width + x
108 }
109
110 fn neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize)> {
111 let mut out = Vec::with_capacity(8);
112 for dy in -1i32..=1 {
113 for dx in -1i32..=1 {
114 if dx == 0 && dy == 0 {
115 continue;
116 }
117 let nx = x as i32 + dx;
118 let ny = y as i32 + dy;
119 if nx >= 0 && ny >= 0 && nx < self.width as i32 && ny < self.height as i32 {
120 out.push((nx as usize, ny as usize));
121 }
122 }
123 }
124 out
125 }
126
127 fn initialize(&mut self, safe_x: usize, safe_y: usize) {
128 let safe = self.idx(safe_x, safe_y);
129
130 let mut positions: Vec<usize> = (0..self.width * self.height)
132 .filter(|&i| i != safe)
133 .collect();
134
135 let mines = self.mines.min(positions.len());
137 for i in 0..mines {
138 let j = i + fastrand::usize(0..(positions.len() - i));
139 positions.swap(i, j);
140 }
141
142 for &pos in &positions[..mines] {
143 self.cells[pos].is_mine = true;
144 }
145
146 for y in 0..self.height {
148 for x in 0..self.width {
149 if !self.cells[self.idx(x, y)].is_mine {
150 let count = self
151 .neighbors(x, y)
152 .iter()
153 .filter(|&&(nx, ny)| self.cells[self.idx(nx, ny)].is_mine)
154 .count();
155 let idx = self.idx(x, y);
156 self.cells[idx].adjacent_mines = count as u8;
157 }
158 }
159 }
160
161 self.initialized = true;
162 }
163
164 fn check_win(&mut self) {
165 let all_safe_revealed = self
166 .cells
167 .iter()
168 .all(|c| c.is_mine || c.state == CellState::Revealed);
169 if all_safe_revealed {
170 self.status = GameStatus::Won;
171 }
172 }
173
174 pub fn reveal(&mut self, x: usize, y: usize) {
178 if self.status != GameStatus::Playing {
179 return;
180 }
181 if !self.initialized {
182 self.initialize(x, y);
183 }
184
185 let mut stack = vec![(x, y)];
187 while let Some((cx, cy)) = stack.pop() {
188 let idx = self.idx(cx, cy);
189 if self.cells[idx].state != CellState::Hidden {
190 continue;
191 }
192 self.cells[idx].state = CellState::Revealed;
193
194 if self.cells[idx].is_mine {
195 self.status = GameStatus::Lost;
196 for cell in &mut self.cells {
198 if cell.is_mine {
199 cell.state = CellState::Revealed;
200 }
201 }
202 return;
203 }
204
205 if self.cells[idx].adjacent_mines == 0 {
206 for neighbor in self.neighbors(cx, cy) {
207 stack.push(neighbor);
208 }
209 }
210 }
211
212 self.check_win();
213 }
214
215 pub fn toggle_flag(&mut self, x: usize, y: usize) {
217 if self.status != GameStatus::Playing {
218 return;
219 }
220 let idx = self.idx(x, y);
221 match self.cells[idx].state {
222 CellState::Hidden => self.cells[idx].state = CellState::Flagged,
223 CellState::Flagged => self.cells[idx].state = CellState::Hidden,
224 CellState::Revealed => {}
225 }
226 }
227}
228
229pub struct MinesweeperWidget<'a> {
239 game: &'a mut MinesweeperGame,
240 cell_size: Option<f32>,
241}
242
243impl<'a> MinesweeperWidget<'a> {
244 pub fn new(game: &'a mut MinesweeperGame) -> Self {
245 Self {
246 game,
247 cell_size: None,
248 }
249 }
250
251 pub fn cell_size(mut self, size: f32) -> Self {
255 self.cell_size = Some(size);
256 self
257 }
258}
259
260fn number_color(n: u8) -> Color32 {
261 match n {
262 1 => Color32::from_rgb(0, 0, 255),
263 2 => Color32::from_rgb(0, 128, 0),
264 3 => Color32::from_rgb(200, 0, 0),
265 4 => Color32::from_rgb(0, 0, 128),
266 5 => Color32::from_rgb(128, 0, 0),
267 6 => Color32::from_rgb(0, 128, 128),
268 7 => Color32::BLACK,
269 _ => Color32::DARK_GRAY,
270 }
271}
272
273fn draw_cell(painter: &egui::Painter, rect: Rect, cell: &Cell, cell_size: f32) {
274 let inner = rect.shrink(1.0);
275 let rounding = CornerRadius::same(2);
276
277 match cell.state {
278 CellState::Hidden => {
279 painter.rect_filled(inner, rounding, Color32::from_rgb(192, 192, 192));
281 let tl = inner.left_top();
283 let tr = inner.right_top();
284 let bl = inner.left_bottom();
285 let br = inner.right_bottom();
286 let highlight = Color32::WHITE;
287 let shadow = Color32::from_rgb(100, 100, 100);
288 let w = 2.0;
289 painter.line_segment([tl, tr], Stroke::new(w, highlight));
290 painter.line_segment([tl, bl], Stroke::new(w, highlight));
291 painter.line_segment([tr, br], Stroke::new(w, shadow));
292 painter.line_segment([bl, br], Stroke::new(w, shadow));
293 }
294 CellState::Flagged => {
295 painter.rect_filled(inner, rounding, Color32::from_rgb(192, 192, 192));
296 let tl = inner.left_top();
297 let tr = inner.right_top();
298 let bl = inner.left_bottom();
299 let br = inner.right_bottom();
300 let highlight = Color32::WHITE;
301 let shadow = Color32::from_rgb(100, 100, 100);
302 let w = 2.0;
303 painter.line_segment([tl, tr], Stroke::new(w, highlight));
304 painter.line_segment([tl, bl], Stroke::new(w, highlight));
305 painter.line_segment([tr, br], Stroke::new(w, shadow));
306 painter.line_segment([bl, br], Stroke::new(w, shadow));
307 let cx = rect.center().x;
309 let top = inner.min.y + cell_size * 0.15;
310 let mid = inner.min.y + cell_size * 0.55;
311 let bot = inner.max.y - cell_size * 0.15;
312 painter.line_segment(
314 [Pos2::new(cx, top), Pos2::new(cx, bot)],
315 Stroke::new(2.0, Color32::BLACK),
316 );
317 let flag_pts = vec![
319 Pos2::new(cx, top),
320 Pos2::new(cx + cell_size * 0.35, (top + mid) / 2.0),
321 Pos2::new(cx, mid),
322 ];
323 painter.add(egui::Shape::convex_polygon(
324 flag_pts,
325 Color32::RED,
326 Stroke::NONE,
327 ));
328 }
329 CellState::Revealed => {
330 if cell.is_mine {
331 painter.rect_filled(inner, CornerRadius::ZERO, Color32::from_rgb(255, 80, 80));
332 let c = rect.center();
334 let r = cell_size * 0.22;
335 painter.circle_filled(c, r, Color32::BLACK);
336 for i in 0..8u32 {
338 let angle = i as f32 * std::f32::consts::TAU / 8.0;
339 let inner_pt = c + Vec2::new(angle.cos(), angle.sin()) * r;
340 let outer_pt = c + Vec2::new(angle.cos(), angle.sin()) * (r * 1.7);
341 painter.line_segment([inner_pt, outer_pt], Stroke::new(2.0, Color32::BLACK));
342 }
343 painter.circle_filled(c + Vec2::new(-r * 0.3, -r * 0.3), r * 0.25, Color32::WHITE);
345 } else {
346 painter.rect_filled(inner, CornerRadius::ZERO, Color32::from_rgb(210, 210, 210));
347 painter.rect_stroke(
348 inner,
349 CornerRadius::ZERO,
350 Stroke::new(0.5, Color32::GRAY),
351 StrokeKind::Inside,
352 );
353 if cell.adjacent_mines > 0 {
354 painter.text(
355 rect.center(),
356 Align2::CENTER_CENTER,
357 cell.adjacent_mines.to_string(),
358 FontId::monospace(cell_size * 0.58),
359 number_color(cell.adjacent_mines),
360 );
361 }
362 }
363 }
364 }
365}
366
367impl Widget for MinesweeperWidget<'_> {
368 fn ui(self, ui: &mut Ui) -> Response {
369 let cell_size = self.cell_size.unwrap_or_else(|| {
370 let available = ui.available_size();
371 let by_width = available.x / self.game.width as f32;
372 let by_height = available.y / self.game.height as f32;
373 by_width.min(by_height).max(1.0)
374 });
375
376 let total = Vec2::new(self.game.width as f32, self.game.height as f32) * cell_size;
377
378 let (response, painter) = ui.allocate_painter(total, Sense::click());
379 let origin = response.rect.min;
380
381 if (response.clicked() || response.secondary_clicked())
383 && self.game.status == GameStatus::Playing
384 {
385 if let Some(pos) = response.interact_pointer_pos() {
386 let local = pos - origin;
387 let cx = (local.x / cell_size).floor() as usize;
388 let cy = (local.y / cell_size).floor() as usize;
389 if cx < self.game.width && cy < self.game.height {
390 if response.clicked() {
391 self.game.reveal(cx, cy);
392 } else {
393 self.game.toggle_flag(cx, cy);
394 }
395 }
396 }
397 }
398
399 for y in 0..self.game.height {
401 for x in 0..self.game.width {
402 let cell_rect = Rect::from_min_size(
403 origin + Vec2::new(x as f32, y as f32) * cell_size,
404 Vec2::splat(cell_size),
405 );
406 let cell = &self.game.cells[y * self.game.width + x];
407 draw_cell(&painter, cell_rect, cell, cell_size);
408 }
409 }
410
411 response
412 }
413}