use crate::ordering::Ordering;
use crate::puzzles::class::{PuzzleClass, SimpleRng, generate_by_digging};
use crate::puzzles::sudoku::Difficulty;
use crate::puzzles::sudoku::csp::{sudoku_csp_skeleton, sudoku_given};
use crate::{Pruning, SolveConfig};
use super::csp::{Thermometer, create_thermo_csp};
const MAX_THERMO_LEN: usize = 5;
fn gen_config(max_solutions: usize) -> SolveConfig {
SolveConfig {
pruning: Pruning::Ac3,
ordering: Ordering::FailFirst,
max_solutions,
..Default::default()
}
}
fn seed_sudoku_solution(n: u32, rng: &mut SimpleRng) -> Vec<u32> {
let m = n * n;
let total = (m * m) as usize;
let mut csp = sudoku_csp_skeleton(n);
let mut seed_board = vec![0u32; total];
let mut first_row: Vec<u32> = (1..=m).collect();
rng.shuffle(&mut first_row);
seed_board[..m as usize].copy_from_slice(&first_row);
csp.solve_with_given(&gen_config(1), &sudoku_given(&seed_board))
.into_iter()
.next()
.expect("a seeded Sudoku board with a fixed first row is always solvable")
}
fn ortho_neighbours(cell: usize, m: usize) -> Vec<usize> {
let (r, c) = (cell / m, cell % m);
let mut out = Vec::with_capacity(4);
if c + 1 < m {
out.push(cell + 1);
}
if c > 0 {
out.push(cell - 1);
}
if r + 1 < m {
out.push(cell + m);
}
if r > 0 {
out.push(cell - m);
}
out
}
fn place_thermometers(
solution: &[u32],
n: u32,
count: usize,
rng: &mut SimpleRng,
) -> Vec<Thermometer> {
let m = (n * n) as usize;
let total = solution.len();
let mut used = vec![false; total];
let mut starts: Vec<usize> = (0..total).collect();
rng.shuffle(&mut starts);
let mut thermos: Vec<Thermometer> = Vec::new();
for &start in &starts {
if thermos.len() >= count {
break;
}
if used[start] {
continue;
}
let mut path = vec![start];
used[start] = true;
let mut cur = start;
while path.len() < MAX_THERMO_LEN {
let candidates: Vec<usize> = ortho_neighbours(cur, m)
.into_iter()
.filter(|&nb| !used[nb] && solution[nb] > solution[cur])
.collect();
if candidates.is_empty() {
break;
}
let next = candidates[rng.next_usize(candidates.len())];
used[next] = true;
path.push(next);
cur = next;
}
if path.len() >= 2 {
thermos.push(path);
} else {
used[start] = false; }
}
thermos
}
fn target_holes_for(difficulty: Difficulty, board_len: usize) -> usize {
match difficulty {
Difficulty::Easy => board_len / 4,
Difficulty::Medium => (board_len as f64 / 1.75) as usize,
Difficulty::Hard => (board_len as f64 / 1.25) as usize,
}
}
pub struct ThermoClass {
pub n: u32,
pub difficulty: Difficulty,
pub thermo_count: usize,
}
impl ThermoClass {
pub fn from_difficulty(n: u32, difficulty: Difficulty) -> Self {
Self {
n,
difficulty,
thermo_count: (n * n) as usize,
}
}
}
impl PuzzleClass for ThermoClass {
type Clue = Thermometer;
type Puzzle = (Vec<u32>, Vec<Thermometer>);
fn seed_solution(&self, rng: &mut SimpleRng) -> Vec<u32> {
seed_sudoku_solution(self.n, rng)
}
fn place_clues(&self, solution: &[u32], rng: &mut SimpleRng) -> Vec<Thermometer> {
place_thermometers(solution, self.n, self.thermo_count, rng)
}
fn solve_candidate(
&self,
board: &[u32],
clues: &[Thermometer],
max_solutions: usize,
) -> Vec<Vec<u32>> {
let (mut csp, given) = create_thermo_csp(board, self.n, clues);
csp.solve_with_given(&gen_config(max_solutions), &given)
}
fn target_holes(&self, board_len: usize) -> usize {
target_holes_for(self.difficulty, board_len)
}
fn assemble(&self, board: Vec<u32>, clues: Vec<Thermometer>) -> Self::Puzzle {
(board, clues)
}
}
pub fn generate_thermo(n: u32, difficulty: Difficulty) -> (Vec<u32>, Vec<Thermometer>) {
generate_by_digging(
&ThermoClass::from_difficulty(n, difficulty),
&mut SimpleRng::from_time(),
)
}
pub fn generate_thermo_seeded(
n: u32,
difficulty: Difficulty,
seed: u64,
) -> (Vec<u32>, Vec<Thermometer>) {
generate_by_digging(
&ThermoClass::from_difficulty(n, difficulty),
&mut SimpleRng::new(seed),
)
}