Last active
February 10, 2025 15:49
-
-
Save MikuroXina/cc1d89090638acc6a5ba791bb71c54e8 to your computer and use it in GitHub Desktop.
An object-oriented mine sweeper implementation with Rust.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// Design reference: https://zenn.dev/yuhi_junior/articles/062cf4f30b083d | |
use itertools::Itertools; | |
use std::cell::RefCell; | |
use std::rc::Rc; | |
#[derive(Debug, Clone, PartialEq, Eq)] | |
pub struct Cell { | |
has_bomb: bool, | |
flagged: bool, | |
revealed: bool, | |
} | |
impl Cell { | |
pub fn new(has_bomb: bool) -> Self { | |
Self { | |
has_bomb, | |
flagged: false, | |
revealed: false, | |
} | |
} | |
} | |
#[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
pub enum RevealResult { | |
Safe, | |
Exploded, | |
Prevented, | |
} | |
pub trait Touchable { | |
fn has_bomb(&self) -> bool; | |
fn is_flagged(&self) -> bool; | |
fn is_revealed(&self) -> bool; | |
fn toggle_flag(&mut self); | |
fn reveal(&mut self) -> RevealResult; | |
} | |
impl<T: Touchable> Touchable for Rc<RefCell<T>> { | |
fn has_bomb(&self) -> bool { | |
self.borrow().has_bomb() | |
} | |
fn is_flagged(&self) -> bool { | |
self.borrow().is_flagged() | |
} | |
fn is_revealed(&self) -> bool { | |
self.borrow().is_revealed() | |
} | |
fn toggle_flag(&mut self) { | |
self.borrow_mut().toggle_flag(); | |
} | |
fn reveal(&mut self) -> RevealResult { | |
self.borrow_mut().reveal() | |
} | |
} | |
impl Touchable for Cell { | |
fn has_bomb(&self) -> bool { | |
self.has_bomb | |
} | |
fn is_flagged(&self) -> bool { | |
self.flagged | |
} | |
fn is_revealed(&self) -> bool { | |
self.revealed | |
} | |
fn toggle_flag(&mut self) { | |
self.flagged = !self.flagged; | |
} | |
fn reveal(&mut self) -> RevealResult { | |
if self.flagged { | |
return RevealResult::Prevented; | |
} | |
self.revealed = true; | |
if self.has_bomb { | |
RevealResult::Exploded | |
} else { | |
RevealResult::Safe | |
} | |
} | |
} | |
#[derive(Debug, Clone)] | |
pub struct WithNeighbors<T> { | |
focus: T, | |
neighbors: Vec<WithNeighbors<T>>, | |
} | |
impl<T> WithNeighbors<T> { | |
pub fn new(focus: T) -> Self { | |
Self { | |
focus, | |
neighbors: vec![], | |
} | |
} | |
pub fn focus(&self) -> &T { | |
&self.focus | |
} | |
pub fn focus_mut(&mut self) -> &mut T { | |
&mut self.focus | |
} | |
pub fn push_neighbor(&mut self, item: WithNeighbors<T>) { | |
self.neighbors.push(item); | |
} | |
} | |
impl<T: Touchable> WithNeighbors<T> { | |
pub fn neighbors_bomb_count(&self) -> usize { | |
self.neighbors | |
.iter() | |
.filter(|neighbor| neighbor.focus().has_bomb()) | |
.count() | |
} | |
pub fn reveal_also_neighbors(&mut self) -> RevealResult { | |
if let end @ (RevealResult::Exploded | RevealResult::Prevented) = self.focus.reveal() { | |
return end; | |
} | |
if self.neighbors_bomb_count() > 0 { | |
return RevealResult::Safe; | |
} | |
for neighbor in &mut self.neighbors { | |
if !neighbor.focus().is_revealed() { | |
neighbor.reveal_also_neighbors(); | |
} | |
} | |
RevealResult::Safe | |
} | |
} | |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
pub struct Position { | |
pub x: usize, | |
pub y: usize, | |
} | |
#[derive(Debug)] | |
pub struct WithGrid<T> { | |
cells: Vec<Vec<WithNeighbors<T>>>, | |
remaining_bombs: usize, | |
} | |
impl<T> WithGrid<T> { | |
pub fn width(&self) -> usize { | |
self.cells[0].len() | |
} | |
pub fn height(&self) -> usize { | |
self.cells.len() | |
} | |
pub fn remaining_bombs(&self) -> usize { | |
self.remaining_bombs | |
} | |
} | |
impl<T: Touchable> WithGrid<T> { | |
pub fn new(cells: Vec<Vec<WithNeighbors<T>>>) -> Self { | |
if !cells.iter().map(|row| row.len()).all_equal() { | |
panic!("not rectangle cells"); | |
} | |
let remaining_bombs = cells | |
.iter() | |
.flatten() | |
.filter(|cell| cell.focus().has_bomb()) | |
.count(); | |
Self { | |
cells, | |
remaining_bombs, | |
} | |
} | |
pub fn reveal(&mut self, pos: Position) -> RevealResult { | |
self.cells[pos.y][pos.x].reveal_also_neighbors() | |
} | |
pub fn toggle_flag(&mut self, pos: Position) { | |
self.cells[pos.y][pos.x].focus_mut().toggle_flag(); | |
} | |
pub fn is_all_safe_revealed(&self) -> bool { | |
self.cells | |
.iter() | |
.flatten() | |
.map(|cell| cell.focus()) | |
.all(|cell: &T| !cell.has_bomb() || cell.is_revealed()) | |
} | |
pub fn is_game_over(&self) -> bool { | |
self.cells | |
.iter() | |
.flatten() | |
.map(|cell| cell.focus()) | |
.any(|cell: &T| cell.has_bomb() && cell.is_revealed()) | |
} | |
} | |
#[derive(Debug, Clone, PartialEq, Eq, Hash)] | |
pub struct CellsCreationRule { | |
width: usize, | |
height: usize, | |
bombs_amount: usize, | |
} | |
pub fn cells_by_rule(rule: CellsCreationRule) -> WithGrid<Rc<RefCell<Cell>>> { | |
fn neighbor_coordinates( | |
(x, y): (usize, usize), | |
width: usize, | |
height: usize, | |
) -> impl Iterator<Item = (usize, usize)> { | |
let dx = [0, 1, 1, 1, 0, -1, -1, -1isize]; | |
let dy = [-1, -1, 0, 1, 1, 1, 0, -1isize]; | |
(0..8) | |
.map(move |i| ((x as isize + dx[i]) as usize, (y as isize + dy[i]) as usize)) | |
.filter(move |(ax, ay)| (0..width).contains(ax) && (0..height).contains(ay)) | |
} | |
let safes_amount = rule.width * rule.height - rule.bombs_amount; | |
let mut cells: Vec<Cell> = [ | |
std::iter::repeat_n(Cell::new(true), rule.bombs_amount).collect::<Vec<_>>(), | |
std::iter::repeat_n(Cell::new(false), safes_amount).collect::<Vec<_>>(), | |
] | |
.concat(); | |
let mut rng = rand::rng(); | |
use rand::prelude::SliceRandom; | |
cells.shuffle(&mut rng); | |
let mut grid_cells: Vec<Vec<_>> = cells | |
.chunks(rule.width) | |
.map(|row| { | |
row.into_iter() | |
.cloned() | |
.map(|cell| WithNeighbors::new(Rc::new(RefCell::new(cell)))) | |
.collect() | |
}) | |
.collect(); | |
for y in 0..rule.height { | |
for x in 0..rule.width { | |
for (ax, ay) in neighbor_coordinates((x, y), rule.width, rule.height) { | |
let neighbor = grid_cells[ay][ax].clone(); | |
grid_cells[y][x].push_neighbor(neighbor); | |
} | |
} | |
} | |
WithGrid::new(grid_cells) | |
} | |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
pub enum Difficulty { | |
Easy, | |
Normal, | |
Hard, | |
} | |
pub fn cells_by_difficulty(diff: Difficulty) -> WithGrid<Rc<RefCell<Cell>>> { | |
match diff { | |
Difficulty::Easy => cells_by_rule(CellsCreationRule { | |
width: 5, | |
height: 5, | |
bombs_amount: 3, | |
}), | |
Difficulty::Normal => cells_by_rule(CellsCreationRule { | |
width: 9, | |
height: 9, | |
bombs_amount: 10, | |
}), | |
Difficulty::Hard => cells_by_rule(CellsCreationRule { | |
width: 16, | |
height: 16, | |
bombs_amount: 40, | |
}), | |
} | |
} | |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
pub enum GameState { | |
Ready, | |
InGame, | |
Cleared, | |
GameOver, | |
} | |
#[derive(Debug)] | |
pub struct MineSweeper { | |
cells: Option<WithGrid<Rc<RefCell<Cell>>>>, | |
state: GameState, | |
} | |
impl MineSweeper { | |
pub fn new() -> Self { | |
Self { | |
cells: None, | |
state: GameState::Ready, | |
} | |
} | |
pub fn remaining_bombs(&self) -> usize { | |
self.cells | |
.as_ref() | |
.map_or(0, |cells| cells.remaining_bombs()) | |
} | |
pub fn reset(&mut self, difficulty: Difficulty) { | |
self.state = GameState::Ready; | |
self.cells = Some(cells_by_difficulty(difficulty)); | |
} | |
pub fn reveal(&mut self, pos: Position) { | |
let Some(cells) = self.cells.as_mut() else { | |
return; | |
}; | |
self.state = GameState::InGame; | |
if let RevealResult::Exploded = cells.reveal(pos) { | |
self.state = GameState::GameOver; | |
} | |
} | |
pub fn toggle_flag(&mut self, pos: Position) { | |
let Some(cells) = self.cells.as_mut() else { | |
return; | |
}; | |
self.state = GameState::InGame; | |
cells.toggle_flag(pos); | |
} | |
pub fn check_state(&mut self) -> GameState { | |
let Some(cells) = self.cells.as_ref() else { | |
return self.state; | |
}; | |
if cells.is_game_over() { | |
self.state = GameState::GameOver; | |
} else if cells.remaining_bombs() == 0 && cells.is_all_safe_revealed() { | |
self.state = GameState::Cleared; | |
} | |
self.state | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment