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}