diff --git a/minesweeper-lib/src/board.rs b/minesweeper-lib/src/board.rs index d762e56..9d59836 100644 --- a/minesweeper-lib/src/board.rs +++ b/minesweeper-lib/src/board.rs @@ -6,6 +6,7 @@ use std::{ }; use serde::{Deserialize, Serialize}; +use tinyvec::{array_vec, ArrayVec}; impl From<&Board> for Vec> where @@ -184,8 +185,8 @@ impl Board { point.row < self.rows && point.col < self.cols } - pub fn neighbors(&self, point: &BoardPoint) -> Vec { - let mut neighbors = Vec::::new(); + pub fn neighbors(&self, point: &BoardPoint) -> ArrayVec<[BoardPoint; 8]> { + let mut neighbors = array_vec!([BoardPoint; 8]); let row = point.row; let col = point.col; diff --git a/minesweeper-lib/src/game.rs b/minesweeper-lib/src/game.rs index 4b7b7d2..eb23f49 100644 --- a/minesweeper-lib/src/game.rs +++ b/minesweeper-lib/src/game.rs @@ -9,6 +9,7 @@ use crate::replay::MinesweeperReplay; use anyhow::{bail, Ok, Result}; use rand::{seq::SliceRandom, thread_rng}; use serde::{Deserialize, Serialize}; +use tinyvec::ArrayVec; #[derive(Clone, Copy, Debug)] pub struct MinesweeperOpts { @@ -271,7 +272,7 @@ impl Minesweeper { .iter() .copied() .filter(|c| !self.board[*c].1.revealed && !self.players[player].flags.contains(c)) - .collect::>(); + .collect::>(); let has_mine = unflagged_neighbors .iter() .copied() @@ -426,7 +427,7 @@ impl Minesweeper { &mut self, unplanted_mines: usize, first_cell: &BoardPoint, - neighbors: Vec, + neighbors: ArrayVec<[BoardPoint; 8]>, ) { if unplanted_mines == 0 { return; diff --git a/minesweeper-lib/src/replay/analysis.rs b/minesweeper-lib/src/replay/analysis.rs index f6d6491..c2d90ea 100644 --- a/minesweeper-lib/src/replay/analysis.rs +++ b/minesweeper-lib/src/replay/analysis.rs @@ -92,6 +92,13 @@ impl MinesweeperAnalysis { AnalysisCell::Revealed(Cell::Empty(_)) ) }; + let is_mine = |point: &BoardPoint, analysis_board: &Board| { + matches!(analysis_board[point], AnalysisCell::Revealed(Cell::Mine)) + || matches!( + analysis_board[point], + AnalysisCell::Hidden(AnalyzedCell::Mine) + ) + }; // loop over replay, updating log for (i, current_log_entry) in log.iter_mut().enumerate() { @@ -116,7 +123,14 @@ impl MinesweeperAnalysis { }; current_log_entry.push((bp, LogEntry { from, to: None })); } - analysis_board[bp] = AnalysisCell::Revealed(rc.contents); + let mut contents = rc.contents; + // reduce cell numbers by the number of mines + analysis_board + .neighbors(&bp) + .iter() + .filter(|&np| is_mine(np, &analysis_board)) + .for_each(|_| contents = contents.decrement()); + analysis_board[bp] = AnalysisCell::Revealed(contents); }); let mut points_to_analyze = new_revealed .iter() @@ -138,7 +152,7 @@ impl MinesweeperAnalysis { .filter(|np| !points_to_analyze.contains(np)) .filter(|np| is_empty(np, &analysis_board)) .filter(|np| has_undetermined_neighbor(np, &analysis_board)) - .collect::>() + .collect::>() }) .collect::>(); additional_points.into_iter().for_each(|p| { @@ -151,7 +165,7 @@ impl MinesweeperAnalysis { .filter(|bp| !points_to_analyze.contains(bp)) .filter(|bp| is_empty(bp, &analysis_board)) .filter(|bp| has_undetermined_neighbor(bp, &analysis_board)) - .collect::>(); + .collect::>(); recheck.into_iter().for_each(|bp| { let _ = points_to_analyze.insert(bp); }); @@ -184,23 +198,7 @@ impl MinesweeperAnalysis { fifty_fiftys: &mut TinyVec<[UnorderedPair; 24]>, ) { let mut has_updates = false; - let mut points_to_reanalyze = HashSet::new(); - - let add_to_reanalyze_if_has_unrevealed_neighbors = - |point: &BoardPoint, - analysis_board: &mut Board, - points_to_reanalyze: &mut HashSet| { - let cell = analysis_board[point]; - if matches!(cell, AnalysisCell::Revealed(Cell::Empty(_))) - && analysis_board - .neighbors(point) - .iter() - .map(|&nbp| analysis_board[nbp]) - .any(|c| matches!(c, AnalysisCell::Hidden(AnalyzedCell::Undetermined))) - { - points_to_reanalyze.insert(*point); - } - }; + let mut points_to_reanalyze = points_to_analyze.iter().copied().collect::>(); points_to_analyze.into_iter().for_each(|bp| { let res = Self::perform_checks(&bp, analysis_board, fifty_fiftys); @@ -214,18 +212,10 @@ impl MinesweeperAnalysis { fifty_fiftys.push(pair); // add neighbors to points_to_reanalyze analysis_board.neighbors(point1).iter().for_each(|nbp| { - add_to_reanalyze_if_has_unrevealed_neighbors( - nbp, - analysis_board, - &mut points_to_reanalyze, - ) + let _ = points_to_reanalyze.insert(*nbp); }); analysis_board.neighbors(point2).iter().for_each(|nbp| { - add_to_reanalyze_if_has_unrevealed_neighbors( - nbp, - analysis_board, - &mut points_to_reanalyze, - ) + let _ = points_to_reanalyze.insert(*nbp); }); } res.guaranteed_plays.into_iter().for_each(|(point, ac)| { @@ -247,56 +237,56 @@ impl MinesweeperAnalysis { current_log_entry.push((point, LogEntry { from, to: Some(ac) })); // add neighbors to points_to_reanalyze analysis_board.neighbors(&point).iter().for_each(|nbp| { - add_to_reanalyze_if_has_unrevealed_neighbors( - nbp, - analysis_board, - &mut points_to_reanalyze, - ) + if matches!(ac, AnalyzedCell::Mine) { + if let AnalysisCell::Revealed(c) = analysis_board[nbp] { + // reduce neighboring cell numbers + analysis_board[nbp] = AnalysisCell::Revealed(c.decrement()); + } + } + let _ = points_to_reanalyze.insert(*nbp); }); }); }); if !has_updates { return; } + let points_to_reanalyze = points_to_reanalyze + .into_iter() + .filter(|point| { + let cell = analysis_board[point]; + matches!(cell, AnalysisCell::Revealed(Cell::Empty(_))) + && analysis_board + .neighbors(point) + .iter() + .map(|&nbp| analysis_board[nbp]) + .any(|c| matches!(c, AnalysisCell::Hidden(AnalyzedCell::Undetermined))) + }) + .collect(); Self::analyze_cells( - points_to_reanalyze.into_iter().collect(), + points_to_reanalyze, analysis_board, current_log_entry, fifty_fiftys, ) } - fn neighbor_mines(point: &BoardPoint, analysis_board: &Board) -> u8 { - let neighbors = analysis_board.neighbors(point); - neighbors.iter().fold(0_u8, |num_mines, &p| { - let ncell = analysis_board[p]; - match ncell { - AnalysisCell::Hidden(AnalyzedCell::Mine) => num_mines + 1, - AnalysisCell::Revealed(Cell::Mine) => num_mines + 1, - _ => num_mines, - } - }) - } - fn neighbor_info( point: &BoardPoint, analysis_board: &Board, - ) -> (usize, ArrayVec<[BoardPoint; 8]>, ArrayVec<[BoardPoint; 8]>) { + ) -> (ArrayVec<[BoardPoint; 8]>, ArrayVec<[BoardPoint; 8]>) { let neighbors = analysis_board.neighbors(point); neighbors.iter().fold( - (0, array_vec!([BoardPoint; 8]), array_vec!([BoardPoint; 8])), - |(mut num_mines, mut revealed_points, mut undetermined_points), p| { + (array_vec!([BoardPoint; 8]), array_vec!([BoardPoint; 8])), + |(mut revealed_points, mut undetermined_points), p| { let ncell = analysis_board[p]; match ncell { AnalysisCell::Hidden(AnalyzedCell::Undetermined) => { undetermined_points.push(*p) } - AnalysisCell::Hidden(AnalyzedCell::Mine) => num_mines += 1, - AnalysisCell::Hidden(_) => {} - AnalysisCell::Revealed(Cell::Mine) => num_mines += 1, AnalysisCell::Revealed(Cell::Empty(_)) => revealed_points.push(*p), + _ => {} }; - (num_mines, revealed_points, undetermined_points) + (revealed_points, undetermined_points) }, ) } @@ -333,11 +323,9 @@ impl MinesweeperAnalysis { found_fifty_fiftys: None, }; - let (num_mines, revealed_points, undetermined_points) = - Self::neighbor_info(point, analysis_board); + let (revealed_points, undetermined_points) = Self::neighbor_info(point, analysis_board); let cell_num = Self::cell_to_num(cell); - let cell_num = cell_num - num_mines; if cell_num == 0 { analysis_result.guaranteed_plays.append( &mut undetermined_points @@ -364,23 +352,45 @@ impl MinesweeperAnalysis { assert!(cell_num < num_undetermined); let fifty_fifty_pairs = find_fifty_fifty_pairs(&undetermined_points); - - let num_fifty_fiftys = fifty_fifty_pairs.len(); - let non_fifty_fiftys = undetermined_points - .iter() - .filter(|&p| { - !fifty_fifty_pairs + let (non_fifty_fiftys, fifty_fifty_points) = undetermined_points.iter().fold( + (array_vec!([BoardPoint; 8]), array_vec!([BoardPoint; 8])), + |(mut non_fifty_fiftys, mut fifty_fifty_points), p| { + if fifty_fifty_pairs .iter() .any(|pair| p == pair.ref_a() || p == pair.ref_b()) - }) - .copied() - .collect::>(); + { + fifty_fifty_points.push(*p); + } else { + non_fifty_fiftys.push(*p); + } + (non_fifty_fiftys, fifty_fifty_points) + }, + ); + let num_unique_fifty_fiftys = fifty_fifty_points.len() / 2; - if non_fifty_fiftys.is_empty() { + if cell_num == 1 && fifty_fifty_points.len() == 3 { + // special case - overlapping 5050s next to 1 + analysis_result.guaranteed_plays.append( + &mut fifty_fifty_points + .into_iter() + .map(|p| { + let overlap = fifty_fifty_pairs + .iter() + .filter(|up| up.ref_a() == &p || up.ref_b() == &p) + .count() + > 1; + if overlap { + (p, AnalyzedCell::Mine) + } else { + (p, AnalyzedCell::Empty) + } + }) + .collect(), + ); return analysis_result; } - if cell_num <= num_fifty_fiftys { + if cell_num == num_unique_fifty_fiftys { // all non-5050 cells are guaranteed plays analysis_result.guaranteed_plays.append( &mut non_fifty_fiftys @@ -391,7 +401,7 @@ impl MinesweeperAnalysis { return analysis_result; } - if cell_num == num_undetermined - num_fifty_fiftys { + if cell_num - num_unique_fifty_fiftys == non_fifty_fiftys.len() { // all non-5050 cells are guaranteed mine analysis_result.guaranteed_plays.append( &mut non_fifty_fiftys @@ -402,41 +412,7 @@ impl MinesweeperAnalysis { return analysis_result; } - if num_fifty_fiftys > cell_num { - let mut seen = HashSet::new(); - let mut overlap: BoardPoint = BoardPoint { row: 255, col: 255 }; - let fifty_fifty_cells = fifty_fifty_pairs - .into_iter() - .flat_map(|pair| [*pair.ref_a(), *pair.ref_b()]) - .filter(|p| { - if !seen.insert(*p) { - overlap = *p; - false - } else { - true - } - }) - .collect::>(); - if cell_num == 1 && fifty_fifty_cells.len() == 2 { - analysis_result.guaranteed_plays.append( - &mut fifty_fifty_cells - .into_iter() - .map(|p| { - if p == overlap { - (p, AnalyzedCell::Mine) - } else { - (p, AnalyzedCell::Empty) - } - }) - .collect(), - ); - return analysis_result; - } else { - return analysis_result; - } - } - - if cell_num - num_fifty_fiftys == 1 && non_fifty_fiftys.len() == 2 { + if cell_num - num_unique_fifty_fiftys == 1 && non_fifty_fiftys.len() == 2 { // new 5050 found general case let pair = UnorderedPair::new(non_fifty_fiftys[0], non_fifty_fiftys[1]); if !fifty_fiftys.contains(&pair) { @@ -446,18 +422,11 @@ impl MinesweeperAnalysis { return analysis_result; } - // check for "1" next to 2 undetermined cells - it's a local 5050 - // find all revealed "1"s with 2 or more undetermined cells as neighbors + // find all revealed "1"s with 2 or more undetermined cells as neighbors - treat as 5050 let mut seen = array_vec!([BoardPoint; 8] => *point); let local_ff_points = revealed_points .into_iter() - .filter(|p| match analysis_board[*p] { - AnalysisCell::Revealed(Cell::Empty(1)) => true, - AnalysisCell::Revealed(Cell::Empty(x)) => { - x - Self::neighbor_mines(p, analysis_board) == 1 - } - _ => false, - }) + .filter(|p| matches!(analysis_board[*p], AnalysisCell::Revealed(Cell::Empty(1)))) .filter(|p| { let neighbors = undetermined_points .iter() @@ -485,6 +454,7 @@ impl MinesweeperAnalysis { && cell_num - local_ff_points.len() == 1 && not_ff.len() == 1 { + // all non-local-5050s are guaranteed mines analysis_result.guaranteed_plays.append(&mut not_ff); return analysis_result; }; diff --git a/web/src/app/minesweeper/entry.rs b/web/src/app/minesweeper/entry.rs index bdaba9b..b831c7e 100644 --- a/web/src/app/minesweeper/entry.rs +++ b/web/src/app/minesweeper/entry.rs @@ -221,7 +221,6 @@ pub fn PresetButtons( let class_signal = move |mode: GameMode| { let selected = selected.get(); - log::debug!("Render mode: {:?} <> {:?}", mode, selected); if mode == selected { button_class!( "w-full rounded rounded-lg", @@ -413,7 +412,6 @@ pub fn JoinOrCreateGame() -> impl IntoView { Effect::new(move |_| { let mode = selected_mode.get(); - log::debug!("Stored mode: {:?}", mode); if mode != GameMode::Custom { let mode_settings = GameSettings::from(mode); set_rows(mode_settings.rows); diff --git a/web/src/app/minesweeper/game.rs b/web/src/app/minesweeper/game.rs index 48726ee..1721524 100644 --- a/web/src/app/minesweeper/game.rs +++ b/web/src/app/minesweeper/game.rs @@ -27,7 +27,10 @@ use super::{ #[cfg(feature = "ssr")] use crate::backend::{AuthSession, GameManager}; -use crate::messages::{ClientMessage, GameMessage}; +use crate::{ + button_class, + messages::{ClientMessage, GameMessage}, +}; #[cfg(feature = "ssr")] use minesweeper_lib::client::ClientPlayer; @@ -516,6 +519,7 @@ fn ReplayGame(replay_data: GameInfoWithLog) -> impl IntoView { let game_info = replay_data.game_info; let game_time = game_time_from_start_end(game_info.start_time, game_info.end_time); let (flag_count, set_flag_count) = create_signal(0); + let (replay_started, set_replay_started) = create_signal(false); let (player_read_signals, player_write_signals) = game_info .players @@ -535,16 +539,6 @@ fn ReplayGame(replay_data: GameInfoWithLog) -> impl IntoView { }) .collect::<(Vec>, Vec>)>(); - let completed_minesweeper = CompletedMinesweeper::from_log( - Board::from_vec(game_info.final_board), - replay_data.log, - game_info.players.into_iter().flatten().collect(), - ); - let replay = completed_minesweeper - .replay(replay_data.player_num.map(|p| p.into())) - .expect("We are guaranteed log is not None") - .with_analysis(); - let cell_row = |(row, cells): (usize, &Vec>)| { view! {
@@ -562,6 +556,19 @@ fn ReplayGame(replay_data: GameInfoWithLog) -> impl IntoView { .map(cell_row) .collect_view(); + let completed_minesweeper = CompletedMinesweeper::from_log( + Board::from_vec(game_info.final_board), + replay_data.log, + game_info.players.into_iter().flatten().collect(), + ); + let replay_data = StoredValue::new(( + completed_minesweeper, + replay_data.player_num, + cell_read_signals, + cell_write_signals, + player_write_signals, + )); + view! { {} @@ -572,13 +579,52 @@ fn ReplayGame(replay_data: GameInfoWithLog) -> impl IntoView { {cells} - + + "Start Replay" + + } + } + > + {move || { + replay_data + .with_value(| + ( + completed_minesweeper, + player_num, + cell_read_signals, + cell_write_signals, + player_write_signals, + )| + { + let replay = completed_minesweeper + .replay(player_num.map(|p| p.into())) + .expect("We are guaranteed log is not None") + .with_analysis(); + view! { + + } + }) + }} + } } diff --git a/web/src/app/minesweeper/replay.rs b/web/src/app/minesweeper/replay.rs index 2b533cb..fa465ea 100644 --- a/web/src/app/minesweeper/replay.rs +++ b/web/src/app/minesweeper/replay.rs @@ -73,7 +73,6 @@ pub fn ReplayControls( let max = replay.len() - 1; let slider_el = NodeRef::::new(); - let (replay_started, set_replay_started) = create_signal(false); let (show_mines, set_show_mines) = create_signal(true); let (show_analysis, set_show_analysis) = create_signal(false); let (is_beginning, set_beginning) = create_signal(true); @@ -139,7 +138,7 @@ pub fn ReplayControls( Effect::new(move |prev| { let show_mines = show_mines.get(); - if replay_started.get_untracked() && prev != Some(show_mines) { + if prev != Some(show_mines) { render_current(); } show_mines @@ -147,7 +146,7 @@ pub fn ReplayControls( Effect::new(move |prev| { let show_analysis = show_analysis.get(); - if replay_started.get_untracked() && prev != Some(show_analysis) { + if prev != Some(show_analysis) { render_current(); } show_analysis @@ -222,109 +221,92 @@ pub fn ReplayControls( view! {
- +
+ + +
+
- - -
- - -
-
- - - -
- {move || { - current_play() - .map(move |play| { - view! { -
- "Player " - {play.player} - ": " - {play.action.to_str()} - " @ Row: " - {play.point.row} - ", Col: " - {play.point.col} -
- } - }) - }} -
+ + +
+ {move || { + current_play() + .map(move |play| { + view! { +
+ "Player " + {play.player} + ": " + {play.action.to_str()} + " @ Row: " + {play.point.row} + ", Col: " + {play.point.col} +
+ } + }) + }}
} }