sat_solver/sudoku/
solver.rs

1use crate::sat::clause_storage::LiteralStorage;
2use crate::sat::cnf::Cnf;
3use crate::sat::literal::Literal;
4use crate::sat::solver::Solutions;
5use itertools::Itertools;
6use std::fmt::Display;
7use std::num::NonZeroI32;
8use std::path::PathBuf;
9
10/// Represents a Sudoku board as a 2D vector of numbers.
11///
12/// `0` typically represents an empty cell.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Board(Vec<Vec<usize>>);
15
16impl Display for Board {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        for row in &self.0 {
19            writeln!(
20                f,
21                "{}",
22                row.iter()
23                    .map(std::string::ToString::to_string)
24                    .collect_vec()
25                    .join(" ")
26            )?;
27        }
28        Ok(())
29    }
30}
31
32impl Board {
33    /// Creates a new `Board` from a 2D vector.
34    ///
35    /// # Arguments
36    ///
37    /// * `board`: A `Vec<Vec<usize>>` representing the Sudoku grid.
38    #[must_use]
39    pub const fn new(board: Vec<Vec<usize>>) -> Self {
40        Self(board)
41    }
42}
43
44impl From<Vec<Vec<usize>>> for Board {
45    /// Converts a `Vec<Vec<usize>>` into a `Board`.
46    fn from(board: Vec<Vec<usize>>) -> Self {
47        Self::new(board)
48    }
49}
50
51impl From<Board> for Vec<Vec<usize>> {
52    /// Converts a `Board` into a `Vec<Vec<usize>>`.
53    fn from(board: Board) -> Self {
54        board.0
55    }
56}
57
58impl From<&Board> for Vec<Vec<usize>> {
59    /// Converts a reference to a `Board` into a `Vec<Vec<usize>>` by cloning.
60    fn from(board: &Board) -> Self {
61        board.0.clone()
62    }
63}
64
65impl<const N: usize> From<&[&[usize; N]; N]> for Board {
66    /// Converts an array of references to fixed-size arrays into a `Board`.
67    ///
68    /// This is useful for creating a `Board` from statically defined Sudoku puzzles
69    /// where rows are defined as separate arrays.
70    fn from(board: &[&[usize; N]; N]) -> Self {
71        Self::new(board.iter().map(|r| r.to_vec()).collect())
72    }
73}
74
75/// An example 4x4 Sudoku puzzle. Values are 1-4, 0 for empty.
76#[allow(dead_code)]
77pub const EXAMPLE_FOUR: [[usize; 4]; 4] = [[2, 3, 0, 0], [4, 0, 0, 1], [0, 1, 2, 0], [0, 0, 0, 0]];
78
79/// An example 9x9 Sudoku puzzle. Values are 1-9, 0 for empty.
80#[allow(dead_code)]
81pub const EXAMPLE_NINE: [[usize; 9]; 9] = [
82    [5, 3, 0, 0, 7, 0, 0, 0, 0],
83    [6, 0, 0, 1, 9, 5, 0, 0, 0],
84    [0, 9, 8, 0, 0, 0, 0, 6, 0],
85    [8, 0, 0, 0, 6, 0, 0, 0, 3],
86    [4, 0, 0, 8, 0, 3, 0, 0, 1],
87    [7, 0, 0, 0, 2, 0, 0, 0, 6],
88    [0, 6, 0, 0, 0, 0, 2, 8, 0],
89    [0, 0, 0, 4, 1, 9, 0, 0, 5],
90    [0, 0, 0, 0, 8, 0, 0, 7, 9],
91];
92
93/// An example 16x16 Sudoku puzzle. Values are 1-16, 0 for empty.
94#[allow(dead_code)]
95pub const EXAMPLE_SIXTEEN: [[usize; 16]; 16] = [
96    [0, 11, 0, 0, 0, 2, 3, 14, 0, 0, 9, 12, 0, 0, 0, 16],
97    [15, 12, 0, 0, 0, 11, 0, 1, 13, 10, 0, 0, 0, 0, 7, 2],
98    [0, 0, 10, 0, 0, 0, 0, 0, 16, 11, 0, 1, 6, 4, 12, 3],
99    [0, 16, 14, 1, 0, 4, 0, 6, 0, 3, 0, 15, 0, 8, 0, 0],
100    [1, 6, 5, 12, 0, 0, 11, 0, 0, 9, 8, 0, 0, 0, 0, 0],
101    [0, 0, 0, 7, 14, 1, 8, 0, 0, 15, 6, 0, 13, 5, 0, 4],
102    [4, 15, 8, 0, 9, 13, 0, 0, 0, 0, 7, 16, 3, 0, 0, 0],
103    [0, 9, 13, 0, 0, 0, 0, 15, 10, 0, 0, 0, 7, 6, 0, 11],
104    [14, 0, 6, 11, 0, 0, 0, 12, 7, 0, 0, 0, 0, 3, 13, 0],
105    [0, 0, 0, 5, 8, 14, 0, 0, 0, 0, 13, 11, 0, 1, 2, 6],
106    [13, 0, 16, 4, 0, 15, 5, 0, 0, 1, 12, 6, 8, 0, 0, 0],
107    [0, 0, 0, 0, 0, 16, 10, 0, 0, 8, 0, 0, 11, 9, 4, 5],
108    [0, 0, 11, 0, 1, 0, 14, 0, 5, 0, 3, 0, 15, 7, 16, 0],
109    [5, 13, 15, 3, 16, 0, 4, 7, 0, 0, 0, 0, 0, 2, 0, 0],
110    [16, 1, 0, 0, 0, 0, 12, 2, 14, 0, 15, 0, 0, 0, 3, 8],
111    [9, 0, 0, 0, 13, 5, 0, 0, 8, 6, 16, 0, 0, 0, 10, 0],
112];
113
114/// An example 25x25 Sudoku puzzle. Values are 1-25, 0 for empty.
115#[allow(dead_code)]
116pub const EXAMPLE_TWENTY_FIVE: [[usize; 25]; 25] = [
117    [
118        0, 2, 0, 0, 0, 3, 14, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 13, 4, 24, 0, 7, 1, 0, 0,
119    ],
120    [
121        0, 10, 17, 0, 0, 0, 6, 18, 0, 0, 22, 16, 0, 12, 0, 0, 0, 0, 1, 0, 0, 0, 13, 19, 0,
122    ],
123    [
124        0, 15, 24, 13, 7, 0, 0, 0, 4, 0, 10, 0, 0, 3, 14, 0, 18, 0, 0, 0, 0, 22, 2, 6, 0,
125    ],
126    [
127        0, 0, 1, 21, 0, 0, 15, 0, 22, 0, 0, 19, 13, 0, 0, 0, 8, 0, 0, 0, 0, 16, 18, 20, 0,
128    ],
129    [
130        0, 5, 0, 0, 20, 7, 25, 19, 0, 0, 0, 21, 17, 18, 2, 10, 12, 22, 9, 15, 11, 0, 0, 0, 0,
131    ],
132    [
133        11, 0, 0, 0, 22, 8, 0, 24, 7, 1, 5, 0, 0, 0, 13, 16, 17, 25, 23, 2, 4, 0, 6, 0, 19,
134    ],
135    [
136        16, 9, 12, 0, 17, 0, 19, 22, 0, 0, 0, 0, 18, 21, 0, 0, 20, 6, 13, 0, 7, 0, 0, 23, 11,
137    ],
138    [
139        0, 0, 6, 0, 21, 9, 16, 0, 3, 0, 0, 22, 20, 19, 0, 0, 0, 0, 15, 8, 25, 0, 0, 0, 0,
140    ],
141    [
142        0, 0, 23, 5, 0, 2, 0, 0, 11, 17, 8, 0, 0, 0, 16, 12, 9, 0, 0, 21, 0, 3, 10, 0, 0,
143    ],
144    [
145        0, 0, 0, 0, 0, 6, 0, 0, 12, 0, 9, 1, 25, 0, 3, 0, 11, 0, 0, 7, 0, 0, 21, 0, 0,
146    ],
147    [
148        0, 0, 9, 0, 0, 23, 0, 5, 17, 4, 16, 0, 11, 0, 22, 18, 2, 0, 21, 13, 0, 0, 7, 0, 0,
149    ],
150    [
151        4, 6, 0, 0, 5, 0, 0, 2, 0, 0, 0, 18, 21, 24, 0, 0, 19, 3, 0, 12, 23, 0, 0, 17, 0,
152    ],
153    [
154        0, 0, 0, 12, 11, 0, 7, 3, 0, 24, 17, 20, 15, 13, 19, 1, 0, 5, 8, 0, 6, 9, 0, 0, 0,
155    ],
156    [
157        0, 22, 0, 0, 14, 19, 0, 6, 16, 0, 0, 8, 9, 7, 0, 0, 0, 24, 0, 0, 3, 0, 0, 1, 18,
158    ],
159    [
160        0, 0, 21, 0, 0, 25, 13, 0, 20, 8, 12, 0, 14, 0, 10, 9, 16, 15, 0, 6, 0, 0, 4, 0, 0,
161    ],
162    [
163        0, 0, 25, 0, 0, 24, 0, 0, 18, 0, 4, 0, 3, 10, 5, 0, 1, 0, 0, 14, 0, 0, 0, 0, 0,
164    ],
165    [
166        0, 0, 5, 3, 0, 17, 0, 0, 23, 7, 13, 0, 0, 0, 18, 19, 21, 0, 0, 22, 0, 11, 12, 0, 0,
167    ],
168    [
169        0, 0, 0, 0, 18, 10, 8, 0, 0, 0, 0, 25, 23, 2, 0, 0, 5, 0, 16, 11, 9, 0, 3, 0, 0,
170    ],
171    [
172        17, 20, 0, 0, 2, 0, 22, 16, 6, 0, 0, 7, 12, 0, 0, 0, 0, 9, 3, 0, 18, 0, 23, 24, 25,
173    ],
174    [
175        6, 0, 4, 0, 16, 1, 11, 12, 25, 3, 19, 0, 0, 0, 21, 17, 23, 8, 0, 18, 2, 0, 0, 0, 14,
176    ],
177    [
178        0, 0, 0, 0, 4, 14, 24, 11, 19, 23, 21, 17, 16, 8, 0, 0, 0, 1, 2, 9, 13, 0, 0, 5, 0,
179    ],
180    [
181        0, 1, 14, 23, 0, 0, 0, 0, 9, 0, 0, 0, 19, 5, 0, 0, 24, 0, 12, 0, 0, 8, 17, 0, 0,
182    ],
183    [
184        0, 16, 11, 8, 0, 0, 0, 0, 1, 0, 6, 4, 0, 0, 23, 0, 15, 0, 0, 0, 14, 12, 9, 10, 0,
185    ],
186    [
187        0, 21, 3, 0, 0, 0, 17, 0, 0, 0, 0, 15, 0, 25, 20, 0, 0, 4, 10, 0, 0, 0, 16, 11, 0,
188    ],
189    [
190        0, 0, 20, 2, 0, 16, 5, 8, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 19, 25, 0, 0, 0, 3, 0,
191    ],
192];
193
194/// Represents the supported sizes of a Sudoku board (N x N).
195#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
196pub enum Size {
197    /// A 4x4 Sudoku board.
198    Four = 4,
199    /// A 9x9 Sudoku board.
200    Nine = 9,
201    /// A 16x16 Sudoku board.
202    Sixteen = 16,
203    /// A 25x25 Sudoku board.
204    TwentyFive = 25,
205}
206
207impl TryFrom<usize> for Size {
208    type Error = ();
209
210    /// Tries to convert a `usize` into a `Size`.
211    ///
212    /// Returns `Ok(Size)` if the value is one of 4, 9, 16, or 25.
213    /// Otherwise, returns `Err(())`.
214    fn try_from(value: usize) -> Result<Self, Self::Error> {
215        match value {
216            4 => Ok(Self::Four),
217            9 => Ok(Self::Nine),
218            16 => Ok(Self::Sixteen),
219            25 => Ok(Self::TwentyFive),
220            _ => Err(()),
221        }
222    }
223}
224
225impl From<Size> for usize {
226    /// Converts a `Size` into its `usize` representation.
227    fn from(size: Size) -> Self {
228        match size {
229            Size::Four => 4,
230            Size::Nine => 9,
231            Size::Sixteen => 16,
232            Size::TwentyFive => 25,
233        }
234    }
235}
236
237impl Display for Size {
238    /// Formats the size as `NxN`. For example, `Size::Nine` will be "9x9".
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        let size: usize = (*self).into();
241        write!(f, "{size}x{size}")
242    }
243}
244
245/// Represents a Sudoku puzzle, including its board and size.
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct Sudoku {
248    /// The game board.
249    pub board: Board,
250    /// The dimension of the Sudoku grid (e.g. 9 for a 9x9 grid).
251    pub size: Size,
252}
253
254impl Display for Sudoku {
255    /// Formats the Sudoku puzzle, including its size and board.
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        writeln!(f, "Sudoku {{ size: {} }}", self.size)?;
258        write!(f, "{}", self.board)
259    }
260}
261
262/// Represents a variable in the SAT encoding of a Sudoku puzzle.
263///
264/// A variable `(row, col, num)` is true if the cell at `(row, col)` contains `num`.
265/// Rows, columns, and numbers are 1-indexed.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct Variable {
268    /// The row index (1-based).
269    pub row: usize,
270    /// The column index (1-based).
271    pub col: usize,
272    /// The number in the cell (1-based).
273    pub num: usize,
274}
275
276impl Variable {
277    /// Creates a new `Variable`.
278    ///
279    /// # Arguments
280    ///
281    /// * `row`: 1-based row index.
282    /// * `col`: 1-based column index.
283    /// * `num`: 1-based number.
284    #[must_use]
285    pub const fn new(row: usize, col: usize, num: usize) -> Self {
286        Self { row, col, num }
287    }
288
289    /// Encodes the variable into a unique positive integer for SAT solvers.
290    ///
291    /// The encoding scheme maps `(row, col, num)` to a unique integer.
292    /// Assumes `row`, `col`, `num` are 1-indexed and within the board's dimensions.
293    /// The resulting integer is also 1-indexed.
294    ///
295    /// Formula: `((row - 1) * board_size^2) + ((col - 1) * board_size) + (num - 1) + 1`
296    ///
297    /// # Arguments
298    ///
299    /// * `size`: The `Size` of the Sudoku board.
300    #[must_use]
301    pub const fn encode(&self, size: Size) -> usize {
302        let board_size = size as usize;
303        let row = self.row;
304        let col = self.col;
305        let num = self.num;
306
307        ((row - 1) * board_size * board_size + (col - 1) * board_size + (num - 1)) + 1
308    }
309}
310
311impl Size {
312    /// Returns the size of the blocks (subgrids) in a Sudoku puzzle.
313    /// For example, a 9x9 Sudoku has 3x3 blocks.
314    #[must_use]
315    pub const fn block_size(self) -> usize {
316        match self {
317            Self::Four => 2,
318            Self::Nine => 3,
319            Self::Sixteen => 4,
320            Self::TwentyFive => 5,
321        }
322    }
323}
324
325/// Generates SAT clauses ensuring each cell contains at least one number.
326/// (`X_r,c,1` OR `X_r,c,2` OR ... OR `X_r,c,N`) for each cell (r,c).
327fn generate_cell_clauses(size: usize) -> Vec<Vec<i32>> {
328    let mut clauses = vec![];
329    for row in 1..=size {
330        for col in 1..=size {
331            let clause = (1..=size)
332                .map(|num| {
333                    i32::try_from(
334                        Variable::new(row, col, num)
335                            .encode(Size::try_from(size).expect("Invalid size")),
336                    )
337                    .expect("Invalid variable")
338                })
339                .collect();
340
341            clauses.push(clause);
342        }
343    }
344    clauses
345}
346
347/// Generates SAT clauses ensuring no number appears more than once in each row.
348/// (-`X_r,c1,k` OR -`X_r,c2,k`) for each row (r), number (k), and each pair of columns c1 < c2.
349fn generate_row_clauses(size: usize) -> Vec<Vec<i32>> {
350    let mut clauses = vec![];
351    for row in 1..=size {
352        for num in 1..=size {
353            for col1 in 1..=size {
354                for col2 in (col1 + 1)..=size {
355                    if col1 > col2 {
356                        continue;
357                    }
358
359                    let var1 = Variable::new(row, col1, num);
360                    let var2 = Variable::new(row, col2, num);
361
362                    clauses.push(vec![
363                        -i32::try_from(var1.encode(Size::try_from(size).expect("Invalid size")))
364                            .expect("Invalid variable"),
365                        -i32::try_from(var2.encode(Size::try_from(size).expect("Invalid size")))
366                            .expect("Invalid variable"),
367                    ]);
368                }
369            }
370        }
371    }
372    clauses
373}
374
375/// Generates SAT clauses ensuring no number appears more than once in each column.
376/// (-`X_r1,c,k` OR -`X_r2,c,k`) for each column (c), number (k), and each pair of rows r1 < r2.
377fn generate_col_clauses(size: usize) -> Vec<Vec<i32>> {
378    let mut clauses = vec![];
379    for col in 1..=size {
380        for num in 1..=size {
381            for row1 in 1..=size {
382                for row2 in (row1 + 1)..=size {
383                    let var1 = Variable::new(row1, col, num);
384                    let var2 = Variable::new(row2, col, num);
385
386                    clauses.push(vec![
387                        -i32::try_from(var1.encode(Size::try_from(size).expect("Invalid size")))
388                            .expect("Invalid variable"),
389                        -i32::try_from(var2.encode(Size::try_from(size).expect("Invalid size")))
390                            .expect("Invalid variable"),
391                    ]);
392                }
393            }
394        }
395    }
396    clauses
397}
398
399/// Generates SAT clauses ensuring no number appears more than once in each block (subgrid).
400/// (-`X_r1,c1,k` OR -`X_r2,c2,k`) for each number k and each pair of distinct cells (r1,c1),
401/// (r2,c2) within the same block.
402fn generate_block_clauses(board_size: usize, block_size: usize) -> Vec<Vec<i32>> {
403    let mut clauses = Vec::new();
404    for n in 1..=board_size {
405        for br_start_idx in (0..board_size).step_by(block_size) {
406            for bc_start_idx in (0..board_size).step_by(block_size) {
407                let mut block_cells = Vec::new();
408                for r_offset in 0..block_size {
409                    for c_offset in 0..block_size {
410                        block_cells
411                            .push((br_start_idx + r_offset + 1, bc_start_idx + c_offset + 1));
412                    }
413                }
414
415                for i in 0..block_cells.len() {
416                    for j in i + 1..block_cells.len() {
417                        let (r1, c1) = block_cells[i];
418                        let (r2, c2) = block_cells[j];
419                        let var1 = Variable::new(r1, c1, n);
420                        let var2 = Variable::new(r2, c2, n);
421
422                        clauses.push(vec![
423                            -i32::try_from(
424                                var1.encode(Size::try_from(board_size).expect("Invalid size")),
425                            )
426                            .expect("Invalid variable"),
427                            -i32::try_from(
428                                var2.encode(Size::try_from(board_size).expect("Invalid size")),
429                            )
430                            .expect("Invalid variable"),
431                        ]);
432                    }
433                }
434            }
435        }
436    }
437    clauses
438}
439
440/// Generates SAT clauses for the pre-filled cells in the Sudoku board.
441/// (`X_r,c,k`) for each cell (r,c) pre-filled with k.
442fn generate_pre_filled_clauses(size: usize, board: &Board) -> Vec<Vec<i32>> {
443    let mut clauses = vec![];
444    for (r_idx, row_vec) in board.0.iter().enumerate() {
445        for (c_idx, &n_val) in row_vec.iter().enumerate() {
446            if n_val != 0 {
447                let var = Variable::new(r_idx + 1, c_idx + 1, n_val);
448                clauses.push(vec![
449                    i32::try_from(var.encode(Size::try_from(size).expect("Invalid size")))
450                        .expect("Invalid variable"),
451                ]);
452            }
453        }
454    }
455    clauses
456}
457
458impl Sudoku {
459    /// Creates a new `Sudoku` puzzle from a `Board`.
460    ///
461    /// The size of the Sudoku is inferred from the dimensions of the board.
462    ///
463    /// # Arguments
464    ///
465    /// * `board`: A `Board` representing the Sudoku grid.
466    ///
467    /// # Returns
468    ///
469    /// A `Result<Self, String>` where `Self` is the new `Sudoku` instance.
470    ///
471    /// # Errors
472    ///
473    /// Returns an error if the board size is not one of the supported Sudoku sizes (4, 9, 16, or 25).
474    pub fn new(board: Board) -> Result<Self, String> {
475        let size_val = board.0.len();
476        let size = Size::try_from(size_val).map_err(|()| {
477            format!("Invalid Sudoku size: {size_val}. Supported sizes are 4, 9, 16, or 25.",)
478        })?;
479        Ok(Self { board, size })
480    }
481
482    /// Decodes a `Sudoku` solution from SAT solver results.
483    ///
484    /// Given a `Solutions` object (typically from a SAT solver), this method
485    /// constructs the solved Sudoku board by checking which SAT variables `X_r,c,k` are true.
486    ///
487    /// # Arguments
488    ///
489    /// * `solutions`: A `Solutions` object containing the truth assignments for SAT variables.
490    ///
491    /// # Returns
492    ///
493    /// A new `Sudoku` instance representing the solved puzzle.
494    ///
495    /// # Panics
496    /// if `var.encode(self.size)` results in 0, as `NonZeroI32::new` would return `None`.
497    /// However, `Variable::encode` is designed to return positive integers.
498    #[must_use]
499    pub fn decode(&self, solutions: &Solutions) -> Self {
500        let size_val: usize = self.size.into();
501        let mut board_vec = vec![vec![0; size_val]; size_val];
502        for row in 1..=size_val {
503            for col in 1..=size_val {
504                for num in 1..=size_val {
505                    let var = Variable::new(row, col, num);
506                    #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
507                    let encoded_var = NonZeroI32::new(var.encode(self.size) as i32)
508                        .expect("Encoded variable should not be zero");
509                    if solutions.check(encoded_var) {
510                        board_vec[row - 1][col - 1] = num;
511                    }
512                }
513            }
514        }
515        Self {
516            board: Board::new(board_vec),
517            size: self.size,
518        }
519    }
520
521    /// Converts the Sudoku puzzle into Conjunctive Normal Form (CNF) for a SAT solver.
522    ///
523    /// This method generates all the necessary clauses to represent the Sudoku problem:
524    /// 1. Cell constraints: Each cell must contain at least one number.
525    /// 2. Row constraints: Each number must appear at most once per row.
526    /// 3. Column constraints: Each number must appear at most once per column.
527    /// 4. Block constraints: Each number must appear at most once per block (subgrid).
528    /// 5. Pre-filled constraints: Given numbers in the puzzle are fixed (unit clauses).
529    ///
530    /// The combination of "at least one" (from cell constraints) and "at most one"
531    /// (from row, col, block constraints for each number-pair in a cell) ensures each cell
532    /// contains exactly one number.
533    ///
534    /// # Type Parameters
535    ///
536    /// * `L`: A type that implements the `Literal` trait from the SAT solver crate.
537    /// * `S`: A type that implements the `LiteralStorage<L>` trait from the SAT solver crate.
538    ///
539    /// # Returns
540    ///
541    /// A `Cnf<L, S>` object representing the Sudoku puzzle in CNF.
542    ///
543    /// # Panics
544    /// If `Size::try_from` or `i32::try_from` fails during clause generation,
545    /// which could happen if variable encodings produce values too large for `i32`
546    /// or if `size` is not a supported Sudoku dimension (though `self.size` should always be valid).
547    #[must_use]
548    pub fn to_cnf<L: Literal, S: LiteralStorage<L>>(&self) -> Cnf<L, S> {
549        let size_val = self.size as usize;
550        let block_size_val = self.size.block_size();
551
552        let cell_clauses = generate_cell_clauses(size_val);
553        let row_clauses = generate_row_clauses(size_val);
554        let col_clauses = generate_col_clauses(size_val);
555        let block_clauses = generate_block_clauses(size_val, block_size_val);
556        let pre_filled_clauses = generate_pre_filled_clauses(size_val, &self.board);
557
558        let clauses: Vec<Vec<i32>> = cell_clauses
559            .into_iter()
560            .chain(row_clauses)
561            .chain(col_clauses)
562            .chain(block_clauses)
563            .chain(pre_filled_clauses)
564            .collect();
565
566        Cnf::new(clauses)
567    }
568
569    /// Returns an iterator over the rows of the Sudoku board.
570    /// Each item yielded by the iterator is a `Vec<usize>` representing a row.
571    pub fn iter(&self) -> impl Iterator<Item = Vec<usize>> {
572        self.board.0.clone().into_iter()
573    }
574
575    /// Parses a `Sudoku` from a string representation.
576    ///
577    /// See [`parse_sudoku`] for details on the expected format and behavior.
578    ///
579    /// # Arguments
580    ///
581    /// * `sudoku_str`: A string slice representing the Sudoku.
582    ///
583    /// # Errors
584    ///
585    /// Returns `Err(String)` if the string format is invalid.
586    pub fn from_string(sudoku_str: &str) -> Result<Self, String> {
587        parse_sudoku(sudoku_str)
588    }
589
590    /// Parses a `Sudoku` from a file.
591    ///
592    /// See [`parse_sudoku_file`] for details.
593    ///
594    /// # Arguments
595    ///
596    /// * `file_path`: Path to the file containing the Sudoku representation.
597    ///
598    /// # Errors
599    ///
600    /// Returns `Err(String)` if the file cannot be read, or if its content
601    /// is an invalid Sudoku representation.
602    pub fn from_file(file_path: &PathBuf) -> Result<Self, String> {
603        parse_sudoku_file(file_path)
604    }
605}
606
607impl TryFrom<Board> for Sudoku {
608    type Error = String;
609    /// Converts a `Board` into a `Sudoku` puzzle.
610    /// Size is inferred from the board.
611    /// # Panics
612    /// if the board size is not one of the supported `Size` values.
613    fn try_from(board: Board) -> Result<Self, Self::Error> {
614        Self::new(board)
615    }
616}
617
618impl From<Sudoku> for Board {
619    /// Converts a `Sudoku` puzzle into its `Board` representation.
620    fn from(sudoku: Sudoku) -> Self {
621        sudoku.board
622    }
623}
624
625/// Parses a Sudoku puzzle from a string.
626///
627/// The input string should ideally have `N` lines, each containing `N` space-separated numbers,
628/// where `N` is a supported Sudoku size (4, 9, 16, 25). `0` represents an empty cell.
629///
630/// Behavior details:
631/// - The number of lines in the input string determines the `size` of the Sudoku. This includes empty lines.
632/// - If `size` is not a supported Sudoku dimension (4, 9, 16, 25), `Sudoku::new` called at the end will panic.
633/// - Empty lines are skipped during parsing; corresponding rows in the board will remain all zeros.
634/// - If a line contains more numbers than `size`, `board[i][j]` access will panic.
635/// - If a line contains fewer numbers than `size`, the remaining cells in that row are treated as empty (0).
636/// - Non-numeric tokens or numbers greater than `size` will result in an error.
637///
638/// # Arguments
639///
640/// * `sudoku_str`: The string representation of the Sudoku.
641///
642/// # Returns
643///
644/// `Ok(Sudoku)` if parsing is successful (before the potential panic in `Sudoku::new`).
645/// `Err(String)` if invalid characters or out-of-range numbers are found.
646///
647/// # Panics
648/// - If a line has more items than `size` (due to `board[i][j]` out-of-bounds access).
649/// - If `size` (derived from `lines.len()`) is not a supported Sudoku size (4, 9, 16, 25),
650///   this function will return `Ok(Sudoku)` but the caller might panic if `Sudoku::new` (called internally) panics.
651///
652/// # Errors
653///
654/// Returns `Err(String)` if the input string is not a valid Sudoku representation,
655pub fn parse_sudoku(sudoku: &str) -> Result<Sudoku, String> {
656    let lines = sudoku.lines().collect_vec();
657    let size = lines.len();
658    let mut board = vec![vec![0; size]; size];
659
660    for (i, line) in lines.iter().enumerate() {
661        if line.is_empty() {
662            continue;
663        }
664        for (j, c) in line.split_ascii_whitespace().enumerate() {
665            if j >= size {
666                return Err(format!(
667                    "Invalid Sudoku: Too many numbers in line {}",
668                    i + 1
669                ));
670            }
671
672            if let Ok(num) = c.parse::<usize>() {
673                if num > size {
674                    return Err(format!("Invalid Sudoku: Number {num} exceeds size {size}"));
675                }
676                board[i][j] = num;
677            } else {
678                return Err(format!("Invalid Sudoku: Invalid character {c}"));
679            }
680        }
681    }
682
683    Sudoku::new(Board::new(board))
684}
685
686/// Parses a Sudoku puzzle from a file.
687///
688/// Reads the file content and uses `parse_sudoku` to parse it.
689/// Refer to [`parse_sudoku`] for details on parsing behavior and potential panics.
690///
691/// # Arguments
692///
693/// * `file_path`: The path to the file.
694///
695/// # Returns
696///
697/// `Ok(Sudoku)` if file reading and parsing are successful.
698/// `Err(String)` if the file cannot be read or its content is invalid according to `parse_sudoku`.
699///
700/// # Panics
701/// Under the same conditions as `parse_sudoku` (e.g. invalid size, too many numbers per line).
702///
703/// # Errors
704/// Returns `Err(String)` if the file cannot be read or if its content is not a valid Sudoku representation.
705pub fn parse_sudoku_file(file_path: &PathBuf) -> Result<Sudoku, String> {
706    let content = std::fs::read_to_string(file_path).map_err(|e| e.to_string())?;
707    parse_sudoku(&content)
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use std::collections::HashSet;
714    #[test]
715    fn board_new_and_display() {
716        let data = vec![vec![1, 0], vec![0, 2]];
717        let board = Board::new(data.clone());
718        assert_eq!(board.0, data);
719        assert_eq!(format!("{board}"), "1 0\n0 2\n");
720    }
721
722    #[test]
723    fn board_from_vec_vec() {
724        let data = vec![vec![1], vec![2]];
725        let board: Board = data.clone().into();
726        assert_eq!(board.0, data);
727    }
728
729    #[test]
730    fn vec_vec_from_board() {
731        let data = vec![vec![1], vec![2]];
732        let board = Board::new(data.clone());
733        let converted_data: Vec<Vec<usize>> = board.into();
734        assert_eq!(converted_data, data);
735    }
736
737    #[test]
738    fn vec_vec_from_ref_board() {
739        let data = vec![vec![1], vec![2]];
740        let board = Board::new(data.clone());
741        let converted_data: Vec<Vec<usize>> = (&board).into();
742        assert_eq!(converted_data, data);
743    }
744
745    #[test]
746    fn board_from_array_of_refs() {
747        let r1: [usize; 2] = [1, 0];
748        let r2: [usize; 2] = [0, 2];
749        let board_data: [&[usize; 2]; 2] = [&r1, &r2];
750        let board = Board::from(&board_data);
751        assert_eq!(board, Board::new(vec![vec![1, 0], vec![0, 2]]));
752    }
753
754    #[test]
755    fn size_try_from_usize() {
756        assert_eq!(Size::try_from(4), Ok(Size::Four));
757        assert_eq!(Size::try_from(9), Ok(Size::Nine));
758        assert_eq!(Size::try_from(16), Ok(Size::Sixteen));
759        assert_eq!(Size::try_from(25), Ok(Size::TwentyFive));
760        assert_eq!(Size::try_from(1), Err(()));
761        assert_eq!(Size::try_from(0), Err(()));
762    }
763
764    #[test]
765    fn usize_from_size() {
766        assert_eq!(usize::from(Size::Four), 4);
767        assert_eq!(usize::from(Size::Nine), 9);
768        assert_eq!(usize::from(Size::Sixteen), 16);
769        assert_eq!(usize::from(Size::TwentyFive), 25);
770    }
771
772    #[test]
773    fn size_display() {
774        assert_eq!(format!("{}", Size::Four), "4x4");
775        assert_eq!(format!("{}", Size::Nine), "9x9");
776    }
777
778    #[test]
779    fn size_block_size() {
780        assert_eq!(Size::Four.block_size(), 2);
781        assert_eq!(Size::Nine.block_size(), 3);
782        assert_eq!(Size::Sixteen.block_size(), 4);
783        assert_eq!(Size::TwentyFive.block_size(), 5);
784    }
785
786    #[test]
787    fn variable_new_and_encode() {
788        let var = Variable::new(1, 1, 1);
789        assert_eq!(
790            var,
791            Variable {
792                row: 1,
793                col: 1,
794                num: 1
795            }
796        );
797
798        assert_eq!(Variable::new(1, 1, 1).encode(Size::Four), 1); // (0*16) + (0*4) + 0 + 1 = 1
799        assert_eq!(Variable::new(1, 1, 4).encode(Size::Four), 4); // (0*16) + (0*4) + 3 + 1 = 4
800        assert_eq!(Variable::new(1, 2, 1).encode(Size::Four), 5); // (0*16) + (1*4) + 0 + 1 = 5
801        assert_eq!(Variable::new(2, 1, 1).encode(Size::Four), 17); // (1*16) + (0*4) + 0 + 1 = 17
802        assert_eq!(Variable::new(4, 4, 4).encode(Size::Four), 64); // (3*16) + (3*4) + 3 + 1 = 48+12+3+1 = 64
803
804        assert_eq!(Variable::new(1, 1, 1).encode(Size::Nine), 1);
805        assert_eq!(Variable::new(9, 9, 9).encode(Size::Nine), 729); // (8*81) + (8*9) + 8 + 1 = 648+72+8+1 = 729
806    }
807
808    // Test Sudoku
809    #[test]
810    fn sudoku_new() {
811        let board_data = vec![vec![0; 4]; 4];
812        let board = Board::new(board_data);
813        let sudoku = Sudoku::new(board.clone());
814        assert!(sudoku.is_ok());
815        let sudoku = sudoku.unwrap();
816        assert_eq!(sudoku.board, board);
817        assert_eq!(sudoku.size, Size::Four);
818    }
819
820    #[test]
821    #[should_panic(expected = "Invalid size for Sudoku board")]
822    fn sudoku_new_invalid_size() {
823        let board_data = vec![vec![0; 5]; 5];
824        let board = Board::new(board_data);
825        let _ = Sudoku::new(board);
826    }
827
828    #[test]
829    fn sudoku_display() {
830        let board_data = vec![
831            vec![1, 0, 0, 0],
832            vec![0, 2, 0, 0],
833            vec![0, 0, 3, 0],
834            vec![0, 0, 0, 4],
835        ];
836        let sudoku = Sudoku::new(Board::new(board_data));
837        assert!(sudoku.is_ok());
838        let sudoku = sudoku.unwrap();
839        let expected_display = "Sudoku { size: 4x4 }\n1 0 0 0\n0 2 0 0\n0 0 3 0\n0 0 0 4\n";
840        assert_eq!(format!("{sudoku}"), expected_display);
841    }
842
843    #[test]
844    fn sudoku_iter() {
845        let sudoku_board_data = vec![
846            vec![1, 2, 0, 0],
847            vec![3, 4, 0, 0],
848            vec![0, 0, 0, 0],
849            vec![0, 0, 0, 0],
850        ];
851        let sudoku = Sudoku::try_from(Board::from(sudoku_board_data));
852        assert!(sudoku.is_ok());
853        let sudoku = sudoku.unwrap();
854        let mut iter = sudoku.iter();
855        assert_eq!(iter.next(), Some(vec![1, 2, 0, 0]));
856        assert_eq!(iter.next(), Some(vec![3, 4, 0, 0]));
857        assert_eq!(iter.next(), Some(vec![0, 0, 0, 0]));
858        assert_eq!(iter.next(), Some(vec![0, 0, 0, 0]));
859        assert_eq!(iter.next(), None);
860    }
861
862    // Test parsing
863    #[test]
864    fn parse_sudoku_valid_4x4() {
865        let s = "1 0 3 0\n0 2 0 0\n0 0 0 4\n0 0 1 0";
866        let sudoku = parse_sudoku(s).unwrap();
867        assert_eq!(sudoku.size, Size::Four);
868        let expected_board = Board::new(vec![
869            vec![1, 0, 3, 0],
870            vec![0, 2, 0, 0],
871            vec![0, 0, 0, 4],
872            vec![0, 0, 1, 0],
873        ]);
874        assert_eq!(sudoku.board, expected_board);
875    }
876
877    #[test]
878    #[should_panic(expected = "Invalid size for Sudoku board")]
879    fn parse_sudoku_invalid_size_panic() {
880        let s = "1 0\n0 1";
881        parse_sudoku(s).unwrap();
882    }
883
884    #[test]
885    fn parse_sudoku_invalid_char() {
886        let s = "1 0 X 0\n0 2 0 0\n0 0 0 4\n0 0 1 0";
887        assert!(parse_sudoku(s).is_err());
888        assert_eq!(
889            parse_sudoku(s).unwrap_err(),
890            "Invalid Sudoku: Invalid character X"
891        );
892    }
893
894    #[test]
895    fn parse_sudoku_number_too_large() {
896        let s = "1 0 5 0\n0 2 0 0\n0 0 0 4\n0 0 1 0"; // 5 in a 4x4
897        let err = parse_sudoku(s).unwrap_err();
898        assert_eq!(err, "Invalid Sudoku: Number 5 exceeds size 4");
899    }
900
901    #[test]
902    fn parse_sudoku_too_many_numbers_in_line() {
903        let s = "1 0 0 0 5\n0 2 0 0\n0 0 0 4\n0 0 1 0";
904        match parse_sudoku(s) {
905            Ok(_) => panic!("Should have failed or panicked."),
906            Err(e) => {
907                assert_eq!(e, "Invalid Sudoku: Too many numbers in line 1");
908            }
909        }
910    }
911
912    #[test]
913    fn parse_sudoku_too_few_numbers_in_line() {
914        let s = "1 0\n0 2 0 0\n0 0 0 4\n0 0 1 0";
915        let sudoku = parse_sudoku(s).unwrap();
916        let expected_board = Board::new(vec![
917            vec![1, 0, 0, 0], // 1 0 -> 1 0 0 0
918            vec![0, 2, 0, 0],
919            vec![0, 0, 0, 4],
920            vec![0, 0, 1, 0],
921        ]);
922        assert_eq!(sudoku.board, expected_board);
923    }
924
925    #[test]
926    fn parse_sudoku_empty_line_behavior() {
927        let s = "1 0 3 0\n0 2 0 0\n\n0 0 0 4\n0 0 1 0";
928        assert!(
929            std::panic::catch_unwind(|| parse_sudoku(s)).is_err(),
930            "Sudoku::new should panic for size 5"
931        );
932    }
933
934    #[test]
935    fn sudoku_decode_basic() {
936        let initial_board_data = vec![vec![0; 4]; 4];
937        let sudoku = Sudoku::new(Board::new(initial_board_data));
938        assert!(sudoku.is_ok());
939        let sudoku = sudoku.unwrap();
940
941        let mut solution_vars = Vec::new();
942        let solution_grid: [[usize; 4]; 4] =
943            [[1, 2, 3, 4], [3, 4, 1, 2], [2, 1, 4, 3], [4, 3, 2, 1]];
944
945        for (r, row) in solution_grid.iter().enumerate() {
946            for (c, &num) in row.iter().enumerate() {
947                #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
948                solution_vars.push(Variable::new(r + 1, c + 1, num).encode(Size::Four) as i32);
949            }
950        }
951
952        let solutions = Solutions::new(&solution_vars);
953        let decoded_sudoku = sudoku.decode(&solutions);
954
955        let expected_board_data: Vec<Vec<usize>> =
956            solution_grid.iter().map(|row| row.to_vec()).collect();
957        assert_eq!(decoded_sudoku.board.0, expected_board_data);
958        assert_eq!(decoded_sudoku.size, Size::Four);
959    }
960
961    #[test]
962    fn test_generate_cell_clauses_content() {
963        let cell_clauses = generate_cell_clauses(4);
964        let clause_for_cell_1_1 = vec![1, 2, 3, 4];
965        assert!(cell_clauses.contains(&clause_for_cell_1_1));
966        assert_eq!(cell_clauses.len(), 4 * 4);
967    }
968
969    #[test]
970    fn test_generate_row_clauses_content() {
971        let row_clauses = generate_row_clauses(4);
972        #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
973        let var111 = Variable::new(1, 1, 1).encode(Size::Four) as i32;
974        #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
975        let var121 = Variable::new(1, 2, 1).encode(Size::Four) as i32;
976        let clause_example = vec![-var111, -var121]; // {-1, -5}
977        assert!(row_clauses.contains(&clause_example));
978        assert_eq!(row_clauses.len(), 96);
979    }
980
981    #[test]
982    fn test_generate_pre_filled_clauses() {
983        let board_data = vec![
984            vec![1, 0, 0, 0],
985            vec![0, 2, 0, 0],
986            vec![0, 0, 0, 0],
987            vec![0, 0, 0, 0],
988        ];
989        let board = Board::new(board_data);
990        let pre_filled = generate_pre_filled_clauses(4, &board);
991
992        #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
993        let var111 = Variable::new(1, 1, 1).encode(Size::Four) as i32;
994        #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
995        let var222 = Variable::new(2, 2, 2).encode(Size::Four) as i32;
996
997        let expected_clauses: HashSet<Vec<i32>> =
998            [vec![var111], vec![var222]].iter().cloned().collect();
999
1000        let actual_clauses: HashSet<Vec<i32>> = pre_filled.iter().cloned().collect();
1001        assert_eq!(actual_clauses, expected_clauses);
1002        assert_eq!(pre_filled.len(), 2);
1003    }
1004}