mineswipe 0.1.0

Library for playing Minesweeper!💣
Documentation
use std::fmt::Debug;
use rand::prelude::IteratorRandom;
use thiserror::Error;

use super::cell::Cell;

/// The game's different states.
#[derive(PartialEq, Debug)]
pub enum State {
  /// Menu screen
  Start,
  /// Game is ongoing
  Ongoing,
  /// Game is over
  Over
}

/// Errors that can occur throughout the game
#[derive(Error, Debug)]
pub enum GridError {
  #[error("Index is out of bounds!")]
  IndexOutOfBounds,
  #[error("A rule of the game has been violated!: {0}")]
  RuleViolation(String)
}

/// A grid for playing minesweeper on
pub struct Grid {
  /// All cells in the grid
  pub cells: Vec<Cell>,
  /// The state of the game
  pub state: State,
  /// The number of rows in the grid
  pub rows: usize, 
  /// The number of columns in the grid
  pub columns: usize,
  /// Amount of mines inside the grid
  pub mines: usize
}

impl Debug for Grid {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    let mut str = String::new();

    for i in 0..self.rows {
      for j in 0..self.columns {
        let index = (i * self.columns) + j;
        let ch = if self.cells[index].mine && self.cells[index].excavated {
          "💣"
        } else if self.cells[index].excavated {
          match self.cells[index].mines {
            0 => "",
            1 => "1️⃣",
            2 => "2️⃣",
            3 => "3️⃣",
            4 => "4️⃣",
            5 => "5️⃣",
            6 => "6️⃣",
            7 => "7️⃣",
            8 => "8️⃣",
            _ => ""
          }
        } else if self.cells[index].flagged {
          "🚩"
        } else {
          ""
        };
        str.push_str(ch);
        str.push('\t');
      }
      str.push_str("\n");
    }
    
    write!(f, "{}", str)
  }
}

impl Grid {
  /// Create a new grid
  pub fn new(rows: usize, columns: usize, mines: usize) -> Result<Self, GridError> {
    if rows < 2 || columns < 2 {
      return Err(GridError::RuleViolation("There must be atleast 2 rows or columns".to_string()));
    }
    
    let total_cells = rows*columns;
    if mines >= total_cells {
      return Err(GridError::RuleViolation(("Can't be more mines than total cells. Atleast one cell must not be a mine.").to_string()));
    }

    let mut cells: Vec<Cell> = Vec::with_capacity(total_cells);

    for i in 0..total_cells {
      // Init all cells to default state
      let cell = Cell::new(false, i);
      cells.push(cell);
    }

    Ok(Grid { cells, state: State::Start, rows, columns, mines })
  }

  /// Is the index in bounds of the grid?
  fn in_bounds(&self, index: usize) -> Result<(), GridError> {
    if index >= self.rows * self.columns {
      Err(GridError::IndexOutOfBounds)
    } else {
      Ok(())
    }
  }

  /// Open the cell
  fn open(&mut self, cell_index: usize) {
    self.cells[cell_index].excavated = true;
    self.cells[cell_index].flagged = false;
  }

  /// Dig up a cell inside the grid 
  pub fn dig(&mut self, cell_index: usize) -> Result<(), GridError> {
    // Check if the index is in bounds
    self.in_bounds(cell_index)?;

    if self.state == State::Start {
      let neighbours = self.cells[cell_index].neighbours(self.rows, self.columns);

      let mut banned = neighbours.clone();
      
      // First pick is not a bomb
      banned.push(cell_index);

      // Pick where to place the mines on the grid
      let picks = (0..self.rows*self.columns)
        .filter(|pick| !banned.contains(pick))
        .choose_multiple(&mut rand::thread_rng(), self.mines);

      for i in picks {
        // Place the mine
        self.cells[i].mine = true;
        
        // Calculate mines
        for cell in self.cells[i].neighbours(self.rows, self.columns) {
          self.cells[cell].mines += 1;
        }
      }

      self.open(cell_index);
      self.open_neighbours(neighbours);
      
      self.state = State::Ongoing;

    } else if self.state == State::Ongoing {
      // Redan grävt här
      if self.cells[cell_index].excavated {
        return Err(GridError::RuleViolation("This cell has already been excavated".to_string()));
      }
      // Träffade en mina 💣
      else if self.cells[cell_index].mine {
        // Förlorade 😿
        self.open(cell_index);
        self.state = State::Over;
      } else {
        // Det var inte en mina!
        self.open(cell_index);

        if self.cells[cell_index].mines == 0 {
          let nbs = self.cells[cell_index].neighbours(self.rows, self.columns);
          self.open_neighbours(nbs);
        }
      }
    }
    if self.full() {
      // Vann! 🏆
      self.state = State::Over;
    }
    Ok(())
  }
  
  /// Open the neighbours of a cell
  fn open_neighbours(&mut self, neighbours: Vec<usize>) {
    for index in neighbours {
      if !self.cells[index].mine && !self.cells[index].excavated {
        self.cells[index].excavated = true;
        if self.cells[index].mines == 0 {
          let nbs = self.cells[index].neighbours(self.rows, self.columns);
          self.open_neighbours(nbs);
        }
      }
    }
  }

  /// Is the grid full or not? If so, the game is over.
  fn full(&self) -> bool {
    self.cells
      .iter()
      .filter(|cell| !cell.excavated)
      .all(|cell | cell.mine && cell.flagged)
  }

  /// Place a flag on a cell
  pub fn flag(&mut self, index: usize) -> Result<(), GridError>{
    if !self.cells[index].excavated {
      self.cells[index].flagged = true;
      if self.full() {
        // Vann! 🏆
        self.state = State::Over;
      }
      Ok(())
    } else {
      Err(GridError::RuleViolation("Can't place a flag on an already excavated cell.".to_string()))
    }
  }

}