use crate::core::{BoardGenerator, Difficulty, SolveStep, Symmetry, TechniqueFlags};
use crate::error::RustokuError;
use crate::format::format_line;
use crate::{Rustoku, generate_board_by_difficulty};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CandidateChange {
pub row: usize,
pub col: usize,
pub before: Vec<u8>,
pub after: Vec<u8>,
pub removed: Vec<u8>,
pub added: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolveStepInfo {
#[serde(rename = "type")]
pub step_type: String,
pub row: usize,
pub col: usize,
pub value: u8,
pub technique: String,
pub step_number: u32,
pub candidates_eliminated: u32,
pub related_cell_count: u8,
pub difficulty_point: u8,
#[serde(default)]
pub candidate_changes: Vec<CandidateChange>,
}
impl From<&SolveStep> for SolveStepInfo {
fn from(step: &SolveStep) -> Self {
match step {
SolveStep::Placement {
row,
col,
value,
flags,
step_number,
candidates_eliminated,
related_cell_count,
difficulty_point,
} => SolveStepInfo {
step_type: "placement".into(),
row: *row,
col: *col,
value: *value,
technique: flags.to_string(),
step_number: *step_number,
candidates_eliminated: *candidates_eliminated,
related_cell_count: *related_cell_count,
difficulty_point: *difficulty_point,
candidate_changes: Vec::new(),
},
SolveStep::CandidateElimination {
row,
col,
value,
flags,
step_number,
candidates_eliminated,
related_cell_count,
difficulty_point,
} => SolveStepInfo {
step_type: "elimination".into(),
row: *row,
col: *col,
value: *value,
technique: flags.to_string(),
step_number: *step_number,
candidates_eliminated: *candidates_eliminated,
related_cell_count: *related_cell_count,
difficulty_point: *difficulty_point,
candidate_changes: Vec::new(),
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolveOutput {
pub board: String,
pub steps: Vec<SolveStepInfo>,
}
fn diff_candidate_grids(before: &[Vec<Vec<u8>>], after: &[Vec<Vec<u8>>]) -> Vec<CandidateChange> {
let mut changes = Vec::new();
for row in 0..9 {
for col in 0..9 {
let before_candidates = before
.get(row)
.and_then(|grid_row| grid_row.get(col))
.cloned()
.unwrap_or_default();
let after_candidates = after
.get(row)
.and_then(|grid_row| grid_row.get(col))
.cloned()
.unwrap_or_default();
if before_candidates == after_candidates {
continue;
}
let removed = before_candidates
.iter()
.copied()
.filter(|candidate| !after_candidates.contains(candidate))
.collect();
let added = after_candidates
.iter()
.copied()
.filter(|candidate| !before_candidates.contains(candidate))
.collect();
changes.push(CandidateChange {
row,
col,
before: before_candidates,
after: after_candidates,
removed,
added,
});
}
}
changes
}
fn replay_step_infos_with_candidate_changes(
puzzle: &str,
steps: &[SolveStep],
) -> Result<Vec<SolveStepInfo>, RustokuError> {
let mut replay = Rustoku::new_from_str(puzzle)?;
let mut infos = Vec::with_capacity(steps.len());
for step in steps {
let before = replay.candidate_grid_snapshot();
replay.apply_trace_step(step);
let after = replay.candidate_grid_snapshot();
let mut info = SolveStepInfo::from(step);
info.candidate_changes = diff_candidate_grids(&before, &after);
infos.push(info);
}
Ok(infos)
}
fn technique_flags_from_str(s: &str) -> Result<TechniqueFlags, RustokuError> {
match s.to_lowercase().as_str() {
"easy" => Ok(TechniqueFlags::EASY),
"medium" => Ok(TechniqueFlags::EASY | TechniqueFlags::MEDIUM),
"hard" => Ok(TechniqueFlags::EASY | TechniqueFlags::MEDIUM | TechniqueFlags::HARD),
"expert" => Ok(TechniqueFlags::all()),
_ => Err(RustokuError::UnknownDifficulty(s.to_string())),
}
}
pub fn solve_any_str(puzzle: &str) -> Result<Option<String>, RustokuError> {
let mut rustoku = Rustoku::new_from_str(puzzle)?;
Ok(rustoku.solve_any().map(|s| format_line(&s.board)))
}
pub fn solve_all_str(puzzle: &str) -> Result<Vec<String>, RustokuError> {
let mut rustoku = Rustoku::new_from_str(puzzle)?;
Ok(rustoku
.solve_all()
.into_iter()
.map(|s| format_line(&s.board))
.collect())
}
pub fn solve_with_steps(
puzzle: &str,
difficulty: &str,
) -> Result<Option<SolveOutput>, RustokuError> {
let flags = technique_flags_from_str(difficulty)?;
let mut rustoku = Rustoku::new_from_str(puzzle)?.with_techniques(flags);
match rustoku.solve_any() {
Some(solution) => Ok(Some(SolveOutput {
board: format_line(&solution.board),
steps: replay_step_infos_with_candidate_changes(puzzle, &solution.solve_path.steps)?,
})),
None => Ok(None),
}
}
pub fn candidates_grid(puzzle: &str) -> Result<Vec<Vec<Vec<u8>>>, RustokuError> {
let rustoku = Rustoku::new_from_str(puzzle)?;
Ok((0..9)
.map(|r| {
(0..9)
.map(|c| {
if rustoku.board.get(r, c) != 0 {
vec![]
} else {
rustoku.candidates.get_candidates(r, c)
}
})
.collect()
})
.collect())
}
pub fn generate_str(difficulty: &str) -> Result<String, RustokuError> {
let diff = Difficulty::from_str(difficulty)
.map_err(|_| RustokuError::UnknownDifficulty(difficulty.to_string()))?;
generate_board_by_difficulty(diff, 100).map(|b| format_line(&b))
}
pub fn is_valid_solution(puzzle: &str) -> Result<bool, RustokuError> {
Rustoku::new_from_str(puzzle).map(|r| r.is_solved())
}
pub fn generate_complex_str(
symmetry_str: &str,
difficulty_str: Option<&str>,
) -> Result<String, RustokuError> {
let symmetry = match symmetry_str.to_lowercase().as_str() {
"none" => Symmetry::None,
"rotational180" => Symmetry::Rotational180,
"rotational90" => Symmetry::Rotational90,
"mirrorvertical" => Symmetry::MirrorVertical,
"mirrorhorizontal" => Symmetry::MirrorHorizontal,
"mirrordiagonal" => Symmetry::MirrorDiagonal,
_ => Symmetry::None,
};
let mut builder = BoardGenerator::new().symmetry(symmetry).max_attempts(250);
if let Some(diff_s) = difficulty_str {
let diff = Difficulty::from_str(diff_s)
.map_err(|_| RustokuError::UnknownDifficulty(diff_s.to_string()))?;
builder = builder.difficulty(diff);
}
builder.generate().map(|b| format_line(&b))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_technique_flags_from_str_valid() {
assert!(technique_flags_from_str("easy").is_ok());
assert!(technique_flags_from_str("medium").is_ok());
assert!(technique_flags_from_str("hard").is_ok());
assert!(technique_flags_from_str("expert").is_ok());
assert!(technique_flags_from_str("EASY").is_ok()); }
#[test]
fn test_technique_flags_from_str_invalid() {
assert!(matches!(
technique_flags_from_str("invalid"),
Err(RustokuError::UnknownDifficulty(_))
));
}
#[test]
fn test_solve_any_str_solvable() {
let puzzle =
"4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
let result = solve_any_str(puzzle);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn test_solve_any_str_unsolvable() {
let puzzle =
"111111111111111111111111111111111111111111111111111111111111111111111111111111";
let result = solve_any_str(puzzle);
assert!(result.is_err());
}
#[test]
fn test_solve_any_str_invalid_input() {
let puzzle = "invalid";
assert!(solve_any_str(puzzle).is_err());
}
#[test]
fn test_solve_all_str() {
let puzzle =
"4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
let result = solve_all_str(puzzle);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_solve_with_steps() {
let puzzle =
"4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
let result = solve_with_steps(puzzle, "expert");
assert!(result.is_ok());
let output = result.unwrap().unwrap();
assert_eq!(output.board.len(), 81);
assert!(!output.steps.is_empty());
assert!(
output
.steps
.iter()
.any(|step| !step.candidate_changes.is_empty())
);
assert!(output.steps.iter().all(|step| {
step.candidate_changes
.iter()
.all(|change| change.before != change.after)
}));
}
#[test]
fn test_candidates_grid() {
let puzzle =
"4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
let result = candidates_grid(puzzle);
assert!(result.is_ok());
let grid = result.unwrap();
assert_eq!(grid.len(), 9);
assert_eq!(grid[0].len(), 9);
assert!(grid[0][0].is_empty());
assert!(!grid[0][1].is_empty());
}
#[test]
fn test_generate_str_valid() {
let result = generate_str("easy");
assert!(result.is_ok());
let board = result.unwrap();
assert_eq!(board.len(), 81);
assert!(solve_any_str(&board).unwrap().is_some());
}
#[test]
fn test_generate_str_invalid() {
assert!(generate_str("invalid").is_err());
}
#[test]
fn test_is_valid_solution_valid() {
let solved =
"417369825632158947958724316825437169791586432346912758289643571573291684164875293";
assert!(is_valid_solution(solved).unwrap());
}
#[test]
fn test_is_valid_solution_invalid() {
let unsolved =
"4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
assert!(!is_valid_solution(unsolved).unwrap());
}
#[test]
fn test_is_valid_solution_malformed() {
assert!(is_valid_solution("invalid").is_err());
}
}