From 3f1165f59e4a4ac69c5ca268afe9b160e8dd571b Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 12 Mar 2026 19:42:08 +0800 Subject: [PATCH 1/6] Add plan for #135: [Model] Minesweeper Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-03-12-minesweeper-model.md | 193 +++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 docs/plans/2026-03-12-minesweeper-model.md diff --git a/docs/plans/2026-03-12-minesweeper-model.md b/docs/plans/2026-03-12-minesweeper-model.md new file mode 100644 index 00000000..ec5f846e --- /dev/null +++ b/docs/plans/2026-03-12-minesweeper-model.md @@ -0,0 +1,193 @@ +# Plan: Add Minesweeper Model (#135) + +## Overview +Add the Minesweeper Consistency problem as a `SatisfactionProblem`. This is a decision problem: given a partially revealed Minesweeper grid, determine if there exists a valid mine assignment for unrevealed cells satisfying all revealed cell constraints. + +**Reference:** Kaye, R. (2000). "Minesweeper is NP-complete." The Mathematical Intelligencer, 22(2), 9–15. + +## Information Summary + +| Property | Value | +|----------|-------| +| **Struct name** | `Minesweeper` | +| **Category** | `misc/` (unique grid-based input) | +| **Problem type** | Satisfaction (`Metric = bool`) | +| **Type parameters** | None | +| **Variables** | k binary variables (one per unrevealed cell) | +| **dims()** | `vec![2; k]` where k = unrevealed.len() | +| **evaluate()** | Check all revealed cell constraints are satisfied | +| **Complexity** | `O(2^num_unrevealed)` brute-force | +| **Getter** | `num_unrevealed()` → number of unrevealed cells | + +## Steps + +### Step 1: Implement the Model (`src/models/misc/minesweeper.rs`) + +**Struct:** +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Minesweeper { + rows: usize, + cols: usize, + revealed: Vec<(usize, usize, u8)>, // (row, col, mine_count) + unrevealed: Vec<(usize, usize)>, // (row, col) +} +``` + +**Constructor (`new`):** +- Validate: rows, cols > 0 +- Validate: all revealed positions in bounds, counts 0..=8 +- Validate: all unrevealed positions in bounds +- Validate: no overlap between revealed and unrevealed positions +- Store fields + +**Accessors:** +- `rows()`, `cols()`, `revealed()`, `unrevealed()` +- `num_unrevealed()` — for complexity expression + +**`inventory::submit!` for ProblemSchemaEntry:** +```rust +inventory::submit! { + ProblemSchemaEntry { + name: "Minesweeper", + description: "Minesweeper Consistency: determine if a valid mine assignment exists", + fields: &[ + SchemaField { name: "rows", type_name: "usize", description: "Number of rows" }, + SchemaField { name: "cols", type_name: "usize", description: "Number of columns" }, + SchemaField { name: "revealed", type_name: "Vec<(usize,usize,u8)>", description: "Revealed cells (row,col,count)" }, + SchemaField { name: "unrevealed", type_name: "Vec<(usize,usize)>", description: "Unrevealed cell positions" }, + ], + } +} +``` + +**Problem trait impl:** +```rust +impl Problem for Minesweeper { + const NAME: &'static str = "Minesweeper"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.unrevealed.len()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + // For each revealed cell (r, c, count): + // Count how many of its 8 neighbors are unrevealed cells with config[i] == 1 + // If count doesn't match, return false + // Return true if all constraints satisfied + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for Minesweeper {} +``` + +**evaluate() algorithm:** +1. Build a HashMap mapping (row, col) → index into unrevealed list +2. For each revealed cell (r, c, count): + - Count neighbors: iterate over 8 directions (dr, dc) ∈ {-1,0,1}²\{(0,0)} + - For each neighbor (r+dr, c+dc) in bounds: + - If it's in the unrevealed map at index i, add config[i] to sum + - If sum != count, return false +3. Return true + +**Variant complexity:** +```rust +crate::declare_variants! { + Minesweeper => "2^num_unrevealed", +} +``` + +### Step 2: Register the Model + +**`src/models/misc/mod.rs`:** Add `mod minesweeper;` and `pub use minesweeper::Minesweeper;` + +**`src/models/mod.rs`:** Add `Minesweeper` to the `pub use misc::{...}` line. + +### Step 3: Register in CLI + +**`problemreductions-cli/src/dispatch.rs`:** +- In `load_problem()`: add `"Minesweeper" => deser_sat::(data)` +- In `serialize_any_problem()`: add `"Minesweeper" => try_ser::(any)` + +**`problemreductions-cli/src/problem_name.rs`:** +- Add alias: `"minesweeper" => "Minesweeper".to_string()` + +### Step 4: Add CLI Creation Support + +**`problemreductions-cli/src/cli.rs`:** Add new CLI flags: +```rust +/// Revealed cells for Minesweeper (semicolon-separated "row,col,count", e.g., "1,1,1;0,0,2") +#[arg(long)] +pub revealed: Option, +``` +Also add `rows` and `cols` as new `Option` fields. + +Update `all_data_flags_empty()` to check `args.revealed.is_none() && args.rows.is_none() && args.cols.is_none()`. + +**`problemreductions-cli/src/commands/create.rs`:** +- Add `"Minesweeper"` match arm that parses `--rows`, `--cols`, `--revealed` +- Unrevealed cells are computed automatically: all grid cells not in the revealed set +- Add example string in `example_for()` + +### Step 5: Write Unit Tests (`src/unit_tests/models/misc/minesweeper.rs`) + +Tests to write: +1. `test_minesweeper_creation` — construct 3×3 instance, verify dimensions +2. `test_minesweeper_evaluate_satisfiable` — Instance 1 from issue (3×3, center=1, YES) +3. `test_minesweeper_evaluate_unsatisfiable` — Instance 2 from issue (contradictory, NO) +4. `test_minesweeper_classic_pattern` — Instance 3 from issue (classic pattern, YES) +5. `test_minesweeper_serialization` — round-trip serde +6. `test_minesweeper_solver` — BruteForce finds satisfying assignment for Instance 1 +7. `test_minesweeper_variant` — verify variant() returns empty + +Link from model file: +```rust +#[cfg(test)] +#[path = "../../unit_tests/models/misc/minesweeper.rs"] +mod tests; +``` + +Also update `src/unit_tests/models/misc/mod.rs` to include the new test module. + +### Step 6: Write Example (`examples/minesweeper_consistency.rs`) + +Use Instance 3 from the issue (the classic pattern). Create the Minesweeper instance, evaluate a valid and invalid config, use BruteForce to find a satisfying assignment. Output JSON. + +Must have `pub fn run()` and `fn main() { run() }`. + +Register in `Cargo.toml` under `[[example]]`. + +### Step 7: Verify + +```bash +make fmt clippy test +``` + +All must pass. Check that the new model appears in `pred list` output and `pred create Minesweeper` shows help. + +## File Manifest + +| File | Action | +|------|--------| +| `src/models/misc/minesweeper.rs` | CREATE — model implementation | +| `src/models/misc/mod.rs` | EDIT — register module | +| `src/models/mod.rs` | EDIT — add re-export | +| `src/unit_tests/models/misc/minesweeper.rs` | CREATE — unit tests | +| `src/unit_tests/models/misc/mod.rs` | EDIT — register test module | +| `problemreductions-cli/src/dispatch.rs` | EDIT — add load/serialize arms | +| `problemreductions-cli/src/problem_name.rs` | EDIT — add alias | +| `problemreductions-cli/src/cli.rs` | EDIT — add --rows, --cols, --revealed flags | +| `problemreductions-cli/src/commands/create.rs` | EDIT — add Minesweeper creation | +| `examples/minesweeper_consistency.rs` | CREATE — example program | +| `Cargo.toml` | EDIT — register example | + +## Dependencies +- Independent tasks: Steps 1-2 (model), Step 5 (tests skeleton) +- Sequential: Step 3-4 (CLI) depends on Step 2 +- Step 6 (example) depends on Steps 1-2 +- Step 7 (verify) depends on all prior steps From 943e2a7f1f833c42c6448d2fa51906bfc7889f58 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 12 Mar 2026 19:53:13 +0800 Subject: [PATCH 2/6] Implement #135: Add Minesweeper Consistency model Add Minesweeper as a SatisfactionProblem in src/models/misc/. The problem determines if a partially revealed Minesweeper grid has a consistent mine assignment satisfying all revealed cell constraints (NP-complete). - Model with evaluate(), serialization, and declare_variants! - CLI support: create, dispatch, alias resolution - 8 unit tests covering satisfiable/unsatisfiable instances and solver - Example: minesweeper_consistency.rs Co-Authored-By: Claude Opus 4.6 --- examples/minesweeper_consistency.rs | 55 ++++++ problemreductions-cli/src/cli.rs | 10 + problemreductions-cli/src/commands/create.rs | 55 +++++- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/problem_name.rs | 1 + src/lib.rs | 2 +- src/models/misc/minesweeper.rs | 190 +++++++++++++++++++ src/models/misc/mod.rs | 3 + src/models/mod.rs | 2 +- src/unit_tests/models/misc/minesweeper.rs | 159 ++++++++++++++++ 10 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 examples/minesweeper_consistency.rs create mode 100644 src/models/misc/minesweeper.rs create mode 100644 src/unit_tests/models/misc/minesweeper.rs diff --git a/examples/minesweeper_consistency.rs b/examples/minesweeper_consistency.rs new file mode 100644 index 00000000..7287cab0 --- /dev/null +++ b/examples/minesweeper_consistency.rs @@ -0,0 +1,55 @@ +// # Minesweeper Consistency Check +// +// ## This Example +// - Instance: 3x3 grid with a classic pattern +// - Revealed cells: (0,0)=1, (1,0)=1, (1,1)=2, (2,0)=0, (2,1)=1 +// - Unrevealed cells: (0,1), (0,2), (1,2), (2,2) +// - Solution: mines at (0,1) and (1,2) + +use problemreductions::models::misc::Minesweeper; +use problemreductions::solvers::{BruteForce, Solver}; +use problemreductions::traits::Problem; + +pub fn run() { + println!("=== Minesweeper Consistency ===\n"); + + // Grid layout: + // 1 ? ? + // 1 2 ? + // 0 1 ? + let problem = Minesweeper::new( + 3, + 3, + vec![(0, 0, 1), (1, 0, 1), (1, 1, 2), (2, 0, 0), (2, 1, 1)], + vec![(0, 1), (0, 2), (1, 2), (2, 2)], + ); + + println!("Grid: {}x{}", problem.rows(), problem.cols()); + println!("Revealed cells: {:?}", problem.revealed()); + println!("Unrevealed cells: {:?}", problem.unrevealed()); + println!("Variables: {}\n", problem.num_variables()); + + let solver = BruteForce::new(); + match solver.find_satisfying(&problem) { + Some(solution) => { + println!("Satisfying assignment found!"); + println!("Config: {:?}", solution); + println!("Verified: {}", problem.evaluate(&solution)); + + // Show mine placement + println!("\nMine placement:"); + for (i, &(r, c)) in problem.unrevealed().iter().enumerate() { + if solution[i] == 1 { + println!(" Mine at ({}, {})", r, c); + } + } + } + None => { + println!("No satisfying assignment exists (inconsistent grid)."); + } + } +} + +fn main() { + run() +} diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 91e9bd25..bb15464f 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -216,6 +216,7 @@ Flags by problem type: BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] + Minesweeper --rows, --cols, --revealed ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): @@ -326,6 +327,15 @@ pub struct CreateArgs { /// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10] #[arg(long, allow_hyphen_values = true)] pub bounds: Option, + /// Number of rows for Minesweeper grid + #[arg(long)] + pub rows: Option, + /// Number of columns for Minesweeper grid + #[arg(long)] + pub cols: Option, + /// Revealed cells for Minesweeper (semicolon-separated "row,col,count", e.g., "1,1,1;0,0,2") + #[arg(long)] + pub revealed: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 3594a24a..3ac8a0cc 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -5,7 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; -use problemreductions::models::misc::{BinPacking, PaintShop}; +use problemreductions::models::misc::{BinPacking, Minesweeper, PaintShop}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; use problemreductions::topology::{ @@ -45,6 +45,9 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.basis.is_none() && args.target_vec.is_none() && args.bounds.is_none() + && args.rows.is_none() + && args.cols.is_none() + && args.revealed.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -83,6 +86,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", "Factoring" => "--target 15 --m 4 --n 4", + "Minesweeper" => "--rows 3 --cols 3 --revealed \"1,1,1\"", _ => "", } } @@ -442,6 +446,30 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // Minesweeper + "Minesweeper" => { + let rows = args.rows.ok_or_else(|| { + anyhow::anyhow!( + "Minesweeper requires --rows, --cols, and --revealed\n\n\ + Usage: pred create Minesweeper --rows 3 --cols 3 --revealed \"1,1,1\"" + ) + })?; + let cols = args + .cols + .ok_or_else(|| anyhow::anyhow!("Minesweeper requires --cols"))?; + let revealed = parse_revealed(args)?; + let revealed_positions: std::collections::HashSet<(usize, usize)> = + revealed.iter().map(|&(r, c, _)| (r, c)).collect(); + let unrevealed: Vec<(usize, usize)> = (0..rows) + .flat_map(|r| (0..cols).map(move |c| (r, c))) + .filter(|pos| !revealed_positions.contains(pos)) + .collect(); + ( + ser(Minesweeper::new(rows, cols, revealed, unrevealed))?, + resolved_variant.clone(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; @@ -960,3 +988,28 @@ fn create_random( } Ok(()) } + +/// Parse `--revealed` as semicolon-separated "row,col,count" entries. +/// E.g., "1,1,1;0,0,2" +fn parse_revealed(args: &CreateArgs) -> Result> { + let revealed_str = args.revealed.as_deref().ok_or_else(|| { + anyhow::anyhow!("Minesweeper requires --revealed (e.g., \"1,1,1;0,0,2\")") + })?; + + revealed_str + .split(';') + .map(|entry| { + let parts: Vec<&str> = entry.trim().split(',').collect(); + if parts.len() != 3 { + bail!( + "Invalid revealed entry '{}': expected format row,col,count", + entry.trim() + ); + } + let row: usize = parts[0].trim().parse()?; + let col: usize = parts[1].trim().parse()?; + let count: u8 = parts[2].trim().parse()?; + Ok((row, col, count)) + }) + .collect() +} diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 7a849842..a49f91fb 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack}; +use problemreductions::models::misc::{BinPacking, Knapsack, Minesweeper}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -245,6 +245,7 @@ pub fn load_problem( _ => deser_opt::>(data), }, "Knapsack" => deser_opt::(data), + "Minesweeper" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } @@ -305,6 +306,7 @@ pub fn serialize_any_problem( _ => try_ser::>(any), }, "Knapsack" => try_ser::(any), + "Minesweeper" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index acd9b4b5..f71c7cb8 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -52,6 +52,7 @@ pub fn resolve_alias(input: &str) -> String { "binpacking" => "BinPacking".to_string(), "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), + "minesweeper" => "Minesweeper".to_string(), _ => input.to_string(), // pass-through for exact names } } diff --git a/src/lib.rs b/src/lib.rs index b0d99699..cf57338a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub mod prelude { KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, TravelingSalesman, }; - pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop}; + pub use crate::models::misc::{BinPacking, Factoring, Knapsack, Minesweeper, PaintShop}; pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering}; // Core traits diff --git a/src/models/misc/minesweeper.rs b/src/models/misc/minesweeper.rs new file mode 100644 index 00000000..889018d4 --- /dev/null +++ b/src/models/misc/minesweeper.rs @@ -0,0 +1,190 @@ +//! Minesweeper Consistency problem implementation. +//! +//! Given a partially revealed Minesweeper grid, determine if there exists a valid +//! mine assignment for unrevealed cells that satisfies all revealed cell constraints. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +inventory::submit! { + ProblemSchemaEntry { + name: "Minesweeper", + module_path: module_path!(), + description: "Determine if a partially revealed Minesweeper grid has a consistent mine assignment", + fields: &[ + FieldInfo { name: "rows", type_name: "usize", description: "Number of rows in the grid" }, + FieldInfo { name: "cols", type_name: "usize", description: "Number of columns in the grid" }, + FieldInfo { name: "revealed", type_name: "Vec<(usize, usize, u8)>", description: "Revealed cells (row, col, adjacent mine count)" }, + FieldInfo { name: "unrevealed", type_name: "Vec<(usize, usize)>", description: "Unrevealed cells (row, col)" }, + ], + } +} + +/// The Minesweeper Consistency problem. +/// +/// Given a partially revealed Minesweeper grid with `rows x cols` cells, +/// some cells are revealed showing the count of adjacent mines, and some +/// cells are unrevealed (potential mine locations). The problem asks whether +/// there exists an assignment of mines to unrevealed cells such that every +/// revealed cell's count constraint is satisfied. +/// +/// This is a satisfaction (decision) problem and is NP-complete. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::Minesweeper; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 3x3 grid, center revealed with count 1 +/// let problem = Minesweeper::new( +/// 3, 3, +/// vec![(1, 1, 1)], +/// vec![(0,0),(0,1),(0,2),(1,0),(1,2),(2,0),(2,1),(2,2)], +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Minesweeper { + /// Number of rows in the grid. + rows: usize, + /// Number of columns in the grid. + cols: usize, + /// Revealed cells: (row, col, adjacent_mine_count). + revealed: Vec<(usize, usize, u8)>, + /// Unrevealed cells: (row, col). + unrevealed: Vec<(usize, usize)>, +} + +impl Minesweeper { + /// Create a new Minesweeper Consistency problem. + /// + /// # Arguments + /// * `rows` - Number of rows + /// * `cols` - Number of columns + /// * `revealed` - Revealed cells with their adjacent mine counts + /// * `unrevealed` - Unrevealed cells (potential mine locations) + /// + /// # Panics + /// Panics if any cell position is out of bounds. + pub fn new( + rows: usize, + cols: usize, + revealed: Vec<(usize, usize, u8)>, + unrevealed: Vec<(usize, usize)>, + ) -> Self { + for &(r, c, _) in &revealed { + assert!( + r < rows && c < cols, + "Revealed cell ({r}, {c}) out of bounds for {rows}x{cols} grid" + ); + } + for &(r, c) in &unrevealed { + assert!( + r < rows && c < cols, + "Unrevealed cell ({r}, {c}) out of bounds for {rows}x{cols} grid" + ); + } + Self { + rows, + cols, + revealed, + unrevealed, + } + } + + /// Get the number of rows. + pub fn rows(&self) -> usize { + self.rows + } + + /// Get the number of columns. + pub fn cols(&self) -> usize { + self.cols + } + + /// Get the revealed cells. + pub fn revealed(&self) -> &[(usize, usize, u8)] { + &self.revealed + } + + /// Get the unrevealed cells. + pub fn unrevealed(&self) -> &[(usize, usize)] { + &self.unrevealed + } + + /// Get the number of unrevealed cells. + pub fn num_unrevealed(&self) -> usize { + self.unrevealed.len() + } +} + +impl Problem for Minesweeper { + const NAME: &'static str = "Minesweeper"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.unrevealed.len()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + // Build position -> index map for unrevealed cells + let pos_to_idx: HashMap<(usize, usize), usize> = self + .unrevealed + .iter() + .enumerate() + .map(|(i, &(r, c))| ((r, c), i)) + .collect(); + + // Neighbor offsets (8-connected) + let deltas: [(i32, i32); 8] = [ + (-1, -1), + (-1, 0), + (-1, 1), + (0, -1), + (0, 1), + (1, -1), + (1, 0), + (1, 1), + ]; + + // Check each revealed cell's constraint + for &(r, c, count) in &self.revealed { + let mut mine_count: u8 = 0; + for &(dr, dc) in &deltas { + let nr = r as i32 + dr; + let nc = c as i32 + dc; + if nr >= 0 && nr < self.rows as i32 && nc >= 0 && nc < self.cols as i32 { + let pos = (nr as usize, nc as usize); + if let Some(&idx) = pos_to_idx.get(&pos) { + if config[idx] == 1 { + mine_count += 1; + } + } + } + } + if mine_count != count { + return false; + } + } + true + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for Minesweeper {} + +crate::declare_variants! { + Minesweeper => "2^num_unrevealed", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/minesweeper.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index cdb66e96..ff1b2ba8 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -4,14 +4,17 @@ //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) +//! - [`Minesweeper`]: Minesweeper Consistency (determine valid mine assignment) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling mod bin_packing; pub(crate) mod factoring; mod knapsack; +mod minesweeper; pub(crate) mod paintshop; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; +pub use minesweeper::Minesweeper; pub use paintshop::PaintShop; diff --git a/src/models/mod.rs b/src/models/mod.rs index 96b4b79d..154199f2 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -15,5 +15,5 @@ pub use graph::{ BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop}; +pub use misc::{BinPacking, Factoring, Knapsack, Minesweeper, PaintShop}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/minesweeper.rs b/src/unit_tests/models/misc/minesweeper.rs new file mode 100644 index 00000000..e275494e --- /dev/null +++ b/src/unit_tests/models/misc/minesweeper.rs @@ -0,0 +1,159 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_minesweeper_creation() { + let problem = Minesweeper::new( + 3, + 3, + vec![(1, 1, 1)], + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + ], + ); + assert_eq!(problem.rows(), 3); + assert_eq!(problem.cols(), 3); + assert_eq!(problem.num_unrevealed(), 8); + assert_eq!(problem.num_variables(), 8); +} + +#[test] +fn test_minesweeper_evaluate_satisfiable() { + let problem = Minesweeper::new( + 3, + 3, + vec![(1, 1, 1)], + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + ], + ); + // Mine at (0,0) only => config[0]=1, rest=0 + assert!(problem.evaluate(&[1, 0, 0, 0, 0, 0, 0, 0])); + // No mines => count would be 0, not 1 + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0, 0, 0])); + // Two mines adjacent => count would be 2, not 1 + assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_minesweeper_evaluate_unsatisfiable() { + // Grid: + // 1 ? 1 + // ? 0 ? + // 1 ? 1 + let problem = Minesweeper::new( + 3, + 3, + vec![(0, 0, 1), (0, 2, 1), (1, 1, 0), (2, 0, 1), (2, 2, 1)], + vec![(0, 1), (1, 0), (1, 2), (2, 1)], + ); + // (1,1)=0 forces all unrevealed neighbors to 0 + // But (0,0)=1 needs at least 1 mine among its unrevealed neighbors + assert!(!problem.evaluate(&[0, 0, 0, 0])); + assert!(!problem.evaluate(&[1, 0, 0, 0])); + assert!(!problem.evaluate(&[0, 1, 0, 0])); + assert!(!problem.evaluate(&[0, 0, 1, 0])); + assert!(!problem.evaluate(&[0, 0, 0, 1])); +} + +#[test] +fn test_minesweeper_classic_pattern() { + // Grid: + // 1 ? ? + // 1 2 ? + // 0 1 ? + let problem = Minesweeper::new( + 3, + 3, + vec![(0, 0, 1), (1, 0, 1), (1, 1, 2), (2, 0, 0), (2, 1, 1)], + vec![(0, 1), (0, 2), (1, 2), (2, 2)], + ); + // Solution: mines at (0,1) and (1,2) + assert!(problem.evaluate(&[1, 0, 1, 0])); + // Wrong: mines at (0,1) and (0,2) + assert!(!problem.evaluate(&[1, 1, 0, 0])); +} + +#[test] +fn test_minesweeper_serialization() { + let problem = Minesweeper::new( + 3, + 3, + vec![(1, 1, 1)], + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + ], + ); + let json = serde_json::to_value(&problem).unwrap(); + let restored: Minesweeper = serde_json::from_value(json).unwrap(); + assert_eq!(restored.rows(), problem.rows()); + assert_eq!(restored.cols(), problem.cols()); + assert_eq!(restored.num_unrevealed(), problem.num_unrevealed()); +} + +#[test] +fn test_minesweeper_solver() { + let problem = Minesweeper::new( + 3, + 3, + vec![(1, 1, 1)], + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + ], + ); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let sol = solution.unwrap(); + assert!(problem.evaluate(&sol)); + // Exactly one mine among 8 unrevealed cells + assert_eq!(sol.iter().sum::(), 1); +} + +#[test] +fn test_minesweeper_variant() { + let v = ::variant(); + assert!(v.is_empty()); +} + +#[test] +fn test_minesweeper_solver_unsatisfiable() { + let problem = Minesweeper::new( + 3, + 3, + vec![(0, 0, 1), (0, 2, 1), (1, 1, 0), (2, 0, 1), (2, 2, 1)], + vec![(0, 1), (1, 0), (1, 2), (2, 1)], + ); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} From 03d79ccd2982c0e054ee6d87e5b57e8d6fdd3ebb Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 12 Mar 2026 20:00:57 +0800 Subject: [PATCH 3/6] Fix review findings: config length guard, example integration test - Add config.len() validation in evaluate() for defensive programming - Register minesweeper_consistency example in tests/suites/examples.rs Co-Authored-By: Claude Opus 4.6 --- src/models/misc/minesweeper.rs | 4 ++++ tests/suites/examples.rs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/models/misc/minesweeper.rs b/src/models/misc/minesweeper.rs index 889018d4..c7c572d7 100644 --- a/src/models/misc/minesweeper.rs +++ b/src/models/misc/minesweeper.rs @@ -132,6 +132,10 @@ impl Problem for Minesweeper { } fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.unrevealed.len() { + return false; + } + // Build position -> index map for unrevealed cells let pos_to_idx: HashMap<(usize, usize), usize> = self .unrevealed diff --git a/tests/suites/examples.rs b/tests/suites/examples.rs index 3c9ad803..942a6350 100644 --- a/tests/suites/examples.rs +++ b/tests/suites/examples.rs @@ -48,6 +48,7 @@ example_test!(reduction_satisfiability_to_minimumdominatingset); example_test!(reduction_spinglass_to_maxcut); example_test!(reduction_spinglass_to_qubo); example_test!(reduction_travelingsalesman_to_ilp); +example_test!(minesweeper_consistency); macro_rules! example_fn { ($test_name:ident, $mod_name:ident) => { @@ -177,3 +178,4 @@ example_fn!( test_travelingsalesman_to_ilp, reduction_travelingsalesman_to_ilp ); +example_fn!(test_minesweeper_consistency, minesweeper_consistency); From 680c803a69b4db9dcc3f064e59722036cfbd2534 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 12 Mar 2026 20:07:09 +0800 Subject: [PATCH 4/6] Strengthen constructor validation: overlap, count, duplicate checks - Validate mine count <= 8 (physical maximum in Minesweeper) - Detect duplicate positions within revealed/unrevealed lists - Detect overlap between revealed and unrevealed positions Co-Authored-By: Claude Opus 4.6 --- src/models/misc/minesweeper.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/models/misc/minesweeper.rs b/src/models/misc/minesweeper.rs index c7c572d7..dff2234b 100644 --- a/src/models/misc/minesweeper.rs +++ b/src/models/misc/minesweeper.rs @@ -70,24 +70,35 @@ impl Minesweeper { /// * `unrevealed` - Unrevealed cells (potential mine locations) /// /// # Panics - /// Panics if any cell position is out of bounds. + /// Panics if any cell position is out of bounds, if mine counts exceed 8, + /// or if revealed and unrevealed positions overlap. pub fn new( rows: usize, cols: usize, revealed: Vec<(usize, usize, u8)>, unrevealed: Vec<(usize, usize)>, ) -> Self { - for &(r, c, _) in &revealed { + let mut all_positions = std::collections::HashSet::new(); + for &(r, c, count) in &revealed { assert!( r < rows && c < cols, "Revealed cell ({r}, {c}) out of bounds for {rows}x{cols} grid" ); + assert!(count <= 8, "Mine count {count} exceeds maximum of 8"); + assert!( + all_positions.insert((r, c)), + "Duplicate position ({r}, {c}) in revealed cells" + ); } for &(r, c) in &unrevealed { assert!( r < rows && c < cols, "Unrevealed cell ({r}, {c}) out of bounds for {rows}x{cols} grid" ); + assert!( + all_positions.insert((r, c)), + "Position ({r}, {c}) appears in both revealed and unrevealed cells" + ); } Self { rows, From b303968f8a798f773d57ac67d5906a4095718b8f Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 12 Mar 2026 20:07:15 +0800 Subject: [PATCH 5/6] chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-03-12-minesweeper-model.md | 193 --------------------- 1 file changed, 193 deletions(-) delete mode 100644 docs/plans/2026-03-12-minesweeper-model.md diff --git a/docs/plans/2026-03-12-minesweeper-model.md b/docs/plans/2026-03-12-minesweeper-model.md deleted file mode 100644 index ec5f846e..00000000 --- a/docs/plans/2026-03-12-minesweeper-model.md +++ /dev/null @@ -1,193 +0,0 @@ -# Plan: Add Minesweeper Model (#135) - -## Overview -Add the Minesweeper Consistency problem as a `SatisfactionProblem`. This is a decision problem: given a partially revealed Minesweeper grid, determine if there exists a valid mine assignment for unrevealed cells satisfying all revealed cell constraints. - -**Reference:** Kaye, R. (2000). "Minesweeper is NP-complete." The Mathematical Intelligencer, 22(2), 9–15. - -## Information Summary - -| Property | Value | -|----------|-------| -| **Struct name** | `Minesweeper` | -| **Category** | `misc/` (unique grid-based input) | -| **Problem type** | Satisfaction (`Metric = bool`) | -| **Type parameters** | None | -| **Variables** | k binary variables (one per unrevealed cell) | -| **dims()** | `vec![2; k]` where k = unrevealed.len() | -| **evaluate()** | Check all revealed cell constraints are satisfied | -| **Complexity** | `O(2^num_unrevealed)` brute-force | -| **Getter** | `num_unrevealed()` → number of unrevealed cells | - -## Steps - -### Step 1: Implement the Model (`src/models/misc/minesweeper.rs`) - -**Struct:** -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Minesweeper { - rows: usize, - cols: usize, - revealed: Vec<(usize, usize, u8)>, // (row, col, mine_count) - unrevealed: Vec<(usize, usize)>, // (row, col) -} -``` - -**Constructor (`new`):** -- Validate: rows, cols > 0 -- Validate: all revealed positions in bounds, counts 0..=8 -- Validate: all unrevealed positions in bounds -- Validate: no overlap between revealed and unrevealed positions -- Store fields - -**Accessors:** -- `rows()`, `cols()`, `revealed()`, `unrevealed()` -- `num_unrevealed()` — for complexity expression - -**`inventory::submit!` for ProblemSchemaEntry:** -```rust -inventory::submit! { - ProblemSchemaEntry { - name: "Minesweeper", - description: "Minesweeper Consistency: determine if a valid mine assignment exists", - fields: &[ - SchemaField { name: "rows", type_name: "usize", description: "Number of rows" }, - SchemaField { name: "cols", type_name: "usize", description: "Number of columns" }, - SchemaField { name: "revealed", type_name: "Vec<(usize,usize,u8)>", description: "Revealed cells (row,col,count)" }, - SchemaField { name: "unrevealed", type_name: "Vec<(usize,usize)>", description: "Unrevealed cell positions" }, - ], - } -} -``` - -**Problem trait impl:** -```rust -impl Problem for Minesweeper { - const NAME: &'static str = "Minesweeper"; - type Metric = bool; - - fn dims(&self) -> Vec { - vec![2; self.unrevealed.len()] - } - - fn evaluate(&self, config: &[usize]) -> bool { - // For each revealed cell (r, c, count): - // Count how many of its 8 neighbors are unrevealed cells with config[i] == 1 - // If count doesn't match, return false - // Return true if all constraints satisfied - } - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![] - } -} - -impl SatisfactionProblem for Minesweeper {} -``` - -**evaluate() algorithm:** -1. Build a HashMap mapping (row, col) → index into unrevealed list -2. For each revealed cell (r, c, count): - - Count neighbors: iterate over 8 directions (dr, dc) ∈ {-1,0,1}²\{(0,0)} - - For each neighbor (r+dr, c+dc) in bounds: - - If it's in the unrevealed map at index i, add config[i] to sum - - If sum != count, return false -3. Return true - -**Variant complexity:** -```rust -crate::declare_variants! { - Minesweeper => "2^num_unrevealed", -} -``` - -### Step 2: Register the Model - -**`src/models/misc/mod.rs`:** Add `mod minesweeper;` and `pub use minesweeper::Minesweeper;` - -**`src/models/mod.rs`:** Add `Minesweeper` to the `pub use misc::{...}` line. - -### Step 3: Register in CLI - -**`problemreductions-cli/src/dispatch.rs`:** -- In `load_problem()`: add `"Minesweeper" => deser_sat::(data)` -- In `serialize_any_problem()`: add `"Minesweeper" => try_ser::(any)` - -**`problemreductions-cli/src/problem_name.rs`:** -- Add alias: `"minesweeper" => "Minesweeper".to_string()` - -### Step 4: Add CLI Creation Support - -**`problemreductions-cli/src/cli.rs`:** Add new CLI flags: -```rust -/// Revealed cells for Minesweeper (semicolon-separated "row,col,count", e.g., "1,1,1;0,0,2") -#[arg(long)] -pub revealed: Option, -``` -Also add `rows` and `cols` as new `Option` fields. - -Update `all_data_flags_empty()` to check `args.revealed.is_none() && args.rows.is_none() && args.cols.is_none()`. - -**`problemreductions-cli/src/commands/create.rs`:** -- Add `"Minesweeper"` match arm that parses `--rows`, `--cols`, `--revealed` -- Unrevealed cells are computed automatically: all grid cells not in the revealed set -- Add example string in `example_for()` - -### Step 5: Write Unit Tests (`src/unit_tests/models/misc/minesweeper.rs`) - -Tests to write: -1. `test_minesweeper_creation` — construct 3×3 instance, verify dimensions -2. `test_minesweeper_evaluate_satisfiable` — Instance 1 from issue (3×3, center=1, YES) -3. `test_minesweeper_evaluate_unsatisfiable` — Instance 2 from issue (contradictory, NO) -4. `test_minesweeper_classic_pattern` — Instance 3 from issue (classic pattern, YES) -5. `test_minesweeper_serialization` — round-trip serde -6. `test_minesweeper_solver` — BruteForce finds satisfying assignment for Instance 1 -7. `test_minesweeper_variant` — verify variant() returns empty - -Link from model file: -```rust -#[cfg(test)] -#[path = "../../unit_tests/models/misc/minesweeper.rs"] -mod tests; -``` - -Also update `src/unit_tests/models/misc/mod.rs` to include the new test module. - -### Step 6: Write Example (`examples/minesweeper_consistency.rs`) - -Use Instance 3 from the issue (the classic pattern). Create the Minesweeper instance, evaluate a valid and invalid config, use BruteForce to find a satisfying assignment. Output JSON. - -Must have `pub fn run()` and `fn main() { run() }`. - -Register in `Cargo.toml` under `[[example]]`. - -### Step 7: Verify - -```bash -make fmt clippy test -``` - -All must pass. Check that the new model appears in `pred list` output and `pred create Minesweeper` shows help. - -## File Manifest - -| File | Action | -|------|--------| -| `src/models/misc/minesweeper.rs` | CREATE — model implementation | -| `src/models/misc/mod.rs` | EDIT — register module | -| `src/models/mod.rs` | EDIT — add re-export | -| `src/unit_tests/models/misc/minesweeper.rs` | CREATE — unit tests | -| `src/unit_tests/models/misc/mod.rs` | EDIT — register test module | -| `problemreductions-cli/src/dispatch.rs` | EDIT — add load/serialize arms | -| `problemreductions-cli/src/problem_name.rs` | EDIT — add alias | -| `problemreductions-cli/src/cli.rs` | EDIT — add --rows, --cols, --revealed flags | -| `problemreductions-cli/src/commands/create.rs` | EDIT — add Minesweeper creation | -| `examples/minesweeper_consistency.rs` | CREATE — example program | -| `Cargo.toml` | EDIT — register example | - -## Dependencies -- Independent tasks: Steps 1-2 (model), Step 5 (tests skeleton) -- Sequential: Step 3-4 (CLI) depends on Step 2 -- Step 6 (example) depends on Steps 1-2 -- Step 7 (verify) depends on all prior steps From da965408c3e883505b41dca787dc4afca0110bb8 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 12 Mar 2026 21:44:28 +0800 Subject: [PATCH 6/6] fix: address Copilot review comments for Minesweeper model Precompute neighbor indices at construction time (and rebuild on deserialization via serde `from`) so evaluate() uses cheap indexed lookups instead of rebuilding a HashMap on every call. Add CLI-side validation for revealed cell bounds, count limits, and duplicates so invalid input returns a structured error instead of panicking. Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/commands/create.rs | 17 ++- src/models/misc/minesweeper.rs | 132 +++++++++++++------ 2 files changed, 108 insertions(+), 41 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 3ac8a0cc..ff2b9613 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -458,11 +458,22 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { .cols .ok_or_else(|| anyhow::anyhow!("Minesweeper requires --cols"))?; let revealed = parse_revealed(args)?; - let revealed_positions: std::collections::HashSet<(usize, usize)> = - revealed.iter().map(|&(r, c, _)| (r, c)).collect(); + // Validate revealed cells before calling Minesweeper::new() + let mut seen = std::collections::HashSet::new(); + for &(r, c, count) in &revealed { + if r >= rows || c >= cols { + bail!("Revealed cell ({r}, {c}) is out of bounds for {rows}x{cols} grid"); + } + if count > 8 { + bail!("Mine count {count} at ({r}, {c}) exceeds maximum of 8"); + } + if !seen.insert((r, c)) { + bail!("Duplicate revealed cell at ({r}, {c})"); + } + } let unrevealed: Vec<(usize, usize)> = (0..rows) .flat_map(|r| (0..cols).map(move |c| (r, c))) - .filter(|pos| !revealed_positions.contains(pos)) + .filter(|pos| !seen.contains(pos)) .collect(); ( ser(Minesweeper::new(rows, cols, revealed, unrevealed))?, diff --git a/src/models/misc/minesweeper.rs b/src/models/misc/minesweeper.rs index dff2234b..c0f6e611 100644 --- a/src/models/misc/minesweeper.rs +++ b/src/models/misc/minesweeper.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::traits::{Problem, SatisfactionProblem}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; inventory::submit! { ProblemSchemaEntry { @@ -48,7 +48,36 @@ inventory::submit! { /// let solution = solver.find_satisfying(&problem); /// assert!(solution.is_some()); /// ``` +/// Raw serialization helper for [`Minesweeper`] that rebuilds the neighbor +/// cache on deserialization. +#[derive(Deserialize)] +struct MinesweeperRaw { + rows: usize, + cols: usize, + revealed: Vec<(usize, usize, u8)>, + unrevealed: Vec<(usize, usize)>, +} + +impl From for Minesweeper { + fn from(raw: MinesweeperRaw) -> Self { + let neighbor_cache = Minesweeper::build_neighbor_cache( + raw.rows, + raw.cols, + &raw.revealed, + &raw.unrevealed, + ); + Minesweeper { + rows: raw.rows, + cols: raw.cols, + revealed: raw.revealed, + unrevealed: raw.unrevealed, + neighbor_cache, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(from = "MinesweeperRaw")] pub struct Minesweeper { /// Number of rows in the grid. rows: usize, @@ -58,9 +87,63 @@ pub struct Minesweeper { revealed: Vec<(usize, usize, u8)>, /// Unrevealed cells: (row, col). unrevealed: Vec<(usize, usize)>, + /// Precomputed neighbor indices for each revealed cell. + /// For each revealed cell, stores the indices into `unrevealed` of its + /// neighboring unrevealed cells, along with the expected mine count. + #[serde(skip)] + neighbor_cache: Vec<(Vec, u8)>, } impl Minesweeper { + /// Build the neighbor cache: for each revealed cell, find which unrevealed + /// cell indices are its neighbors. + fn build_neighbor_cache( + rows: usize, + cols: usize, + revealed: &[(usize, usize, u8)], + unrevealed: &[(usize, usize)], + ) -> Vec<(Vec, u8)> { + let pos_to_idx: HashMap<(usize, usize), usize> = unrevealed + .iter() + .enumerate() + .map(|(i, &(r, c))| ((r, c), i)) + .collect(); + + let deltas: [(i32, i32); 8] = [ + (-1, -1), + (-1, 0), + (-1, 1), + (0, -1), + (0, 1), + (1, -1), + (1, 0), + (1, 1), + ]; + + revealed + .iter() + .map(|&(r, c, count)| { + let neighbors: Vec = deltas + .iter() + .filter_map(|&(dr, dc)| { + let nr = r as i32 + dr; + let nc = c as i32 + dc; + if nr >= 0 + && nr < rows as i32 + && nc >= 0 + && nc < cols as i32 + { + pos_to_idx.get(&(nr as usize, nc as usize)).copied() + } else { + None + } + }) + .collect(); + (neighbors, count) + }) + .collect() + } + /// Create a new Minesweeper Consistency problem. /// /// # Arguments @@ -78,7 +161,7 @@ impl Minesweeper { revealed: Vec<(usize, usize, u8)>, unrevealed: Vec<(usize, usize)>, ) -> Self { - let mut all_positions = std::collections::HashSet::new(); + let mut all_positions = HashSet::new(); for &(r, c, count) in &revealed { assert!( r < rows && c < cols, @@ -100,11 +183,13 @@ impl Minesweeper { "Position ({r}, {c}) appears in both revealed and unrevealed cells" ); } + let neighbor_cache = Self::build_neighbor_cache(rows, cols, &revealed, &unrevealed); Self { rows, cols, revealed, unrevealed, + neighbor_cache, } } @@ -147,42 +232,13 @@ impl Problem for Minesweeper { return false; } - // Build position -> index map for unrevealed cells - let pos_to_idx: HashMap<(usize, usize), usize> = self - .unrevealed - .iter() - .enumerate() - .map(|(i, &(r, c))| ((r, c), i)) - .collect(); - - // Neighbor offsets (8-connected) - let deltas: [(i32, i32); 8] = [ - (-1, -1), - (-1, 0), - (-1, 1), - (0, -1), - (0, 1), - (1, -1), - (1, 0), - (1, 1), - ]; - - // Check each revealed cell's constraint - for &(r, c, count) in &self.revealed { - let mut mine_count: u8 = 0; - for &(dr, dc) in &deltas { - let nr = r as i32 + dr; - let nc = c as i32 + dc; - if nr >= 0 && nr < self.rows as i32 && nc >= 0 && nc < self.cols as i32 { - let pos = (nr as usize, nc as usize); - if let Some(&idx) = pos_to_idx.get(&pos) { - if config[idx] == 1 { - mine_count += 1; - } - } - } - } - if mine_count != count { + // Use precomputed neighbor cache for O(1) lookups per neighbor. + for (neighbors, count) in &self.neighbor_cache { + let mine_count: u8 = neighbors + .iter() + .filter(|&&idx| config[idx] == 1) + .count() as u8; + if mine_count != *count { return false; } }