Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `cli` and `image` Cargo features, enabled by default, so library users can
exclude the CLI or image support and their dependencies.
- Add Windows support by replacing `termion` with `crossterm`.
- Add a heuristic for two adjacent rows or columns that must contain a queen of
a particular color, allowing puzzles such as
[#15](https://github.com/dschafer/qsolve/issues/15) to be solved without
forcing chains.

## [1.0.0] - 2025-04-23

Expand Down
1 change: 1 addition & 0 deletions games/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ These are sample games taken from various Queens sites online:
- [LinkedIn](https://www.linkedin.com/games/queens). Since there's no archive of these games avaiable, the screenshot of Game #1 is taken from [MojoDojo](https://mojodojo.io/blog/you-can-now-play-games-on-linkedin/).
- [QueensGame](https://queensgame.vercel.app/level/1). This is an archive of LinkedIn Queens.
- [Game of Crowns](https://gameofcrowns.sanish.me/challenges/1). This is a dedicated site with original puzzles.
- [GitHub issue #15](https://github.com/dschafer/qsolve/issues/15). This puzzle is a solver regression test.
7 changes: 7 additions & 0 deletions games/github-issue-15.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
BmYYYrr
BmYYYYr
BmmYYYr
BmmMkYr
BBBMkMb
BBBMMMb
BBBMbbb
227 changes: 227 additions & 0 deletions src/heuristic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
board::Board,
datastructure::{Coord, CoordSet, LineSet, SquareColorSet},
solvestate::{SolveState, SolveStrategy, SquareVal},
squarecolor::SquareColor,
};

#[derive(Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -158,6 +159,38 @@ pub fn all_heuristics(board: &Board) -> Vec<Box<dyn Heuristic>> {
}) as _
}));

for (first, second) in (0..board.size()).tuple_windows() {
let rows = [board.row_coords(first), board.row_coords(second)];
v.extend(
rows.iter()
.flatten()
.map(|coord| board.color(&coord))
.unique()
.map(|color| {
Box::new(TwoLinesRequireColor {
lines: rows,
color,
desc: format!("Rows {} and {}", first + 1, second + 1),
}) as _
}),
);

let cols = [board.col_coords(first), board.col_coords(second)];
v.extend(
cols.iter()
.flatten()
.map(|coord| board.color(&coord))
.unique()
.map(|color| {
Box::new(TwoLinesRequireColor {
lines: cols,
color,
desc: format!("Cols {} and {}", first + 1, second + 1),
}) as _
}),
);
}

v.extend(
(0..board.size())
.powerset()
Expand Down Expand Up @@ -288,6 +321,83 @@ impl Heuristic for AllPossibilitiesEliminateSquare {
}
}

#[derive(Debug)]
struct TwoLinesRequireColor {
lines: [CoordSet; 2],
color: SquareColor,
desc: String,
}

impl Heuristic for TwoLinesRequireColor {
fn seen_coords(&self, solve_state: &SolveState) -> CoordSet {
self.lines
.iter()
.flatten()
.filter(|&coord| solve_state.square(&coord).is_none())
.collect()
}

fn changes(&self, solve_state: &SolveState) -> Option<Changes> {
trace!("Heuristic Start: TwoLinesRequireColor {self:?}");
if self
.lines
.iter()
.flatten()
.any(|coord| solve_state.square(&coord) == Some(SquareVal::Queen))
{
trace!("Heuristic Invalid: TwoLinesRequireColor {self:?}");
return None;
}

let candidates = self.lines.map(|line| {
line.iter()
.filter(|&coord| solve_state.square(&coord).is_none())
.collect::<Vec<_>>()
});
let legal_pairs = candidates[0]
.iter()
.copied()
.cartesian_product(candidates[1].iter().copied())
.filter(|(first, second)| !solve_state.board.queen_borders(first).contains(second))
.collect::<Vec<_>>();
if legal_pairs.is_empty() {
trace!("Heuristic Invalid: TwoLinesRequireColor {self:?}");
return None;
}
if legal_pairs.iter().any(|(first, second)| {
solve_state.board.color(first) != self.color
&& solve_state.board.color(second) != self.color
}) {
trace!("Heuristic Invalid: TwoLinesRequireColor {self:?}");
return None;
}

trace!("Heuristic Success: TwoLinesRequireColor {self:?}");
let line_coords = CoordSet::from_iter(self.lines.iter().flatten());
let x = solve_state
.board
.coords_for_color(&self.color)
.iter()
.filter(|coord| !line_coords.contains(coord))
.filter(|coord| solve_state.square(coord).is_none())
.collect::<CoordSet>();
if x.is_empty() {
trace!("Heuristic No-op: TwoLinesRequireColor {self:?}");
None
} else {
trace!("Heuristic Return: TwoLinesRequireColor {self:?}");
Some(Changes::AddX { x })
}
}

fn description(&self) -> String {
format!(
"At most one non-'{:?}' queen can be placed in {}.\nOne queen there must be '{:?}'; x out that color outside {}.",
self.color, self.desc, self.color, self.desc
)
}
}

#[derive(Debug)]
struct NLinesContainOnlyNColors {
lines: Vec<CoordSet>,
Expand Down Expand Up @@ -583,6 +693,123 @@ mod tests {
assert!(heuristic.description().contains("desc"));
}

#[test]
fn two_rows_require_color() -> Result<()> {
let input_str = "BmYYYrr\nBmYYYYr\nBmmYYYr\nBmmMkYr\nBBBMkMb\nBBBMMMb\nBBBMbbb\n\n.x.xx..\n.x.xxx.\nx.xxxx.\nx.xx.x.\n.xxx.x.\n.xx.xx.\n.xx.x..";
let queens_file = QueensFile::from_str(input_str)?;
let ss = SolveState::from(&queens_file);
assert!(ss.is_valid());
let heuristic = TwoLinesRequireColor {
lines: [ss.board.row_coords(1), ss.board.row_coords(2)],
color: SquareColor::Red,
desc: String::new(),
};
assert_eq!(
heuristic.changes(&ss),
Some(Changes::AddX {
x: CoordSet::from_iter([(0, 5), (0, 6), (3, 6)])
})
);
assert_eq!(
heuristic.seen_coords(&ss),
CoordSet::from_iter([(1, 0), (1, 2), (1, 6), (2, 1), (2, 6)])
);
Ok(())
}

#[test]
fn two_columns_require_color() -> Result<()> {
let input_str = "BBBBBBB\nmmmmBBB\nYYmmBBB\nYYYMMMM\nYYYkkMb\nrYYYMMb\nrrrrbbb\n\n..xx...\nxx..xxx\n..xxxxx\nxxxxx..\nxxx..xx\n.xxxxx.\n.......";
let queens_file = QueensFile::from_str(input_str)?;
let ss = SolveState::from(&queens_file);
assert!(ss.is_valid());
let heuristic = TwoLinesRequireColor {
lines: [ss.board.col_coords(1), ss.board.col_coords(2)],
color: SquareColor::Red,
desc: String::new(),
};
assert_eq!(
heuristic.changes(&ss),
Some(Changes::AddX {
x: CoordSet::from_iter([(5, 0), (6, 0), (6, 3)])
})
);
Ok(())
}

#[test]
fn two_lines_require_color_needs_legal_pair() -> Result<()> {
let input_str = "rrrr\nkkkk\nbbbb\ncccc\n\n.xxx\nx.xx\nxx.x\nxxxx";
let queens_file = QueensFile::from_str(input_str)?;
let ss = SolveState::from(&queens_file);
assert!(ss.is_valid());
let heuristic = TwoLinesRequireColor {
lines: [ss.board.row_coords(0), ss.board.row_coords(1)],
color: SquareColor::Blue,
desc: String::new(),
};
assert_eq!(heuristic.changes(&ss), None);
Ok(())
}

#[test]
fn two_lines_require_color_ignores_solved_lines() -> Result<()> {
let input_str = "rrrr\nkkkk\nbbbb\ncccc\n\nQ...\n....\n....\n....";
let queens_file = QueensFile::from_str(input_str)?;
let ss = SolveState::from(&queens_file);
assert!(ss.is_valid());
let heuristic = TwoLinesRequireColor {
lines: [ss.board.row_coords(0), ss.board.row_coords(1)],
color: SquareColor::Blue,
desc: String::new(),
};
assert_eq!(heuristic.changes(&ss), None);
Ok(())
}

#[test]
fn two_lines_require_color_ignores_non_required_color() -> Result<()> {
let input_str = "BmYYYrr\nBmYYYYr\nBmmYYYr\nBmmMkYr\nBBBMkMb\nBBBMMMb\nBBBMbbb\n\n.x.xx..\n.x.xxx.\nx.xxxx.\nx.xx.x.\n.xxx.x.\n.xx.xx.\n.xx.x..";
let queens_file = QueensFile::from_str(input_str)?;
let ss = SolveState::from(&queens_file);
assert!(ss.is_valid());
let heuristic = TwoLinesRequireColor {
lines: [ss.board.row_coords(1), ss.board.row_coords(2)],
color: SquareColor::BrightBlue,
desc: String::new(),
};
assert_eq!(heuristic.changes(&ss), None);
Ok(())
}

#[test]
fn two_lines_require_color_ignores_noop() -> Result<()> {
let input_str = "BmYYYrr\nBmYYYYr\nBmmYYYr\nBmmMkYr\nBBBMkMb\nBBBMMMb\nBBBMbbb\n\n.x.xxxx\n.x.xxx.\nx.xxxx.\nx.xx.xx\n.xxx.x.\n.xx.xx.\n.xx.x..";
let queens_file = QueensFile::from_str(input_str)?;
let ss = SolveState::from(&queens_file);
assert!(ss.is_valid());
let heuristic = TwoLinesRequireColor {
lines: [ss.board.row_coords(1), ss.board.row_coords(2)],
color: SquareColor::Red,
desc: String::new(),
};
assert_eq!(heuristic.changes(&ss), None);
Ok(())
}

#[test]
fn two_lines_require_color_description() {
let heuristic = TwoLinesRequireColor {
lines: [CoordSet::default(); 2],
color: SquareColor::Red,
desc: "Rows 2 and 3".to_string(),
};
let description = heuristic.description();
assert!(description.contains("Rows 2 and 3"));
assert!(description.contains("Red"));
assert_eq!(description.lines().count(), 2);
}

#[test]
fn nlines_contain_only_ncolors() -> Result<()> {
let input_str = "rrrr\nrkkk\nbbbb\ncccc\n\n....\n....\n....\n....";
Expand Down
Loading