Skip to content

Commit

Permalink
Refactor Avoidable and Deadly Rectangle solvers
Browse files Browse the repository at this point in the history
- Add `RectangleIter` to iterate all two-block rectangles
  • Loading branch information
dharkness committed Aug 26, 2023
1 parent 8cee9af commit d76c062
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 320 deletions.
180 changes: 180 additions & 0 deletions src/layout/cells/rectangle.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use std::fmt;
use std::hash::{Hash, Hasher};
use std::iter::FusedIterator;

use crate::layout::{Coord, House};

use super::{Cell, CellSet};

Expand All @@ -15,6 +18,10 @@ pub struct Rectangle {
}

impl Rectangle {
pub fn iter() -> RectangleIter {
RectangleIter::default()
}

pub const fn new(top_left: Cell, bottom_right: Cell) -> Rectangle {
let top_right = Cell::from_coords(top_left.row_coord(), bottom_right.column_coord());
let bottom_left = Cell::from_coords(bottom_right.row_coord(), top_left.column_coord());
Expand Down Expand Up @@ -43,6 +50,40 @@ impl Rectangle {
pub fn from(c1: Cell, c2: Cell, c3: Cell, c4: Cell) -> Rectangle {
Rectangle::new(c1.min(c2).min(c3).min(c4), c1.max(c2).max(c3).max(c4))
}

/// Returns a flipped copy to move the origin to the top-left cell.
pub fn with_origin(self, origin: Cell) -> Rectangle {
if origin == self.bottom_right {
Rectangle {
top_left: self.bottom_right,
top_right: self.bottom_left,
bottom_left: self.top_right,
bottom_right: self.top_left,
cells: self.cells,
block_count: self.block_count,
}
} else if origin == self.top_right {
Rectangle {
top_left: self.top_right,
top_right: self.top_left,
bottom_left: self.bottom_right,
bottom_right: self.bottom_left,
cells: self.cells,
block_count: self.block_count,
}
} else if origin == self.bottom_left {
Rectangle {
top_left: self.bottom_left,
top_right: self.bottom_right,
bottom_left: self.top_left,
bottom_right: self.top_right,
cells: self.cells,
block_count: self.block_count,
}
} else {
self
}
}
}

impl Hash for Rectangle {
Expand Down Expand Up @@ -103,3 +144,142 @@ impl fmt::Display for Rectangle {
)
}
}

/// Iterates through all unique two-block rectangles.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RectangleIter {
horiz_vert: usize,
block: usize,
cell: usize,
}

impl Iterator for RectangleIter {
type Item = Rectangle;

fn next(&mut self) -> Option<Rectangle> {
if self.horiz_vert == 2 {
return None;
}

let (from, to) = BLOCKS[self.horiz_vert][self.block];
let ((tl, _bl), (_tr, br)) = CELL_COORDS[self.horiz_vert][self.cell];

let rect = Rectangle::new(from.cell(tl), to.cell(br));

self.cell += 1;
if self.cell == 27 {
self.cell = 0;
self.block += 1;
if self.block == 9 {
self.block = 0;
self.horiz_vert += 1;
}
}

Some(rect)
}
}

impl FusedIterator for RectangleIter {}

/// A pair of coordinates, either two different boxes or two different cells in the same box.
type IndexPair = (u8, u8);

/// A pair of cell coordinates in a box.
type CoordPair = (Coord, Coord);

/// The block pairs (from, to) to check for deadly rectangles.
/// All possible rectangles between the two blocks are checked
/// using the coordinates below.
const BLOCKS: [[(House, House); 9]; 2] = {
#[rustfmt::skip]
const BLOCKS: [[IndexPair; 9]; 2] = [
// horizontal
[
(0, 1), (0, 2), (1, 2),
(3, 4), (3, 5), (4, 5),
(6, 7), (6, 8), (7, 8),
],
// vertical
[
(0, 3), (0, 6), (3, 6),
(1, 4), (1, 7), (4, 7),
(2, 5), (2, 8), (5, 8),
],
];
const DEFAULT: House = House::block(Coord::new(0));

let mut blocks: [[(House, House); 9]; 2] = [[(DEFAULT, DEFAULT); 9]; 2];
let mut horiz_vert = 0;

while horiz_vert < 2 {
let mut i = 0;
while i < 9 {
let (f, t) = BLOCKS[horiz_vert][i];
blocks[horiz_vert][i] = (House::block(Coord::new(f)), House::block(Coord::new(t)));
i += 1;
}
horiz_vert += 1;
}

blocks
};

/// Cell coordinates (top-left, bottom-right) for each rectangle.
/// each in a different block in the pairs above.
const CELL_COORDS: [[(CoordPair, CoordPair); 27]; 2] = {
#[rustfmt::skip]
const COORDS: [[(IndexPair, IndexPair); 27]; 2] = [
// horizontal
[
((0, 3), (0, 3)), ((0, 3), (1, 4)), ((0, 3), (2, 5)),
((0, 6), (0, 6)), ((0, 6), (1, 7)), ((0, 6), (2, 8)),
((3, 6), (3, 6)), ((3, 6), (4, 7)), ((3, 6), (5, 8)),

((1, 4), (0, 3)), ((1, 4), (1, 4)), ((1, 4), (2, 5)),
((1, 7), (0, 6)), ((1, 7), (1, 7)), ((1, 7), (2, 8)),
((4, 7), (3, 6)), ((4, 7), (4, 7)), ((4, 7), (5, 8)),

((2, 5), (0, 3)), ((2, 5), (1, 4)), ((2, 5), (2, 5)),
((2, 8), (0, 6)), ((2, 8), (1, 7)), ((2, 8), (2, 8)),
((5, 8), (3, 6)), ((5, 8), (4, 7)), ((5, 8), (5, 8)),
],
// vertical
[
((0, 1), (0, 1)), ((0, 1), (3, 4)), ((0, 1), (6, 7)),
((0, 2), (0, 2)), ((0, 2), (3, 5)), ((0, 2), (6, 8)),
((1, 2), (1, 2)), ((1, 2), (4, 5)), ((1, 2), (7, 8)),

((3, 4), (0, 1)), ((3, 4), (3, 4)), ((3, 4), (6, 7)),
((3, 5), (0, 2)), ((3, 5), (3, 5)), ((3, 5), (6, 8)),
((4, 5), (1, 2)), ((4, 5), (4, 5)), ((4, 5), (7, 8)),

((6, 7), (0, 1)), ((6, 7), (3, 4)), ((6, 7), (6, 7)),
((6, 8), (0, 2)), ((6, 8), (3, 5)), ((6, 8), (6, 8)),
((7, 8), (1, 2)), ((7, 8), (4, 5)), ((7, 8), (7, 8)),
],
];
const DEFAULT_COORD: Coord = Coord::new(0);
const DEFAULT: (CoordPair, CoordPair) = (
(DEFAULT_COORD, DEFAULT_COORD),
(DEFAULT_COORD, DEFAULT_COORD),
);

let mut coords: [[(CoordPair, CoordPair); 27]; 2] = [[DEFAULT; 27]; 2];
let mut horiz_vert = 0;

while horiz_vert < 2 {
let mut i = 0;
while i < 27 {
let ((tl, bl), (tr, br)) = COORDS[horiz_vert][i];
coords[horiz_vert][i] = (
(Coord::new(tl), Coord::new(bl)),
(Coord::new(tr), Coord::new(br)),
);
i += 1;
}
horiz_vert += 1;
}

coords
};
8 changes: 4 additions & 4 deletions src/puzzle/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ impl Board {
self.knowns
}

pub const fn solved(&self) -> CellSet {
self.knowns.minus(self.givens)
}

pub fn known_iter(&self) -> impl Iterator<Item = (Cell, Known)> + '_ {
self.knowns
.into_iter()
Expand Down Expand Up @@ -216,10 +220,6 @@ impl Board {
}

pub fn set_known(&mut self, cell: Cell, known: Known, effects: &mut Effects) -> bool {
if !self.is_candidate(cell, known) || self.is_known(cell) {
return false;
}

if let Some(rectangles) = creates_deadly_rectangles(self, cell, known) {
rectangles.into_iter().for_each(|r| {
effects.add_error(Error::DeadlyRectangle(r));
Expand Down
87 changes: 9 additions & 78 deletions src/solvers/avoidable_rectangles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,88 +3,19 @@ use super::*;
// Type 1
// .5....... .6.5.42.. ..8.71... 4....36.8 ......... 89.1..7.. 3........ ...2.7.1. .72.3..9.
//
// TODO add types 2 and 3 by looping over all possible two-block rectangles
// TODO Add types 2 and 3
// http://sudopedia.enjoysudoku.com/Avoidable_Rectangle.html
pub fn find_avoidable_rectangles(board: &Board) -> Option<Effects> {
let mut effects = Effects::new();

let candidates = board.knowns() - board.givens();
if candidates.size() < 3 {
return None;
}

for rectangle in candidates
.iter()
.combinations(3)
.map(Rectangle::try_from)
.filter_map(Result::ok)
.filter(|r| r.block_count == 2)
{
let top_left = rectangle.top_left;
let top_right = rectangle.top_right;
let bottom_left = rectangle.bottom_left;
let bottom_right = rectangle.bottom_right;

let top_left_value = board.value(top_left);
let top_right_value = board.value(top_right);
let bottom_left_value = board.value(bottom_left);
let bottom_right_value = board.value(bottom_right);

for (known, cell) in [
(
top_left_value,
bottom_right_value,
top_right_value,
bottom_left,
),
(
top_left_value,
bottom_right_value,
bottom_left_value,
top_right,
),
(
top_right_value,
bottom_left_value,
top_left_value,
bottom_right,
),
(
top_right_value,
bottom_left_value,
bottom_right_value,
top_left,
),
]
.iter()
.filter(|(pair1, pair2, _, _)| pair1 == pair2)
.filter_map(|(_, _, single, unsolved)| single.known().map(|known| (known, *unsolved)))
.filter(|(known, cell)| board.candidates(*cell).has(*known))
{
effects.add_erase(Strategy::AvoidableRectangle, cell, known);
}
// if top_left_value == bottom_right_value {
// if let Some(known) = top_right_value.known() {
// if board.candidates(bottom_left).has(known) {
// effects.add_erase(Strategy::AvoidableRectangle, bottom_left, known);
// }
// } else if let Some(known) = bottom_left_value.known() {
// if board.candidates(top_right).has(known) {
// effects.add_erase(Strategy::AvoidableRectangle, top_right, known);
// }
// }
// } else if top_right_value == bottom_left_value {
// if let Some(known) = top_left_value.known() {
// if board.candidates(bottom_right).has(known) {
// effects.add_erase(Strategy::AvoidableRectangle, bottom_right, known);
// }
// } else if let Some(known) = bottom_right_value.known() {
// if board.candidates(top_left).has(known) {
// effects.add_erase(Strategy::AvoidableRectangle, top_left, known);
// }
// }
// }
}
let candidates = board.solved();
Rectangle::iter()
.map(|r| (r, r.cells - candidates))
.filter_map(|(r, cs)| cs.as_single().map(|c| (r.with_origin(c), c)))
.filter(|(r, _)| board.value(r.top_right) == board.value(r.bottom_left))
.filter_map(|(r, c)| board.value(r.bottom_right).known().map(|k| (c, k)))
.filter(|(c, k)| board.candidates(*c).has(*k))
.for_each(|(c, k)| effects.add_erase(Strategy::AvoidableRectangle, c, k));

if effects.has_actions() {
Some(effects)
Expand Down
Loading

0 comments on commit d76c062

Please sign in to comment.