#![doc = include_str!("../README.md")]
use egui::{
Align2, Color32, CornerRadius, FontId, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
Vec2, Widget,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CellState {
Hidden,
Revealed,
Flagged,
}
#[derive(Clone, Debug)]
pub struct Cell {
pub is_mine: bool,
pub state: CellState,
pub adjacent_mines: u8,
}
impl Default for Cell {
fn default() -> Self {
Self {
is_mine: false,
state: CellState::Hidden,
adjacent_mines: 0,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GameStatus {
Playing,
Won,
Lost,
}
pub struct MinesweeperGame {
pub width: usize,
pub height: usize,
pub mines: usize,
pub cells: Vec<Cell>,
pub status: GameStatus,
initialized: bool,
}
impl MinesweeperGame {
pub fn new(width: usize, height: usize, mines: usize) -> Self {
assert!(width > 0 && height > 0);
let mines = mines.min(width * height - 1);
Self {
width,
height,
mines,
cells: vec![Cell::default(); width * height],
status: GameStatus::Playing,
initialized: false,
}
}
pub fn reset(&mut self) {
*self = Self::new(self.width, self.height, self.mines);
}
pub fn flags_placed(&self) -> usize {
self.cells
.iter()
.filter(|c| c.state == CellState::Flagged)
.count()
}
#[inline]
fn idx(&self, x: usize, y: usize) -> usize {
y * self.width + x
}
fn neighbors(&self, x: usize, y: usize) -> Vec<(usize, usize)> {
let mut out = Vec::with_capacity(8);
for dy in -1i32..=1 {
for dx in -1i32..=1 {
if dx == 0 && dy == 0 {
continue;
}
let nx = x as i32 + dx;
let ny = y as i32 + dy;
if nx >= 0 && ny >= 0 && nx < self.width as i32 && ny < self.height as i32 {
out.push((nx as usize, ny as usize));
}
}
}
out
}
fn initialize(&mut self, safe_x: usize, safe_y: usize) {
let safe = self.idx(safe_x, safe_y);
let mut positions: Vec<usize> = (0..self.width * self.height)
.filter(|&i| i != safe)
.collect();
let mines = self.mines.min(positions.len());
for i in 0..mines {
let j = i + fastrand::usize(0..(positions.len() - i));
positions.swap(i, j);
}
for &pos in &positions[..mines] {
self.cells[pos].is_mine = true;
}
for y in 0..self.height {
for x in 0..self.width {
if !self.cells[self.idx(x, y)].is_mine {
let count = self
.neighbors(x, y)
.iter()
.filter(|&&(nx, ny)| self.cells[self.idx(nx, ny)].is_mine)
.count();
let idx = self.idx(x, y);
self.cells[idx].adjacent_mines = count as u8;
}
}
}
self.initialized = true;
}
fn check_win(&mut self) {
let all_safe_revealed = self
.cells
.iter()
.all(|c| c.is_mine || c.state == CellState::Revealed);
if all_safe_revealed {
self.status = GameStatus::Won;
}
}
pub fn reveal(&mut self, x: usize, y: usize) {
if self.status != GameStatus::Playing {
return;
}
if !self.initialized {
self.initialize(x, y);
}
let mut stack = vec![(x, y)];
while let Some((cx, cy)) = stack.pop() {
let idx = self.idx(cx, cy);
if self.cells[idx].state != CellState::Hidden {
continue;
}
self.cells[idx].state = CellState::Revealed;
if self.cells[idx].is_mine {
self.status = GameStatus::Lost;
for cell in &mut self.cells {
if cell.is_mine {
cell.state = CellState::Revealed;
}
}
return;
}
if self.cells[idx].adjacent_mines == 0 {
for neighbor in self.neighbors(cx, cy) {
stack.push(neighbor);
}
}
}
self.check_win();
}
pub fn toggle_flag(&mut self, x: usize, y: usize) {
if self.status != GameStatus::Playing {
return;
}
let idx = self.idx(x, y);
match self.cells[idx].state {
CellState::Hidden => self.cells[idx].state = CellState::Flagged,
CellState::Flagged => self.cells[idx].state = CellState::Hidden,
CellState::Revealed => {}
}
}
}
pub struct MinesweeperWidget<'a> {
game: &'a mut MinesweeperGame,
cell_size: Option<f32>,
}
impl<'a> MinesweeperWidget<'a> {
pub fn new(game: &'a mut MinesweeperGame) -> Self {
Self {
game,
cell_size: None,
}
}
pub fn cell_size(mut self, size: f32) -> Self {
self.cell_size = Some(size);
self
}
}
fn number_color(n: u8) -> Color32 {
match n {
1 => Color32::from_rgb(0, 0, 255),
2 => Color32::from_rgb(0, 128, 0),
3 => Color32::from_rgb(200, 0, 0),
4 => Color32::from_rgb(0, 0, 128),
5 => Color32::from_rgb(128, 0, 0),
6 => Color32::from_rgb(0, 128, 128),
7 => Color32::BLACK,
_ => Color32::DARK_GRAY,
}
}
fn draw_cell(painter: &egui::Painter, rect: Rect, cell: &Cell, cell_size: f32) {
let inner = rect.shrink(1.0);
let rounding = CornerRadius::same(2);
match cell.state {
CellState::Hidden => {
painter.rect_filled(inner, rounding, Color32::from_rgb(192, 192, 192));
let tl = inner.left_top();
let tr = inner.right_top();
let bl = inner.left_bottom();
let br = inner.right_bottom();
let highlight = Color32::WHITE;
let shadow = Color32::from_rgb(100, 100, 100);
let w = 2.0;
painter.line_segment([tl, tr], Stroke::new(w, highlight));
painter.line_segment([tl, bl], Stroke::new(w, highlight));
painter.line_segment([tr, br], Stroke::new(w, shadow));
painter.line_segment([bl, br], Stroke::new(w, shadow));
}
CellState::Flagged => {
painter.rect_filled(inner, rounding, Color32::from_rgb(192, 192, 192));
let tl = inner.left_top();
let tr = inner.right_top();
let bl = inner.left_bottom();
let br = inner.right_bottom();
let highlight = Color32::WHITE;
let shadow = Color32::from_rgb(100, 100, 100);
let w = 2.0;
painter.line_segment([tl, tr], Stroke::new(w, highlight));
painter.line_segment([tl, bl], Stroke::new(w, highlight));
painter.line_segment([tr, br], Stroke::new(w, shadow));
painter.line_segment([bl, br], Stroke::new(w, shadow));
let cx = rect.center().x;
let top = inner.min.y + cell_size * 0.15;
let mid = inner.min.y + cell_size * 0.55;
let bot = inner.max.y - cell_size * 0.15;
painter.line_segment(
[Pos2::new(cx, top), Pos2::new(cx, bot)],
Stroke::new(2.0, Color32::BLACK),
);
let flag_pts = vec![
Pos2::new(cx, top),
Pos2::new(cx + cell_size * 0.35, (top + mid) / 2.0),
Pos2::new(cx, mid),
];
painter.add(egui::Shape::convex_polygon(
flag_pts,
Color32::RED,
Stroke::NONE,
));
}
CellState::Revealed => {
if cell.is_mine {
painter.rect_filled(inner, CornerRadius::ZERO, Color32::from_rgb(255, 80, 80));
let c = rect.center();
let r = cell_size * 0.22;
painter.circle_filled(c, r, Color32::BLACK);
for i in 0..8u32 {
let angle = i as f32 * std::f32::consts::TAU / 8.0;
let inner_pt = c + Vec2::new(angle.cos(), angle.sin()) * r;
let outer_pt = c + Vec2::new(angle.cos(), angle.sin()) * (r * 1.7);
painter.line_segment([inner_pt, outer_pt], Stroke::new(2.0, Color32::BLACK));
}
painter.circle_filled(c + Vec2::new(-r * 0.3, -r * 0.3), r * 0.25, Color32::WHITE);
} else {
painter.rect_filled(inner, CornerRadius::ZERO, Color32::from_rgb(210, 210, 210));
painter.rect_stroke(
inner,
CornerRadius::ZERO,
Stroke::new(0.5, Color32::GRAY),
StrokeKind::Inside,
);
if cell.adjacent_mines > 0 {
painter.text(
rect.center(),
Align2::CENTER_CENTER,
cell.adjacent_mines.to_string(),
FontId::monospace(cell_size * 0.58),
number_color(cell.adjacent_mines),
);
}
}
}
}
}
impl Widget for MinesweeperWidget<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let cell_size = self.cell_size.unwrap_or_else(|| {
let available = ui.available_size();
let by_width = available.x / self.game.width as f32;
let by_height = available.y / self.game.height as f32;
by_width.min(by_height).max(1.0)
});
let total = Vec2::new(self.game.width as f32, self.game.height as f32) * cell_size;
let (response, painter) = ui.allocate_painter(total, Sense::click());
let origin = response.rect.min;
if (response.clicked() || response.secondary_clicked())
&& self.game.status == GameStatus::Playing
{
if let Some(pos) = response.interact_pointer_pos() {
let local = pos - origin;
let cx = (local.x / cell_size).floor() as usize;
let cy = (local.y / cell_size).floor() as usize;
if cx < self.game.width && cy < self.game.height {
if response.clicked() {
self.game.reveal(cx, cy);
} else {
self.game.toggle_flag(cx, cy);
}
}
}
}
for y in 0..self.game.height {
for x in 0..self.game.width {
let cell_rect = Rect::from_min_size(
origin + Vec2::new(x as f32, y as f32) * cell_size,
Vec2::splat(cell_size),
);
let cell = &self.game.cells[y * self.game.width + x];
draw_cell(&painter, cell_rect, cell, cell_size);
}
}
response
}
}