Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions examples/minesweeper_consistency.rs
Original file line number Diff line number Diff line change
@@ -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()
}
10 changes: 10 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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<String>,
/// Number of rows for Minesweeper grid
#[arg(long)]
pub rows: Option<usize>,
/// Number of columns for Minesweeper grid
#[arg(long)]
pub cols: Option<usize>,
/// Revealed cells for Minesweeper (semicolon-separated "row,col,count", e.g., "1,1,1;0,0,2")
#[arg(long)]
pub revealed: Option<String>,
}

#[derive(clap::Args)]
Expand Down
66 changes: 65 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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\"",
_ => "",
}
}
Expand Down Expand Up @@ -442,6 +446,41 @@ 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)?;
// 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| !seen.contains(pos))
.collect();
(
ser(Minesweeper::new(rows, cols, revealed, unrevealed))?,
resolved_variant.clone(),
)
Comment on lines +449 to +481
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI create path relies on Minesweeper::new() assertions for validation (bounds, duplicates, overlaps, count<=8). If a user passes invalid --revealed coordinates or duplicates, the CLI will panic instead of returning a structured error. Prefer validating revealed in the CLI (bounds check against rows/cols, duplicate positions) and returning a bail!(...)/anyhow!(...) error before calling Minesweeper::new().

Copilot uses AI. Check for mistakes.
}

_ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)),
};

Expand Down Expand Up @@ -960,3 +999,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<Vec<(usize, usize, u8)>> {
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()
}
4 changes: 3 additions & 1 deletion problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -245,6 +245,7 @@ pub fn load_problem(
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
},
"Knapsack" => deser_opt::<Knapsack>(data),
"Minesweeper" => deser_sat::<Minesweeper>(data),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
}
}
Expand Down Expand Up @@ -305,6 +306,7 @@ pub fn serialize_any_problem(
_ => try_ser::<ClosestVectorProblem<i32>>(any),
},
"Knapsack" => try_ser::<Knapsack>(any),
"Minesweeper" => try_ser::<Minesweeper>(any),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
}
}
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading