1use crate::core::{BoardGenerator, Difficulty, SolveStep, Symmetry, TechniqueFlags};
13use crate::error::RustokuError;
14use crate::format::format_line;
15use crate::{Rustoku, generate_board_by_difficulty};
16use serde::{Deserialize, Serialize};
17use std::str::FromStr;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CandidateChange {
23 pub row: usize,
24 pub col: usize,
25 pub before: Vec<u8>,
26 pub after: Vec<u8>,
27 pub removed: Vec<u8>,
28 pub added: Vec<u8>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SolveStepInfo {
34 #[serde(rename = "type")]
36 pub step_type: String,
37 pub row: usize,
38 pub col: usize,
39 pub value: u8,
40 pub technique: String,
42 pub step_number: u32,
43 pub candidates_eliminated: u32,
44 pub related_cell_count: u8,
45 pub difficulty_point: u8,
47 #[serde(default)]
48 pub candidate_changes: Vec<CandidateChange>,
49}
50
51impl From<&SolveStep> for SolveStepInfo {
52 fn from(step: &SolveStep) -> Self {
53 match step {
54 SolveStep::Placement {
55 row,
56 col,
57 value,
58 flags,
59 step_number,
60 candidates_eliminated,
61 related_cell_count,
62 difficulty_point,
63 } => SolveStepInfo {
64 step_type: "placement".into(),
65 row: *row,
66 col: *col,
67 value: *value,
68 technique: flags.to_string(),
69 step_number: *step_number,
70 candidates_eliminated: *candidates_eliminated,
71 related_cell_count: *related_cell_count,
72 difficulty_point: *difficulty_point,
73 candidate_changes: Vec::new(),
74 },
75 SolveStep::CandidateElimination {
76 row,
77 col,
78 value,
79 flags,
80 step_number,
81 candidates_eliminated,
82 related_cell_count,
83 difficulty_point,
84 } => SolveStepInfo {
85 step_type: "elimination".into(),
86 row: *row,
87 col: *col,
88 value: *value,
89 technique: flags.to_string(),
90 step_number: *step_number,
91 candidates_eliminated: *candidates_eliminated,
92 related_cell_count: *related_cell_count,
93 difficulty_point: *difficulty_point,
94 candidate_changes: Vec::new(),
95 },
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SolveOutput {
103 pub board: String,
105 pub steps: Vec<SolveStepInfo>,
107}
108
109fn diff_candidate_grids(before: &[Vec<Vec<u8>>], after: &[Vec<Vec<u8>>]) -> Vec<CandidateChange> {
110 let mut changes = Vec::new();
111
112 for row in 0..9 {
113 for col in 0..9 {
114 let before_candidates = before
115 .get(row)
116 .and_then(|grid_row| grid_row.get(col))
117 .cloned()
118 .unwrap_or_default();
119 let after_candidates = after
120 .get(row)
121 .and_then(|grid_row| grid_row.get(col))
122 .cloned()
123 .unwrap_or_default();
124
125 if before_candidates == after_candidates {
126 continue;
127 }
128
129 let removed = before_candidates
130 .iter()
131 .copied()
132 .filter(|candidate| !after_candidates.contains(candidate))
133 .collect();
134 let added = after_candidates
135 .iter()
136 .copied()
137 .filter(|candidate| !before_candidates.contains(candidate))
138 .collect();
139
140 changes.push(CandidateChange {
141 row,
142 col,
143 before: before_candidates,
144 after: after_candidates,
145 removed,
146 added,
147 });
148 }
149 }
150
151 changes
152}
153
154fn replay_step_infos_with_candidate_changes(
155 puzzle: &str,
156 steps: &[SolveStep],
157) -> Result<Vec<SolveStepInfo>, RustokuError> {
158 let mut replay = Rustoku::new_from_str(puzzle)?;
159 let mut infos = Vec::with_capacity(steps.len());
160
161 for step in steps {
162 let before = replay.candidate_grid_snapshot();
163 replay.apply_trace_step(step);
164 let after = replay.candidate_grid_snapshot();
165
166 let mut info = SolveStepInfo::from(step);
167 info.candidate_changes = diff_candidate_grids(&before, &after);
168 infos.push(info);
169 }
170
171 Ok(infos)
172}
173
174fn technique_flags_from_str(s: &str) -> Result<TechniqueFlags, RustokuError> {
181 match s.to_lowercase().as_str() {
182 "easy" => Ok(TechniqueFlags::EASY),
183 "medium" => Ok(TechniqueFlags::EASY | TechniqueFlags::MEDIUM),
184 "hard" => Ok(TechniqueFlags::EASY | TechniqueFlags::MEDIUM | TechniqueFlags::HARD),
185 "expert" => Ok(TechniqueFlags::all()),
186 _ => Err(RustokuError::UnknownDifficulty(s.to_string())),
187 }
188}
189
190pub fn solve_any_str(puzzle: &str) -> Result<Option<String>, RustokuError> {
192 let mut rustoku = Rustoku::new_from_str(puzzle)?;
193 Ok(rustoku.solve_any().map(|s| format_line(&s.board)))
194}
195
196pub fn solve_all_str(puzzle: &str) -> Result<Vec<String>, RustokuError> {
198 let mut rustoku = Rustoku::new_from_str(puzzle)?;
199 Ok(rustoku
200 .solve_all()
201 .into_iter()
202 .map(|s| format_line(&s.board))
203 .collect())
204}
205
206pub fn solve_with_steps(
211 puzzle: &str,
212 difficulty: &str,
213) -> Result<Option<SolveOutput>, RustokuError> {
214 let flags = technique_flags_from_str(difficulty)?;
215 let mut rustoku = Rustoku::new_from_str(puzzle)?.with_techniques(flags);
216 match rustoku.solve_any() {
217 Some(solution) => Ok(Some(SolveOutput {
218 board: format_line(&solution.board),
219 steps: replay_step_infos_with_candidate_changes(puzzle, &solution.solve_path.steps)?,
220 })),
221 None => Ok(None),
222 }
223}
224
225pub fn candidates_grid(puzzle: &str) -> Result<Vec<Vec<Vec<u8>>>, RustokuError> {
230 let rustoku = Rustoku::new_from_str(puzzle)?;
231 Ok((0..9)
232 .map(|r| {
233 (0..9)
234 .map(|c| {
235 if rustoku.board.get(r, c) != 0 {
236 vec![]
237 } else {
238 rustoku.candidates.get_candidates(r, c)
239 }
240 })
241 .collect()
242 })
243 .collect())
244}
245
246pub fn generate_str(difficulty: &str) -> Result<String, RustokuError> {
248 let diff = Difficulty::from_str(difficulty)
249 .map_err(|_| RustokuError::UnknownDifficulty(difficulty.to_string()))?;
250 generate_board_by_difficulty(diff, 100).map(|b| format_line(&b))
251}
252
253pub fn is_valid_solution(puzzle: &str) -> Result<bool, RustokuError> {
255 Rustoku::new_from_str(puzzle).map(|r| r.is_solved())
256}
257
258pub fn generate_complex_str(
260 symmetry_str: &str,
261 difficulty_str: Option<&str>,
262) -> Result<String, RustokuError> {
263 let symmetry = match symmetry_str.to_lowercase().as_str() {
264 "none" => Symmetry::None,
265 "rotational180" => Symmetry::Rotational180,
266 "rotational90" => Symmetry::Rotational90,
267 "mirrorvertical" => Symmetry::MirrorVertical,
268 "mirrorhorizontal" => Symmetry::MirrorHorizontal,
269 "mirrordiagonal" => Symmetry::MirrorDiagonal,
270 _ => Symmetry::None,
271 };
272
273 let mut builder = BoardGenerator::new().symmetry(symmetry).max_attempts(250);
274
275 if let Some(diff_s) = difficulty_str {
276 let diff = Difficulty::from_str(diff_s)
277 .map_err(|_| RustokuError::UnknownDifficulty(diff_s.to_string()))?;
278 builder = builder.difficulty(diff);
279 }
280
281 builder.generate().map(|b| format_line(&b))
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_technique_flags_from_str_valid() {
290 assert!(technique_flags_from_str("easy").is_ok());
291 assert!(technique_flags_from_str("medium").is_ok());
292 assert!(technique_flags_from_str("hard").is_ok());
293 assert!(technique_flags_from_str("expert").is_ok());
294 assert!(technique_flags_from_str("EASY").is_ok()); }
296
297 #[test]
298 fn test_technique_flags_from_str_invalid() {
299 assert!(matches!(
300 technique_flags_from_str("invalid"),
301 Err(RustokuError::UnknownDifficulty(_))
302 ));
303 }
304
305 #[test]
306 fn test_solve_any_str_solvable() {
307 let puzzle =
308 "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
309 let result = solve_any_str(puzzle);
310 assert!(result.is_ok());
311 assert!(result.unwrap().is_some());
312 }
313
314 #[test]
315 fn test_solve_any_str_unsolvable() {
316 let puzzle =
318 "111111111111111111111111111111111111111111111111111111111111111111111111111111";
319 let result = solve_any_str(puzzle);
320 assert!(result.is_err());
322 }
323
324 #[test]
325 fn test_solve_any_str_invalid_input() {
326 let puzzle = "invalid";
327 assert!(solve_any_str(puzzle).is_err());
328 }
329
330 #[test]
331 fn test_solve_all_str() {
332 let puzzle =
333 "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
334 let result = solve_all_str(puzzle);
335 assert!(result.is_ok());
336 assert_eq!(result.unwrap().len(), 1);
337 }
338
339 #[test]
340 fn test_solve_with_steps() {
341 let puzzle =
342 "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
343 let result = solve_with_steps(puzzle, "expert");
344 assert!(result.is_ok());
345 let output = result.unwrap().unwrap();
346 assert_eq!(output.board.len(), 81);
347 assert!(!output.steps.is_empty());
348 assert!(
349 output
350 .steps
351 .iter()
352 .any(|step| !step.candidate_changes.is_empty())
353 );
354 assert!(output.steps.iter().all(|step| {
355 step.candidate_changes
356 .iter()
357 .all(|change| change.before != change.after)
358 }));
359 }
360
361 #[test]
362 fn test_candidates_grid() {
363 let puzzle =
364 "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
365 let result = candidates_grid(puzzle);
366 assert!(result.is_ok());
367 let grid = result.unwrap();
368 assert_eq!(grid.len(), 9);
369 assert_eq!(grid[0].len(), 9);
370 assert!(grid[0][0].is_empty());
372 assert!(!grid[0][1].is_empty());
374 }
375
376 #[test]
377 fn test_generate_str_valid() {
378 let result = generate_str("easy");
379 assert!(result.is_ok());
380 let board = result.unwrap();
381 assert_eq!(board.len(), 81);
382 assert!(solve_any_str(&board).unwrap().is_some());
384 }
385
386 #[test]
387 fn test_generate_str_invalid() {
388 assert!(generate_str("invalid").is_err());
389 }
390
391 #[test]
392 fn test_is_valid_solution_valid() {
393 let solved =
394 "417369825632158947958724316825437169791586432346912758289643571573291684164875293";
395 assert!(is_valid_solution(solved).unwrap());
396 }
397
398 #[test]
399 fn test_is_valid_solution_invalid() {
400 let unsolved =
401 "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......";
402 assert!(!is_valid_solution(unsolved).unwrap());
403 }
404
405 #[test]
406 fn test_is_valid_solution_malformed() {
407 assert!(is_valid_solution("invalid").is_err());
408 }
409}