use crate::domain::bitset::BitsetDomain;
use crate::ordering::Ordering;
use crate::puzzles::sudoku::rng::SimpleRng;
use crate::{Csp, Pruning, SolveConfig};
use super::csp::{FutoshikiPuzzle, create_futoshiki_csp};
const KEEP_DENSITY: f64 = 0.75;
fn gen_config(max_solutions: usize) -> SolveConfig {
SolveConfig {
pruning: Pruning::Ac3,
ordering: Ordering::FailFirst,
max_solutions,
..Default::default()
}
}
fn csp_from_board(board: &[u32], n: u32, inequalities: &[(usize, usize)]) -> Csp<BitsetDomain> {
let fixed_cells: Vec<(usize, u32)> = board
.iter()
.enumerate()
.filter(|&(_, &v)| v != 0)
.map(|(i, &v)| (i, v))
.collect();
let puzzle = FutoshikiPuzzle {
n,
fixed_cells,
inequalities: inequalities.to_vec(),
};
create_futoshiki_csp(&puzzle)
}
fn seed_latin_square(n: u32, rng: &mut SimpleRng) -> Vec<u32> {
let total = (n * n) as usize;
let mut first_row: Vec<u32> = (1..=n).collect();
rng.shuffle(&mut first_row);
let mut board = vec![0u32; total];
board[..n as usize].copy_from_slice(&first_row);
let mut csp = csp_from_board(&board, n, &[]);
csp.solve_with_given(&gen_config(1), &[])
.into_iter()
.next()
.expect("an empty Futoshiki board with a fixed first row is always solvable")
}
fn place_inequalities(
square: &[u32],
n: u32,
count: usize,
rng: &mut SimpleRng,
) -> Vec<(usize, usize)> {
let nn = n as usize;
let mut pairs: Vec<(usize, usize)> = Vec::new();
for r in 0..nn {
for c in 0..nn {
let cell = r * nn + c;
if c + 1 < nn {
pairs.push((cell, cell + 1));
}
if r + 1 < nn {
pairs.push((cell, cell + nn));
}
}
}
rng.shuffle(&mut pairs);
pairs
.into_iter()
.take(count)
.map(|(u, v)| {
if square[u] > square[v] {
(u, v)
} else {
(v, u)
}
})
.collect()
}
fn dig_holes(
solution: &[u32],
n: u32,
inequalities: &[(usize, usize)],
target_holes: usize,
rng: &mut SimpleRng,
) -> Vec<u32> {
let total = (n * n) as usize;
let mut board = solution.to_vec();
let mut indices: Vec<usize> = (0..total).collect();
rng.shuffle(&mut indices);
let uniqueness_config = gen_config(2);
let mut holes = 0usize;
for &idx in &indices {
if holes >= target_holes {
break;
}
let saved = board[idx];
board[idx] = 0;
let mut csp = csp_from_board(&board, n, inequalities);
let solutions = csp.solve_with_given(&uniqueness_config, &[]);
if solutions.len() == 1 {
holes += 1;
} else {
board[idx] = saved;
}
}
board
}
fn default_inequality_count(n: u32) -> usize {
let nn = n as usize;
let available = 2 * nn * (nn - 1);
nn.min(available)
}
fn holes_for_density(n: u32, keep_density: f64) -> usize {
let total = (n * n) as usize;
let keep = (total as f64 * keep_density).round() as usize;
total.saturating_sub(keep.min(total))
}
pub fn measure_difficulty(board: &[u32], n: u32, inequalities: &[(usize, usize)]) -> u32 {
let mut csp = csp_from_board(board, n, inequalities);
let config = SolveConfig {
pruning: Pruning::ForwardChecking,
ordering: Ordering::FailFirst,
max_solutions: 1,
..Default::default()
};
csp.solve(&config);
csp.stats().backtracks as u32
}
pub fn generate_futoshiki(n: u32) -> (Vec<u32>, Vec<(usize, usize)>) {
generate_with_rng(
n,
KEEP_DENSITY,
default_inequality_count(n),
&mut SimpleRng::from_time(),
)
}
pub fn generate_futoshiki_seeded(n: u32, seed: u64) -> (Vec<u32>, Vec<(usize, usize)>) {
generate_with_rng(
n,
KEEP_DENSITY,
default_inequality_count(n),
&mut SimpleRng::new(seed),
)
}
pub fn generate_futoshiki_tuned_seeded(
n: u32,
keep_density: f64,
inequality_count: usize,
seed: u64,
) -> (Vec<u32>, Vec<(usize, usize)>) {
let available = 2 * (n as usize) * (n as usize - 1);
let count = inequality_count.min(available);
let density = keep_density.clamp(f64::MIN_POSITIVE, 1.0);
generate_with_rng(n, density, count, &mut SimpleRng::new(seed))
}
fn generate_with_rng(
n: u32,
keep_density: f64,
inequality_count: usize,
rng: &mut SimpleRng,
) -> (Vec<u32>, Vec<(usize, usize)>) {
let square = seed_latin_square(n, rng);
let inequalities = place_inequalities(&square, n, inequality_count, rng);
let target_holes = holes_for_density(n, keep_density);
let board = dig_holes(&square, n, &inequalities, target_holes, rng);
(board, inequalities)
}