From b1b61846499958947719596edb2a42ff5f1f9eda Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 8 Jun 2024 00:24:52 +0700 Subject: [PATCH 01/14] Implemented gaming part of example `mines`. Not implemented: reveal nearby unmarket cells by middle mouse click and count scores. --- cursive/examples/mines/game.rs | 249 +++++++++++++++++++++++++++------ cursive/examples/mines/main.rs | 216 ++++++++++++++++------------ 2 files changed, 336 insertions(+), 129 deletions(-) diff --git a/cursive/examples/mines/game.rs b/cursive/examples/mines/game.rs index b390970e..2f435704 100644 --- a/cursive/examples/mines/game.rs +++ b/cursive/examples/mines/game.rs @@ -1,6 +1,8 @@ +use rand::seq::SliceRandom; +use std::ops::{Index, IndexMut}; +use ahash::AHashSet; use cursive::Vec2; -use rand::{thread_rng, Rng}; -// use std::cmp::max; +use cursive_core::Rect; #[derive(Clone, Copy)] pub struct Options { @@ -8,15 +10,141 @@ pub struct Options { pub mines: usize, } -#[derive(Clone, Copy)] -pub enum Cell { +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CellContent { Bomb, + // numer of near bombs Free(usize), } +#[derive(Copy, Clone)] +pub struct Cell { + is_opened: bool, + pub content: CellContent, +} + +impl Cell { + pub fn new(content: CellContent) -> Self { + Self { + is_opened: false, + content, + } + } +} + +pub struct Field { + size: Vec2, + cells: Vec, +} + +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 mut cells = Vec::new(); + for i in 0..self.cells.len() - exclusion_cells.len() { + let cell = if i < bombs_count { + Cell::new(CellContent::Bomb) + } else { + Cell::new(CellContent::Free(0)) + }; + + cells.push(cell); + } + + // shuffle them + let mut rng = rand::thread_rng(); + cells.shuffle(&mut rng); + + // push empty cells near of cursor to avoid bombs in this positions + for pos in exclusion_cells { + cells.insert(self.pos_to_cell_idx(pos), Cell::new(CellContent::Free(0))); + } + + self.cells = cells; + + // 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 { + 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 { + 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 for Field { + type Output = Cell; + + fn index(&self, pos: Vec2) -> &Self::Output { + &self.cells[self.pos_to_cell_idx(pos)] + } +} + +impl IndexMut 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 cells: Vec, + pub bombs_count: usize, + pub field: Field, + is_bomb_placed: bool, } impl Board { @@ -31,56 +159,97 @@ impl Board { }); } - let mut board = Board { + Board { size: options.size, - cells: vec![Cell::Free(0); n_cells], - }; + bombs_count: options.mines, + is_bomb_placed: false, + field: Field::new(options.size), + } + } + + fn check_victory(&self) -> bool { + self.field.cells.iter().filter(|x| matches!(x.content, CellContent::Free(_))).all(|x| x.is_opened) + } - for _ in 0..options.mines { - // Find a free cell to put a bomb - let i = loop { - let i = thread_rng().gen_range(0..n_cells); + fn place_bombs_if_needed(&mut self, pos: Vec2) { + if !self.is_bomb_placed { + self.field.place_bombs(pos, self.bombs_count); - if let Cell::Bomb = board.cells[i] { - continue; - } + self.is_bomb_placed = true; + } + } - break i; - }; + pub fn reveal(&mut self, pos: Vec2) -> RevealResult { + self.place_bombs_if_needed(pos); - // We know we'll go through since that's how we picked i... - board.cells[i] = Cell::Bomb; - // Increase count on adjacent cells + let cell = &mut self.field[pos]; + match cell.content { + CellContent::Bomb => RevealResult::Loss, + CellContent::Free(_) => { + cell.is_opened = true; - let pos = Vec2::new(i % options.size.x, i / options.size.x); - for p in board.neighbours(pos) { - if let Some(&mut Cell::Free(ref mut n)) = board.get_mut(p) { - *n += 1; + match self.auto_reveal(pos) { + AutoRevealResult::Victory => RevealResult::Victory, + AutoRevealResult::Revealed(mut opened_poses) => { + opened_poses.push(pos); + + RevealResult::Revealed(opened_poses) + } } } } - - board } - fn get_mut(&mut self, pos: Vec2) -> Option<&mut Cell> { - self.cell_id(pos).map(move |i| &mut self.cells[i]) + pub fn auto_reveal(&mut self, pos: Vec2) -> AutoRevealResult { + self.place_bombs_if_needed(pos); + + + let mut opened = AHashSet::new(); + if let CellContent::Free(0) = self.field[pos].content { + for near_pos in self.field.neighbours(pos) { + self.check_neighbours_for_auto_reveal(near_pos, &mut opened); + } + } + + match self.check_victory() { + true => AutoRevealResult::Victory, + false => AutoRevealResult::Revealed(opened.into_iter().collect()) + } } - pub fn cell_id(&self, pos: Vec2) -> Option { - if pos < self.size { - Some(pos.x + pos.y * self.size.x) - } else { - None + fn check_neighbours_for_auto_reveal(&mut self, pos: Vec2, opened: &mut AHashSet) { + if self.field[pos].is_opened || self.field[pos].content == CellContent::Bomb || opened.contains(&pos) { + return; + } + + debug_assert!(matches!(self.field[pos].content, CellContent::Free(_)), "failed logic for auto reveal"); + + self.field[pos].is_opened = true; + opened.insert(pos); + + if let CellContent::Free(0) = self.field[pos].content { + for pos in self.field.neighbours(pos) { + self.check_neighbours_for_auto_reveal(pos, opened); + } } } +} - pub fn neighbours(&self, pos: Vec2) -> Vec { - let pos_min = pos.saturating_sub((1, 1)); - let pos_max = (pos + (2, 2)).or_min(self.size); - (pos_min.x..pos_max.x) - .flat_map(|x| (pos_min.y..pos_max.y).map(move |y| Vec2::new(x, y))) - .filter(|&p| p != pos) - .collect() +impl Index for Board { + type Output = Cell; + + fn index(&self, pos: Vec2) -> &Self::Output { + &self.field[pos] } } + +pub enum RevealResult { + Revealed(Vec), + Loss, + Victory, +} + +pub enum AutoRevealResult { + Revealed(Vec), + Victory, +} \ No newline at end of file diff --git a/cursive/examples/mines/main.rs b/cursive/examples/mines/main.rs index e28901bc..8a2add7a 100644 --- a/cursive/examples/mines/main.rs +++ b/cursive/examples/mines/main.rs @@ -1,5 +1,6 @@ mod game; +use crate::game::{AutoRevealResult, CellContent, RevealResult}; use cursive::{ direction::Direction, event::{Event, EventResult, MouseButton, MouseEvent}, @@ -8,6 +9,8 @@ use cursive::{ views::{Button, Dialog, LinearLayout, Panel, SelectView}, Cursive, Printer, Vec2, }; +use std::ops::{Index, IndexMut}; +use cursive_core::traits::Nameable; fn main() { let mut siv = cursive::default(); @@ -22,7 +25,15 @@ fn main() { .child(Button::new_raw(" Best scores ", |s| { s.add_layer(Dialog::info("Not yet!").title("Scores")) })) - .child(Button::new_raw(" Exit ", |s| s.quit())), + .child(Button::new_raw(" Controls ", |s| { + s.add_layer(Dialog::info( + "Controls: +Reveal cell: left click +Mark as mine: right-click +Reveal nearby unmarked cells: middle-click", + ).title("Controls")) + })) + .child(Button::new_raw(" Exit ", |s| s.quit())), ), ); @@ -67,9 +78,33 @@ fn show_options(siv: &mut Cursive) { #[derive(Clone, Copy, PartialEq)] enum Cell { - Visible(usize), - Flag, Unknown, + Flag, + Visible(usize), + Bomb, +} + +// NOTE: coordinates [y][x] +struct Overlay(Vec>); + +impl Overlay { + pub fn new(size: Vec2) -> Self { + Self(vec![vec![Cell::Unknown; size.x]; size.y]) + } +} + +impl Index for Overlay { + type Output = Cell; + + fn index(&self, pos: Vec2) -> &Self::Output { + &self.0[pos.y][pos.x] + } +} + +impl IndexMut for Overlay { + fn index_mut(&mut self, pos: Vec2) -> &mut Self::Output { + &mut self.0[pos.y][pos.x] + } } struct BoardView { @@ -77,7 +112,7 @@ struct BoardView { board: game::Board, // Visible board - overlay: Vec, + overlay: Overlay, focused: Option, _missing_mines: usize, @@ -85,108 +120,121 @@ struct BoardView { impl BoardView { pub fn new(options: game::Options) -> Self { - let overlay = vec![Cell::Unknown; options.size.x * options.size.y]; let board = game::Board::new(options); BoardView { board, - overlay, + overlay: Overlay::new(options.size), focused: None, _missing_mines: options.mines, } } fn get_cell(&self, mouse_pos: Vec2, offset: Vec2) -> Option { - mouse_pos - .checked_sub(offset) - .map(|pos| pos.map_x(|x| x / 2)) - .and_then(|pos| { - if pos.fits_in(self.board.size) { - Some(pos) - } else { - None - } - }) + let pos = mouse_pos.checked_sub(offset)?; + let pos = pos.map_x(|x| x / 2); + if pos.fits_in(self.board.size - (1, 1)) { + Some(pos) + } else { + None + } } fn flag(&mut self, pos: Vec2) { - if let Some(i) = self.board.cell_id(pos) { - let new_cell = match self.overlay[i] { - Cell::Unknown => Cell::Flag, - Cell::Flag => Cell::Unknown, - other => other, - }; - self.overlay[i] = new_cell; - } + let new_cell = match self.overlay[pos] { + Cell::Unknown => Cell::Flag, + Cell::Flag => Cell::Unknown, + other => other, + }; + self.overlay[pos] = new_cell; } fn reveal(&mut self, pos: Vec2) -> EventResult { - if let Some(i) = self.board.cell_id(pos) { - if self.overlay[i] != Cell::Unknown { - return EventResult::Consumed(None); - } + if self.overlay[pos] != Cell::Unknown { + return EventResult::Consumed(None); + } - // Action! - match self.board.cells[i] { - game::Cell::Bomb => { - return EventResult::with_cb(|s| { - s.add_layer(Dialog::text("BOOOM").button("Ok", |s| { - s.pop_layer(); - s.pop_layer(); - })); - }) - } - game::Cell::Free(n) => { - self.overlay[i] = Cell::Visible(n); - if n == 0 { - // Reveal all surrounding cells - for p in self.board.neighbours(pos) { - self.reveal(p); - } - } - } + match self.board.reveal(pos) { + RevealResult::Revealed(opened_cells) => { + self.open_cells(opened_cells); + EventResult::Consumed(None) + } + RevealResult::Loss => { + self.open_all_mines(); + Self::result_loss() + } + RevealResult::Victory => { + self.open_all_cells(); + Self::result_victory() } } - EventResult::Consumed(None) } fn auto_reveal(&mut self, pos: Vec2) -> EventResult { - if let Some(i) = self.board.cell_id(pos) { - if let Cell::Visible(n) = self.overlay[i] { - // First: is every possible cell tagged? - let neighbours = self.board.neighbours(pos); - let tagged = neighbours - .iter() - .filter_map(|&pos| self.board.cell_id(pos)) - .map(|i| self.overlay[i]) - .filter(|&cell| cell == Cell::Flag) - .count(); - if tagged != n { - return EventResult::Consumed(None); - } - - for p in neighbours { - let result = self.reveal(p); - if result.has_callback() { - return result; - } - } + match self.board.auto_reveal(pos) { + AutoRevealResult::Revealed(opened_cells) => { + self.open_cells(opened_cells); + return EventResult::Consumed(None); + } + AutoRevealResult::Victory => { + self.open_all_cells(); + Self::result_victory() } } + } + + fn result_loss() -> EventResult { + EventResult::with_cb(|s| Self::change_board_button_label(s, "Defeted")) + } + + fn result_victory() -> EventResult { + EventResult::with_cb(|s| Self::change_board_button_label(s, "Victory!")) + } + + fn change_board_button_label(s: &mut Cursive, label: &str) { + s.call_on_name("board", |d: &mut Dialog| { + d.buttons_mut().last().expect("button must exists").set_label(label); + }); + } + + fn open_cells(&mut self, opened_cells: Vec) { + for pos in opened_cells { + let CellContent::Free(near_bombs) = self.board[pos].content else { + panic!("must be variant CellContent::Free()") + }; + + self.overlay[pos] = Cell::Visible(near_bombs); + } + } + + fn open_all_cells(&mut self) { + for pos in self.board.field.all_cell_pos_iter() { + self.overlay[pos] = match self.board[pos].content { + CellContent::Bomb => Cell::Bomb, + CellContent::Free(near_bombs) => Cell::Visible(near_bombs), + }; + } + } - EventResult::Consumed(None) + fn open_all_mines(&mut self) { + for pos in self.board.field.all_cell_pos_iter() { + if let Cell::Bomb = self.overlay[pos] { + self.overlay[pos] = Cell::Bomb; + } + } } } impl cursive::view::View for BoardView { fn draw(&self, printer: &Printer) { - for (i, cell) in self.overlay.iter().enumerate() { + for (i, cell) in self.overlay.0.iter().flatten().enumerate() { let x = (i % self.board.size.x) * 2; let y = i / self.board.size.x; let text = match *cell { - Cell::Unknown => "[]", - Cell::Flag => "()", + Cell::Unknown => " □", + Cell::Flag => " ■", Cell::Visible(n) => [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n], + Cell::Bomb => "\u{01F4A3} " }; let color = match *cell { @@ -242,9 +290,7 @@ impl cursive::view::View for BoardView { self.flag(pos); return EventResult::Consumed(None); } - MouseButton::Middle => { - return self.auto_reveal(pos); - } + MouseButton::Middle => return self.auto_reveal(pos), _ => (), } } @@ -266,19 +312,11 @@ impl cursive::view::View for BoardView { fn new_game(siv: &mut Cursive, options: game::Options) { let _board = game::Board::new(options); - siv.add_layer( - Dialog::new() - .title("Minesweeper") - .content(LinearLayout::horizontal().child(Panel::new(BoardView::new(options)))) - .button("Quit game", |s| { - s.pop_layer(); - }), - ); + let dialog = Dialog::new() + .title("Minesweeper") + .content(LinearLayout::horizontal().child(Panel::new(BoardView::new(options)))) + .button("Quit game", |s| { s.pop_layer(); }) + .with_name("board"); - siv.add_layer(Dialog::info( - "Controls: -Reveal cell: left click -Mark as mine: right-click -Reveal nearby unmarked cells: middle-click", - )); + siv.add_layer(dialog); } From 4e6e8ef4e810c4e37886b7bc52979efa86cf6863 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 8 Jun 2024 01:02:31 +0700 Subject: [PATCH 02/14] Disable game board after end of game --- cursive/examples/mines/main.rs | 80 +++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/cursive/examples/mines/main.rs b/cursive/examples/mines/main.rs index 8a2add7a..e6d63a34 100644 --- a/cursive/examples/mines/main.rs +++ b/cursive/examples/mines/main.rs @@ -115,6 +115,7 @@ struct BoardView { overlay: Overlay, focused: Option, + enabled: bool, _missing_mines: usize, } @@ -125,6 +126,7 @@ impl BoardView { board, overlay: Overlay::new(options.size), focused: None, + enabled: true, _missing_mines: options.mines, } } @@ -183,15 +185,19 @@ impl BoardView { } fn result_loss() -> EventResult { - EventResult::with_cb(|s| Self::change_board_button_label(s, "Defeted")) + EventResult::with_cb(|s| Self::make_end_game_result(s, "Defeted")) } fn result_victory() -> EventResult { - EventResult::with_cb(|s| Self::change_board_button_label(s, "Victory!")) + EventResult::with_cb(|s| Self::make_end_game_result(s, "Victory!")) } - fn change_board_button_label(s: &mut Cursive, label: &str) { - s.call_on_name("board", |d: &mut Dialog| { + fn make_end_game_result(s: &mut Cursive, button_label: &'static str) { + s.call_on_name("board", |b: &mut BoardView| b.enabled = false); + Self::change_game_button_label(s, button_label); + } + fn change_game_button_label(s: &mut Cursive, label: &str) { + s.call_on_name("game", |d: &mut Dialog| { d.buttons_mut().last().expect("button must exists").set_label(label); }); } @@ -263,42 +269,44 @@ impl cursive::view::View for BoardView { } fn on_event(&mut self, event: Event) -> EventResult { - match event { - Event::Mouse { - offset, - position, - event: MouseEvent::Press(_btn), - } => { - // Get cell for position - if let Some(pos) = self.get_cell(position, offset) { - self.focused = Some(pos); - return EventResult::Consumed(None); + if self.enabled { + match event { + Event::Mouse { + offset, + position, + event: MouseEvent::Press(_btn), + } => { + // Get cell for position + if let Some(pos) = self.get_cell(position, offset) { + self.focused = Some(pos); + return EventResult::Consumed(None); + } } - } - Event::Mouse { - offset, - position, - event: MouseEvent::Release(btn), - } => { - // Get cell for position - if let Some(pos) = self.get_cell(position, offset) { - if self.focused == Some(pos) { - // We got a click here! - match btn { - MouseButton::Left => return self.reveal(pos), - MouseButton::Right => { - self.flag(pos); - return EventResult::Consumed(None); + Event::Mouse { + offset, + position, + event: MouseEvent::Release(btn), + } => { + // Get cell for position + if let Some(pos) = self.get_cell(position, offset) { + if self.focused == Some(pos) { + // We got a click here! + match btn { + MouseButton::Left => return self.reveal(pos), + MouseButton::Right => { + self.flag(pos); + return EventResult::Consumed(None); + } + MouseButton::Middle => return self.auto_reveal(pos), + _ => (), } - MouseButton::Middle => return self.auto_reveal(pos), - _ => (), } - } - self.focused = None; + self.focused = None; + } } + _ => (), } - _ => (), } EventResult::Ignored @@ -314,9 +322,9 @@ fn new_game(siv: &mut Cursive, options: game::Options) { let dialog = Dialog::new() .title("Minesweeper") - .content(LinearLayout::horizontal().child(Panel::new(BoardView::new(options)))) + .content(LinearLayout::horizontal().child(Panel::new(BoardView::new(options).with_name("board")))) .button("Quit game", |s| { s.pop_layer(); }) - .with_name("board"); + .with_name("game"); siv.add_layer(dialog); } From ec91a6650944f93aba36f4e19e3e79262e599a77 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 8 Jun 2024 20:33:50 +0700 Subject: [PATCH 03/14] small refactoring --- cursive/examples/mines/main.rs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/cursive/examples/mines/main.rs b/cursive/examples/mines/main.rs index e6d63a34..02c49bc1 100644 --- a/cursive/examples/mines/main.rs +++ b/cursive/examples/mines/main.rs @@ -21,18 +21,9 @@ fn main() { .padding_lrtb(2, 2, 1, 1) .content( LinearLayout::vertical() - .child(Button::new_raw(" New game ", show_options)) - .child(Button::new_raw(" Best scores ", |s| { - s.add_layer(Dialog::info("Not yet!").title("Scores")) - })) - .child(Button::new_raw(" Controls ", |s| { - s.add_layer(Dialog::info( - "Controls: -Reveal cell: left click -Mark as mine: right-click -Reveal nearby unmarked cells: middle-click", - ).title("Controls")) - })) + .child(Button::new_raw(" New game ", show_options)) + .child(Button::new_raw(" Controls ", show_controls)) + .child(Button::new_raw(" Scores ", show_scores)) .child(Button::new_raw(" Exit ", |s| s.quit())), ), ); @@ -40,6 +31,19 @@ Reveal nearby unmarked cells: middle-click", siv.run(); } +fn show_controls(s: &mut Cursive) { + s.add_layer(Dialog::info( + "Controls: +Reveal cell: left click +Mark as mine: right-click +Reveal nearby unmarked cells: middle-click", + ).title("Controls")) +} + +fn show_scores(s: &mut Cursive) { + s.add_layer(Dialog::info("Not yet!").title("Scores")) +} + fn show_options(siv: &mut Cursive) { siv.add_layer( Dialog::new() @@ -132,8 +136,7 @@ impl BoardView { } fn get_cell(&self, mouse_pos: Vec2, offset: Vec2) -> Option { - let pos = mouse_pos.checked_sub(offset)?; - let pos = pos.map_x(|x| x / 2); + let pos = mouse_pos.checked_sub(offset)?.map_x(|x| x / 2); if pos.fits_in(self.board.size - (1, 1)) { Some(pos) } else { From b9252bc605b12ee35e27b5b961e2353b56f85667 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 8 Jun 2024 20:49:38 +0700 Subject: [PATCH 04/14] refactoring: rearrange code in files --- cursive/examples/mines/board/mod.rs | 2 + .../mines/{game.rs => board/model.rs} | 0 cursive/examples/mines/board/view.rs | 249 ++++++++++++++++ cursive/examples/mines/main.rs | 281 ++---------------- 4 files changed, 270 insertions(+), 262 deletions(-) create mode 100644 cursive/examples/mines/board/mod.rs rename cursive/examples/mines/{game.rs => board/model.rs} (100%) create mode 100644 cursive/examples/mines/board/view.rs diff --git a/cursive/examples/mines/board/mod.rs b/cursive/examples/mines/board/mod.rs new file mode 100644 index 00000000..d79c5023 --- /dev/null +++ b/cursive/examples/mines/board/mod.rs @@ -0,0 +1,2 @@ +pub mod view; +pub mod model; diff --git a/cursive/examples/mines/game.rs b/cursive/examples/mines/board/model.rs similarity index 100% rename from cursive/examples/mines/game.rs rename to cursive/examples/mines/board/model.rs diff --git a/cursive/examples/mines/board/view.rs b/cursive/examples/mines/board/view.rs new file mode 100644 index 00000000..84de6d13 --- /dev/null +++ b/cursive/examples/mines/board/view.rs @@ -0,0 +1,249 @@ +use cursive_core::{Cursive, Printer, Vec2}; +use std::ops::{Index, IndexMut}; +use cursive_core::direction::Direction; +use cursive_core::event::{Event, EventResult, MouseButton, MouseEvent}; +use cursive_core::theme::{BaseColor, Color, ColorStyle}; +use cursive_core::view::CannotFocus; +use cursive_core::views::Dialog; +use crate::board::model; +use crate::board::model::{AutoRevealResult, CellContent, RevealResult}; + +#[derive(Clone, Copy, PartialEq)] +enum Cell { + Unknown, + Flag, + Visible(usize), + Bomb, +} + +// NOTE: coordinates [y][x] +struct Overlay(Vec>); + +impl Overlay { + pub fn new(size: Vec2) -> Self { + Self(vec![vec![Cell::Unknown; size.x]; size.y]) + } +} + +impl Index for Overlay { + type Output = Cell; + + fn index(&self, pos: Vec2) -> &Self::Output { + &self.0[pos.y][pos.x] + } +} + +impl IndexMut for Overlay { + fn index_mut(&mut self, pos: Vec2) -> &mut Self::Output { + &mut self.0[pos.y][pos.x] + } +} + +pub(crate) struct BoardView { + // Actual board, unknown to the player. + board: model::Board, + + // Visible board + overlay: Overlay, + + focused: Option, + enabled: bool, + _missing_mines: usize, +} + +impl BoardView { + pub(crate) fn new(options: model::Options) -> Self { + let board = model::Board::new(options); + BoardView { + board, + overlay: Overlay::new(options.size), + focused: None, + enabled: true, + _missing_mines: options.mines, + } + } + + fn get_cell(&self, mouse_pos: Vec2, offset: Vec2) -> Option { + let pos = mouse_pos.checked_sub(offset)?.map_x(|x| x / 2); + if pos.fits_in(self.board.size - (1, 1)) { + Some(pos) + } else { + None + } + } + + fn flag(&mut self, pos: Vec2) { + let new_cell = match self.overlay[pos] { + Cell::Unknown => Cell::Flag, + Cell::Flag => Cell::Unknown, + other => other, + }; + self.overlay[pos] = new_cell; + } + + fn reveal(&mut self, pos: Vec2) -> EventResult { + if self.overlay[pos] != Cell::Unknown { + return EventResult::Consumed(None); + } + + match self.board.reveal(pos) { + RevealResult::Revealed(opened_cells) => { + self.open_cells(opened_cells); + EventResult::Consumed(None) + } + RevealResult::Loss => { + self.open_all_mines(); + Self::result_loss() + } + RevealResult::Victory => { + self.open_all_cells(); + Self::result_victory() + } + } + } + + fn auto_reveal(&mut self, pos: Vec2) -> EventResult { + match self.board.auto_reveal(pos) { + AutoRevealResult::Revealed(opened_cells) => { + self.open_cells(opened_cells); + return EventResult::Consumed(None); + } + AutoRevealResult::Victory => { + self.open_all_cells(); + Self::result_victory() + } + } + } + + fn result_loss() -> EventResult { + EventResult::with_cb(|s| Self::make_end_game_result(s, "Defeted")) + } + + fn result_victory() -> EventResult { + EventResult::with_cb(|s| Self::make_end_game_result(s, "Victory!")) + } + + fn make_end_game_result(s: &mut Cursive, button_label: &'static str) { + s.call_on_name("board", |b: &mut BoardView| b.enabled = false); + Self::change_game_button_label(s, button_label); + } + fn change_game_button_label(s: &mut Cursive, label: &str) { + s.call_on_name("game", |d: &mut Dialog| { + d.buttons_mut().last().expect("button must exists").set_label(label); + }); + } + + fn open_cells(&mut self, opened_cells: Vec) { + for pos in opened_cells { + let CellContent::Free(near_bombs) = self.board[pos].content else { + panic!("must be variant CellContent::Free()") + }; + + self.overlay[pos] = Cell::Visible(near_bombs); + } + } + + fn open_all_cells(&mut self) { + for pos in self.board.field.all_cell_pos_iter() { + self.overlay[pos] = match self.board[pos].content { + CellContent::Bomb => Cell::Bomb, + CellContent::Free(near_bombs) => Cell::Visible(near_bombs), + }; + } + } + + fn open_all_mines(&mut self) { + for pos in self.board.field.all_cell_pos_iter() { + if let Cell::Bomb = self.overlay[pos] { + self.overlay[pos] = Cell::Bomb; + } + } + } +} + +impl cursive::view::View for BoardView { + fn draw(&self, printer: &Printer) { + for (i, cell) in self.overlay.0.iter().flatten().enumerate() { + let x = (i % self.board.size.x) * 2; + let y = i / self.board.size.x; + + let text = match *cell { + Cell::Unknown => " □", + Cell::Flag => " ■", + Cell::Visible(n) => [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n], + Cell::Bomb => "\u{01F4A3} " + }; + + let color = match *cell { + Cell::Unknown => Color::RgbLowRes(3, 3, 3), + Cell::Flag => Color::RgbLowRes(4, 4, 2), + Cell::Visible(1) => Color::RgbLowRes(3, 5, 3), + Cell::Visible(2) => Color::RgbLowRes(5, 5, 3), + Cell::Visible(3) => Color::RgbLowRes(5, 4, 3), + Cell::Visible(4) => Color::RgbLowRes(5, 3, 3), + Cell::Visible(5) => Color::RgbLowRes(5, 2, 2), + Cell::Visible(6) => Color::RgbLowRes(5, 0, 1), + Cell::Visible(7) => Color::RgbLowRes(5, 0, 2), + Cell::Visible(8) => Color::RgbLowRes(5, 0, 3), + _ => Color::Dark(BaseColor::White), + }; + + printer.with_color( + ColorStyle::new(Color::Dark(BaseColor::Black), color), + |printer| printer.print((x, y), text), + ); + } + } + + fn take_focus(&mut self, _: Direction) -> Result { + Ok(EventResult::Consumed(None)) + } + + fn on_event(&mut self, event: Event) -> EventResult { + if self.enabled { + match event { + Event::Mouse { + offset, + position, + event: MouseEvent::Press(_btn), + } => { + // Get cell for position + if let Some(pos) = self.get_cell(position, offset) { + self.focused = Some(pos); + return EventResult::Consumed(None); + } + } + Event::Mouse { + offset, + position, + event: MouseEvent::Release(btn), + } => { + // Get cell for position + if let Some(pos) = self.get_cell(position, offset) { + if self.focused == Some(pos) { + // We got a click here! + match btn { + MouseButton::Left => return self.reveal(pos), + MouseButton::Right => { + self.flag(pos); + return EventResult::Consumed(None); + } + MouseButton::Middle => return self.auto_reveal(pos), + _ => (), + } + } + + self.focused = None; + } + } + _ => (), + } + } + + EventResult::Ignored + } + + fn required_size(&mut self, _: Vec2) -> Vec2 { + self.board.size.map_x(|x| 2 * x) + } +} diff --git a/cursive/examples/mines/main.rs b/cursive/examples/mines/main.rs index 02c49bc1..a164399c 100644 --- a/cursive/examples/mines/main.rs +++ b/cursive/examples/mines/main.rs @@ -1,15 +1,12 @@ -mod game; +mod board; -use crate::game::{AutoRevealResult, CellContent, RevealResult}; +use crate::board::model::{self}; use cursive::{ - direction::Direction, - event::{Event, EventResult, MouseButton, MouseEvent}, - theme::{BaseColor, Color, ColorStyle}, - view::CannotFocus, - views::{Button, Dialog, LinearLayout, Panel, SelectView}, - Cursive, Printer, Vec2, + Cursive, + Vec2, views::{Button, Dialog, LinearLayout, Panel, SelectView}, }; use std::ops::{Index, IndexMut}; +use board::view::BoardView; use cursive_core::traits::Nameable; fn main() { @@ -31,19 +28,6 @@ fn main() { siv.run(); } -fn show_controls(s: &mut Cursive) { - s.add_layer(Dialog::info( - "Controls: -Reveal cell: left click -Mark as mine: right-click -Reveal nearby unmarked cells: middle-click", - ).title("Controls")) -} - -fn show_scores(s: &mut Cursive) { - s.add_layer(Dialog::info("Not yet!").title("Scores")) -} - fn show_options(siv: &mut Cursive) { siv.add_layer( Dialog::new() @@ -52,21 +36,21 @@ fn show_options(siv: &mut Cursive) { SelectView::new() .item( "Easy: 8x8, 10 mines", - game::Options { + model::Options { size: Vec2::new(8, 8), mines: 10, }, ) .item( "Medium: 16x16, 40 mines", - game::Options { + model::Options { size: Vec2::new(16, 16), mines: 40, }, ) .item( "Difficult: 24x24, 99 mines", - game::Options { + model::Options { size: Vec2::new(24, 24), mines: 99, }, @@ -80,248 +64,21 @@ fn show_options(siv: &mut Cursive) { ); } -#[derive(Clone, Copy, PartialEq)] -enum Cell { - Unknown, - Flag, - Visible(usize), - Bomb, -} - -// NOTE: coordinates [y][x] -struct Overlay(Vec>); - -impl Overlay { - pub fn new(size: Vec2) -> Self { - Self(vec![vec![Cell::Unknown; size.x]; size.y]) - } -} - -impl Index for Overlay { - type Output = Cell; - - fn index(&self, pos: Vec2) -> &Self::Output { - &self.0[pos.y][pos.x] - } -} - -impl IndexMut for Overlay { - fn index_mut(&mut self, pos: Vec2) -> &mut Self::Output { - &mut self.0[pos.y][pos.x] - } -} - -struct BoardView { - // Actual board, unknown to the player. - board: game::Board, - - // Visible board - overlay: Overlay, - - focused: Option, - enabled: bool, - _missing_mines: usize, -} - -impl BoardView { - pub fn new(options: game::Options) -> Self { - let board = game::Board::new(options); - BoardView { - board, - overlay: Overlay::new(options.size), - focused: None, - enabled: true, - _missing_mines: options.mines, - } - } - - fn get_cell(&self, mouse_pos: Vec2, offset: Vec2) -> Option { - let pos = mouse_pos.checked_sub(offset)?.map_x(|x| x / 2); - if pos.fits_in(self.board.size - (1, 1)) { - Some(pos) - } else { - None - } - } - - fn flag(&mut self, pos: Vec2) { - let new_cell = match self.overlay[pos] { - Cell::Unknown => Cell::Flag, - Cell::Flag => Cell::Unknown, - other => other, - }; - self.overlay[pos] = new_cell; - } - - fn reveal(&mut self, pos: Vec2) -> EventResult { - if self.overlay[pos] != Cell::Unknown { - return EventResult::Consumed(None); - } - - match self.board.reveal(pos) { - RevealResult::Revealed(opened_cells) => { - self.open_cells(opened_cells); - EventResult::Consumed(None) - } - RevealResult::Loss => { - self.open_all_mines(); - Self::result_loss() - } - RevealResult::Victory => { - self.open_all_cells(); - Self::result_victory() - } - } - } - - fn auto_reveal(&mut self, pos: Vec2) -> EventResult { - match self.board.auto_reveal(pos) { - AutoRevealResult::Revealed(opened_cells) => { - self.open_cells(opened_cells); - return EventResult::Consumed(None); - } - AutoRevealResult::Victory => { - self.open_all_cells(); - Self::result_victory() - } - } - } - - fn result_loss() -> EventResult { - EventResult::with_cb(|s| Self::make_end_game_result(s, "Defeted")) - } - - fn result_victory() -> EventResult { - EventResult::with_cb(|s| Self::make_end_game_result(s, "Victory!")) - } - - fn make_end_game_result(s: &mut Cursive, button_label: &'static str) { - s.call_on_name("board", |b: &mut BoardView| b.enabled = false); - Self::change_game_button_label(s, button_label); - } - fn change_game_button_label(s: &mut Cursive, label: &str) { - s.call_on_name("game", |d: &mut Dialog| { - d.buttons_mut().last().expect("button must exists").set_label(label); - }); - } - - fn open_cells(&mut self, opened_cells: Vec) { - for pos in opened_cells { - let CellContent::Free(near_bombs) = self.board[pos].content else { - panic!("must be variant CellContent::Free()") - }; - - self.overlay[pos] = Cell::Visible(near_bombs); - } - } - - fn open_all_cells(&mut self) { - for pos in self.board.field.all_cell_pos_iter() { - self.overlay[pos] = match self.board[pos].content { - CellContent::Bomb => Cell::Bomb, - CellContent::Free(near_bombs) => Cell::Visible(near_bombs), - }; - } - } - - fn open_all_mines(&mut self) { - for pos in self.board.field.all_cell_pos_iter() { - if let Cell::Bomb = self.overlay[pos] { - self.overlay[pos] = Cell::Bomb; - } - } - } +fn show_controls(s: &mut Cursive) { + s.add_layer(Dialog::info( + "Controls: +Reveal cell: left click +Mark as mine: right-click +Reveal nearby unmarked cells: middle-click", + ).title("Controls")) } -impl cursive::view::View for BoardView { - fn draw(&self, printer: &Printer) { - for (i, cell) in self.overlay.0.iter().flatten().enumerate() { - let x = (i % self.board.size.x) * 2; - let y = i / self.board.size.x; - - let text = match *cell { - Cell::Unknown => " □", - Cell::Flag => " ■", - Cell::Visible(n) => [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n], - Cell::Bomb => "\u{01F4A3} " - }; - - let color = match *cell { - Cell::Unknown => Color::RgbLowRes(3, 3, 3), - Cell::Flag => Color::RgbLowRes(4, 4, 2), - Cell::Visible(1) => Color::RgbLowRes(3, 5, 3), - Cell::Visible(2) => Color::RgbLowRes(5, 5, 3), - Cell::Visible(3) => Color::RgbLowRes(5, 4, 3), - Cell::Visible(4) => Color::RgbLowRes(5, 3, 3), - Cell::Visible(5) => Color::RgbLowRes(5, 2, 2), - Cell::Visible(6) => Color::RgbLowRes(5, 0, 1), - Cell::Visible(7) => Color::RgbLowRes(5, 0, 2), - Cell::Visible(8) => Color::RgbLowRes(5, 0, 3), - _ => Color::Dark(BaseColor::White), - }; - - printer.with_color( - ColorStyle::new(Color::Dark(BaseColor::Black), color), - |printer| printer.print((x, y), text), - ); - } - } - - fn take_focus(&mut self, _: Direction) -> Result { - Ok(EventResult::Consumed(None)) - } - - fn on_event(&mut self, event: Event) -> EventResult { - if self.enabled { - match event { - Event::Mouse { - offset, - position, - event: MouseEvent::Press(_btn), - } => { - // Get cell for position - if let Some(pos) = self.get_cell(position, offset) { - self.focused = Some(pos); - return EventResult::Consumed(None); - } - } - Event::Mouse { - offset, - position, - event: MouseEvent::Release(btn), - } => { - // Get cell for position - if let Some(pos) = self.get_cell(position, offset) { - if self.focused == Some(pos) { - // We got a click here! - match btn { - MouseButton::Left => return self.reveal(pos), - MouseButton::Right => { - self.flag(pos); - return EventResult::Consumed(None); - } - MouseButton::Middle => return self.auto_reveal(pos), - _ => (), - } - } - - self.focused = None; - } - } - _ => (), - } - } - - EventResult::Ignored - } - - fn required_size(&mut self, _: Vec2) -> Vec2 { - self.board.size.map_x(|x| 2 * x) - } +fn show_scores(s: &mut Cursive) { + s.add_layer(Dialog::info("Not yet!").title("Scores")) } -fn new_game(siv: &mut Cursive, options: game::Options) { - let _board = game::Board::new(options); +fn new_game(siv: &mut Cursive, options: model::Options) { + let _board = model::Board::new(options); let dialog = Dialog::new() .title("Minesweeper") From a45e6cd2a4a4449a2da690e59384979d96ba2325 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sun, 9 Jun 2024 00:08:30 +0700 Subject: [PATCH 05/14] 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. --- cursive/examples/mines/board/model.rs | 109 +++++++++++------ cursive/examples/mines/board/view.rs | 162 ++++++-------------------- cursive/examples/mines/main.rs | 1 - 3 files changed, 109 insertions(+), 163 deletions(-) diff --git a/cursive/examples/mines/board/model.rs b/cursive/examples/mines/board/model.rs index 2f435704..214b9806 100644 --- a/cursive/examples/mines/board/model.rs +++ b/cursive/examples/mines/board/model.rs @@ -1,11 +1,13 @@ use rand::seq::SliceRandom; use std::ops::{Index, IndexMut}; +use std::slice::Iter; use ahash::AHashSet; use cursive::Vec2; use cursive_core::Rect; +use crate::board::model::CellState::*; #[derive(Clone, Copy)] -pub struct Options { +pub(crate) struct Options { pub size: Vec2, pub mines: usize, } @@ -17,22 +19,31 @@ pub enum CellContent { 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 { - is_opened: bool, - pub content: CellContent, + pub(crate) state: CellState, + + pub(crate) content: CellContent, } impl Cell { pub fn new(content: CellContent) -> Self { Self { - is_opened: false, + state: Closed, content, } } } -pub struct Field { +pub(crate) struct Field { size: Vec2, cells: Vec, } @@ -145,6 +156,7 @@ pub struct Board { pub bombs_count: usize, pub field: Field, is_bomb_placed: bool, + pub is_ended: bool, } impl Board { @@ -164,11 +176,13 @@ impl Board { bombs_count: options.mines, is_bomb_placed: false, field: Field::new(options.size), + is_ended: false, } } - fn check_victory(&self) -> bool { - self.field.cells.iter().filter(|x| matches!(x.content, CellContent::Free(_))).all(|x| x.is_opened) + pub fn iter(&self) -> Iter<'_, Cell> + { + self.field.cells.iter() } fn place_bombs_if_needed(&mut self, pos: Vec2) { @@ -184,55 +198,81 @@ impl Board { let cell = &mut self.field[pos]; match cell.content { - CellContent::Bomb => RevealResult::Loss, - CellContent::Free(_) => { - cell.is_opened = true; + CellContent::Bomb => { + self.is_ended = true; - match self.auto_reveal(pos) { - AutoRevealResult::Victory => RevealResult::Victory, - AutoRevealResult::Revealed(mut opened_poses) => { - opened_poses.push(pos); + RevealResult::Loss + } + CellContent::Free(_) => { + cell.state = Opened; - RevealResult::Revealed(opened_poses) - } - } + self.auto_reveal(pos) } } } - pub fn auto_reveal(&mut self, pos: Vec2) -> AutoRevealResult { + 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(0) = self.field[pos].content { - for near_pos in self.field.neighbours(pos) { - self.check_neighbours_for_auto_reveal(near_pos, &mut opened); + 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 => AutoRevealResult::Victory, - false => AutoRevealResult::Revealed(opened.into_iter().collect()) + true => { + self.is_ended = true; + RevealResult::Victory + } + false => RevealResult::Revealed } } - fn check_neighbours_for_auto_reveal(&mut self, pos: Vec2, opened: &mut AHashSet) { - if self.field[pos].is_opened || self.field[pos].content == CellContent::Bomb || opened.contains(&pos) { - return; + 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, } + } - debug_assert!(matches!(self.field[pos].content, CellContent::Free(_)), "failed logic for auto reveal"); + // NOTE: Returned when was bomb + fn check_neighbours_for_auto_reveal(&mut self, pos: Vec2, opened: &mut AHashSet) -> bool { + if self.field[pos].state == Opened || opened.contains(&pos) { + return true; + } - self.field[pos].is_opened = true; + self.field[pos].state = Opened; opened.insert(pos); - if let CellContent::Free(0) = self.field[pos].content { - for pos in self.field.neighbours(pos) { - self.check_neighbours_for_auto_reveal(pos, opened); + 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 for Board { @@ -244,12 +284,7 @@ impl Index for Board { } pub enum RevealResult { - Revealed(Vec), + Revealed, Loss, Victory, -} - -pub enum AutoRevealResult { - Revealed(Vec), - Victory, } \ No newline at end of file diff --git a/cursive/examples/mines/board/view.rs b/cursive/examples/mines/board/view.rs index 84de6d13..b4d596a0 100644 --- a/cursive/examples/mines/board/view.rs +++ b/cursive/examples/mines/board/view.rs @@ -1,62 +1,26 @@ use cursive_core::{Cursive, Printer, Vec2}; -use std::ops::{Index, IndexMut}; use cursive_core::direction::Direction; use cursive_core::event::{Event, EventResult, MouseButton, MouseEvent}; use cursive_core::theme::{BaseColor, Color, ColorStyle}; use cursive_core::view::CannotFocus; use cursive_core::views::Dialog; -use crate::board::model; -use crate::board::model::{AutoRevealResult, CellContent, RevealResult}; - -#[derive(Clone, Copy, PartialEq)] -enum Cell { - Unknown, - Flag, - Visible(usize), - Bomb, -} - -// NOTE: coordinates [y][x] -struct Overlay(Vec>); - -impl Overlay { - pub fn new(size: Vec2) -> Self { - Self(vec![vec![Cell::Unknown; size.x]; size.y]) - } -} - -impl Index for Overlay { - type Output = Cell; - - fn index(&self, pos: Vec2) -> &Self::Output { - &self.0[pos.y][pos.x] - } -} - -impl IndexMut for Overlay { - fn index_mut(&mut self, pos: Vec2) -> &mut Self::Output { - &mut self.0[pos.y][pos.x] - } -} +use crate::board::model::{Board, CellContent, CellState, Options, RevealResult}; pub(crate) struct BoardView { // Actual board, unknown to the player. - board: model::Board, - - // Visible board - overlay: Overlay, + board: Board, focused: Option, enabled: bool, + _missing_mines: usize, } impl BoardView { - pub(crate) fn new(options: model::Options) -> Self { - let board = model::Board::new(options); + pub(crate) fn new(options: Options) -> Self { + let board = Board::new(options); BoardView { board, - overlay: Overlay::new(options.size), focused: None, enabled: true, _missing_mines: options.mines, @@ -73,54 +37,23 @@ impl BoardView { } fn flag(&mut self, pos: Vec2) { - let new_cell = match self.overlay[pos] { - Cell::Unknown => Cell::Flag, - Cell::Flag => Cell::Unknown, - other => other, - }; - self.overlay[pos] = new_cell; + self.board.toggle_flag(pos); } fn reveal(&mut self, pos: Vec2) -> EventResult { - if self.overlay[pos] != Cell::Unknown { - return EventResult::Consumed(None); - } - - match self.board.reveal(pos) { - RevealResult::Revealed(opened_cells) => { - self.open_cells(opened_cells); - EventResult::Consumed(None) - } - RevealResult::Loss => { - self.open_all_mines(); - Self::result_loss() - } - RevealResult::Victory => { - self.open_all_cells(); - Self::result_victory() - } - } + Self::handle_reveal_result(self.board.reveal(pos)) } fn auto_reveal(&mut self, pos: Vec2) -> EventResult { - match self.board.auto_reveal(pos) { - AutoRevealResult::Revealed(opened_cells) => { - self.open_cells(opened_cells); - return EventResult::Consumed(None); - } - AutoRevealResult::Victory => { - self.open_all_cells(); - Self::result_victory() - } - } + Self::handle_reveal_result(self.board.auto_reveal(pos)) } - fn result_loss() -> EventResult { - EventResult::with_cb(|s| Self::make_end_game_result(s, "Defeted")) - } - - fn result_victory() -> EventResult { - EventResult::with_cb(|s| Self::make_end_game_result(s, "Victory!")) + fn handle_reveal_result(reveal_result: RevealResult) -> EventResult { + match reveal_result { + RevealResult::Revealed => EventResult::Consumed(None), + RevealResult::Victory => EventResult::with_cb(|s| Self::make_end_game_result(s, "Victory!")), + RevealResult::Loss => EventResult::with_cb(|s| Self::make_end_game_result(s, "Defeted")), + } } fn make_end_game_result(s: &mut Cursive, button_label: &'static str) { @@ -132,59 +65,38 @@ impl BoardView { d.buttons_mut().last().expect("button must exists").set_label(label); }); } - - fn open_cells(&mut self, opened_cells: Vec) { - for pos in opened_cells { - let CellContent::Free(near_bombs) = self.board[pos].content else { - panic!("must be variant CellContent::Free()") - }; - - self.overlay[pos] = Cell::Visible(near_bombs); - } - } - - fn open_all_cells(&mut self) { - for pos in self.board.field.all_cell_pos_iter() { - self.overlay[pos] = match self.board[pos].content { - CellContent::Bomb => Cell::Bomb, - CellContent::Free(near_bombs) => Cell::Visible(near_bombs), - }; - } - } - - fn open_all_mines(&mut self) { - for pos in self.board.field.all_cell_pos_iter() { - if let Cell::Bomb = self.overlay[pos] { - self.overlay[pos] = Cell::Bomb; - } - } - } } impl cursive::view::View for BoardView { fn draw(&self, printer: &Printer) { - for (i, cell) in self.overlay.0.iter().flatten().enumerate() { + use CellState::*; + use CellContent::*; + + for (i, cell) in self.board.iter().enumerate() { let x = (i % self.board.size.x) * 2; let y = i / self.board.size.x; - let text = match *cell { - Cell::Unknown => " □", - Cell::Flag => " ■", - Cell::Visible(n) => [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n], - Cell::Bomb => "\u{01F4A3} " + let text = match (cell.state, cell.content, self.board.is_ended) { + (Closed, _, false) => " □", + (Marked, _, false) => " ■", + (Opened, Free(n), false) | (_, Free(n), true) => [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n], + (Opened, Bomb, false) | (_, Bomb, true) => "\u{01F4A3} " }; - let color = match *cell { - Cell::Unknown => Color::RgbLowRes(3, 3, 3), - Cell::Flag => Color::RgbLowRes(4, 4, 2), - Cell::Visible(1) => Color::RgbLowRes(3, 5, 3), - Cell::Visible(2) => Color::RgbLowRes(5, 5, 3), - Cell::Visible(3) => Color::RgbLowRes(5, 4, 3), - Cell::Visible(4) => Color::RgbLowRes(5, 3, 3), - Cell::Visible(5) => Color::RgbLowRes(5, 2, 2), - Cell::Visible(6) => Color::RgbLowRes(5, 0, 1), - Cell::Visible(7) => Color::RgbLowRes(5, 0, 2), - Cell::Visible(8) => Color::RgbLowRes(5, 0, 3), + let color = match (cell.state, cell.content, self.board.is_ended) { + (Closed, _, false) => Color::RgbLowRes(3, 3, 3), + (Marked, _, false) => Color::RgbLowRes(4, 4, 2), + (Opened, Free(n), false) | (_, Free(n), true) => match n { + 1 => Color::RgbLowRes(3, 5, 3), + 2 => Color::RgbLowRes(5, 5, 3), + 3 => Color::RgbLowRes(5, 4, 3), + 4 => Color::RgbLowRes(5, 3, 3), + 5 => Color::RgbLowRes(5, 2, 2), + 6 => Color::RgbLowRes(5, 0, 1), + 7 => Color::RgbLowRes(5, 0, 2), + 8 => Color::RgbLowRes(5, 0, 3), + _ => Color::Dark(BaseColor::White), + } _ => Color::Dark(BaseColor::White), }; diff --git a/cursive/examples/mines/main.rs b/cursive/examples/mines/main.rs index a164399c..93cb3f04 100644 --- a/cursive/examples/mines/main.rs +++ b/cursive/examples/mines/main.rs @@ -5,7 +5,6 @@ use cursive::{ Cursive, Vec2, views::{Button, Dialog, LinearLayout, Panel, SelectView}, }; -use std::ops::{Index, IndexMut}; use board::view::BoardView; use cursive_core::traits::Nameable; From 1dc2c8743ee4a2dea14e7fd960bb0ab420200caa Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 22 Jun 2024 22:55:06 +0700 Subject: [PATCH 06/14] unify Rect use Co-authored-by: Alexandre Bury --- cursive/examples/mines/board/model.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cursive/examples/mines/board/model.rs b/cursive/examples/mines/board/model.rs index 214b9806..b726541e 100644 --- a/cursive/examples/mines/board/model.rs +++ b/cursive/examples/mines/board/model.rs @@ -2,8 +2,7 @@ use rand::seq::SliceRandom; use std::ops::{Index, IndexMut}; use std::slice::Iter; use ahash::AHashSet; -use cursive::Vec2; -use cursive_core::Rect; +use cursive::{Rect, Vec2}; use crate::board::model::CellState::*; #[derive(Clone, Copy)] From 837a20175e7a759f86e281949de35a7fa1ca08c2 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 22 Jun 2024 22:56:10 +0700 Subject: [PATCH 07/14] simplified pub Co-authored-by: Alexandre Bury --- cursive/examples/mines/board/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursive/examples/mines/board/model.rs b/cursive/examples/mines/board/model.rs index b726541e..f190e8ef 100644 --- a/cursive/examples/mines/board/model.rs +++ b/cursive/examples/mines/board/model.rs @@ -6,7 +6,7 @@ use cursive::{Rect, Vec2}; use crate::board::model::CellState::*; #[derive(Clone, Copy)] -pub(crate) struct Options { +pub struct Options { pub size: Vec2, pub mines: usize, } From c5d160ada53cf2c6d5d9050de5c760ddfbc0a875 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 22 Jun 2024 23:00:46 +0700 Subject: [PATCH 08/14] simplified pub Co-authored-by: Alexandre Bury --- cursive/examples/mines/board/model.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cursive/examples/mines/board/model.rs b/cursive/examples/mines/board/model.rs index f190e8ef..e5f9823a 100644 --- a/cursive/examples/mines/board/model.rs +++ b/cursive/examples/mines/board/model.rs @@ -28,9 +28,9 @@ pub enum CellState { #[derive(Copy, Clone)] pub struct Cell { - pub(crate) state: CellState, + pub state: CellState, - pub(crate) content: CellContent, + pub content: CellContent, } impl Cell { From fc48eb5a38f8e0147b6d232886e314a3388022a8 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 22 Jun 2024 23:01:01 +0700 Subject: [PATCH 09/14] simplified pub Co-authored-by: Alexandre Bury --- cursive/examples/mines/board/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursive/examples/mines/board/model.rs b/cursive/examples/mines/board/model.rs index e5f9823a..5d6e28f4 100644 --- a/cursive/examples/mines/board/model.rs +++ b/cursive/examples/mines/board/model.rs @@ -42,7 +42,7 @@ impl Cell { } } -pub(crate) struct Field { +pub struct Field { size: Vec2, cells: Vec, } From abb2b081a1fcd385dec708509cfee7f13750cacd Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 22 Jun 2024 23:26:40 +0700 Subject: [PATCH 10/14] removed redundant code Co-authored-by: Alexandre Bury --- cursive/examples/mines/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cursive/examples/mines/main.rs b/cursive/examples/mines/main.rs index 93cb3f04..a01654c5 100644 --- a/cursive/examples/mines/main.rs +++ b/cursive/examples/mines/main.rs @@ -77,7 +77,6 @@ fn show_scores(s: &mut Cursive) { } fn new_game(siv: &mut Cursive, options: model::Options) { - let _board = model::Board::new(options); let dialog = Dialog::new() .title("Minesweeper") From 4b29584fe5ee0bf424a2c2d2538532a2bbbdf64a Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 22 Jun 2024 23:27:20 +0700 Subject: [PATCH 11/14] simplified pub Co-authored-by: Alexandre Bury --- cursive/examples/mines/board/view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursive/examples/mines/board/view.rs b/cursive/examples/mines/board/view.rs index b4d596a0..f70ed8a8 100644 --- a/cursive/examples/mines/board/view.rs +++ b/cursive/examples/mines/board/view.rs @@ -17,7 +17,7 @@ pub(crate) struct BoardView { } impl BoardView { - pub(crate) fn new(options: Options) -> Self { + pub fn new(options: Options) -> Self { let board = Board::new(options); BoardView { board, From fcf90fc3190bc9602513da280dde6445c46e4ffe Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sat, 22 Jun 2024 23:27:41 +0700 Subject: [PATCH 12/14] simplified pub Co-authored-by: Alexandre Bury --- cursive/examples/mines/board/view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursive/examples/mines/board/view.rs b/cursive/examples/mines/board/view.rs index f70ed8a8..4f33344f 100644 --- a/cursive/examples/mines/board/view.rs +++ b/cursive/examples/mines/board/view.rs @@ -6,7 +6,7 @@ use cursive_core::view::CannotFocus; use cursive_core::views::Dialog; use crate::board::model::{Board, CellContent, CellState, Options, RevealResult}; -pub(crate) struct BoardView { +pub struct BoardView { // Actual board, unknown to the player. board: Board, From fc4dee4fb61695dbb5bde3b6b292ad42b1d66161 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sun, 7 Jul 2024 20:14:30 +0700 Subject: [PATCH 13/14] removed redundant allocation of new Vec Co-authored-by: Alexandre Bury --- cursive/examples/mines/board/model.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/cursive/examples/mines/board/model.rs b/cursive/examples/mines/board/model.rs index 5d6e28f4..f45c8a96 100644 --- a/cursive/examples/mines/board/model.rs +++ b/cursive/examples/mines/board/model.rs @@ -71,28 +71,18 @@ impl Field { .collect(); // init bombs on board - let mut cells = Vec::new(); - for i in 0..self.cells.len() - exclusion_cells.len() { - let cell = if i < bombs_count { - Cell::new(CellContent::Bomb) - } else { - Cell::new(CellContent::Free(0)) - }; - - cells.push(cell); - } - - // shuffle them + 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(); - cells.shuffle(&mut rng); + self.cells.shuffle(&mut rng); // push empty cells near of cursor to avoid bombs in this positions for pos in exclusion_cells { - cells.insert(self.pos_to_cell_idx(pos), Cell::new(CellContent::Free(0))); + self.cells.insert(self.pos_to_cell_idx(pos), Cell::new(CellContent::Free(0))); } - self.cells = cells; - // recalculate near bombs for pos in self.all_cell_pos_iter() { if let CellContent::Free(_) = self[pos].content { From 98c86a9fd2b8431f02521b09a10c2f647d5b7b96 Mon Sep 17 00:00:00 2001 From: Evgeny Khudoba Date: Sun, 7 Jul 2024 20:22:49 +0700 Subject: [PATCH 14/14] Removed redundant space --- cursive/examples/mines/board/view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cursive/examples/mines/board/view.rs b/cursive/examples/mines/board/view.rs index 4f33344f..efae15dd 100644 --- a/cursive/examples/mines/board/view.rs +++ b/cursive/examples/mines/board/view.rs @@ -80,7 +80,7 @@ impl cursive::view::View for BoardView { (Closed, _, false) => " □", (Marked, _, false) => " ■", (Opened, Free(n), false) | (_, Free(n), true) => [" ", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8"][n], - (Opened, Bomb, false) | (_, Bomb, true) => "\u{01F4A3} " + (Opened, Bomb, false) | (_, Bomb, true) => "\u{01F4A3}" }; let color = match (cell.state, cell.content, self.board.is_ended) {