-
Notifications
You must be signed in to change notification settings - Fork 247
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed marking cells with bombs for example
mines
(#792)
* Implemented gaming part of example `mines`. Not implemented: reveal nearby unmarket cells by middle mouse click and count scores. * Disable game board after end of game * small refactoring * refactoring: rearrange code in files * Fixed feature marking cells where assumed bomb. All state moved to model (flag marks) therefore `border::view::Overlay` became meaningless type and was removed. Now method `BoardView::draw()` using data about cells from model. * unify Rect use Co-authored-by: Alexandre Bury <[email protected]> * simplified pub Co-authored-by: Alexandre Bury <[email protected]> * simplified pub Co-authored-by: Alexandre Bury <[email protected]> * simplified pub Co-authored-by: Alexandre Bury <[email protected]> * removed redundant code Co-authored-by: Alexandre Bury <[email protected]> * simplified pub Co-authored-by: Alexandre Bury <[email protected]> * simplified pub Co-authored-by: Alexandre Bury <[email protected]> * removed redundant allocation of new Vec<Cell> Co-authored-by: Alexandre Bury <[email protected]> * Removed redundant space --------- Co-authored-by: Alexandre Bury <[email protected]>
- Loading branch information
Showing
5 changed files
with
471 additions
and
311 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod view; | ||
pub mod model; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
use rand::seq::SliceRandom; | ||
use std::ops::{Index, IndexMut}; | ||
use std::slice::Iter; | ||
use ahash::AHashSet; | ||
use cursive::{Rect, Vec2}; | ||
use crate::board::model::CellState::*; | ||
|
||
#[derive(Clone, Copy)] | ||
pub struct Options { | ||
pub size: Vec2, | ||
pub mines: usize, | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq)] | ||
pub enum CellContent { | ||
Bomb, | ||
// numer of near bombs | ||
Free(usize), | ||
} | ||
|
||
#[derive(Debug, Copy, Clone, Eq, PartialEq)] | ||
pub enum CellState { | ||
Closed, | ||
// Marked by user, that this cell contains bomb | ||
Marked, | ||
Opened, | ||
} | ||
|
||
#[derive(Copy, Clone)] | ||
pub struct Cell { | ||
pub state: CellState, | ||
|
||
pub content: CellContent, | ||
} | ||
|
||
impl Cell { | ||
pub fn new(content: CellContent) -> Self { | ||
Self { | ||
state: Closed, | ||
content, | ||
} | ||
} | ||
} | ||
|
||
pub struct Field { | ||
size: Vec2, | ||
cells: Vec<Cell>, | ||
} | ||
|
||
impl Field { | ||
fn new(size: Vec2) -> Self { | ||
Self { | ||
size, | ||
// init stub for cells, see method `Field::place_bombs()` details | ||
cells: vec![Cell::new(CellContent::Free(0)); size.x * size.y], | ||
} | ||
} | ||
|
||
fn pos_to_cell_idx(&self, pos: Vec2) -> usize { | ||
pos.x + pos.y * self.size.x | ||
} | ||
|
||
fn place_bombs(&mut self, click_pos: Vec2, bombs_count: usize) { | ||
// For avoiding losing on first player's move we should place bombs excluding | ||
// position where player clicked and it's neighbours | ||
|
||
// calculation cells from starting rect | ||
let rect = self.neighbours_rect(click_pos); | ||
let exclusion_cells: Vec<_> = (rect.top()..rect.bottom()) | ||
.flat_map(|y| (rect.left()..rect.right()).map(move |x| Vec2::new(x, y))) | ||
.collect(); | ||
|
||
// init bombs on board | ||
let n_cells = self.cells.len(); | ||
self.cells.clear(); | ||
self.cells.resize(bombs_count, Cell:: new(CellContent::Bomb)); | ||
self.cells.resize(n_cells - exclusion_cells.len(), Cell:: new(CellContent::Free(0))); | ||
let mut rng = rand::thread_rng(); | ||
self.cells.shuffle(&mut rng); | ||
|
||
// push empty cells near of cursor to avoid bombs in this positions | ||
for pos in exclusion_cells { | ||
self.cells.insert(self.pos_to_cell_idx(pos), Cell::new(CellContent::Free(0))); | ||
} | ||
|
||
// recalculate near bombs | ||
for pos in self.all_cell_pos_iter() { | ||
if let CellContent::Free(_) = self[pos].content { | ||
self[pos].content = CellContent::Free(self.calc_neighbors_bomb_count(pos)); | ||
} | ||
} | ||
} | ||
|
||
pub fn all_cell_pos_iter(&self) -> impl Iterator<Item=Vec2> { | ||
let size = self.size; | ||
(0..size.y).flat_map(move |x| (0..size.x).map(move |y| Vec2::new(y, x))) | ||
} | ||
|
||
fn neighbours_rect(&self, pos: Vec2) -> Rect { | ||
let pos_min = pos.saturating_sub((1, 1)); | ||
let pos_max = (pos + (2, 2)).or_min(self.size); | ||
|
||
Rect::from_corners(pos_min, pos_max) | ||
} | ||
|
||
fn neighbours(&self, pos: Vec2) -> impl Iterator<Item=Vec2> { | ||
let pos_min = pos.saturating_sub((1, 1)); | ||
let pos_max = (pos + (2, 2)).or_min(self.size); | ||
|
||
(pos_min.y..pos_max.y) | ||
.flat_map(move |x| (pos_min.x..pos_max.x).map(move |y| Vec2::new(y, x))) | ||
.filter(move |&p| p != pos) | ||
} | ||
|
||
fn calc_neighbors_bomb_count(&self, cell_pos: Vec2) -> usize { | ||
let mut bombs_count = 0; | ||
for near_pos in self.neighbours(cell_pos) { | ||
if self[near_pos].content == CellContent::Bomb { | ||
bombs_count += 1; | ||
} | ||
} | ||
|
||
bombs_count | ||
} | ||
} | ||
|
||
impl Index<Vec2> for Field { | ||
type Output = Cell; | ||
|
||
fn index(&self, pos: Vec2) -> &Self::Output { | ||
&self.cells[self.pos_to_cell_idx(pos)] | ||
} | ||
} | ||
|
||
impl IndexMut<Vec2> for Field { | ||
fn index_mut(&mut self, pos: Vec2) -> &mut Self::Output { | ||
let idx = self.pos_to_cell_idx(pos); | ||
&mut self.cells[idx] | ||
} | ||
} | ||
|
||
|
||
pub struct Board { | ||
pub size: Vec2, | ||
pub bombs_count: usize, | ||
pub field: Field, | ||
is_bomb_placed: bool, | ||
pub is_ended: bool, | ||
} | ||
|
||
impl Board { | ||
pub fn new(options: Options) -> Self { | ||
let n_cells = options.size.x * options.size.y; | ||
if options.mines > n_cells { | ||
// Something is wrong here... | ||
// Use different options instead. | ||
return Board::new(Options { | ||
size: options.size, | ||
mines: n_cells, | ||
}); | ||
} | ||
|
||
Board { | ||
size: options.size, | ||
bombs_count: options.mines, | ||
is_bomb_placed: false, | ||
field: Field::new(options.size), | ||
is_ended: false, | ||
} | ||
} | ||
|
||
pub fn iter(&self) -> Iter<'_, Cell> | ||
{ | ||
self.field.cells.iter() | ||
} | ||
|
||
fn place_bombs_if_needed(&mut self, pos: Vec2) { | ||
if !self.is_bomb_placed { | ||
self.field.place_bombs(pos, self.bombs_count); | ||
|
||
self.is_bomb_placed = true; | ||
} | ||
} | ||
|
||
pub fn reveal(&mut self, pos: Vec2) -> RevealResult { | ||
self.place_bombs_if_needed(pos); | ||
|
||
let cell = &mut self.field[pos]; | ||
match cell.content { | ||
CellContent::Bomb => { | ||
self.is_ended = true; | ||
|
||
RevealResult::Loss | ||
} | ||
CellContent::Free(_) => { | ||
cell.state = Opened; | ||
|
||
self.auto_reveal(pos) | ||
} | ||
} | ||
} | ||
|
||
pub(crate) fn auto_reveal(&mut self, pos: Vec2) -> RevealResult { | ||
self.place_bombs_if_needed(pos); | ||
|
||
|
||
let mut opened = AHashSet::new(); | ||
if let CellContent::Free(n) = self.field[pos].content { | ||
let market_cells = self.field.neighbours(pos).filter(|pos| self.field[*pos].state == Marked).count(); | ||
if market_cells == n { | ||
for near_pos in self.field.neighbours(pos) { | ||
if !self.check_neighbours_for_auto_reveal(near_pos, &mut opened) { | ||
return RevealResult::Loss; | ||
} | ||
} | ||
} | ||
} | ||
|
||
match self.check_victory() { | ||
true => { | ||
self.is_ended = true; | ||
RevealResult::Victory | ||
} | ||
false => RevealResult::Revealed | ||
} | ||
} | ||
|
||
pub(crate) fn toggle_flag(&mut self, pos: Vec2) { | ||
self.place_bombs_if_needed(pos); | ||
|
||
let cell = &mut self.field[pos]; | ||
cell.state = match cell.state { | ||
Closed => Marked, | ||
Marked => Closed, | ||
Opened => Opened, | ||
} | ||
} | ||
|
||
// NOTE: Returned when was bomb | ||
fn check_neighbours_for_auto_reveal(&mut self, pos: Vec2, opened: &mut AHashSet<Vec2>) -> bool { | ||
if self.field[pos].state == Opened || opened.contains(&pos) { | ||
return true; | ||
} | ||
|
||
self.field[pos].state = Opened; | ||
opened.insert(pos); | ||
|
||
match self.field[pos].content { | ||
CellContent::Bomb => false, | ||
CellContent::Free(0) => { | ||
for pos in self.field.neighbours(pos) { | ||
if !self.check_neighbours_for_auto_reveal(pos, opened) { | ||
return false; | ||
} | ||
} | ||
true | ||
} | ||
_ => true, | ||
} | ||
} | ||
|
||
fn check_victory(&self) -> bool { | ||
self.field.cells.iter().filter(|x| matches!(x.content, CellContent::Free(_))).all(|x| x.state == Opened) | ||
} | ||
} | ||
|
||
impl Index<Vec2> for Board { | ||
type Output = Cell; | ||
|
||
fn index(&self, pos: Vec2) -> &Self::Output { | ||
&self.field[pos] | ||
} | ||
} | ||
|
||
pub enum RevealResult { | ||
Revealed, | ||
Loss, | ||
Victory, | ||
} |
Oops, something went wrong.