1pub mod cli;
16
17use crate::cli::{Config, OutputStyle};
18use anyhow::{anyhow, Context};
19use std::{
20 fs::File,
21 io::{self, BufRead, Write},
22 path::PathBuf,
23};
24
25#[derive(Debug, Clone)]
27pub struct SudokuSolver {
28 config: Config,
29 root: Node,
30 solution: Option<Box<Node>>,
31}
32
33impl SudokuSolver {
34 pub fn new(config: Config) -> anyhow::Result<Self> {
36 let starting_point = parse(&config.input)?;
37 Ok(Self {
38 config,
39 root: Node {
40 candidate: starting_point,
41 ..Default::default()
42 },
43 solution: None,
44 })
45 }
46
47 pub fn solve(&mut self) -> anyhow::Result<()> {
49 self.solution = self.root.backtrack(&self.config);
50
51 self.output()?;
52
53 Ok(())
54 }
55
56 pub fn solution(&self) -> Option<[u8; 81]> {
58 let candidate = self.solution.as_ref()?.candidate;
59 Some(candidate)
60 }
61
62 fn output(&mut self) -> anyhow::Result<()> {
63 let solution = self
64 .solution
65 .as_ref()
66 .ok_or_else(|| anyhow!("Couldn't solve puzzle."))?;
67
68 solution.print_solution(&self.config)?;
69 Ok(())
70 }
71}
72
73#[derive(Debug, Clone)]
74struct Node {
75 pub candidate: [u8; 81],
77 pub most_recent: usize,
79 pub children: [Option<Box<Node>>; 9],
81}
82
83impl Default for Node {
84 fn default() -> Self {
85 Self {
86 candidate: [0; 81],
87 most_recent: 0,
89 children: Default::default(),
90 }
91 }
92}
93
94impl Node {
95 fn backtrack(&mut self, config: &Config) -> Option<Box<Node>> {
97 if let Some(del) = config.delay {
99 std::thread::sleep(std::time::Duration::from_millis(del));
100 }
101 if config.print_partials {
102 self.print_solution(config).unwrap();
103 }
104
105 if self.reject() {
107 return None;
108 } else if self.accept() {
109 return Some(Box::new(self.clone()));
110 }
111
112 let to_change = self.first();
114 let mut next = self.children[self.most_recent].as_mut();
115
116 while let Some(child) = next {
118 if let Some(solution) = child.backtrack(config) {
119 return Some(solution);
120 }
121 if self.next(to_change).is_some() {
122 next = self.children[self.most_recent].as_mut();
123 } else {
124 break;
125 }
126 }
127
128 None
129 }
130
131 fn first(&mut self) -> usize {
133 let mut child = Node {
134 candidate: self.candidate,
135 ..Default::default()
136 };
137 let to_change = child
138 .candidate
139 .iter()
140 .position(|&x| x == 0)
141 .expect("Already checked that puzzle isn't complete in `backtrack`");
142
143 child.candidate[to_change] = 1;
144
145 self.children[self.most_recent] = Some(Box::new(child));
147
148 to_change
149 }
150
151 fn next(&mut self, to_change: usize) -> Option<()> {
153 if self.most_recent >= 8 {
154 return None;
155 }
156 let prev = &mut self.children[self.most_recent]
157 .as_mut()
158 .expect("Known to be valid since we're calling this next");
159
160 let mut child = Node {
161 candidate: prev.candidate,
162 ..Default::default()
163 };
164 child.candidate[to_change] += 1;
165
166 self.most_recent += 1;
167 self.children[self.most_recent] = Some(Box::new(child));
168
169 Some(())
170 }
171
172 fn accept(&self) -> bool {
174 !self.candidate.contains(&0)
175 }
176
177 fn reject(&self) -> bool {
179 let mut counter = [0u8; 10]; let candidate = &self.candidate;
181
182 for i in 0..9 {
184 for j in 0..9 {
185 counter[candidate[(i * 9 + j) as usize] as usize] += 1;
186 }
187
188 for j in counter.iter_mut().skip(1) {
189 if *j > 1 {
190 return true;
191 }
192
193 *j = 0;
194 }
195 }
196
197 for i in 0..9 {
199 for j in 0..9 {
200 counter[candidate[(j * 9 + i) as usize] as usize] += 1;
201 }
202
203 for j in counter.iter_mut().skip(1) {
204 if *j > 1 {
205 return true;
206 }
207
208 *j = 0;
209 }
210 }
211
212 for i in 0..3 {
215 for j in 0..3 {
217 let offset: usize = (i * 27) + (j * 3);
218 for k in 0..3 {
220 counter[candidate[offset + k * 9] as usize] += 1;
221 counter[candidate[offset + k * 9 + 1] as usize] += 1;
222 counter[candidate[offset + k * 9 + 2] as usize] += 1;
223 }
224
225 for k in counter.iter_mut().skip(1) {
226 if *k > 1 {
227 return true;
228 }
229
230 *k = 0;
231 }
232 }
233 }
234
235 false
236 }
237
238 fn print_solution(&self, config: &Config) -> anyhow::Result<()> {
240 let puzzle = &self.candidate;
241 let mut output = match &config.output {
242 Some(path) => Box::new(File::create(path).context("Could not open output file.")?)
243 as Box<dyn Write>,
244 None => Box::new(io::stdout()) as Box<dyn Write>,
245 };
246 match config.style {
247 OutputStyle::Bordered => {
248 for i in 0..3 {
249 let mut offset = i * 27;
250 output.write_all(b"+-------+-------+-------+\n")?;
251 for _ in 0..3 {
252 writeln!(
253 output,
254 "| {} {} {} | {} {} {} | {} {} {} |",
255 puzzle[offset],
256 puzzle[offset + 1],
257 puzzle[offset + 2],
258 puzzle[offset + 3],
259 puzzle[offset + 4],
260 puzzle[offset + 5],
261 puzzle[offset + 6],
262 puzzle[offset + 7],
263 puzzle[offset + 8]
264 )?;
265 offset += 9;
266 }
267 }
268 output.write_all(b"+-------+-------+-------+\n")?;
269 }
270 OutputStyle::MultiLine => {
271 for i in 0..9 {
272 let offset = i * 9;
273 writeln!(
274 output,
275 "{} {} {} {} {} {} {} {} {}",
276 puzzle[offset],
277 puzzle[offset + 1],
278 puzzle[offset + 2],
279 puzzle[offset + 3],
280 puzzle[offset + 4],
281 puzzle[offset + 5],
282 puzzle[offset + 6],
283 puzzle[offset + 7],
284 puzzle[offset + 8]
285 )?;
286 }
287 output.write_all(b"\n")?;
288 }
289 OutputStyle::Simple => {
290 for i in puzzle {
291 write!(output, "{}", i)?;
292 }
293 writeln!(output)?;
294 }
295 }
296 Ok(())
297 }
298}
299
300fn parse(path: &Option<PathBuf>) -> anyhow::Result<[u8; 81]> {
302 let contents = match path {
303 Some(path) => std::fs::read_to_string(path).context("Could not read input file.")?,
304 None => {
305 let mut buf = String::new();
306 for line in io::stdin().lock().lines() {
307 buf.push_str(line?.as_str())
308 }
309 buf
310 }
311 };
312
313 let mut i = 0;
314 let mut puzzle = [0; 81];
315 for c in contents.chars() {
316 if c.is_whitespace() {
317 continue;
318 }
319 let this_square = c.to_digit(10).unwrap_or(0);
320 puzzle[i] = this_square as u8;
321 i += 1;
322 if i == 81 {
323 break;
324 }
325 }
326
327 if i < 81 {
328 Err(anyhow!(
329 "Not enough input. The file was not long enough to construct a complete puzzle."
330 ))
331 } else {
332 Ok(puzzle)
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use crate::cli::OutputStyle;
339
340 use super::*;
341
342 #[test]
343 fn sudoku1() {
344 let config = Config {
345 input: Some(PathBuf::from("puzzle/sudoku1.txt")),
346 output: None,
347 style: OutputStyle::Bordered,
348 print_partials: false,
349 delay: None,
350 };
351 let mut solution = SudokuSolver::new(config).unwrap();
352 solution.solve().unwrap();
353
354 let confirmed_solution: [u8; 81] = [
355 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9, 1, 2, 3, 7, 8, 9, 1, 2, 3, 4, 5, 6, 2, 1,
356 4, 3, 6, 5, 8, 9, 7, 3, 6, 5, 8, 9, 7, 2, 1, 4, 8, 9, 7, 2, 1, 4, 3, 6, 5, 5, 3, 1, 6,
357 4, 2, 9, 7, 8, 6, 4, 2, 9, 7, 8, 5, 3, 1, 9, 7, 8, 5, 3, 1, 6, 4, 2,
358 ];
359 assert_eq!(solution.solution().unwrap(), confirmed_solution);
360 }
361
362 #[test]
363 #[should_panic]
364 fn not_enough_input() {
365 let config = Config {
366 input: Some(PathBuf::from("puzzle/sudoku8.txt")),
367 output: None,
368 print_partials: false,
369 style: OutputStyle::Bordered,
370 delay: None,
371 };
372 let mut solution = SudokuSolver::new(config).unwrap();
373 solution.solve().unwrap();
374 }
375}