Skip to content

Instantly share code, notes, and snippets.

@MikuroXina
Last active February 10, 2025 15:49
Show Gist options
  • Save MikuroXina/cc1d89090638acc6a5ba791bb71c54e8 to your computer and use it in GitHub Desktop.
Save MikuroXina/cc1d89090638acc6a5ba791bb71c54e8 to your computer and use it in GitHub Desktop.
An object-oriented mine sweeper implementation with Rust.
/// 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