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..ff2b9613 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,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(), + ) + } + _ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)), }; @@ -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> { + 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..c0f6e611 --- /dev/null +++ b/src/models/misc/minesweeper.rs @@ -0,0 +1,261 @@ +//! 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, HashSet}; + +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()); +/// ``` +/// 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, + /// 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)>, + /// 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 + /// * `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, 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 { + let mut all_positions = 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" + ); + } + let neighbor_cache = Self::build_neighbor_cache(rows, cols, &revealed, &unrevealed); + Self { + rows, + cols, + revealed, + unrevealed, + neighbor_cache, + } + } + + /// 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 { + if config.len() != self.unrevealed.len() { + return false; + } + + // 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; + } + } + 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()); +} 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);