extern crate alloc;
use alloc::{collections::vec_deque::VecDeque, vec::Vec};
use core::array::IntoIter;
use rand::{Rng, RngExt as _};
use thiserror::Error;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Game {
board: Vec<Cell>,
width: usize,
height: usize,
mine_count: usize,
flag_count: usize,
status: Status,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Status {
Ongoing,
Exploded,
Finished,
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
is_mine: bool,
adjacent_mine_count: usize,
status: CellStatus,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CellStatus {
Hidden,
Revealed,
Flagged,
Exploded,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("too many mines")]
TooManyMines,
#[error("too many flags")]
TooManyFlags,
#[error("attempted to click a flagged cell")]
ClickOnFlagged,
#[error("attempted to click an already revealed cell")]
ClickOnRevealed,
#[error("click has no effect")]
InvalidClick,
#[error("game has ended")]
GameEnded,
}
struct AdjacentCellCoords {
potentially_adjacent_cell_coords: IntoIter<(usize, usize), 8>,
board_width: usize,
board_height: usize,
}
impl Game {
pub fn new<R>(
rng: &mut R,
width: usize,
height: usize,
mine_count: usize,
) -> Result<Self, Error>
where
R: Rng,
{
if width * height < mine_count {
return Err(Error::TooManyMines);
}
let mut board = alloc::vec::Vec::with_capacity(width * height);
let mut remaining_mine_count = mine_count;
for idx in 0..width * height {
let is_mine = rng.random_range(0..width * height - idx) < remaining_mine_count;
if is_mine {
remaining_mine_count -= 1;
}
board.push(Cell {
is_mine,
adjacent_mine_count: 0,
status: CellStatus::Hidden,
});
}
for row in 0..height {
for col in 0..width {
board[row * width + col].adjacent_mine_count =
AdjacentCellCoords::new(width, height, row, col)
.filter(|(row, col)| board[row * width + col].is_mine)
.count();
}
}
Ok(Self {
board,
width,
height,
mine_count,
flag_count: 0,
status: Status::Ongoing,
})
}
pub fn get(&self, row: usize, col: usize) -> &Cell {
assert!(row < self.height);
assert!(col < self.width);
&self.board[row * self.width + col]
}
pub fn click(&mut self, row: usize, col: usize, auto_flag: bool) -> Result<(), Error> {
assert!(row < self.height);
assert!(col < self.width);
if matches!(self.status, Status::Finished | Status::Exploded) {
return Err(Error::GameEnded);
}
match self.board[row * self.width + col].status {
CellStatus::Hidden => self.reveal([(row, col)]),
CellStatus::Revealed => {
let adjacent_mine_count = self.board[row * self.width + col].adjacent_mine_count;
let mut adjacent_flagged_count = 0;
let mut adjacent_hidden_cell_coords = Vec::new();
for (row, col) in AdjacentCellCoords::new(self.width, self.height, row, col) {
match self.board[row * self.width + col].status {
CellStatus::Hidden => adjacent_hidden_cell_coords.push((row, col)),
CellStatus::Revealed => {}
CellStatus::Flagged => adjacent_flagged_count += 1,
CellStatus::Exploded => unreachable!(),
}
}
if adjacent_flagged_count == adjacent_mine_count {
if adjacent_hidden_cell_coords.is_empty() {
return Err(Error::InvalidClick);
}
self.reveal(adjacent_hidden_cell_coords);
} else if auto_flag
&& adjacent_hidden_cell_coords.len() + adjacent_flagged_count
== adjacent_mine_count
{
if self.flag_count + adjacent_hidden_cell_coords.len() > self.mine_count {
return Err(Error::TooManyFlags);
}
for (row, col) in adjacent_hidden_cell_coords {
self.board[row * self.width + col].status = CellStatus::Flagged;
self.flag_count += 1;
}
} else {
return Err(Error::InvalidClick);
}
}
CellStatus::Flagged => return Err(Error::ClickOnFlagged),
CellStatus::Exploded => unreachable!(),
}
self.update_status();
Ok(())
}
pub fn flag(&mut self, row: usize, col: usize) -> Result<(), Error> {
assert!(row < self.height);
assert!(col < self.width);
if matches!(self.status, Status::Finished | Status::Exploded) {
return Err(Error::GameEnded);
}
match self.board[row * self.width + col].status {
CellStatus::Hidden => {
if self.flag_count == self.mine_count {
return Err(Error::TooManyFlags);
}
self.board[row * self.width + col].status = CellStatus::Flagged;
self.flag_count += 1;
}
CellStatus::Flagged => {
self.board[row * self.width + col].status = CellStatus::Hidden;
self.flag_count -= 1;
}
CellStatus::Revealed => return Err(Error::ClickOnRevealed),
CellStatus::Exploded => unreachable!(),
}
self.update_status();
Ok(())
}
pub const fn mine_count(&self) -> usize {
self.mine_count
}
pub const fn flag_count(&self) -> usize {
self.flag_count
}
pub const fn status(&self) -> &Status {
&self.status
}
fn reveal(&mut self, coords: impl Into<VecDeque<(usize, usize)>>) {
let mut coords = coords.into();
while let Some((row, col)) = coords.pop_front() {
let cell_idx = row * self.width + col;
if !matches!(self.board[cell_idx].status, CellStatus::Hidden) {
continue;
}
if self.board[cell_idx].is_mine {
self.board[cell_idx].status = CellStatus::Exploded;
self.status = Status::Exploded;
break;
}
self.board[cell_idx].status = CellStatus::Revealed;
if self.board[cell_idx].adjacent_mine_count == 0 {
coords.extend(AdjacentCellCoords::new(self.width, self.height, row, col));
}
}
}
fn update_status(&mut self) {
if matches!(self.status, Status::Exploded) {
return;
}
let all_revealed = self
.board
.iter()
.filter(|cell| !cell.is_mine)
.all(|cell| matches!(cell.status, CellStatus::Revealed));
let all_flagged = self
.board
.iter()
.filter(|cell| cell.is_mine)
.all(|cell| matches!(cell.status, CellStatus::Flagged));
if all_revealed || all_flagged {
self.status = Status::Finished
}
}
}
impl Cell {
pub const fn is_mine(&self) -> bool {
self.is_mine
}
pub const fn adjacent_mine_count(&self) -> usize {
self.adjacent_mine_count
}
pub const fn status(&self) -> &CellStatus {
&self.status
}
}
impl AdjacentCellCoords {
fn new(board_width: usize, board_height: usize, row: usize, col: usize) -> Self {
Self {
potentially_adjacent_cell_coords: [
(row.wrapping_sub(1), col.wrapping_sub(1)),
(row.wrapping_sub(1), col),
(row.wrapping_sub(1), col + 1),
(row, col.wrapping_sub(1)),
(row, col + 1),
(row + 1, col.wrapping_sub(1)),
(row + 1, col),
(row + 1, col + 1),
]
.into_iter(),
board_width,
board_height,
}
}
}
impl Iterator for AdjacentCellCoords {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
loop {
let (row, col) = self.potentially_adjacent_cell_coords.next()?;
if row < self.board_height && col < self.board_width {
return Some((row, col));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cell(is_mine: bool, adjacent_mine_count: usize, status: CellStatus) -> Cell {
Cell {
is_mine,
adjacent_mine_count,
status,
}
}
#[test]
fn new_preserves_mine_count_and_allows_flags() {
let mut game = Game::new(&mut rand::rng(), 2, 2, 1).unwrap();
assert_eq!(game.mine_count(), 1);
game.flag(0, 0).unwrap();
assert_eq!(game.flag_count(), 1);
}
#[test]
fn reveal_empty_region_finishes_without_reprocessing_revealed_cells() {
let mut game = Game {
board: alloc::vec![
cell(false, 0, CellStatus::Hidden),
cell(false, 0, CellStatus::Hidden),
cell(false, 0, CellStatus::Hidden),
cell(false, 0, CellStatus::Hidden),
],
width: 2,
height: 2,
mine_count: 0,
flag_count: 0,
status: Status::Ongoing,
};
game.click(0, 0, false).unwrap();
assert!(
game.board
.iter()
.all(|cell| matches!(cell.status, CellStatus::Revealed))
);
assert_eq!(game.status(), &Status::Finished);
}
#[test]
fn reveal_skips_flagged_cells_and_keeps_flag_count() {
let mut game = Game {
board: alloc::vec![
cell(false, 0, CellStatus::Hidden),
cell(false, 1, CellStatus::Flagged),
cell(true, 0, CellStatus::Hidden),
],
width: 3,
height: 1,
mine_count: 1,
flag_count: 1,
status: Status::Ongoing,
};
game.click(0, 0, false).unwrap();
assert_eq!(game.get(0, 1).status(), &CellStatus::Flagged);
assert_eq!(game.flag_count(), 1);
assert_eq!(game.status(), &Status::Ongoing);
}
#[test]
fn auto_flag_updates_flag_count() {
let mut game = Game {
board: alloc::vec![
cell(false, 1, CellStatus::Revealed),
cell(true, 0, CellStatus::Hidden),
cell(false, 2, CellStatus::Hidden),
cell(true, 0, CellStatus::Hidden),
],
width: 4,
height: 1,
mine_count: 2,
flag_count: 0,
status: Status::Ongoing,
};
game.click(0, 0, true).unwrap();
assert_eq!(game.get(0, 1).status(), &CellStatus::Flagged);
assert_eq!(game.flag_count(), 1);
game.flag(0, 1).unwrap();
assert_eq!(game.get(0, 1).status(), &CellStatus::Hidden);
assert_eq!(game.flag_count(), 0);
}
#[test]
fn auto_flag_respects_global_flag_limit() {
let mut game = Game {
board: alloc::vec![
cell(false, 1, CellStatus::Revealed),
cell(true, 0, CellStatus::Hidden),
cell(false, 1, CellStatus::Hidden),
cell(true, 0, CellStatus::Flagged),
],
width: 4,
height: 1,
mine_count: 1,
flag_count: 1,
status: Status::Ongoing,
};
assert!(matches!(game.click(0, 0, true), Err(Error::TooManyFlags)));
assert_eq!(game.flag_count(), 1);
assert_eq!(game.get(0, 1).status(), &CellStatus::Hidden);
}
#[test]
fn revealed_click_with_no_hidden_neighbors_is_invalid() {
let mut game = Game {
board: alloc::vec![
cell(false, 0, CellStatus::Revealed),
cell(false, 0, CellStatus::Revealed),
cell(false, 0, CellStatus::Hidden),
],
width: 3,
height: 1,
mine_count: 0,
flag_count: 0,
status: Status::Ongoing,
};
assert!(matches!(game.click(0, 0, false), Err(Error::InvalidClick)));
}
#[test]
fn explosion_status_is_not_overwritten() {
let mut game = Game {
board: alloc::vec![
cell(false, 1, CellStatus::Revealed),
cell(true, 0, CellStatus::Hidden),
],
width: 2,
height: 1,
mine_count: 1,
flag_count: 0,
status: Status::Ongoing,
};
game.click(0, 1, false).unwrap();
assert_eq!(game.get(0, 1).status(), &CellStatus::Exploded);
assert_eq!(game.status(), &Status::Exploded);
}
}