qsolve 1.0.0

A command-line tool for solving Queens puzzles
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
use std::fmt::{Display, Formatter, Write};

use anyhow::{Result, anyhow};
use clap::ValueEnum;
use itertools::{Itertools, Position};
use log::trace;
use owo_colors::{AnsiColors, OwoColorize};

use crate::{
    board::Board,
    datastructure::{Coord, CoordSet},
    file::QueensFile,
    heuristic::Changes,
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// The assigned state of a square during the solving process.
///
/// As a solver solves a given board, it does so by confirming and eliminating
/// certain squares. This represents the state of those squares -- Queen means
/// it has confirmed it contains a queen, and X means it knows that square
/// cannot contain a queen.
pub enum SquareVal {
    /// The square contains a queen.
    ///
    /// This value is assigned in the solving process if we are sure that
    /// the square contains a queen.
    Queen,
    /// The square cannot contain a queen
    ///
    /// This value is assigned in the solving process if we are sure that
    /// the square cannot contain a queen.
    X,
}

impl SquareVal {
    /// Attempts to convert a character to a `Option<SquareVal>`
    ///
    /// Note that this is not [From] or [TryFrom] because this returns `Result<Option<SquareVal>>`.
    /// That is, this can return either a SquareVal or None or Err, because some chars represent
    /// SquareVals, some represent "blanks", and some are invalid.
    ///
    /// # Examples
    /// ```
    /// use qsolve::solvestate::SquareVal;
    /// assert_eq!(SquareVal::try_from('Q').unwrap(), Some(SquareVal::Queen));
    /// assert_eq!(SquareVal::try_from('x').unwrap(), Some(SquareVal::X));
    /// assert!(SquareVal::try_from('.').unwrap().is_none());
    /// assert!(SquareVal::try_from('S').is_err());
    /// ```
    pub fn try_from(c: char) -> Result<Option<Self>> {
        match c {
            'Q' => Ok(Some(SquareVal::Queen)),
            'x' => Ok(Some(SquareVal::X)),
            ' ' => Ok(None),
            '.' => Ok(None),
            '_' => Ok(None),
            _ => Err(anyhow!("Blank")),
        }
    }
}

impl SquareVal {
    /// Converts the given SquareVal to a character for display.
    ///
    /// Sometimes the square needs to be "highlighted" in some way -- to demonstrate
    /// that the square is under consideration by a heuristic, for example. In general,
    /// we assume the caller will bold the character to do so, but if the square is blank,
    /// a bold space and a space look the same. So this function will oftentimes return a
    /// different character in that case.
    ///
    /// The Charset passed in allows customization of the display.
    pub fn as_char(sv: Option<SquareVal>, highlighted: bool, charset: &Charset) -> char {
        match (sv, highlighted, charset) {
            (Some(SquareVal::Queen), _, Charset::Ascii) => 'Q',
            (Some(SquareVal::Queen), _, Charset::Unicode) => '\u{265B}',
            (Some(SquareVal::X), _, Charset::Ascii) => 'x',
            (Some(SquareVal::X), _, Charset::Unicode) => '\u{00D7}',
            (None, true, Charset::Ascii) => '.',
            (None, true, Charset::Unicode) => '\u{2218}',
            (None, false, _) => ' ',
        }
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
/// What strategy to use for solving the puzzle
pub enum SolveStrategy {
    /// Optimize for generating a solution quickly
    #[default]
    Fast,
    /// Optimize for generating a solution in as few steps as possible
    Short,
    /// Optimize for generating a solution using the simplest moves
    Simple,
}

impl Display for SolveStrategy {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
/// What characters to use in the animation
pub enum Charset {
    /// Uses ASCII characters; Q for queens, x for impossible
    Ascii,
    /// Uses Unicode characters; a queen for queens, and a smaller centered
    /// x for impossible.
    #[default]
    Unicode,
}

#[derive(Clone, Debug)]
/// A representation of a board in the process of being solved. This contains
/// a board (which is constant across a given solving process) and a (possibly
/// incomplete) set of markings on the squares of the board, indicating whether
/// a queen is present, impossible, or still to be determined.
///
/// # Invariants
///
/// We always assume that whenever a Queen has been placed, all squares that the
/// Queen eliminates (be that by row, column, color or adjacency) are x'd out.
/// Especially when taking input from the user, it's important to ensure that
/// invariant is followed. The easiest way to do so is by calling
/// [SolveState::apply_changes] once for each Queen.
pub struct SolveState<'a> {
    /// The board that this solve state is solving
    pub board: &'a Board,

    /// A list of the solve state's values for each square.
    ///
    /// This is stored as a 1D vector, where the first N
    /// values are the first row, the next N values the second row,
    /// and so on.
    ///
    /// Each value is an `Option<SquareVal>`, so it can either be:
    ///  * None, meaning blank
    ///  * Some(Queen), meaning we know a queen is there
    ///  * Some(X), meaning we know no queen can be there
    squares: Vec<Option<SquareVal>>,
}

impl<'a> From<&'a QueensFile> for SolveState<'a> {
    fn from(queens_file: &'a QueensFile) -> Self {
        let mut solve_state = SolveState {
            board: &queens_file.board,
            squares: queens_file
                .squares
                .clone()
                .map(|x| x.into())
                .unwrap_or_else(|| vec![None; queens_file.board.square_count()]),
        };

        // So a Queens File might have Queens listed and not have the x's that those
        // Queens imply. This library assumes a SolveState always has those x's in place,
        // so we need to check that here to avoid violating that invariant.

        for (idx, _) in solve_state
            .clone()
            .squares
            .iter()
            .enumerate()
            .filter(|&(_, &sv)| sv == Some(SquareVal::Queen))
        {
            let queen = solve_state.board.idx_to_coord(&idx);
            let x = solve_state
                .board
                .queen_borders(&queen)
                .iter()
                .filter(|&coord| solve_state.square(&coord).is_none())
                .collect::<CoordSet>();
            solve_state.apply_changes(&Changes::AddQueen { queen, x });
        }

        trace!("From<QueensFile> for SolveState done:\n{}", solve_state);

        solve_state
    }
}

impl SolveState<'_> {
    /// Returns whether the board is complete: that is, whether
    /// there are the same number of queens as their are rows/cols/colors.
    pub fn complete(&self) -> bool {
        self.squares
            .iter()
            .filter_map(|&x| x.map(|sv| sv == SquareVal::Queen))
            .filter(|x| *x)
            .count()
            == self.board.size()
    }

    /// Returns whether the board is valid.
    ///
    /// This requires:
    /// * No column contains multiple queens.
    /// * No row contains multiple queens.
    /// * No color contains multiple queens.
    /// * No queens border each other.
    pub fn is_valid(&self) -> bool {
        let size = self.board.size();
        let rows_valid = (0..size).all(|r| {
            self.board
                .row_coords(r)
                .iter()
                .map(|c| self.square(&c))
                .filter(|&sv| sv == Some(SquareVal::Queen))
                .count()
                <= 1
        });
        let cols_valid = (0..size).all(|c| {
            self.board
                .col_coords(c)
                .iter()
                .map(|c| self.square(&c))
                .filter(|&sv| sv == Some(SquareVal::Queen))
                .count()
                <= 1
        });
        let colors_valid = self.board.all_colors().iter().all(|&&color| {
            self.board
                .all_coords()
                .iter()
                .filter(|c| self.board.color(c) == color)
                .map(|c| self.square(&c))
                .filter(|&sv| sv == Some(SquareVal::Queen))
                .count()
                <= 1
        });
        let queen_coords = self
            .squares
            .iter()
            .enumerate()
            .filter(|&(_, &square)| square == Some(SquareVal::Queen))
            .map(|(idx, _)| self.board.idx_to_coord(&idx))
            .collect::<CoordSet>();
        let queens_valid = queen_coords.clone().iter().all(|c| {
            self.board
                .queen_borders(&c)
                .intersection(&queen_coords)
                .is_empty()
        });
        rows_valid && cols_valid && colors_valid && queens_valid
    }

    /// Returns the value in the given square.
    pub fn square(&self, coord: &Coord) -> Option<SquareVal> {
        self.squares[self.board.coord_to_idx(coord)]
    }

    /// Applies all of the provided changes, mutating the underlying
    /// SolveState accordingly.
    pub fn apply_changes(&mut self, changes: &Changes) {
        match changes {
            Changes::AddQueen { queen, x } => {
                self.squares[self.board.coord_to_idx(queen)] = Some(SquareVal::Queen);
                for coord in x {
                    self.squares[self.board.coord_to_idx(&coord)] = Some(SquareVal::X)
                }
            }
            Changes::AddX { x } => {
                for coord in x {
                    self.squares[self.board.coord_to_idx(&coord)] = Some(SquareVal::X)
                }
            }
        }
    }

    /// Returns a string colored by OwoColorize that represents the
    /// SolveState, highlighting the given Coordinates.
    pub fn ansi_string(&self, highlight: CoordSet, charset: Charset) -> Result<String> {
        let mut f = String::new();
        for row_num in 0..self.board.size() {
            for col_num in 0..self.board.size() {
                let coord = (row_num, col_num);
                let highlight = highlight.contains(&coord);
                let square = self.square(&coord);
                let ansi_color = AnsiColors::from(self.board.color(&coord));
                let fg_color = self.board.color(&coord).fg_color();
                let c = SquareVal::as_char(square, highlight, &charset);
                if highlight {
                    write!(
                        f,
                        "{}",
                        c.color(fg_color).on_color(ansi_color).bold().underline()
                    )?
                } else {
                    write!(f, "{}", c.color(fg_color).on_color(ansi_color))?
                }
            }
            if row_num != self.board.size() - 1 {
                writeln!(f)?
            };
        }
        Ok(f)
    }
}

impl<'a> From<&'a Board> for SolveState<'a> {
    fn from(board: &'a Board) -> Self {
        let squares = vec![None; board.square_count()];
        SolveState { board, squares }
    }
}

impl Display for SolveState<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{}", self.board)?;
        writeln!(f)?;
        for (pos, row) in self.squares.chunks_exact(self.board.size()).with_position() {
            for square in row {
                write!(f, "{}", SquareVal::as_char(*square, false, &Charset::Ascii))?;
            }
            if pos != Position::Last {
                writeln!(f)?;
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use regex::Regex;

    use super::*;

    #[test]
    fn squareval_as_char() {
        let vals = [None, Some(SquareVal::Queen), Some(SquareVal::X)];
        for v in vals {
            let ascii_char = SquareVal::as_char(v, false, &Charset::Ascii);
            assert!(ascii_char.is_ascii() || ascii_char.is_whitespace());
            let unicode_char = SquareVal::as_char(v, false, &Charset::Unicode);
            assert!(!unicode_char.is_ascii() || unicode_char.is_whitespace());
            let ascii_highlighted_char = SquareVal::as_char(v, true, &Charset::Ascii);
            assert!(ascii_highlighted_char.is_ascii() || ascii_highlighted_char.is_whitespace());
            let unicode_highlighted_char = SquareVal::as_char(v, true, &Charset::Unicode);
            assert!(
                !unicode_highlighted_char.is_ascii() || unicode_highlighted_char.is_whitespace()
            );
        }
    }

    #[test]
    fn solvestate_from_board() {
        let board = Board::from_str("wwww\nkkkk\nrrrr\nbbbb").unwrap();
        let ss = SolveState::from(&board);
        assert!(ss.squares.iter().all(Option::is_none));
    }

    #[test]
    fn solvestate_from_queens_file() {
        let board_str = "wwww\nkkkk\nrrrr\nbbbb";
        let squares_str = "Qxxx\nxx..\nx...\nx. _";
        let qf_str = format!("{}\n\n{}", board_str, squares_str);
        let qf = QueensFile::from_str(&qf_str).unwrap();
        let ss = SolveState::from(&qf);
        assert!(ss.is_valid());

        assert_eq!(
            format!("{}", ss),
            qf_str.replace(".", " ").replace("_", " ")
        );
    }

    #[test]
    fn solvestate_ansi_string() {
        let board_str = "wwww\nkkkk\nrrrr\nbbbb";
        let squares_str = "Qxxx\nxx..\nx...\nx. _";
        let qf_str = format!("{}\n\n{}", board_str, squares_str);
        let qf = QueensFile::from_str(&qf_str).unwrap();
        let ss = SolveState::from(&qf);
        assert!(ss.is_valid());

        let ansi_string = ss.ansi_string(CoordSet::default(), Charset::Ascii).unwrap();
        let ansi_re = Regex::new(r"\u{1b}\[[0-9;]*m").unwrap();
        let ansi_removed = ansi_re.replace_all(&ansi_string, "");
        assert_eq!(
            ansi_removed,
            squares_str.replace(".", " ").replace("_", " ")
        );
    }

    #[test]
    fn solvestate_ansi_string_highlighted() {
        let board_str = "wwww\nkkkk\nrrrr\nbbbb";
        let squares_str = "Qxxx\nxx..\nx...\nx. _";
        let qf_str = format!("{}\n\n{}", board_str, squares_str);
        let qf = QueensFile::from_str(&qf_str).unwrap();
        let ss = SolveState::from(&qf);
        assert!(ss.is_valid());

        let ansi_string = ss
            .ansi_string(CoordSet::from_iter(vec![(0, 0)]), Charset::Ascii)
            .unwrap();
        let ansi_re = Regex::new(r"\u{1b}\[[0-9;]*m").unwrap();
        let ansi_removed = ansi_re.replace_all(&ansi_string, "");
        assert_eq!(
            ansi_removed,
            squares_str.replace(".", " ").replace("_", " ")
        );
    }

    #[test]
    fn solvestrategy_display() {
        assert_eq!(format!("{}", SolveStrategy::Fast), "Fast");
        assert_eq!(format!("{}", SolveStrategy::Short), "Short");
        assert_eq!(format!("{}", SolveStrategy::Simple), "Simple");
    }
}