domino_lib/solve/mod.rs
1//! This module provides a recursive backtracking algorithm for solving a Domino puzzle.
2//!
3//! It includes functions to determine missing tiles, check valid placements, and recursively
4//! search for a valid solution.
5
6use std::collections::HashSet;
7use std::time::Instant;
8
9use crate::{utils::get_n, DominoError, Puzzle, Solution, Tile};
10
11/// Attempts to solve a given puzzle using a recursive backtracking approach.
12///
13/// This function clones the puzzle, determines the missing tiles, and then attempts to solve
14/// the puzzle by filling in the missing tiles while ensuring valid adjacency constraints.
15///
16/// # Arguments
17///
18/// * `puzzle` - A reference to the `Puzzle` structure representing the current puzzle state.
19///
20/// # Returns
21///
22/// * `Ok(Solution)` - If a valid solution is found.
23/// * `Err(DominoError::UnsolvablePuzzle)` - If no solution exists.
24/// * `Err(DominoError::InvalidPuzzle)` - If the puzzle input is invalid.
25pub fn solve_puzzle(puzzle: &Puzzle) -> Result<Solution, DominoError> {
26 let mut new_puzzle = puzzle.clone();
27 let missing_tiles = get_missing_tiles(puzzle)?;
28 let start_instant = Instant::now();
29
30 if solve_puzzle_r(&mut new_puzzle, &missing_tiles, 0, &start_instant) {
31 let solution = new_puzzle.into_iter().map(|tile| tile.unwrap()).collect();
32 return Ok(solution);
33 } else {
34 return Err(DominoError::UnsolvablePuzzle);
35 }
36}
37
38/// Recursive backtracking function to solve the puzzle.
39///
40/// This function iterates over the missing tiles, attempting to place each tile in the puzzle.
41/// It ensures that the placement is valid according to adjacency constraints and backtracks
42/// if necessary.
43///
44/// # Arguments
45///
46/// * `puzzle` - A mutable reference to the puzzle being solved.
47/// * `missing_tiles` - A reference to a `HashSet` of available tiles.
48/// * `current_position` - The index in the puzzle currently being filled.
49/// * `start_instant` - A reference to the `Instant` tracking execution time.
50///
51/// # Returns
52///
53/// * `true` - If a valid solution is found.
54/// * `false` - If no valid placement is possible.
55fn solve_puzzle_r(
56 puzzle: &mut Puzzle,
57 missing_tiles: &HashSet<Tile>,
58 current_position: usize,
59 start_instant: &Instant,
60) -> bool {
61 // Base case: all positions are filled successfully
62 if current_position == puzzle.len() {
63 return true;
64 }
65
66 // Skip already filled positions
67 if puzzle[current_position].is_some() {
68 return solve_puzzle_r(puzzle, missing_tiles, current_position + 1, start_instant);
69 }
70
71 // Try each missing tile
72 for &element in missing_tiles {
73 if is_valid_placement(puzzle, element, current_position) {
74 puzzle[current_position] = Some(element);
75
76 if solve_puzzle_r(puzzle, missing_tiles, current_position + 1, start_instant) {
77 return true;
78 }
79
80 // Backtrack if no solution is found
81 puzzle[current_position] = None;
82 }
83
84 let flipped_element = element.flip();
85 if is_valid_placement(puzzle, flipped_element, current_position) {
86 puzzle[current_position] = Some(flipped_element);
87
88 if solve_puzzle_r(puzzle, missing_tiles, current_position + 1, start_instant) {
89 return true;
90 }
91
92 // Backtrack
93 puzzle[current_position] = None;
94 }
95 }
96 false
97}
98
99/// Checks whether a tile can be placed at a given position in the puzzle.
100///
101/// Ensures that:
102/// - The tile is not already used elsewhere in the puzzle.
103/// - The left neighbor (if any) matches the left side of the tile.
104/// - The right neighbor (if any) matches the right side of the tile.
105///
106/// # Arguments
107///
108/// * `puzzle` - A reference to the puzzle grid.
109/// * `tile` - The tile being placed.
110/// * `position` - The index where the tile is being placed.
111///
112/// # Returns
113///
114/// * `true` - If the placement is valid.
115/// * `false` - If the placement violates constraints.
116fn is_valid_placement(puzzle: &Puzzle, tile: Tile, position: usize) -> bool {
117 let puzzle_length = puzzle.len();
118
119 puzzle.iter().all(|&slot| slot != Some(tile))
120 && (puzzle[(puzzle_length + position - 1) % puzzle_length].is_none()
121 || puzzle[(puzzle_length + position - 1) % puzzle_length].unwrap().1 == tile.0)
122 && (puzzle[(position + 1) % puzzle_length].is_none()
123 || puzzle[(position + 1) % puzzle_length].unwrap().0 == tile.1)
124}
125
126/// Determines the missing tiles in the puzzle by comparing with a full tileset.
127///
128/// The function generates the full set of valid tiles and removes the tiles already present in the puzzle.
129///
130/// # Arguments
131///
132/// * `puzzle` - A reference to the `Puzzle` structure.
133///
134/// # Returns
135///
136/// * `Ok(HashSet<Tile>)` - The set of missing tiles that need to be placed.
137/// * `Err(DominoError::InvalidPuzzle)` - If the puzzle input is invalid.
138pub fn get_missing_tiles(puzzle: &Puzzle) -> Result<HashSet<Tile>, DominoError> {
139 let n = get_n(puzzle)?;
140
141 // Generate the complete set of valid tiles
142 let tileset: HashSet<Tile> = (0..=n)
143 .flat_map(|i| (0..=n).map(move |j| Tile(i, j)))
144 .filter(|tile| {
145 if n % 2 == 0 {
146 true
147 } else {
148 (tile.0 as i32 - tile.1 as i32).abs() != ((n as i32 + 1) / 2)
149 }
150 })
151 .collect();
152
153 // Collect all used tiles (including flipped versions)
154 let used_tiles: HashSet<Tile> = puzzle
155 .iter()
156 .filter_map(|&tile| tile.map(|t| vec![t, t.flip()]))
157 .flatten()
158 .collect();
159
160 // Compute the set of missing tiles
161 let missing_tiles: HashSet<Tile> = tileset.difference(&used_tiles).cloned().collect();
162
163 Ok(missing_tiles)
164}