From 2d224be30c89a579d87d61a5972855c976222878 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Sat, 28 Mar 2026 00:45:12 +0100 Subject: [PATCH 1/9] WIP Introduce Shape data structure to be able to represent tiles and boards that are not made out of squares --- puzzled_common/src/lib.rs | 3 ++ puzzled_common/src/shape.rs | 103 ++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 puzzled_common/src/shape.rs diff --git a/puzzled_common/src/lib.rs b/puzzled_common/src/lib.rs index 2e816cf..08d629a 100644 --- a/puzzled_common/src/lib.rs +++ b/puzzled_common/src/lib.rs @@ -1 +1,4 @@ pub mod array_util; +pub mod shape; + +pub use shape::*; diff --git a/puzzled_common/src/shape.rs b/puzzled_common/src/shape.rs new file mode 100644 index 0000000..82b059f --- /dev/null +++ b/puzzled_common/src/shape.rs @@ -0,0 +1,103 @@ +use ndarray::{s, Array2, Axis}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Shape { + shape_type: ShapeType, + data: Array2, +} + +impl Shape { + pub fn new(shape_type: ShapeType, data: Array2) -> Self { + Self { shape_type, data } + } + + pub fn shape_type(&self) -> ShapeType { + self.shape_type + } + + pub fn dim(&self) -> (usize, usize) { + self.data.dim() + } + + pub fn rotate_clockwise(&mut self) { + match self.shape_type { + ShapeType::Square => { + self.data.reverse_axes(); + self.data.invert_axis(Axis(1)); + } + ShapeType::Triangle => { + todo!() + } + ShapeType::Hexagon => { + todo!() + } + }; + } + + pub fn trim_matching(&mut self, to_trim: bool) -> TrimSides { + let mut array = self.data.clone(); + let mut trim_sides = TrimSides::default(); + loop { + if array.nrows() == 0 || array.ncols() == 0 { + break; + } + + let left_col_all_true = array.column(0).iter().all(|&cell| cell == to_trim); + if left_col_all_true { + array = array.slice(s![.., 1..]).to_owned(); + trim_sides.lower_y += 1; + continue; + } + + let right_col_all_true = array + .column(array.ncols() - 1) + .iter() + .all(|&cell| cell == to_trim); + if right_col_all_true { + array = array.slice(s![.., ..array.ncols() - 1]).to_owned(); + trim_sides.upper_y += 1; + continue; + } + + let top_row_all_true = array.row(0).iter().all(|&cell| cell == to_trim); + if top_row_all_true { + array = array.slice(s![1.., ..]).to_owned(); + trim_sides.lower_x += 1; + continue; + } + + let bottom_row_all_true = array + .row(array.nrows() - 1) + .iter() + .all(|&cell| cell == to_trim); + if bottom_row_all_true { + array = array.slice(s![..array.nrows() - 1, ..]).to_owned(); + trim_sides.upper_x += 1; + continue; + } + + break; + } + trim_sides + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ShapeType { + Square, + Triangle, + Hexagon, +} + +/// Represents the number of rows and columns removed from the sides of a 2D array. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct TrimSides { + /// The number of rows removed from the lower side of the x-axis. + pub lower_x: usize, + /// The number of rows removed from the higher side of the x-axis. + pub upper_x: usize, + /// The number of columns removed from the lower side of the y-axis. + pub lower_y: usize, + /// The number of columns removed from the higher side of the y-axis. + pub upper_y: usize, +} From 8174ad918502ec031dda36797b28d906162ea58a Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Fri, 10 Apr 2026 11:12:18 +0200 Subject: [PATCH 2/9] Start integrating shape struct --- Cargo.lock | 1 + puzzle_config/src/config/board.rs | 7 +- puzzle_config/src/config/tile.rs | 8 +- puzzle_solver/src/board.rs | 8 +- puzzle_solver/src/lib.rs | 92 ++++++++++--------- puzzle_solver/src/plausibility.rs | 10 +- puzzle_solver/src/result.rs | 15 ++- puzzle_solver/src/tile.rs | 89 ++++++++++-------- puzzled/Cargo.toml | 1 + puzzled/src/app/components/tile.rs | 23 ++--- .../app/puzzle/puzzle_area/puzzle_state.rs | 3 +- puzzled_common/src/lib.rs | 3 +- puzzled_common/src/shape.rs | 48 +++++++++- 13 files changed, 185 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a73a81..27a34ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,6 +883,7 @@ dependencies = [ "once_cell", "puzzle_config", "puzzle_solver", + "puzzled_common", "rand", "simple_logger", "tokio", diff --git a/puzzle_config/src/config/board.rs b/puzzle_config/src/config/board.rs index 228d4ed..cf3eda7 100644 --- a/puzzle_config/src/config/board.rs +++ b/puzzle_config/src/config/board.rs @@ -1,16 +1,17 @@ use crate::config::area::AreaConfig; use crate::{Target, TargetIndex, TargetTemplate}; use ndarray::Array2; +use puzzled_common::Shape; use std::hash::{Hash, Hasher}; /// Configuration for the board layout and areas. #[derive(Debug, Clone, Eq, PartialEq)] pub enum BoardConfig { Simple { - layout: Array2, + layout: Shape, }, Area { - layout: Box>, + layout: Box, area_indices: Box>, display_values: Box>, value_order: Box>, @@ -59,7 +60,7 @@ impl BoardConfig { None } - pub fn layout(&self) -> &Array2 { + pub fn layout(&self) -> &Shape { match self { BoardConfig::Simple { layout } => layout, BoardConfig::Area { layout, .. } => layout, diff --git a/puzzle_config/src/config/tile.rs b/puzzle_config/src/config/tile.rs index aff619f..7e36fa6 100644 --- a/puzzle_config/src/config/tile.rs +++ b/puzzle_config/src/config/tile.rs @@ -1,11 +1,11 @@ use crate::config::color::ColorConfig; -use ndarray::Array2; +use puzzled_common::Shape; use std::hash::{DefaultHasher, Hash, Hasher}; /// Configuration for a tile that can be placed on the board. #[derive(Debug, Clone, Eq, PartialEq)] pub struct TileConfig { - base: Array2, + base: Shape, color: ColorConfig, name: Option, } @@ -18,13 +18,13 @@ impl TileConfig { /// * `base`: Base shape of the tile as a 2D boolean array. /// /// returns: TileConfig - pub fn new(base: Array2, color: ColorConfig, name: Option) -> TileConfig { + pub fn new(base: Shape, color: ColorConfig, name: Option) -> TileConfig { TileConfig { base, color, name } } /// Base shape of the tile as a 2D boolean array. /// True indicates a filled cell, false indicates an empty cell. - pub fn base(&self) -> &Array2 { + pub fn base(&self) -> &Shape { &self.base } diff --git a/puzzle_solver/src/board.rs b/puzzle_solver/src/board.rs index 2c93a12..670eda1 100644 --- a/puzzle_solver/src/board.rs +++ b/puzzle_solver/src/board.rs @@ -1,7 +1,7 @@ use log::debug; use ndarray::Array2; -use puzzled_common::array_util; use puzzled_common::array_util::TrimSides; +use puzzled_common::{array_util, Shape}; use std::ops::{Index, IndexMut}; /// Represents a 2D board for the puzzle, where each cell is either true (filled) or false (empty). @@ -19,7 +19,7 @@ use std::ops::{Index, IndexMut}; /// board[[2, 3]] = true; /// assert_eq!(board[[2, 3]], true); /// ``` -pub struct Board(Array2); +pub struct Board(Shape); impl Board { /// Creates a new Board with the given dimensions, initialized to all false (empty). @@ -40,7 +40,7 @@ impl Board { /// assert!(board.get_array().iter().all(|&b| b == false)); /// ``` pub fn new(dims: (usize, usize)) -> Self { - Board(Array2::default(dims)) + Board(Shape::default(dims)) } /// Returns a reference to the internal 2D array representing the board. @@ -60,7 +60,7 @@ impl Board { /// let board = Board::new((3, 4)); /// assert_eq!(board.get_array(), Array2::default((3, 4))); /// ``` - pub fn get_array(&self) -> &Array2 { + pub fn get_array(&self) -> &Shape { &self.0 } diff --git a/puzzle_solver/src/lib.rs b/puzzle_solver/src/lib.rs index 55b96a5..6cc9e0f 100644 --- a/puzzle_solver/src/lib.rs +++ b/puzzle_solver/src/lib.rs @@ -94,7 +94,7 @@ pub async fn solve_all_filling( #[cfg(test)] mod tests { use super::*; - use ndarray::arr2; + use puzzled_common::shape::shape_square; use tokio_util::sync::CancellationToken; #[tokio::test] @@ -102,8 +102,8 @@ mod tests { let mut board = Board::new((3, 4)); board[[0, 0]] = true; let tiles = vec![ - Tile::new(arr2(&[[true, true, true], [true, true, false]])), - Tile::new(arr2(&[[true, true, true], [true, true, true]])), + Tile::new(shape_square(&[[true, true, true], [true, true, false]])), + Tile::new(shape_square(&[[true, true, true], [true, true, true]])), ]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; @@ -112,14 +112,14 @@ mod tests { let placements = solution.placements(); assert_eq!(placements.len(), 2); let expected_placement_1 = TilePlacement::new( - arr2(&[[true, true, true], [true, true, false]]), - arr2(&[[false, true], [true, true], [true, true]]), + shape_square(&[[true, true, true], [true, true, false]]), + shape_square(&[[false, true], [true, true], [true, true]]), (0, 0), ); assert!(placements.contains(&expected_placement_1)); let expected_placement_2 = TilePlacement::new( - arr2(&[[true, true, true], [true, true, true]]), - arr2(&[[true, true], [true, true], [true, true]]), + shape_square(&[[true, true, true], [true, true, true]]), + shape_square(&[[true, true], [true, true], [true, true]]), (0, 2), ); assert!(placements.contains(&expected_placement_2)); @@ -127,7 +127,7 @@ mod tests { #[tokio::test] async fn test_solve_all_filling_success_board_padding() { - let board = arr2(&[ + let board = shape_square(&[ [true, true, true, true, true, true, true], [true, true, true, true, true, true, true], [true, true, true, true, true, true, true], @@ -141,8 +141,8 @@ mod tests { ]) .into(); let tiles = vec![ - Tile::new(arr2(&[[true, true, true], [true, true, false]])), - Tile::new(arr2(&[[true, true, true], [true, true, true]])), + Tile::new(shape_square(&[[true, true, true], [true, true, false]])), + Tile::new(shape_square(&[[true, true, true], [true, true, true]])), ]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; @@ -152,14 +152,14 @@ mod tests { dbg!(&placements); assert_eq!(placements.len(), 2); let expected_placement_1 = TilePlacement::new( - arr2(&[[true, true, true], [true, true, false]]), - arr2(&[[false, true], [true, true], [true, true]]), + shape_square(&[[true, true, true], [true, true, false]]), + shape_square(&[[false, true], [true, true], [true, true]]), (3, 1), ); assert!(placements.contains(&expected_placement_1)); let expected_placement_2 = TilePlacement::new( - arr2(&[[true, true, true], [true, true, true]]), - arr2(&[[true, true], [true, true], [true, true]]), + shape_square(&[[true, true, true], [true, true, true]]), + shape_square(&[[true, true], [true, true], [true, true]]), (3, 3), ); assert!(placements.contains(&expected_placement_2)); @@ -169,7 +169,10 @@ mod tests { async fn test_solve_all_filling_success_one_tile() { let mut board = Board::new((3, 2)); board[[1, 0]] = true; - let tiles = vec![Tile::new(arr2(&[[true, true, true], [true, false, true]]))]; + let tiles = vec![Tile::new(shape_square(&[ + [true, true, true], + [true, false, true], + ]))]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; assert!(result.is_ok()); @@ -177,8 +180,8 @@ mod tests { let placements = solution.placements(); assert_eq!(placements.len(), 1); let expected_placement_1 = TilePlacement::new( - arr2(&[[true, true, true], [true, false, true]]), - arr2(&[[true, true], [false, true], [true, true]]), + shape_square(&[[true, true, true], [true, false, true]]), + shape_square(&[[true, true], [false, true], [true, true]]), (0, 0), ); assert!(placements.contains(&expected_placement_1)); @@ -188,8 +191,8 @@ mod tests { async fn test_solve_all_filling_failure() { let board = Board::new((3, 4)); let tiles = vec![ - Tile::new(arr2(&[[true, true, true], [false, true, true]])), - Tile::new(arr2(&[[true, true, true], [true, true, false]])), + Tile::new(shape_square(&[[true, true, true], [false, true, true]])), + Tile::new(shape_square(&[[true, true, true], [true, true, false]])), ]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; @@ -208,7 +211,10 @@ mod tests { #[tokio::test] async fn test_solve_all_filling_too_few_tiles() { let board = Board::new((3, 4)); - let tiles = vec![Tile::new(arr2(&[[true, true, true], [true, true, true]]))]; + let tiles = vec![Tile::new(shape_square(&[ + [true, true, true], + [true, true, true], + ]))]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; assert!(result.is_err()); @@ -218,9 +224,9 @@ mod tests { async fn test_solve_all_filling_too_many_tiles() { let board = Board::new((3, 4)); let tiles = vec![ - Tile::new(arr2(&[[true, true, true], [true, true, true]])), - Tile::new(arr2(&[[true, true, false], [true, false, true]])), - Tile::new(arr2(&[[true, false, true], [true, true, true]])), + Tile::new(shape_square(&[[true, true, true], [true, true, true]])), + Tile::new(shape_square(&[[true, true, false], [true, false, true]])), + Tile::new(shape_square(&[[true, false, true], [true, true, true]])), ]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; @@ -232,8 +238,8 @@ mod tests { let mut board = Board::new((3, 4)); board[[0, 0]] = true; let tiles = vec![ - Tile::new(arr2(&[[true, false, true], [true, true, true]])), - Tile::new(arr2(&[[true, true, true], [true, true, true]])), + Tile::new(shape_square(&[[true, false, true], [true, true, true]])), + Tile::new(shape_square(&[[true, true, true], [true, true, true]])), ]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; @@ -258,7 +264,7 @@ mod tests { #[tokio::test] async fn test_solve_1() { - let board = arr2(&[ + let board = shape_square(&[ [true, true, false, false, true], [true, true, false, false, true], [true, true, true, false, false], @@ -267,8 +273,8 @@ mod tests { ]) .into(); let tiles = vec![ - Tile::new(arr2(&[[false, true, true], [true, true, true]])), - Tile::new(arr2(&[ + Tile::new(shape_square(&[[false, true, true], [true, true, true]])), + Tile::new(shape_square(&[ [true, true, false], [true, true, false], [false, true, true], @@ -281,18 +287,18 @@ mod tests { let placements = solution.placements(); assert_eq!(placements.len(), 2); let expected_placement_1 = TilePlacement::new( - arr2(&[[false, true, true], [true, true, true]]), - arr2(&[[false, true, true], [true, true, true]]), + shape_square(&[[false, true, true], [true, true, true]]), + shape_square(&[[false, true, true], [true, true, true]]), (3, 0), ); assert!(placements.contains(&expected_placement_1)); let expected_placement_2 = TilePlacement::new( - arr2(&[ + shape_square(&[ [true, true, false], [true, true, false], [false, true, true], ]), - arr2(&[ + shape_square(&[ [true, true, false], [true, true, false], [false, true, true], @@ -304,8 +310,8 @@ mod tests { #[tokio::test] async fn test_solve_tile_can_not_be_placed() { - let board = arr2(&[[false, false], [false, false]]).into(); - let tiles = vec![Tile::new(arr2(&[[true, true, true, true]]))]; + let board = shape_square(&[[false, false], [false, false]]).into(); + let tiles = vec![Tile::new(shape_square(&[[true, true, true, true]]))]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; assert!(result.is_err()); @@ -313,22 +319,22 @@ mod tests { assert_eq!( error, UnsolvableReason::TileCannotBePlaced { - base: arr2(&[[true, true, true, true]]), + base: shape_square(&[[true, true, true, true]]), } ); } #[tokio::test] async fn test_solve_all_filling_same_tiles() { - let board = arr2(&[ + let board = shape_square(&[ [true, false, false], [false, true, false], [false, false, true], ]) .into(); let tiles = vec![ - Tile::new(arr2(&[[false, true], [true, true]])), - Tile::new(arr2(&[[false, true], [true, true]])), + Tile::new(shape_square(&[[false, true], [true, true]])), + Tile::new(shape_square(&[[false, true], [true, true]])), ]; let result = solve_all_filling(board, &tiles, CancellationToken::new()).await; @@ -338,14 +344,14 @@ mod tests { dbg!(&placements); assert_eq!(placements.len(), 2); let expected_placement_1 = TilePlacement::new( - arr2(&[[false, true], [true, true]]), - arr2(&[[true, false], [true, true]]), + shape_square(&[[false, true], [true, true]]), + shape_square(&[[true, false], [true, true]]), (1, 0), ); assert!(placements.contains(&expected_placement_1)); let expected_placement_2 = TilePlacement::new( - arr2(&[[false, true], [true, true]]), - arr2(&[[true, true], [false, true]]), + shape_square(&[[false, true], [true, true]]), + shape_square(&[[true, true], [false, true]]), (0, 1), ); assert!(placements.contains(&expected_placement_2)); @@ -357,7 +363,7 @@ mod tests { let board = Board::new((100, 10)); let mut tiles = Vec::with_capacity(100); for _ in 0..100 { - tiles.push(Tile::new(arr2(&[ + tiles.push(Tile::new(shape_square(&[ [true, true, true, true, true], [true, true, true, true, true], ]))); diff --git a/puzzle_solver/src/plausibility.rs b/puzzle_solver/src/plausibility.rs index 114026b..9179143 100644 --- a/puzzle_solver/src/plausibility.rs +++ b/puzzle_solver/src/plausibility.rs @@ -34,13 +34,13 @@ mod tests { use super::*; use crate::board::Board; use crate::tile::Tile; - use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_check_passing() { let board = Board::new((3, 3)); - let tile1 = Tile::new(arr2(&[[true, true], [true, true], [true, false]])); - let tile2 = Tile::new(arr2(&[[false, true], [false, true], [true, true]])); + let tile1 = Tile::new(shape_square(&[[true, true], [true, true], [true, false]])); + let tile2 = Tile::new(shape_square(&[[false, true], [false, true], [true, true]])); let tiles = vec![tile1, tile2]; assert!(check(&board, &tiles)); @@ -49,8 +49,8 @@ mod tests { #[test] fn test_check_failing() { let board = Board::new((3, 3)); - let tile1 = Tile::new(arr2(&[[true, true], [true, false]])); - let tile2 = Tile::new(arr2(&[[false, true], [false, true]])); + let tile1 = Tile::new(shape_square(&[[true, true], [true, false]])); + let tile2 = Tile::new(shape_square(&[[false, true], [false, true]])); let tiles = vec![tile1, tile2]; assert!(!check(&board, &tiles)); diff --git a/puzzle_solver/src/result.rs b/puzzle_solver/src/result.rs index b602972..5e51880 100644 --- a/puzzle_solver/src/result.rs +++ b/puzzle_solver/src/result.rs @@ -1,4 +1,5 @@ use ndarray::Array2; +use puzzled_common::Shape; /// Represents a successful solution to the puzzle. #[derive(Debug)] @@ -22,20 +23,16 @@ impl Solution { #[derive(Debug, Eq, PartialEq)] pub struct TilePlacement { /// The base of the tile being placed. - base: Array2, + base: Shape, /// The rotation in which the tile is placed. - rotation: Array2, + rotation: Shape, /// The (x, y) position where the tile is placed. position: (usize, usize), } impl TilePlacement { /// Creates a new `TilePlacement` with the given base, rotation, and position. - pub(crate) fn new( - base: Array2, - rotation: Array2, - position: (usize, usize), - ) -> Self { + pub(crate) fn new(base: Shape, rotation: Shape, position: (usize, usize)) -> Self { Self { base, rotation, @@ -44,12 +41,12 @@ impl TilePlacement { } /// Returns a reference to the base layout of the tile. - pub fn base(&self) -> &Array2 { + pub fn base(&self) -> &Shape { &self.base } /// Returns a reference to the rotation of the tile as placed. - pub fn rotation(&self) -> &Array2 { + pub fn rotation(&self) -> &Shape { &self.rotation } diff --git a/puzzle_solver/src/tile.rs b/puzzle_solver/src/tile.rs index b07017d..44a3c15 100644 --- a/puzzle_solver/src/tile.rs +++ b/puzzle_solver/src/tile.rs @@ -1,6 +1,7 @@ use log::debug; use ndarray::Array2; use puzzled_common::array_util::{debug_print, rotate_90}; +use puzzled_common::Shape; use std::collections::HashSet; /// Represents a tile to place on a board. @@ -10,9 +11,9 @@ use std::collections::HashSet; pub struct Tile { /// The base 2D boolean array representing the tile. /// This is kept for convenience to give back to users who want the original base. - pub(crate) base: Array2, + pub(crate) base: Shape, /// All unique rotations and flips of the tile, containing the base orientation as well. - pub(crate) all_rotations: Vec>, + pub(crate) all_rotations: Vec, } impl Tile { @@ -36,8 +37,8 @@ impl Tile { /// let base = arr2(&[[true, false], [true, true]]); /// let tile = Tile::new(base); /// ``` - pub fn new(base: Array2) -> Tile { - let mut all_rotations_set: HashSet> = HashSet::new(); + pub fn new(base: Shape) -> Tile { + let mut all_rotations_set: HashSet = HashSet::new(); all_rotations_set.insert(base.clone()); @@ -82,7 +83,7 @@ impl Tile { /// let tile = Tile::new(base.clone()); /// assert_eq!(tile.base(), &base); /// ``` - pub fn base(&self) -> &Array2 { + pub fn base(&self) -> &Shape { &self.base } @@ -102,94 +103,104 @@ impl Tile { #[cfg(test)] mod tests { use super::*; - use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_new() { - let base = arr2(&[[true, false], [true, true]]); + let base = shape_square(&[[true, false], [true, true]]); let tile = Tile::new(base.clone()); assert_eq!(tile.base(), &base); assert_eq!(tile.all_rotations.len(), 4); assert!( tile.all_rotations - .contains(&arr2(&[[true, false], [true, true]])) + .contains(&shape_square(&[[true, false], [true, true]])) ); assert!( tile.all_rotations - .contains(&arr2(&[[true, true], [true, false]])) + .contains(&shape_square(&[[true, true], [true, false]])) ); assert!( tile.all_rotations - .contains(&arr2(&[[true, true], [false, true]])) + .contains(&shape_square(&[[true, true], [false, true]])) ); assert!( tile.all_rotations - .contains(&arr2(&[[false, true], [true, true]])) + .contains(&shape_square(&[[false, true], [true, true]])) ); } #[test] fn test_new_1x1() { - let base = arr2(&[[true]]); + let base = shape_square(&[[true]]); let tile = Tile::new(base.clone()); assert_eq!(tile.base(), &base); assert_eq!(tile.all_rotations.len(), 1); - assert!(tile.all_rotations.contains(&arr2(&[[true]]))); + assert!(tile.all_rotations.contains(&shape_square(&[[true]]))); } #[test] fn test_new_1x2() { - let base = arr2(&[[true, false]]); + let base = shape_square(&[[true, false]]); let tile = Tile::new(base.clone()); assert_eq!(tile.base(), &base); assert_eq!(tile.all_rotations.len(), 4); - assert!(tile.all_rotations.contains(&arr2(&[[true, false]]))); - assert!(tile.all_rotations.contains(&arr2(&[[false], [true]]))); - assert!(tile.all_rotations.contains(&arr2(&[[false, true]]))); - assert!(tile.all_rotations.contains(&arr2(&[[true], [false]]))); + assert!(tile.all_rotations.contains(&shape_square(&[[true, false]]))); + assert!( + tile.all_rotations + .contains(&shape_square(&[[false], [true]])) + ); + assert!(tile.all_rotations.contains(&shape_square(&[[false, true]]))); + assert!( + tile.all_rotations + .contains(&shape_square(&[[true], [false]])) + ); } #[test] fn test_new_2x3() { - let base = arr2(&[[true, false], [true, true], [true, true]]); + let base = shape_square(&[[true, false], [true, true], [true, true]]); let tile = Tile::new(base.clone()); assert_eq!(tile.base(), &base); assert_eq!(tile.all_rotations.len(), 8); + assert!(tile.all_rotations.contains(&shape_square(&[ + [true, false], + [true, true], + [true, true] + ]))); + assert!(tile.all_rotations.contains(&shape_square(&[ + [true, true], + [true, true], + [false, true] + ]))); + assert!(tile.all_rotations.contains(&shape_square(&[ + [true, true], + [true, true], + [true, false] + ]))); + assert!(tile.all_rotations.contains(&shape_square(&[ + [false, true], + [true, true], + [true, true] + ]))); assert!( tile.all_rotations - .contains(&arr2(&[[true, false], [true, true], [true, true]])) - ); - assert!( - tile.all_rotations - .contains(&arr2(&[[true, true], [true, true], [false, true]])) - ); - assert!( - tile.all_rotations - .contains(&arr2(&[[true, true], [true, true], [true, false]])) - ); - assert!( - tile.all_rotations - .contains(&arr2(&[[false, true], [true, true], [true, true]])) - ); - assert!( - tile.all_rotations - .contains(&arr2(&[[false, true, true], [true, true, true]])) + .contains(&shape_square(&[[false, true, true], [true, true, true]])) ); assert!( tile.all_rotations - .contains(&arr2(&[[true, true, true], [false, true, true]])) + .contains(&shape_square(&[[true, true, true], [false, true, true]])) ); assert!( tile.all_rotations - .contains(&arr2(&[[true, true, false], [true, true, true]])) + .contains(&shape_square(&[[true, true, false], [true, true, true]])) ); assert!( tile.all_rotations - .contains(&arr2(&[[true, true, true], [true, true, false]])) + .contains(&shape_square(&[[true, true, true], [true, true, false]])) ); } } diff --git a/puzzled/Cargo.toml b/puzzled/Cargo.toml index 76755e5..2c17cff 100644 --- a/puzzled/Cargo.toml +++ b/puzzled/Cargo.toml @@ -4,6 +4,7 @@ edition = "2024" version.workspace = true [dependencies] +puzzled_common = { workspace = true } puzzle_solver = { workspace = true } puzzle_config = { workspace = true } gettext-rs = { workspace = true } diff --git a/puzzled/src/app/components/tile.rs b/puzzled/src/app/components/tile.rs index 6ce6043..b519a0d 100644 --- a/puzzled/src/app/components/tile.rs +++ b/puzzled/src/app/components/tile.rs @@ -8,8 +8,9 @@ use adw::prelude::GdkCairoContextExt; use adw::subclass::prelude::*; use gtk::cairo::Context; use gtk::prelude::{DrawingAreaExtManual, WidgetExt}; -use ndarray::{Array2, Axis}; +use ndarray::Array2; use puzzle_config::ColorConfig; +use puzzled_common::Shape; use std::cell::Ref; use std::collections::HashMap; @@ -30,15 +31,16 @@ pub enum DrawingMode { mod imp { use super::*; + use puzzled_common::Shape; use std::cell::{Cell, RefCell}; use std::collections::HashMap; #[derive(Debug, Default)] pub struct PuzzledTileView { pub id: Cell, - pub base: RefCell>, + pub base: RefCell, pub name: RefCell>, - pub current_rotation: RefCell>, + pub current_rotation: RefCell, pub position_cells: Cell>, pub position_pixels: Cell, pub color: RefCell>, @@ -99,7 +101,7 @@ impl TileView { /// Creates a new TileView with the given id and base layout. /// The name is used to refer to the tile layout when calculating possible solutions for given /// tiles. - pub fn new(id: usize, base: Array2, color: ColorConfig, name: Option) -> Self { + pub fn new(id: usize, base: Shape, color: ColorConfig, name: Option) -> Self { let obj: TileView = glib::Object::builder().build(); obj.imp().id.replace(id); @@ -195,7 +197,7 @@ impl TileView { /// Returns the base layout of the tile, which is the original orientation without any /// rotations or flips. /// This does not change over the lifetime of the tile. - pub fn base(&self) -> Ref<'_, Array2> { + pub fn base(&self) -> Ref<'_, Shape> { self.imp().base.borrow() } @@ -209,20 +211,19 @@ impl TileView { /// Rotates the tile one step clockwise. pub fn rotate_clockwise(&self) { - let previous = self.current_rotation().clone(); - let mut layout = previous.reversed_axes(); - layout.invert_axis(Axis(0)); + let mut layout = self.current_rotation().clone(); + layout.rotate_clockwise(); self.set_current_rotation(layout); } /// Flips the tile horizontally. pub fn flip_horizontal(&self) { let mut layout = self.current_rotation().clone(); - layout.invert_axis(Axis(0)); + layout.flip_default(); self.set_current_rotation(layout); } - fn set_current_rotation(&self, rotation: Array2) { + fn set_current_rotation(&self, rotation: Shape) { self.imp() .drawing_modes .replace(Array2::default(rotation.dim())); @@ -231,7 +232,7 @@ impl TileView { } /// Returns the current layout of the tile, which changes when the tile is rotated or flipped. - pub fn current_rotation(&self) -> Ref<'_, Array2> { + pub fn current_rotation(&self) -> Ref<'_, Shape> { self.imp().current_rotation.borrow() } diff --git a/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs b/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs index 78acd7a..e351836 100644 --- a/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs +++ b/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs @@ -2,6 +2,7 @@ use crate::model::extension::PuzzleTypeExtension; use crate::offset::CellOffset; use ndarray::Array2; use puzzle_config::PuzzleConfig; +use puzzled_common::Shape; use std::cell::Ref; use std::collections::HashSet; @@ -50,7 +51,7 @@ impl Default for Cell { pub struct UnusedTile { /// Used to identify the tile when having multiple identical tiles. pub id: usize, - pub base: Array2, + pub base: Shape, pub name: Option, } diff --git a/puzzled_common/src/lib.rs b/puzzled_common/src/lib.rs index 08d629a..12d7a31 100644 --- a/puzzled_common/src/lib.rs +++ b/puzzled_common/src/lib.rs @@ -1,4 +1,5 @@ pub mod array_util; pub mod shape; -pub use shape::*; +pub use shape::Shape; +pub use shape::ShapeType; diff --git a/puzzled_common/src/shape.rs b/puzzled_common/src/shape.rs index 82b059f..71d9d72 100644 --- a/puzzled_common/src/shape.rs +++ b/puzzled_common/src/shape.rs @@ -1,6 +1,9 @@ -use ndarray::{s, Array2, Axis}; +use crate::ShapeType::Square; +use ndarray::iter::{IndexedIter, Iter}; +use ndarray::{arr2, s, Array2, Axis, Ix2}; +use std::ops::Index; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] pub struct Shape { shape_type: ShapeType, data: Array2, @@ -19,6 +22,18 @@ impl Shape { self.data.dim() } + pub fn get(&self, index: (usize, usize)) -> Option<&bool> { + self.data.get(index) + } + + pub fn iter(&self) -> Iter<'_, bool, Ix2> { + self.data.iter() + } + + pub fn indexed_iter(&self) -> IndexedIter<'_, bool, Ix2> { + self.data.indexed_iter() + } + pub fn rotate_clockwise(&mut self) { match self.shape_type { ShapeType::Square => { @@ -34,6 +49,20 @@ impl Shape { }; } + pub fn flip_default(&mut self) { + match self.shape_type { + ShapeType::Square => { + self.data.invert_axis(Axis(0)); + } + ShapeType::Triangle => { + todo!() + } + ShapeType::Hexagon => { + todo!() + } + } + } + pub fn trim_matching(&mut self, to_trim: bool) -> TrimSides { let mut array = self.data.clone(); let mut trim_sides = TrimSides::default(); @@ -82,8 +111,17 @@ impl Shape { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +impl Index<(usize, usize)> for Shape { + type Output = bool; + + fn index(&self, index: (usize, usize)) -> &Self::Output { + &self.data[index] + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum ShapeType { + #[default] Square, Triangle, Hexagon, @@ -101,3 +139,7 @@ pub struct TrimSides { /// The number of columns removed from the higher side of the y-axis. pub upper_y: usize, } + +pub fn shape_square(data: &[[bool; N]]) -> Shape { + Shape::new(Square, arr2(data)) +} From a3f3d2135a3686bf39647c5f2c6cc887e09aaec8 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Sun, 12 Apr 2026 13:42:38 +0200 Subject: [PATCH 3/9] Introduce Shape data structure to be able to represent tiles and boards that are not made out of squares Working state. TODO - tests - clean up --- puzzle_config/src/config/board.rs | 7 +- puzzle_config/src/config/tile.rs | 12 +- puzzle_config/src/json/converter.rs | 33 +- puzzle_config/src/lib.rs | 8 +- puzzle_config/src/random/growing.rs | 19 +- puzzle_config/src/random/mod.rs | 51 +- puzzle_solver/src/backtracking/mod.rs | 14 +- puzzle_solver/src/backtracking/positioned.rs | 339 ++++--- .../src/backtracking/pruner/banned.rs | 92 +- puzzle_solver/src/backtracking/pruner/mod.rs | 46 +- puzzle_solver/src/bitmask.rs | 29 +- puzzle_solver/src/board.rs | 25 +- puzzle_solver/src/result.rs | 3 +- puzzle_solver/src/tile.rs | 27 +- puzzled/src/app/puzzle/info.rs | 4 +- .../app/puzzle/puzzle_area/puzzle_state.rs | 8 +- puzzled/src/application.rs | 14 +- puzzled/src/solver/combination_solutions.rs | 2 +- puzzled_common/src/array_util.rs | 726 --------------- puzzled_common/src/lib.rs | 1 - puzzled_common/src/shape.rs | 145 --- puzzled_common/src/shape/iterators.rs | 47 + puzzled_common/src/shape/mod.rs | 836 ++++++++++++++++++ 23 files changed, 1299 insertions(+), 1189 deletions(-) delete mode 100644 puzzled_common/src/array_util.rs delete mode 100644 puzzled_common/src/shape.rs create mode 100644 puzzled_common/src/shape/iterators.rs create mode 100644 puzzled_common/src/shape/mod.rs diff --git a/puzzle_config/src/config/board.rs b/puzzle_config/src/config/board.rs index cf3eda7..98e8113 100644 --- a/puzzle_config/src/config/board.rs +++ b/puzzle_config/src/config/board.rs @@ -2,6 +2,7 @@ use crate::config::area::AreaConfig; use crate::{Target, TargetIndex, TargetTemplate}; use ndarray::Array2; use puzzled_common::Shape; +use puzzled_common::ShapeType::Square; use std::hash::{Hash, Hasher}; /// Configuration for the board layout and areas. @@ -168,7 +169,7 @@ pub fn from_predefined_board(name: &str) -> Option { .get(0..2) .map(|dims| (dims[0], dims[1])); dim.map(|(rows, cols)| BoardConfig::Simple { - layout: Array2::from_shape_fn((rows as usize, cols as usize), |_| true), + layout: Shape::from_elem((rows as usize, cols as usize), Square, true), }) } @@ -178,10 +179,12 @@ mod tests { use crate::config::area::{AreaConfig, AreaValueFormatter}; use crate::config::target::{TargetIndex, TargetTemplate}; use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_puzzle_config_get_display_values_for_area() { - let board_layout = arr2(&[[true, true, false], [true, true, true], [false, true, true]]); + let board_layout = + shape_square(&[[true, true, false], [true, true, true], [false, true, true]]); let area_indices = arr2(&[[0, 0, -1], [0, 1, 1], [-1, 1, 1]]); let display_values = arr2(&[ ["A".to_string(), "B".to_string(), "".to_string()], diff --git a/puzzle_config/src/config/tile.rs b/puzzle_config/src/config/tile.rs index 7e36fa6..d8d3714 100644 --- a/puzzle_config/src/config/tile.rs +++ b/puzzle_config/src/config/tile.rs @@ -69,22 +69,22 @@ impl Hash for TileConfig { #[cfg(test)] mod tests { use super::*; - use ndarray::array; + use puzzled_common::shape::shape_square; #[test] fn test_hash() { let tile1 = TileConfig::new( - array![[true, false], [false, true]], + shape_square(&[[true, false], [false, true]]), ColorConfig::default_with_index(0), None, ); let tile2 = TileConfig::new( - array![[true, false], [false, true]], + shape_square(&[[true, false], [false, true]]), ColorConfig::default_with_index(1), None, ); let tile3 = TileConfig::new( - array![[false, true], [true, false]], + shape_square(&[[false, true], [true, false]]), ColorConfig::default_with_index(1), None, ); @@ -108,12 +108,12 @@ mod tests { #[test] fn test_hash_slice_any_order() { let tile1 = TileConfig::new( - array![[true, false], [false, true]], + shape_square(&[[true, false], [false, true]]), ColorConfig::default_with_index(0), None, ); let tile2 = TileConfig::new( - array![[false, true], [true, false]], + shape_square(&[[false, true], [true, false]]), ColorConfig::default_with_index(1), None, ); diff --git a/puzzle_config/src/json/converter.rs b/puzzle_config/src/json/converter.rs index ab253f7..98cfb57 100644 --- a/puzzle_config/src/json/converter.rs +++ b/puzzle_config/src/json/converter.rs @@ -9,6 +9,8 @@ use crate::{ TileConfig, }; use ndarray::Array2; +use puzzled_common::Shape; +use puzzled_common::ShapeType::Square; use std::num::NonZero; use time::OffsetDateTime; @@ -99,19 +101,19 @@ fn rotate_board_to_landscape(arr: Array2) -> Array2 { fn rotate_board(board: BoardConfig) -> BoardConfig { match board { - BoardConfig::Simple { layout } => { - let layout = rotate_board_to_landscape(layout); + BoardConfig::Simple { mut layout } => { + layout.rotate_to_landscape(); BoardConfig::Simple { layout } } BoardConfig::Area { - layout, + mut layout, area_indices, display_values, value_order, area_configs, target_template, } => { - let layout = Box::new(rotate_board_to_landscape(*layout)); + layout.rotate_to_landscape(); let area_indices = Box::new(rotate_board_to_landscape(*area_indices)); let display_values = Box::new(rotate_board_to_landscape(*display_values)); let value_order = Box::new(rotate_board_to_landscape(*value_order)); @@ -206,12 +208,12 @@ impl Convertable> for (usize, Tile, Option) { } } -impl Convertable<(Array2, Option)> for (usize, TileLayout) { +impl Convertable<(Shape, Option)> for (usize, TileLayout) { fn convert( self, predefined: &Predefined, custom: &mut Custom, - ) -> Result<(Array2, Option), ReadError> { + ) -> Result<(Shape, Option), ReadError> { match self.1 { TileLayout::Ref(name) => { if let Some(custom_tile) = custom.get_tile(&name) { @@ -249,13 +251,13 @@ impl Convertable<(Array2, Option)> for (usize, TileLayout) { return Err(ReadError::TileWidthOrHeightCannotBeZero); } } - let mut base = Array2::::default((height, width)); + let mut base = Shape::from_elem((height, width), Square, false); for (i, row) in array.iter().enumerate() { for (j, &value) in row.iter().enumerate() { base[(i, j)] = value != 0; } } - let base = base.reversed_axes(); + base.transpose(); Ok((base, None)) } } @@ -305,13 +307,13 @@ impl Convertable for Board { return Err(ReadError::BoardWidthOrHeightCannotBeZero); } } - let mut array = Array2::::default((height, width)); + let mut array = Shape::from_elem((height, width), Square, false); for (i, row) in layout.iter().enumerate() { for (j, &value) in row.iter().enumerate() { array[(i, j)] = value < 1; } } - let array = array.reversed_axes(); + array.transpose(); Ok(BoardConfig::Simple { layout: array }) } Board::AreaBoard { @@ -337,14 +339,15 @@ impl Convertable for Board { return Err(ReadError::BoardWidthOrHeightCannotBeZero); } } - let mut array = Array2::::default((height, width)); + let mut array = Shape::from_elem((height, width), Square, false); for (i, row) in area_layout.iter().enumerate() { for (j, &value) in row.iter().enumerate() { array[(i, j)] = value >= 0; } } - array.reversed_axes() + array.transpose(); + array }; Ok(BoardConfig::Area { @@ -444,7 +447,7 @@ impl Convertable for DefaultFactory { #[cfg(test)] mod tests { use super::*; - use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_convert_predefined_tile() { @@ -459,7 +462,7 @@ mod tests { .convert(&predefined, &mut Custom::default()) .unwrap(); let expected_tile = TileConfig::new( - arr2(&[[true, false], [true, true]]).reversed_axes(), + shape_square(&[[true, false], [true, true]]).transposed(), ColorConfig::default_with_index(0), Some("L3".to_string()), ); @@ -487,7 +490,7 @@ mod tests { .convert(&Predefined::default(), &mut Custom::default()) .unwrap(); let expected_tile = TileConfig::new( - arr2(&[[true, false], [true, true]]).reversed_axes(), + shape_square(&[[true, false], [true, true]]).transposed(), ColorConfig::default_with_index(0), Some("L3".to_string()), ); diff --git a/puzzle_config/src/lib.rs b/puzzle_config/src/lib.rs index 9ed4f23..0cbb211 100644 --- a/puzzle_config/src/lib.rs +++ b/puzzle_config/src/lib.rs @@ -56,7 +56,7 @@ impl Predefined { #[cfg(test)] mod tests { use crate::create_json_loader; - use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_load_puzzle_collection_from_json() { @@ -125,17 +125,17 @@ mod tests { assert_eq!(2, puzzle.tiles().len()); assert_eq!( puzzle.board_config().layout(), - arr2(&[[true, true, true], [true, false, true], [true, true, true]]) + &shape_square(&[[true, true, true], [true, false, true], [true, true, true]]) ); let ref_tile = &puzzle.tiles()[0]; assert_eq!( ref_tile.base(), - arr2(&[[true, false], [true, true]]).reversed_axes() + &shape_square(&[[true, true], [false, true]]) ); let custom_tile = &puzzle.tiles()[1]; assert_eq!( custom_tile.base(), - arr2(&[[true, false, true], [true, true, true]]).reversed_axes() + &shape_square(&[[true, true], [false, true], [true, true]]) ); } } diff --git a/puzzle_config/src/random/growing.rs b/puzzle_config/src/random/growing.rs index 4f80a05..847e0ab 100644 --- a/puzzle_config/src/random/growing.rs +++ b/puzzle_config/src/random/growing.rs @@ -1,20 +1,18 @@ use crate::random::{Algorithm, RandomPuzzleSettings}; use ndarray::Array2; -use puzzled_common::array_util; +use puzzled_common::Shape; +use puzzled_common::ShapeType::Square; use rand::{Rng, RngExt}; use std::collections::BTreeMap; -pub fn create_puzzle( - settings: &RandomPuzzleSettings, - rng: &mut dyn Rng, -) -> (Array2, Vec>) { +pub fn create_puzzle(settings: &RandomPuzzleSettings, rng: &mut dyn Rng) -> (Shape, Vec) { let tile_count = match settings.algorithm { Algorithm::Growing { tile_count, .. } => tile_count, }; let base_board = generate_base_board(settings, rng); let complete_board = grow_until_complete(rng, base_board); - let board = complete_board.map(|x| x.is_some()); + let board = Shape::new(Square, complete_board.map(|x| x.is_some())); let tiles = (0..tile_count) .map(|i| extract_tile(i as u32, &complete_board)) .filter(|tile| tile.iter().any(|&x| x)) @@ -128,8 +126,9 @@ fn grow_tile_index( (changed, new_board) } -fn extract_tile(tile_index: u32, complete_board: &Array2>) -> Array2 { - let mut base = complete_board.map(|&x| x.filter(|&i| i == tile_index).is_none()); - array_util::remove_true_rows_cols_from_sides(&mut base); - base.map(|x| !x) +fn extract_tile(tile_index: u32, complete_board: &Array2>) -> Shape { + let base = complete_board.map(|&x| x.filter(|&i| i == tile_index).is_none()); + let mut shape = Shape::new(Square, base); + shape.trim_matching(true); + shape.map(|x| !x) } diff --git a/puzzle_config/src/random/mod.rs b/puzzle_config/src/random/mod.rs index 1616376..293915d 100644 --- a/puzzle_config/src/random/mod.rs +++ b/puzzle_config/src/random/mod.rs @@ -2,8 +2,7 @@ use crate::{ BoardConfig, ColorConfig, PreviewConfig, ProgressionConfig, PuzzleConfig, PuzzleConfigCollection, TileConfig, }; -use ndarray::Array2; -use puzzled_common::array_util::TileRotationIterator; +use puzzled_common::Shape; use rand::rngs::Xoshiro256PlusPlus; use rand::{Rng, RngExt, SeedableRng}; @@ -64,15 +63,17 @@ pub fn random_puzzle(settings: &RandomPuzzleSettings) -> PuzzleConfigCollection ) } -fn random_orientation(rng: &mut dyn Rng, array: Array2) -> Array2 { - let iterator = TileRotationIterator::new(array); - iterator.into_iter().nth(rng.random_range(0..8)).unwrap() +fn random_orientation(rng: &mut dyn Rng, shape: Shape) -> Shape { + shape + .rotations_flips_iter() + .nth(rng.random_range(0..8)) + .unwrap() } #[cfg(test)] mod tests { use super::*; - use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_normal() { @@ -90,7 +91,7 @@ mod tests { let puzzle = &collection.puzzles()[0]; assert_eq!( BoardConfig::Simple { - layout: arr2(&[ + layout: shape_square(&[ [true, true, true, true, true], [true, true, true, true, true], [true, true, true, true, true], @@ -104,7 +105,7 @@ mod tests { assert_eq!(5, puzzle.tiles().len()); assert_eq!( TileConfig::new( - arr2(&[ + shape_square(&[ [true, true, true, true, true], [true, true, true, true, false] ]), @@ -115,7 +116,7 @@ mod tests { ); assert_eq!( TileConfig::new( - arr2(&[ + shape_square(&[ [false, false, true], [true, true, true], [false, false, true], @@ -129,7 +130,7 @@ mod tests { ); assert_eq!( TileConfig::new( - arr2(&[[true, true, true, true]]), + shape_square(&[[true, true, true, true]]), ColorConfig::default_with_index(2), None ), @@ -137,7 +138,7 @@ mod tests { ); assert_eq!( TileConfig::new( - arr2(&[[false, false, true], [true, true, true]]), + shape_square(&[[false, false, true], [true, true, true]]), ColorConfig::default_with_index(3), None ), @@ -145,7 +146,7 @@ mod tests { ); assert_eq!( TileConfig::new( - arr2(&[[true], [true], [true], [true], [true]]), + shape_square(&[[true], [true], [true], [true], [true]]), ColorConfig::default_with_index(4), None ), @@ -169,25 +170,41 @@ mod tests { let puzzle = &collection.puzzles()[0]; assert_eq!( BoardConfig::Simple { - layout: arr2(&[[true, true], [true, true],]) + layout: shape_square(&[[true, true], [true, true],]) }, *puzzle.board_config() ); assert_eq!(4, puzzle.tiles().len()); assert_eq!( - TileConfig::new(arr2(&[[true]]), ColorConfig::default_with_index(0), None), + TileConfig::new( + shape_square(&[[true]]), + ColorConfig::default_with_index(0), + None + ), puzzle.tiles()[0] ); assert_eq!( - TileConfig::new(arr2(&[[true]]), ColorConfig::default_with_index(1), None), + TileConfig::new( + shape_square(&[[true]]), + ColorConfig::default_with_index(1), + None + ), puzzle.tiles()[1] ); assert_eq!( - TileConfig::new(arr2(&[[true]]), ColorConfig::default_with_index(2), None), + TileConfig::new( + shape_square(&[[true]]), + ColorConfig::default_with_index(2), + None + ), puzzle.tiles()[2] ); assert_eq!( - TileConfig::new(arr2(&[[true]]), ColorConfig::default_with_index(3), None), + TileConfig::new( + shape_square(&[[true]]), + ColorConfig::default_with_index(3), + None + ), puzzle.tiles()[3] ); } diff --git a/puzzle_solver/src/backtracking/mod.rs b/puzzle_solver/src/backtracking/mod.rs index 587aa2d..20def7a 100644 --- a/puzzle_solver/src/backtracking/mod.rs +++ b/puzzle_solver/src/backtracking/mod.rs @@ -5,7 +5,7 @@ use crate::board::Board; use crate::result::{Solution, TilePlacement, UnsolvableReason}; use crate::tile::Tile; use log::debug; -use puzzled_common::array_util; +use puzzled_common::ShapeType::Square; use tokio_util::sync::CancellationToken; pub mod core; @@ -93,17 +93,17 @@ fn create_tile_placement( ) -> TilePlacement { let bitmask_placement = &positioned_tile.bitmasks()[placement_index]; let placement_board = - bitmask_placement.to_array2(board.get_array().dim().0, board.get_array().dim().1); - let mut inverted_placement = placement_board.mapv(|v| !v); - array_util::remove_true_rows_cols_from_sides(&mut inverted_placement); - let rotation = inverted_placement.mapv(|v| !v); + bitmask_placement.to_shape(board.get_array().dim().0, board.get_array().dim().1, Square); + let mut inverted_placement = placement_board.map(|v| !v); + inverted_placement.trim_matching(true); + let rotation = inverted_placement.map(|v| !v); let x: usize = { let mut x_set = false; let mut x_start = 0usize; for x in 0..placement_board.dim().0 { for y in 0..placement_board.dim().1 { - if placement_board[[x, y]] { + if placement_board[(x, y)] { x_start = x; x_set = true; break; @@ -121,7 +121,7 @@ fn create_tile_placement( let mut y_start = 0usize; for y in 0..placement_board.dim().1 { for x in 0..placement_board.dim().0 { - if placement_board[[x, y]] { + if placement_board[(x, y)] { y_start = y; y_set = true; break; diff --git a/puzzle_solver/src/backtracking/positioned.rs b/puzzle_solver/src/backtracking/positioned.rs index c0bc08b..564930a 100644 --- a/puzzle_solver/src/backtracking/positioned.rs +++ b/puzzle_solver/src/backtracking/positioned.rs @@ -3,8 +3,7 @@ use crate::bitmask::Bitmask; use crate::board::Board; use crate::tile::Tile; use log::debug; -use ndarray::Array2; -use puzzled_common::array_util; +use puzzled_common::Shape; /// A tile with all its possible placements on the board represented as bitmasks. /// @@ -29,13 +28,13 @@ impl PositionedTile { /// /// returns: PositionedTile pub(crate) fn new(tile: &Tile, board: &Board, pruner: &Pruner) -> Self { - let all_placements: Vec> = tile + let all_placements: Vec = tile .all_rotations .iter() - .flat_map(|rotation| array_util::place_on_all_positions(board.get_array(), rotation)) + .flat_map(|rotation| board.get_array().place_on_all_positions(rotation)) .map(|array| { let mut array = array.clone(); - array_util::remove_parent(board.get_array(), &mut array); + array.remove_parent(board.get_array()); array }) .collect(); @@ -65,13 +64,13 @@ impl PositionedTile { #[cfg(test)] mod tests { use super::*; - use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_positioned_tile_new() { let mut board = Board::new((3, 4)); board[[0, 0]] = true; - let tile = Tile::new(arr2(&[[true, true, true], [true, true, false]])); + let tile = Tile::new(shape_square(&[[true, true, true], [true, true, false]])); let positioned_tile = PositionedTile::new( &tile, @@ -80,128 +79,216 @@ mod tests { ); assert_eq!(positioned_tile.bitmasks().len(), 15); - assert!(!positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, true, false], - [false, true, true, true], - [false, false, false, false] - ])))); - assert!(!positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [false, true, true, true], - [false, true, true, false] - ])))); - assert!(!positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, true, false], - [false, true, true, false], - [false, true, true, false] - ])))); - assert!(!positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, true, false], - [false, false, true, true], - [false, false, true, true] - ])))); - assert!(!positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, true, false], - [false, true, true, false], - [false, false, true, false] - ])))); - assert!(!positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, true, true], - [false, false, true, true], - [false, false, true, false] - ])))); - assert!(!positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [true, true, true, false], - [false, true, true, false] - ])))); - - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [true, true, false, false], - [true, true, true, false] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [false, true, true, false], - [false, true, true, true] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, true, true], - [false, true, true, false], - [false, false, false, false] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [true, true, true, false], - [true, true, false, false] - ])))); - - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, false, false], - [true, true, false, false], - [true, true, false, false] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, true], - [false, false, true, true], - [false, false, true, true] - ])))); - - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, false, false], - [false, true, true, false], - [false, true, true, false] - ])))); - - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, true, true], - [false, false, true, true], - [false, false, false, true] - ])))); - - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, true, false], - [false, true, true, false], - [false, true, false, false] - ])))); - - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, true, false], - [true, true, true, false], - [false, false, false, false] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, true, true], - [false, true, true, true], - [false, false, false, false] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [false, true, true, false], - [true, true, true, false] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [false, false, true, true], - [false, true, true, true] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, true, true, true], - [false, false, true, true], - [false, false, false, false] - ])))); - assert!(positioned_tile.bitmasks.contains(&Bitmask::from(&arr2(&[ - [false, false, false, false], - [false, true, true, true], - [false, false, true, true] - ])))); + assert!( + !positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, true, false], + [false, true, true, true], + [false, false, false, false] + ]))) + ); + assert!( + !positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [false, true, true, true], + [false, true, true, false] + ]))) + ); + assert!( + !positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, true, false], + [false, true, true, false], + [false, true, true, false] + ]))) + ); + assert!( + !positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, true, false], + [false, false, true, true], + [false, false, true, true] + ]))) + ); + assert!( + !positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, true, false], + [false, true, true, false], + [false, false, true, false] + ]))) + ); + assert!( + !positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, true, true], + [false, false, true, true], + [false, false, true, false] + ]))) + ); + assert!( + !positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [true, true, true, false], + [false, true, true, false] + ]))) + ); + + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [true, true, false, false], + [true, true, true, false] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [false, true, true, false], + [false, true, true, true] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, true, true], + [false, true, true, false], + [false, false, false, false] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [true, true, true, false], + [true, true, false, false] + ]))) + ); + + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, false, false], + [true, true, false, false], + [true, true, false, false] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, true], + [false, false, true, true], + [false, false, true, true] + ]))) + ); + + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, false, false], + [false, true, true, false], + [false, true, true, false] + ]))) + ); + + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, true, true], + [false, false, true, true], + [false, false, false, true] + ]))) + ); + + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, true, false], + [false, true, true, false], + [false, true, false, false] + ]))) + ); + + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, true, false], + [true, true, true, false], + [false, false, false, false] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, true, true], + [false, true, true, true], + [false, false, false, false] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [false, true, true, false], + [true, true, true, false] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [false, false, true, true], + [false, true, true, true] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, true, true, true], + [false, false, true, true], + [false, false, false, false] + ]))) + ); + assert!( + positioned_tile + .bitmasks + .contains(&Bitmask::from(&shape_square(&[ + [false, false, false, false], + [false, true, true, true], + [false, false, true, true] + ]))) + ); } #[test] fn test_positioned_tile_new_no_placements() { let board = Board::new((2, 2)); - let tile = Tile::new(arr2(&[ + let tile = Tile::new(shape_square(&[ [true, true, true], [true, false, false], [false, false, false], @@ -222,7 +309,7 @@ mod tests { board[[0, 1]] = true; board[[1, 0]] = true; board[[1, 1]] = true; - let tile = Tile::new(arr2(&[[true]])); + let tile = Tile::new(shape_square(&[[true]])); let positioned_tile = PositionedTile::new( &tile, @@ -235,7 +322,7 @@ mod tests { #[test] fn test_positioned_tile_new_duplicates() { let board = Board::new((3, 3)); - let tile = Tile::new(arr2(&[[true, true], [true, true]])); + let tile = Tile::new(shape_square(&[[true, true], [true, true]])); let positioned_tile = PositionedTile::new( &tile, diff --git a/puzzle_solver/src/backtracking/pruner/banned.rs b/puzzle_solver/src/backtracking/pruner/banned.rs index dd28c3e..5da046a 100644 --- a/puzzle_solver/src/backtracking/pruner/banned.rs +++ b/puzzle_solver/src/backtracking/pruner/banned.rs @@ -1,8 +1,8 @@ use crate::bitmask::Bitmask; use crate::board::Board; use crate::tile::Tile; -use ndarray::{arr2, Array2}; -use puzzled_common::array_util; +use puzzled_common::shape::shape_square; +use puzzled_common::Shape; use std::hash::Hash; #[derive(Hash, Eq, PartialEq, Debug)] @@ -23,7 +23,10 @@ pub fn create_banned_bitmasks_for_filling( ) -> Vec> { let min_tile_size = tiles .iter() - .map(|tile| array_util::count_biggest_connected_area_of_cells_matching(tile.base(), true)) + .map(|tile| { + tile.base() + .count_biggest_connected_area_of_cells_matching(true) + }) .min() .unwrap_or(0); @@ -72,12 +75,12 @@ fn create_banned_bitmasks_for_cell( } fn banned_bitmasks_1(board: &Board, x: usize, y: usize, banned_bitmasks: &mut Vec) { - let pattern = arr2(&[ + let pattern = shape_square(&[ [false, true, false], [true, false, true], [false, true, false], ]); - let area = arr2(&[ + let area = shape_square(&[ [false, true, false], [true, true, true], [false, true, false], @@ -93,12 +96,12 @@ fn banned_bitmasks_1(board: &Board, x: usize, y: usize, banned_bitmasks: &mut Ve #[allow(non_snake_case)] fn banned_bitmasks_D2(board: &Board, x: usize, y: usize, banned_bitmasks: &mut Vec) { - let pattern = arr2(&[ + let mut pattern = shape_square(&[ [false, true, true, false], [true, false, false, true], [false, true, true, false], ]); - let area = arr2(&[ + let mut area = shape_square(&[ [false, true, true, false], [true, true, true, true], [false, true, true, false], @@ -114,8 +117,8 @@ fn banned_bitmasks_D2(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - let pattern = array_util::rotate_90(&pattern); - let area = array_util::rotate_90(&area); + pattern.rotate_clockwise(); + area.rotate_clockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -131,13 +134,13 @@ fn banned_bitmasks_D2(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V #[allow(non_snake_case)] fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut Vec) { - let pattern = arr2(&[ + let mut pattern = shape_square(&[ [false, true, true, false], [true, false, false, true], [true, false, true, false], [false, true, false, false], ]); - let area = arr2(&[ + let mut area = shape_square(&[ [false, true, true, false], [true, true, true, true], [true, true, true, false], @@ -155,8 +158,8 @@ fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - let pattern = array_util::rotate_90(&pattern); - let area = array_util::rotate_90(&area); + pattern.rotate_clockwise(); + area.rotate_clockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -169,8 +172,8 @@ fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - let pattern = array_util::rotate_90(&pattern); - let area = array_util::rotate_90(&area); + pattern.rotate_clockwise(); + area.rotate_clockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -183,8 +186,8 @@ fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - let pattern = array_util::rotate_90(&pattern); - let area = array_util::rotate_90(&area); + pattern.rotate_clockwise(); + area.rotate_clockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -200,12 +203,12 @@ fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V #[allow(non_snake_case)] fn banned_bitmasks_I3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut Vec) { - let pattern = arr2(&[ + let mut pattern = shape_square(&[ [false, true, true, true, false], [true, false, false, false, true], [false, true, true, true, false], ]); - let area = arr2(&[ + let mut area = shape_square(&[ [false, true, true, true, false], [true, true, true, true, true], [false, true, true, true, false], @@ -222,8 +225,8 @@ fn banned_bitmasks_I3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - let pattern = array_util::rotate_90(&pattern); - let area = array_util::rotate_90(&area); + pattern.rotate_clockwise(); + area.rotate_clockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -239,19 +242,19 @@ fn banned_bitmasks_I3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V #[allow(non_snake_case)] fn banned_bitmasks_O4(board: &Board, x: usize, y: usize, banned_bitmasks: &mut Vec) { - let pattern = arr2(&[ + let pattern = shape_square(&[ [false, true, true, false], [true, false, false, true], [true, false, false, true], [false, true, true, false], ]); - let area = arr2(&[ + let mut area = shape_square(&[ [false, true, true, false], [true, true, true, true], [true, true, true, true], [false, true, true, false], ]); - let area = array_util::rotate_90(&area); + area.rotate_clockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, &area, @@ -265,8 +268,8 @@ fn banned_bitmasks_O4(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V } fn create_banned_bitmask_for_pattern_at_if_possible( - pattern: &Array2, - area: &Array2, + pattern: &Shape, + area: &Shape, x: isize, y: isize, board: &Board, @@ -275,8 +278,8 @@ fn create_banned_bitmask_for_pattern_at_if_possible( for py in 0..pattern.dim().1 { let board_x = x + px as isize; let board_y = y + py as isize; - if area[[px, py]] - && !pattern[[px, py]] + if area[(px, py)] + && !pattern[(px, py)] && *board .get_array() .get((board_x as usize, board_y as usize)) @@ -304,8 +307,8 @@ fn create_banned_bitmask_for_pattern_at_if_possible( /// /// returns: BannedBitmask fn create_banned_bitmask_for_pattern_at( - pattern: &Array2, - area: &Array2, + pattern: &Shape, + area: &Shape, x: isize, y: isize, board: &Board, @@ -313,10 +316,10 @@ fn create_banned_bitmask_for_pattern_at( let mut board_array = board.get_array().clone(); board_array.fill(false); - let pattern_board = array_util::or_arrays_at(&board_array, pattern, x, y); + let pattern_board = board_array.or_arrays_at(pattern, x, y); let pattern_bitmask = Bitmask::from(&pattern_board); - let area_board = array_util::or_arrays_at(&board_array, area, x, y); + let area_board = board_array.or_arrays_at(area, x, y); let area_bitmask = Bitmask::from(&area_board); BannedBitmask { @@ -329,16 +332,15 @@ fn create_banned_bitmask_for_pattern_at( mod tests { use super::*; use crate::board::Board; - use ndarray::arr2; #[test] fn test_create_banned_bitmask_for_pattern_at_middle() { - let pattern = arr2(&[ + let pattern = shape_square(&[ [false, true, false], [true, false, true], [false, true, false], ]); - let area = arr2(&[ + let area = shape_square(&[ [false, true, false], [true, true, true], [false, true, false], @@ -346,14 +348,14 @@ mod tests { let board = Board::new((5, 5)); let banned_bitmask = create_banned_bitmask_for_pattern_at(&pattern, &area, 1, 1, &board); - let expected_pattern_board = arr2(&[ + let expected_pattern_board = shape_square(&[ [false, false, false, false, false], [false, false, true, false, false], [false, true, false, true, false], [false, false, true, false, false], [false, false, false, false, false], ]); - let expected_area_board = arr2(&[ + let expected_area_board = shape_square(&[ [false, false, false, false, false], [false, false, true, false, false], [false, true, true, true, false], @@ -369,12 +371,12 @@ mod tests { #[test] fn test_create_banned_bitmask_for_pattern_at_minus1_minus1() { - let pattern = arr2(&[ + let pattern = shape_square(&[ [false, true, false], [true, false, true], [false, true, false], ]); - let area = arr2(&[ + let area = shape_square(&[ [false, true, false], [true, true, true], [false, true, false], @@ -382,14 +384,14 @@ mod tests { let board = Board::new((5, 5)); let banned_bitmask = create_banned_bitmask_for_pattern_at(&pattern, &area, -1, -1, &board); - let expected_pattern_board = arr2(&[ + let expected_pattern_board = shape_square(&[ [false, true, false, false, false], [true, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], ]); - let expected_area_board = arr2(&[ + let expected_area_board = shape_square(&[ [true, true, false, false, false], [true, false, false, false, false], [false, false, false, false, false], @@ -405,12 +407,12 @@ mod tests { #[test] fn test_create_banned_bitmask_for_pattern_at_3_3() { - let pattern = arr2(&[ + let pattern = shape_square(&[ [false, true, false], [true, false, true], [false, true, false], ]); - let area = arr2(&[ + let area = shape_square(&[ [false, true, false], [true, true, true], [false, true, false], @@ -418,14 +420,14 @@ mod tests { let board = Board::new((5, 5)); let banned_bitmask = create_banned_bitmask_for_pattern_at(&pattern, &area, 3, 3, &board); - let expected_pattern_board = arr2(&[ + let expected_pattern_board = shape_square(&[ [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, true], [false, false, false, true, false], ]); - let expected_area_board = arr2(&[ + let expected_area_board = shape_square(&[ [false, false, false, false, false], [false, false, false, false, false], [false, false, false, false, false], diff --git a/puzzle_solver/src/backtracking/pruner/mod.rs b/puzzle_solver/src/backtracking/pruner/mod.rs index 568087d..3648a1f 100644 --- a/puzzle_solver/src/backtracking/pruner/mod.rs +++ b/puzzle_solver/src/backtracking/pruner/mod.rs @@ -50,83 +50,83 @@ mod tests { use crate::bitmask::Bitmask; use crate::board::Board; use crate::tile::Tile; - use ndarray::arr2; + use puzzled_common::shape::shape_square; #[test] fn test_pruner_trominoes() { - let board: Board = arr2(&[ + let board: Board = shape_square(&[ [false, false, false], [true, false, true], [true, false, false], ]) .into(); let tiles = vec![ - Tile::new(arr2(&[[true, true, true]])), - Tile::new(arr2(&[[true, true], [true, false]])), + Tile::new(shape_square(&[[true, true, true]])), + Tile::new(shape_square(&[[true, true], [true, false]])), ]; let pruner = Pruner::new_for_filling(&board, &tiles); assert!(pruner.banned_bitmasks.len() > 0); // Assert prune - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [false, true, false], [true, false, true], [true, false, false], ])))); - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [true, false, true], [true, true, false], [true, false, true], ])))); - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [false, true, false], [true, false, true], [true, false, false], ])))); - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [false, true, false], [true, false, true], [true, true, false], ])))); - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [false, false, true], [true, true, false], [true, false, true], ])))); - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [false, false, false], [true, true, false], [true, false, true], ])))); - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [false, true, false], [true, true, true], [true, true, false], ])))); - assert!(pruner.prune(&Bitmask::from(&arr2(&[ + assert!(pruner.prune(&Bitmask::from(&shape_square(&[ [false, true, true], [true, true, true], [true, false, false], ])))); // Assert not prune - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [false, false, false], [true, false, true], [true, false, false], ])))); - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [false, false, false], [true, true, true], [true, true, true], ])))); - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [true, true, true], [true, false, true], [true, false, false], ])))); - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [true, true, true], [true, true, true], [true, true, true], @@ -135,37 +135,37 @@ mod tests { #[test] fn test_pruner_simple() { - let board: Board = arr2(&[ + let board: Board = shape_square(&[ [true, false, false, false], [false, false, false, false], [false, false, false, false], ]) .into(); let tiles = vec![ - Tile::new(arr2(&[[false, true, true], [true, true, true]])), - Tile::new(arr2(&[[true, true, true], [true, true, true]])), + Tile::new(shape_square(&[[false, true, true], [true, true, true]])), + Tile::new(shape_square(&[[true, true, true], [true, true, true]])), ]; let pruner = Pruner::new_for_filling(&board, &tiles); assert!(pruner.banned_bitmasks.len() > 0); // Assert not prune - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [true, false, false, false], [false, false, false, false], [false, false, false, false], ])))); - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [true, true, false, false], [true, true, false, false], [true, true, false, false], ])))); - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [true, false, true, true], [false, false, true, true], [false, false, true, true], ])))); - assert!(!pruner.prune(&Bitmask::from(&arr2(&[ + assert!(!pruner.prune(&Bitmask::from(&shape_square(&[ [true, true, true, true], [true, true, true, true], [true, true, true, true], diff --git a/puzzle_solver/src/bitmask.rs b/puzzle_solver/src/bitmask.rs index db9407d..5dd79ef 100644 --- a/puzzle_solver/src/bitmask.rs +++ b/puzzle_solver/src/bitmask.rs @@ -1,4 +1,4 @@ -use ndarray::Array2; +use puzzled_common::{Shape, ShapeType}; use std::ops::{BitAnd, BitOr, BitXor, Index}; /// Must be the same as the bits in the primitive type used in the bitmask array. @@ -304,13 +304,13 @@ impl Bitmask { TOTAL_BITS } - pub(crate) fn to_array2(&self, rows: usize, cols: usize) -> Array2 { - let mut array = Array2::from_elem((rows, cols), false); + pub(crate) fn to_shape(&self, rows: usize, cols: usize, shape_type: ShapeType) -> Shape { + let mut array = Shape::from_elem((rows, cols), shape_type, false); for x in 0..cols { for y in 0..rows { let index = x * rows + y; if index < self.relevant_bits() { - array[[y, x]] = self.get_bit(index); + array[(y, x)] = self.get_bit(index); } } } @@ -356,17 +356,17 @@ impl Default for Bitmask { } } -impl From<&Array2> for Bitmask { +impl From<&Shape> for Bitmask { /// Creates a Bitmask from a 2D array of booleans. /// The relevant bits are determined by the number of elements in the array. /// Each cell in the array corresponds to a bit in the bitmask. - fn from(value: &Array2) -> Self { + fn from(value: &Shape) -> Self { let relevant_bits = value.iter().count(); let mut bitmask = Bitmask::new(relevant_bits); let (xs, ys) = value.dim(); for x in 0..ys { for y in 0..xs { - if value[[y, x]] { + if value[(y, x)] { let index = x * xs + y; bitmask.set_bit(index); } @@ -403,7 +403,8 @@ impl Index for Bitmask { #[cfg(test)] mod tests { use super::*; - use ndarray::arr2; + use puzzled_common::shape::shape_square; + use puzzled_common::ShapeType::Square; #[test] fn test_new() { @@ -684,7 +685,7 @@ mod tests { #[test] fn test_from_array2_bool() { - let array = arr2(&[[true, false, true, true], [false, true, false, false]]); + let array = shape_square(&[[true, false, true, true], [false, true, false, false]]); let bitmask = Bitmask::from(&array); @@ -701,7 +702,7 @@ mod tests { #[test] fn test_from_array2_bool_empty() { - let array = arr2(&[[]]); + let array = shape_square(&[[]]); let bitmask = Bitmask::from(&array); @@ -710,7 +711,7 @@ mod tests { #[test] fn test_from_array2_bool_one() { - let array = arr2(&[[true]]); + let array = shape_square(&[[true]]); let bitmask = Bitmask::from(&array); @@ -752,11 +753,11 @@ mod tests { #[test] fn test_to_array2() { - let bitmask = Bitmask::from(&arr2(&[[true, false, true], [false, false, true]])); + let bitmask = Bitmask::from(&shape_square(&[[true, false, true], [false, false, true]])); - let array = bitmask.to_array2(2, 3); + let array = bitmask.to_shape(2, 3, Square); - let expected = arr2(&[[true, false, true], [false, false, true]]); + let expected = shape_square(&[[true, false, true], [false, false, true]]); assert_eq!(array, expected); } diff --git a/puzzle_solver/src/board.rs b/puzzle_solver/src/board.rs index 670eda1..f739e21 100644 --- a/puzzle_solver/src/board.rs +++ b/puzzle_solver/src/board.rs @@ -1,7 +1,8 @@ use log::debug; use ndarray::Array2; -use puzzled_common::array_util::TrimSides; -use puzzled_common::{array_util, Shape}; +use puzzled_common::shape::TrimSides; +use puzzled_common::Shape; +use puzzled_common::ShapeType::Square; use std::ops::{Index, IndexMut}; /// Represents a 2D board for the puzzle, where each cell is either true (filled) or false (empty). @@ -40,7 +41,7 @@ impl Board { /// assert!(board.get_array().iter().all(|&b| b == false)); /// ``` pub fn new(dims: (usize, usize)) -> Self { - Board(Shape::default(dims)) + Board(Shape::from_elem(dims, Square, false)) } /// Returns a reference to the internal 2D array representing the board. @@ -69,14 +70,14 @@ impl Board { pub(crate) fn debug_print(&self) { if log::log_enabled!(log::Level::Debug) { debug!("Board:"); - array_util::debug_print(&self.0); + self.0.debug_print(); } } /// Trims the board by removing any rows or columns on the edges that are entirely /// true (filled). pub(crate) fn trim(&mut self) -> TrimSides { - array_util::remove_true_rows_cols_from_sides(&mut self.0) + self.0.trim_matching(true) } } @@ -84,18 +85,18 @@ impl Index<[usize; 2]> for Board { type Output = bool; fn index(&self, index: [usize; 2]) -> &Self::Output { - &self.0[[index[0], index[1]]] + &self.0[(index[0], index[1])] } } impl IndexMut<[usize; 2]> for Board { fn index_mut(&mut self, index: [usize; 2]) -> &mut Self::Output { - &mut self.0[[index[0], index[1]]] + &mut self.0[(index[0], index[1])] } } -impl From> for Board { - fn from(array: Array2) -> Self { +impl From for Board { + fn from(array: Shape) -> Self { Board(array) } } @@ -107,13 +108,13 @@ mod tests { #[test] fn test_new_0_0() { let board = Board::new((0, 0)); - assert_eq!(board.get_array().shape(), &[0, 0]); + assert_eq!(board.get_array().dim(), (0, 0)); } #[test] fn test_new_3_4() { let board = Board::new((3, 4)); - assert_eq!(board.get_array().shape(), &[3, 4]); + assert_eq!(board.get_array().dim(), (3, 4)); assert!(board.get_array().iter().all(|&b| b == false)); } @@ -136,7 +137,7 @@ mod tests { board.trim(); - assert_eq!(board.get_array().shape(), &[3, 3]); + assert_eq!(board.get_array().dim(), (3, 3)); assert_eq!(board[[0, 0]], false); assert_eq!(board[[0, 1]], false); assert_eq!(board[[0, 2]], false); diff --git a/puzzle_solver/src/result.rs b/puzzle_solver/src/result.rs index 5e51880..a3e223c 100644 --- a/puzzle_solver/src/result.rs +++ b/puzzle_solver/src/result.rs @@ -1,4 +1,3 @@ -use ndarray::Array2; use puzzled_common::Shape; /// Represents a successful solution to the puzzle. @@ -66,7 +65,7 @@ pub enum UnsolvableReason { NoFit, PlausibilityCheckFailed, TileCannotBePlaced { - base: Array2, + base: Shape, }, BoardTooLarge, /// Indicates that the solving process was canceled before a solution could be found. diff --git a/puzzle_solver/src/tile.rs b/puzzle_solver/src/tile.rs index 44a3c15..32caed7 100644 --- a/puzzle_solver/src/tile.rs +++ b/puzzle_solver/src/tile.rs @@ -1,6 +1,4 @@ use log::debug; -use ndarray::Array2; -use puzzled_common::array_util::{debug_print, rotate_90}; use puzzled_common::Shape; use std::collections::HashSet; @@ -40,24 +38,9 @@ impl Tile { pub fn new(base: Shape) -> Tile { let mut all_rotations_set: HashSet = HashSet::new(); - all_rotations_set.insert(base.clone()); - - let mut tmp = rotate_90(&base); - all_rotations_set.insert(tmp.clone()); - tmp = rotate_90(&tmp); - all_rotations_set.insert(tmp.clone()); - tmp = rotate_90(&tmp); - all_rotations_set.insert(tmp.clone()); - - tmp = base.clone().reversed_axes(); - all_rotations_set.insert(tmp.clone()); - - tmp = rotate_90(&tmp); - all_rotations_set.insert(tmp.clone()); - tmp = rotate_90(&tmp); - all_rotations_set.insert(tmp.clone()); - tmp = rotate_90(&tmp); - all_rotations_set.insert(tmp.clone()); + base.rotations_flips_iter().for_each(|rotation| { + all_rotations_set.insert(rotation); + }); let all_rotations = all_rotations_set.into_iter().collect(); Tile { @@ -91,11 +74,11 @@ impl Tile { #[allow(dead_code)] pub(crate) fn debug_print(&self) { debug!("Tile Base: "); - debug_print(&self.base); + &self.base.debug_print(); debug!("All Rotations: "); for rotation in &self.all_rotations { debug!("Rotation:"); - debug_print(rotation); + rotation.debug_print(); } } } diff --git a/puzzled/src/app/puzzle/info.rs b/puzzled/src/app/puzzle/info.rs index 73fe9f9..d954767 100644 --- a/puzzled/src/app/puzzle/info.rs +++ b/puzzled/src/app/puzzle/info.rs @@ -68,8 +68,8 @@ impl PuzzlePage { "Board Dimensions", &format!( "{} x {}", - puzzle_config.board_config().layout().nrows(), - puzzle_config.board_config().layout().ncols() + puzzle_config.board_config().layout().dim().0, + puzzle_config.board_config().layout().dim().1 ), ); action_rows.push(board_dimensions); diff --git a/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs b/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs index e351836..e51e38d 100644 --- a/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs +++ b/puzzled/src/app/puzzle/puzzle_area/puzzle_state.rs @@ -106,14 +106,12 @@ impl PuzzleState { let this_is_on_board = puzzle_config .board_config() .layout() - .get::<(usize, usize)>((position.0 as usize, position.1 as usize)) + .get((position.0 as usize, position.1 as usize)) .unwrap_or(&false); for (dr, dc) in DELTAS.iter() { let neighbor_pos = ((position.0 + dr) as usize, (position.1 + dc) as usize); - if let Some(neighbour_on_board) = puzzle_config - .board_config() - .layout() - .get::<(usize, usize)>(neighbor_pos) + if let Some(neighbour_on_board) = + puzzle_config.board_config().layout().get(neighbor_pos) && !this_is_on_board && *neighbour_on_board { diff --git a/puzzled/src/application.rs b/puzzled/src/application.rs index 480eca7..a2cd9a2 100644 --- a/puzzled/src/application.rs +++ b/puzzled/src/application.rs @@ -31,6 +31,7 @@ use gtk::{gio, glib, CssProvider, License, Settings, STYLE_PROVIDER_PRIORITY_APP use log::info; use ndarray::array; use puzzle_config::ColorConfig; +use puzzled_common::shape::shape_square; use std::fmt::Debug; mod imp { @@ -249,7 +250,7 @@ impl PuzzledApplication { .expect("Missing `overlapping_fixed` in resource"); let left_tile = TileView::new( 0, - array![[true, false], [true, true]], + shape_square(&[[true, false], [true, true]]), ColorConfig::default_with_index(0), None, ); @@ -259,7 +260,7 @@ impl PuzzledApplication { let right_tile = TileView::new( 0, - array![[true, true], [false, true]], + shape_square(&[[true, true], [false, true]]), ColorConfig::default_with_index(5), None, ); @@ -275,7 +276,7 @@ impl PuzzledApplication { .expect("Missing `outside_fixed` in resource"); let tile = TileView::new( 0, - array![[true, true], [false, true]], + shape_square(&[[true, true], [false, true]]), ColorConfig::default_with_index(0), None, ); @@ -294,7 +295,12 @@ impl PuzzledApplication { color_config.blue(), 128, ); - let tile = TileView::new(0, array![[true, true], [false, true]], color_config, None); + let tile = TileView::new( + 0, + shape_square(&[[true, true], [false, true]]), + color_config, + None, + ); tile.set_width_request(CELL_SIZE * 2); tile.set_height_request(CELL_SIZE * 2); diff --git a/puzzled/src/solver/combination_solutions.rs b/puzzled/src/solver/combination_solutions.rs index cb157b2..b8b1cbc 100644 --- a/puzzled/src/solver/combination_solutions.rs +++ b/puzzled/src/solver/combination_solutions.rs @@ -60,7 +60,7 @@ impl CombinationsSolver { let mut message: String = "".to_string(); for (i, placement) in solution.placements().iter().enumerate() { let tile = - tiles.iter().find(|t| t.base == placement.base()).unwrap(); + tiles.iter().find(|t| t.base == *placement.base()).unwrap(); message = format!( "{} {}", message, diff --git a/puzzled_common/src/array_util.rs b/puzzled_common/src/array_util.rs deleted file mode 100644 index 5a22e0d..0000000 --- a/puzzled_common/src/array_util.rs +++ /dev/null @@ -1,726 +0,0 @@ -use ndarray::{s, Array2, Axis}; - -/// Rotates a 2D boolean array 90 degrees clockwise and returns the new array. -/// -/// This is a convenience function to allow semantically clearer code when rotating arrays. -/// -/// # Arguments -/// -/// * `array`: The rotated 2D boolean array. -/// -/// returns: ArrayBase, Dim<[usize; 2]>, as RawData>::Elem> -pub fn rotate_90(array: &Array2) -> Array2 { - let mut array = array.clone().reversed_axes(); - array.invert_axis(Axis(1)); - array -} - -/// Represents the number of rows and columns removed from the sides of a 2D array. -#[derive(Debug, Default, PartialEq, Eq)] -pub struct TrimSides { - /// The number of rows removed from the lower side of the x-axis. - pub lower_x: usize, - /// The number of rows removed from the higher side of the x-axis. - pub upper_x: usize, - /// The number of columns removed from the lower side of the y-axis. - pub lower_y: usize, - /// The number of columns removed from the higher side of the y-axis. - pub upper_y: usize, -} - -/// Removes rows and columns from the sides of a 2D boolean array where all cells are `true`. -/// -/// # Arguments -/// -/// * `array`: The mutable reference to the 2D boolean array to be modified. -/// -/// returns: () -pub fn remove_true_rows_cols_from_sides(array: &mut Array2) -> TrimSides { - let mut trim_sides = TrimSides::default(); - loop { - if array.nrows() == 0 || array.ncols() == 0 { - break; - } - - let left_col_all_true = array.column(0).iter().all(|&cell| cell); - if left_col_all_true { - *array = array.slice(s![.., 1..]).to_owned(); - trim_sides.lower_y += 1; - continue; - } - - let right_col_all_true = array.column(array.ncols() - 1).iter().all(|&cell| cell); - if right_col_all_true { - *array = array.slice(s![.., ..array.ncols() - 1]).to_owned(); - trim_sides.upper_y += 1; - continue; - } - - let top_row_all_true = array.row(0).iter().all(|&cell| cell); - if top_row_all_true { - *array = array.slice(s![1.., ..]).to_owned(); - trim_sides.lower_x += 1; - continue; - } - - let bottom_row_all_true = array.row(array.nrows() - 1).iter().all(|&cell| cell); - if bottom_row_all_true { - *array = array.slice(s![..array.nrows() - 1, ..]).to_owned(); - trim_sides.upper_x += 1; - continue; - } - - break; - } - trim_sides -} - -/// Places the `child` array onto the `parent` array at the specified offsets using a logical OR -/// operation. -/// This means that if either the parent or child cell is `true`, the resulting cell will be `true`. -/// -/// # Arguments -/// -/// * `parent`: The parent 2D boolean array. -/// * `child`: The child 2D boolean array to be placed onto the parent. -/// * `x_offset`: The x-axis offset for placing the child array. -/// * `y_offset`: The y-axis offset for placing the child array. -/// -/// returns: Array2 -pub fn or_arrays_at( - parent: &Array2, - child: &Array2, - x_offset: isize, - y_offset: isize, -) -> Array2 { - let mut new_array = parent.clone(); - let child_xs = child.nrows(); - let child_ys = child.ncols(); - - for x in 0..child_xs { - for y in 0..child_ys { - let parent_x = x as isize + x_offset; - let parent_y = y as isize + y_offset; - if parent_x >= 0 - && parent_x < parent.nrows() as isize - && parent_y >= 0 - && parent_y < parent.ncols() as isize - { - new_array[[parent_x as usize, parent_y as usize]] |= child[[x, y]]; - } - } - } - - new_array -} - -/// Generates all possible placements of the `child` array onto the `parent` array -/// using a logical OR operation. -/// -/// # Arguments -/// -/// * `parent`: The parent 2D boolean array. -/// * `child`: The child 2D boolean array to be placed onto the parent. -/// -/// returns: Vec> -pub fn place_on_all_positions(parent: &Array2, child: &Array2) -> Vec> { - let mut placements = Vec::new(); - let parent_rows = parent.nrows(); - let parent_cols = parent.ncols(); - let child_rows = child.nrows(); - let child_cols = child.ncols(); - - if child_rows > parent_rows || child_cols > parent_cols { - return placements; - } - - for row_offset in 0..=(parent_rows - child_rows) { - for col_offset in 0..=(parent_cols - child_cols) { - let mut new_array = parent.clone(); - let mut valid = true; - for r in 0..child_rows { - for c in 0..child_cols { - if child[[r, c]] && parent[[row_offset + r, col_offset + c]] { - valid = false; - break; - } - new_array[[row_offset + r, col_offset + c]] |= child[[r, c]]; - } - if !valid { - break; - } - } - if valid { - placements.push(new_array); - } - } - } - - placements -} - -/// Removes the `true` values from the `child` array wherever the `parent` array has `true` values. -/// -/// # Arguments -/// -/// * `parent`: The parent 2D boolean array. -/// * `child`: The mutable reference to the child 2D boolean array to be modified. -/// -/// returns: () -pub fn remove_parent(parent: &Array2, child: &mut Array2) { - for row in 0..parent.nrows() { - for col in 0..parent.ncols() { - if parent[[row, col]] { - child[[row, col]] = false; - } - } - } -} - -pub fn count_biggest_connected_area_of_cells_matching( - array: &Array2, - target_value: bool, -) -> usize { - let mut visited = Array2::from_elem(array.dim(), false); - let mut max_area = 0; - - for ((x, y), value) in array.indexed_iter() { - if *value == target_value && !visited[[x, y]] { - let mut area = 0; - let mut stack = vec![(x, y)]; - - while let Some((cx, cy)) = stack.pop() { - if cx < array.nrows() - && cy < array.ncols() - && !visited[[cx, cy]] - && array[[cx, cy]] == target_value - { - visited[[cx, cy]] = true; - area += 1; - - // Add neighbors to the stack - if cx > 0 { - stack.push((cx - 1, cy)); - } - if cx < array.nrows() - 1 { - stack.push((cx + 1, cy)); - } - if cy > 0 { - stack.push((cx, cy - 1)); - } - if cy < array.ncols() - 1 { - stack.push((cx, cy + 1)); - } - } - } - - max_area = max_area.max(area); - } - } - - max_area -} - -pub struct TileRotationIterator { - current: Array2, - iteration: u8, -} - -impl TileRotationIterator { - pub fn new(tile: Array2) -> Self { - Self { - current: tile, - iteration: 0, - } - } -} - -impl Iterator for TileRotationIterator { - type Item = Array2; - - fn next(&mut self) -> Option { - if self.iteration >= 8 { - return None; - } - if self.iteration == 4 { - self.current = self.current.clone().reversed_axes(); - } - let current = self.current.clone(); - let rotated = rotate_90(&self.current); - self.current = rotated; - self.iteration += 1; - Some(current) - } -} - -/// Prints a 2D boolean array to the debug log, using 'â–ˆ' for `true` and 'â–‘' for `false`. -#[allow(dead_code)] -pub fn debug_print(array: &Array2) { - for row in array.rows() { - let row_str: String = row - .iter() - .map(|&cell| if cell { '#' } else { '-' }) - .collect(); - println!("{}", row_str); - } -} - -#[cfg(test)] -mod test { - use super::*; - use ndarray::{arr2, Array2}; - - #[test] - fn test_rotate_90_size_1() { - let array = arr2(&[[true]]); - let rotated = rotate_90(&array); - let expected = arr2(&[[true]]); - assert_eq!(expected, rotated); - } - - #[test] - fn test_rotate_90_size_2() { - let array = arr2(&[[true, false]]); - let rotated = rotate_90(&array); - let expected = arr2(&[[true], [false]]); - assert_eq!(expected, rotated); - } - - #[test] - fn test_rotate_90() { - let array = arr2(&[ - [true, false, false], - [true, true, true], - [true, false, true], - ]); - let rotated = rotate_90(&array); - let expected = arr2(&[ - [true, true, true], - [false, true, false], - [true, true, false], - ]); - assert_eq!(expected, rotated); - } - - #[test] - fn test_remove_true_rows_cols_from_sides_empty() { - let mut array: Array2 = Array2::default((0, 0)); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected: Array2 = Array2::default((0, 0)); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 0, - upper_y: 0, - upper_x: 0, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_sides_true() { - let mut array = arr2(&[[true]]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected: Array2 = arr2(&[[]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 1, - upper_y: 0, - upper_x: 0, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_sides_false() { - let mut array = arr2(&[[false]]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected: Array2 = arr2(&[[false]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 0, - upper_y: 0, - upper_x: 0, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_sides_lower_y_upper_y() { - let mut array = arr2(&[ - [true, true, false, true], - [true, false, false, true], - [true, true, false, true], - ]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[[true, false], [false, false], [true, false]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 1, - upper_y: 1, - upper_x: 0, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_sides_lower_x_upper_x() { - let mut array = arr2(&[ - [true, true, true, true], - [false, true, false, false], - [true, true, true, true], - ]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[[false, true, false, false]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 1, - lower_y: 0, - upper_y: 0, - upper_x: 1, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_sides_all_sides() { - let mut array = arr2(&[ - [true, true, true, true, true], - [true, true, false, false, true], - [true, false, true, false, true], - [true, true, true, true, true], - ]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[[true, false, false], [false, true, false]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 1, - lower_y: 1, - upper_y: 1, - upper_x: 1, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_lower_y() { - let mut array = arr2(&[[true, true, false, false], [true, false, true, false]]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[[true, false, false], [false, true, false]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 1, - upper_y: 0, - upper_x: 0, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_upper_y() { - let mut array = arr2(&[[false, false, true, true], [false, true, false, true]]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[[false, false, true], [false, true, false]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 0, - upper_y: 1, - upper_x: 0, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_lower_x() { - let mut array = arr2(&[ - [true, true, true], - [false, true, false], - [true, false, true], - ]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[[false, true, false], [true, false, true]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 1, - lower_y: 0, - upper_y: 0, - upper_x: 0, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_from_upper_x() { - let mut array = arr2(&[ - [false, true, false], - [true, false, true], - [true, true, true], - ]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[[false, true, false], [true, false, true]]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 0, - upper_y: 0, - upper_x: 1, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_remove_true_rows_cols_test() { - let mut array = arr2(&[ - [false, false, false, false], - [false, false, false, false], - [true, true, true, true], - [false, true, false, true], - [true, true, true, true], - ]); - let trim_sides = remove_true_rows_cols_from_sides(&mut array); - let expected = arr2(&[ - [false, false, false, false], - [false, false, false, false], - [true, true, true, true], - [false, true, false, true], - ]); - assert_eq!(expected, array); - let expected_trim_sides = TrimSides { - lower_x: 0, - lower_y: 0, - upper_y: 0, - upper_x: 1, - }; - assert_eq!(expected_trim_sides, trim_sides); - } - - #[test] - fn test_or_arrays_at() { - let parent = arr2(&[ - [false, false, false], - [false, false, false], - [false, false, false], - ]); - let child = arr2(&[[true, false], [false, true]]); - let result = or_arrays_at(&parent, &child, 1, 1); - let expected = arr2(&[ - [false, false, false], - [false, true, false], - [false, false, true], - ]); - assert_eq!(expected, result); - } - - #[test] - fn test_or_arrays_at_empty_child() { - let parent = arr2(&[ - [false, false, false], - [false, false, false], - [false, false, false], - ]); - let child = arr2(&[[]]); - let result = or_arrays_at(&parent, &child, 1, 1); - let expected = arr2(&[ - [false, false, false], - [false, false, false], - [false, false, false], - ]); - assert_eq!(expected, result); - } - - #[test] - fn test_or_arrays_at_child_1x1() { - let parent = arr2(&[ - [false, false, false], - [false, false, false], - [false, false, false], - ]); - let child = arr2(&[[true]]); - let result = or_arrays_at(&parent, &child, 1, 1); - let expected = arr2(&[ - [false, false, false], - [false, true, false], - [false, false, false], - ]); - assert_eq!(expected, result); - } - - #[test] - fn test_or_arrays_at_child_off_parent() { - let parent = arr2(&[ - [false, false, false], - [false, false, false], - [false, false, false], - ]); - let child = arr2(&[[true, true], [true, true]]); - let result = or_arrays_at(&parent, &child, 2, 2); - let expected = arr2(&[ - [false, false, false], - [false, false, false], - [false, false, true], - ]); - assert_eq!(expected, result); - } - - #[test] - fn test_or_arrays_at_true_parent() { - let parent = arr2(&[[true, true, true], [true, true, true], [true, true, true]]); - let child = arr2(&[[true, false], [true, false]]); - let result = or_arrays_at(&parent, &child, 1, 1); - let expected = arr2(&[[true, true, true], [true, true, true], [true, true, true]]); - assert_eq!(expected, result); - } - - #[test] - fn test_or_arrays_at_smaller_parent() { - let parent = arr2(&[[false, false], [false, false]]); - let child = arr2(&[[true, true, true], [true, true, true], [true, true, true]]); - let result = or_arrays_at(&parent, &child, 0, 0); - let expected = arr2(&[[true, true], [true, true]]); - assert_eq!(expected, result); - } - - #[test] - fn test_place_on_all_positions() { - let parent = arr2(&[ - [false, false, false], - [false, false, false], - [false, false, false], - ]); - let child = arr2(&[[true, false], [false, true]]); - let placements = place_on_all_positions(&parent, &child); - assert_eq!(placements.len(), 4); - assert!(placements.contains(&arr2(&[ - [true, false, false], - [false, true, false], - [false, false, false], - ]))); - assert!(placements.contains(&arr2(&[ - [false, true, false], - [false, false, true], - [false, false, false], - ]))); - assert!(placements.contains(&arr2(&[ - [false, false, false], - [true, false, false], - [false, true, false], - ]))); - assert!(placements.contains(&arr2(&[ - [false, false, false], - [false, true, false], - [false, false, true], - ]))); - } - - #[test] - fn test_place_on_all_positions_same_size() { - let parent = arr2(&[[false, false], [false, false]]); - let child = arr2(&[[true, false], [false, true]]); - let placements = place_on_all_positions(&parent, &child); - assert_eq!(placements.len(), 1); - assert!(placements.contains(&arr2(&[[true, false], [false, true],]))); - } - - #[test] - fn test_place_on_all_positions_smaller_parent() { - let parent = arr2(&[[false, false], [false, false]]); - let child = arr2(&[[true, false, true], [false, true, false]]); - let placements = place_on_all_positions(&parent, &child); - assert_eq!(placements.len(), 0); - } - - #[test] - fn test_place_on_all_positions_with_blocking() { - let parent = arr2(&[ - [false, false, false], - [false, true, false], - [false, false, false], - ]); - let child = arr2(&[[true, false], [false, true]]); - let placements = place_on_all_positions(&parent, &child); - assert_eq!(placements.len(), 2); - assert!(placements.contains(&arr2(&[ - [false, true, false], - [false, true, true], - [false, false, false], - ]))); - assert!(placements.contains(&arr2(&[ - [false, false, false], - [true, true, false], - [false, true, false], - ]))); - } - - #[test] - fn test_remove_parent() { - let parent = arr2(&[ - [true, false, true], - [false, true, false], - [true, true, true], - ]); - let mut child = arr2(&[[true, true, true], [true, true, true], [true, true, true]]); - remove_parent(&parent, &mut child); - let expected = arr2(&[ - [false, true, false], - [true, false, true], - [false, false, false], - ]); - assert_eq!(expected, child); - } - - #[test] - fn test_remove_parent_smaller_parent() { - let parent = arr2(&[[true, false], [false, true]]); - let mut child = arr2(&[[true, true, true], [true, true, true], [true, true, true]]); - remove_parent(&parent, &mut child); - let expected = arr2(&[[false, true, true], [true, false, true], [true, true, true]]); - assert_eq!(expected, child); - } - - #[test] - #[should_panic] - fn test_remove_parent_bigger_parent_panic() { - let parent = arr2(&[ - [true, false, true], - [false, true, false], - [true, true, true], - ]); - let mut child = arr2(&[[true, true], [true, true]]); - remove_parent(&parent, &mut child); - } - - #[test] - fn test_count_biggest_connected_area_of_cells_matching() { - let array = arr2(&[ - [true, false, true], - [false, true, false], - [true, true, true], - ]); - let count_true = count_biggest_connected_area_of_cells_matching(&array, true); - let count_false = count_biggest_connected_area_of_cells_matching(&array, false); - assert_eq!(count_true, 4); - assert_eq!(count_false, 1); - } - - #[test] - fn test_tile_rotation_iterator() { - let base = arr2(&[[true, false], [false, false]]); - let mut iter = TileRotationIterator::new(base); - - assert_eq!(iter.next(), Some(arr2(&[[true, false], [false, false]]))); - assert_eq!(iter.next(), Some(arr2(&[[false, true], [false, false]]))); - assert_eq!(iter.next(), Some(arr2(&[[false, false], [false, true]]))); - assert_eq!(iter.next(), Some(arr2(&[[false, false], [true, false]]))); - - assert_eq!(iter.next(), Some(arr2(&[[true, false], [false, false]]))); - assert_eq!(iter.next(), Some(arr2(&[[false, true], [false, false]]))); - assert_eq!(iter.next(), Some(arr2(&[[false, false], [false, true]]))); - assert_eq!(iter.next(), Some(arr2(&[[false, false], [true, false]]))); - assert_eq!(iter.next(), None); - } -} diff --git a/puzzled_common/src/lib.rs b/puzzled_common/src/lib.rs index 12d7a31..20dbfd5 100644 --- a/puzzled_common/src/lib.rs +++ b/puzzled_common/src/lib.rs @@ -1,4 +1,3 @@ -pub mod array_util; pub mod shape; pub use shape::Shape; diff --git a/puzzled_common/src/shape.rs b/puzzled_common/src/shape.rs deleted file mode 100644 index 71d9d72..0000000 --- a/puzzled_common/src/shape.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::ShapeType::Square; -use ndarray::iter::{IndexedIter, Iter}; -use ndarray::{arr2, s, Array2, Axis, Ix2}; -use std::ops::Index; - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct Shape { - shape_type: ShapeType, - data: Array2, -} - -impl Shape { - pub fn new(shape_type: ShapeType, data: Array2) -> Self { - Self { shape_type, data } - } - - pub fn shape_type(&self) -> ShapeType { - self.shape_type - } - - pub fn dim(&self) -> (usize, usize) { - self.data.dim() - } - - pub fn get(&self, index: (usize, usize)) -> Option<&bool> { - self.data.get(index) - } - - pub fn iter(&self) -> Iter<'_, bool, Ix2> { - self.data.iter() - } - - pub fn indexed_iter(&self) -> IndexedIter<'_, bool, Ix2> { - self.data.indexed_iter() - } - - pub fn rotate_clockwise(&mut self) { - match self.shape_type { - ShapeType::Square => { - self.data.reverse_axes(); - self.data.invert_axis(Axis(1)); - } - ShapeType::Triangle => { - todo!() - } - ShapeType::Hexagon => { - todo!() - } - }; - } - - pub fn flip_default(&mut self) { - match self.shape_type { - ShapeType::Square => { - self.data.invert_axis(Axis(0)); - } - ShapeType::Triangle => { - todo!() - } - ShapeType::Hexagon => { - todo!() - } - } - } - - pub fn trim_matching(&mut self, to_trim: bool) -> TrimSides { - let mut array = self.data.clone(); - let mut trim_sides = TrimSides::default(); - loop { - if array.nrows() == 0 || array.ncols() == 0 { - break; - } - - let left_col_all_true = array.column(0).iter().all(|&cell| cell == to_trim); - if left_col_all_true { - array = array.slice(s![.., 1..]).to_owned(); - trim_sides.lower_y += 1; - continue; - } - - let right_col_all_true = array - .column(array.ncols() - 1) - .iter() - .all(|&cell| cell == to_trim); - if right_col_all_true { - array = array.slice(s![.., ..array.ncols() - 1]).to_owned(); - trim_sides.upper_y += 1; - continue; - } - - let top_row_all_true = array.row(0).iter().all(|&cell| cell == to_trim); - if top_row_all_true { - array = array.slice(s![1.., ..]).to_owned(); - trim_sides.lower_x += 1; - continue; - } - - let bottom_row_all_true = array - .row(array.nrows() - 1) - .iter() - .all(|&cell| cell == to_trim); - if bottom_row_all_true { - array = array.slice(s![..array.nrows() - 1, ..]).to_owned(); - trim_sides.upper_x += 1; - continue; - } - - break; - } - trim_sides - } -} - -impl Index<(usize, usize)> for Shape { - type Output = bool; - - fn index(&self, index: (usize, usize)) -> &Self::Output { - &self.data[index] - } -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ShapeType { - #[default] - Square, - Triangle, - Hexagon, -} - -/// Represents the number of rows and columns removed from the sides of a 2D array. -#[derive(Debug, Default, PartialEq, Eq)] -pub struct TrimSides { - /// The number of rows removed from the lower side of the x-axis. - pub lower_x: usize, - /// The number of rows removed from the higher side of the x-axis. - pub upper_x: usize, - /// The number of columns removed from the lower side of the y-axis. - pub lower_y: usize, - /// The number of columns removed from the higher side of the y-axis. - pub upper_y: usize, -} - -pub fn shape_square(data: &[[bool; N]]) -> Shape { - Shape::new(Square, arr2(data)) -} diff --git a/puzzled_common/src/shape/iterators.rs b/puzzled_common/src/shape/iterators.rs new file mode 100644 index 0000000..ff48859 --- /dev/null +++ b/puzzled_common/src/shape/iterators.rs @@ -0,0 +1,47 @@ +use crate::Shape; +use ndarray::iter::{IndexedIter, Iter}; +use ndarray::Ix2; + +impl Shape { + pub fn iter(&self) -> Iter<'_, bool, Ix2> { + self.data.iter() + } + + pub fn indexed_iter(&self) -> IndexedIter<'_, bool, Ix2> { + self.data.indexed_iter() + } + + pub fn rotations_flips_iter(&self) -> TileRotationIterator { + TileRotationIterator::new(self.clone()) + } +} + +pub struct TileRotationIterator { + current: Shape, + iteration: u8, +} + +impl TileRotationIterator { + fn new(tile: Shape) -> Self { + Self { + current: tile, + iteration: 0, + } + } +} + +impl Iterator for TileRotationIterator { + type Item = Shape; + + fn next(&mut self) -> Option { + if self.iteration >= 8 { + return None; + } + if self.iteration == 4 { + self.current.flip_default(); + } + self.current.rotate_clockwise(); + self.iteration += 1; + Some(self.current.clone()) + } +} diff --git a/puzzled_common/src/shape/mod.rs b/puzzled_common/src/shape/mod.rs new file mode 100644 index 0000000..032d9cb --- /dev/null +++ b/puzzled_common/src/shape/mod.rs @@ -0,0 +1,836 @@ +mod iterators; + +use crate::ShapeType::Square; +use ndarray::{arr2, s, Array2, Axis}; +use std::fmt::{Display, Formatter}; +use std::ops::{Index, IndexMut}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct Shape { + shape_type: ShapeType, + data: Array2, +} + +impl Shape { + pub fn new(shape_type: ShapeType, data: Array2) -> Self { + Self { shape_type, data } + } + + pub fn from_elem((x, y): (usize, usize), shape_type: ShapeType, value: bool) -> Self { + Self { + shape_type, + data: Array2::from_elem((x, y), value), + } + } + + pub fn shape_type(&self) -> ShapeType { + self.shape_type + } + + pub fn dim(&self) -> (usize, usize) { + self.data.dim() + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn get(&self, index: (usize, usize)) -> Option<&bool> { + self.data.get(index) + } + + pub fn map(&self, f: F) -> Self + where + F: FnMut(&bool) -> bool, + { + Shape { + shape_type: self.shape_type, + data: self.data.map(f), + } + } + + pub fn fill(&mut self, value: bool) { + self.data.fill(value); + } + + pub fn rotate_clockwise(&mut self) { + match self.shape_type { + ShapeType::Square => { + self.data.reverse_axes(); + self.data.invert_axis(Axis(1)); + } + ShapeType::Triangle => { + todo!() + } + ShapeType::Hexagon => { + todo!() + } + }; + } + + pub fn rotate_to_landscape(&mut self) { + let dim = self.dim(); + if dim.0 < dim.1 { + self.data.reverse_axes(); + } + } + + pub fn flip_default(&mut self) { + match self.shape_type { + ShapeType::Square => { + self.data.invert_axis(Axis(0)); + } + ShapeType::Triangle => { + todo!() + } + ShapeType::Hexagon => { + todo!() + } + } + } + + pub fn transpose(&mut self) { + self.data.reverse_axes(); + } + + pub fn transposed(&self) -> Self { + Self { + shape_type: self.shape_type, + data: self.data.t().into_owned(), + } + } + + /// Removes rows and columns from the sides of a 2D boolean array where all cells are matching` + /// the given value. + /// + /// # Arguments + /// + /// * `to_trim`: The values to trim. + /// + /// returns: () + pub fn trim_matching(&mut self, to_trim: bool) -> TrimSides { + let mut array = self.data.clone(); + let mut trim_sides = TrimSides::default(); + loop { + if array.nrows() == 0 || array.ncols() == 0 { + break; + } + + let left_col_all_true = array.column(0).iter().all(|&cell| cell == to_trim); + if left_col_all_true { + array = array.slice(s![.., 1..]).to_owned(); + trim_sides.lower_y += 1; + continue; + } + + let right_col_all_true = array + .column(array.ncols() - 1) + .iter() + .all(|&cell| cell == to_trim); + if right_col_all_true { + array = array.slice(s![.., ..array.ncols() - 1]).to_owned(); + trim_sides.upper_y += 1; + continue; + } + + let top_row_all_true = array.row(0).iter().all(|&cell| cell == to_trim); + if top_row_all_true { + array = array.slice(s![1.., ..]).to_owned(); + trim_sides.lower_x += 1; + continue; + } + + let bottom_row_all_true = array + .row(array.nrows() - 1) + .iter() + .all(|&cell| cell == to_trim); + if bottom_row_all_true { + array = array.slice(s![..array.nrows() - 1, ..]).to_owned(); + trim_sides.upper_x += 1; + continue; + } + + break; + } + trim_sides + } + + pub fn count_biggest_connected_area_of_cells_matching(&self, target_value: bool) -> usize { + let mut visited = Array2::from_elem(self.dim(), false); + let mut max_area = 0; + + for ((x, y), value) in self.indexed_iter() { + if *value == target_value && !visited[[x, y]] { + let mut area = 0; + let mut stack = vec![(x, y)]; + + while let Some((cx, cy)) = stack.pop() { + if cx < self.data.nrows() + && cy < self.data.ncols() + && !visited[[cx, cy]] + && self[(cx, cy)] == target_value + { + visited[[cx, cy]] = true; + area += 1; + + // Add neighbors to the stack + if cx > 0 { + stack.push((cx - 1, cy)); + } + if cx < self.data.nrows() - 1 { + stack.push((cx + 1, cy)); + } + if cy > 0 { + stack.push((cx, cy - 1)); + } + if cy < self.data.ncols() - 1 { + stack.push((cx, cy + 1)); + } + } + } + + max_area = max_area.max(area); + } + } + + max_area + } + + /// Places the `child` array onto `self` at the specified offsets using a logical OR + /// operation. + /// This means that if either the parent or child cell is `true`, the resulting cell will be + /// `true`. + /// + /// # Arguments + /// + /// * `child`: The child shape to be placed onto the parent. + /// * `x_offset`: The x-axis offset for placing the child. + /// * `y_offset`: The y-axis offset for placing the child. + /// + /// returns: Shape + pub fn or_arrays_at(&self, child: &Shape, x_offset: isize, y_offset: isize) -> Shape { + let mut new_array = self.clone(); + let child_xs = child.data.nrows(); + let child_ys = child.data.ncols(); + + for x in 0..child_xs { + for y in 0..child_ys { + let parent_x = x as isize + x_offset; + let parent_y = y as isize + y_offset; + if parent_x >= 0 + && parent_x < self.data.nrows() as isize + && parent_y >= 0 + && parent_y < self.data.ncols() as isize + { + new_array[(parent_x as usize, parent_y as usize)] |= child[(x, y)]; + } + } + } + + new_array + } + + /// Generates all possible placements of the `child` array onto self using a logical OR + /// operation. + /// + /// # Arguments + /// + /// * `child`: The child shape to be placed onto self. + /// + /// returns: Vec + pub fn place_on_all_positions(&self, child: &Shape) -> Vec { + let mut placements = Vec::new(); + let parent_rows = self.data.nrows(); + let parent_cols = self.data.ncols(); + let child_rows = child.data.nrows(); + let child_cols = child.data.ncols(); + + if child_rows > parent_rows || child_cols > parent_cols { + return placements; + } + + for row_offset in 0..=(parent_rows - child_rows) { + for col_offset in 0..=(parent_cols - child_cols) { + let mut new_array = self.clone(); + let mut valid = true; + for r in 0..child_rows { + for c in 0..child_cols { + if child[(r, c)] && self[(row_offset + r, col_offset + c)] { + valid = false; + break; + } + new_array[(row_offset + r, col_offset + c)] |= child[(r, c)]; + } + if !valid { + break; + } + } + if valid { + placements.push(new_array); + } + } + } + + placements + } + + /// Removes the `true` values from `self` wherever parent has `true` values. + /// + /// # Arguments + /// + /// * `parent`: The mutable reference to the parent shape to be removed from self. + /// + /// returns: () + pub fn remove_parent(&mut self, parent: &Shape) { + for row in 0..parent.data.nrows() { + for col in 0..parent.data.ncols() { + if self[(row, col)] { + self[(row, col)] = false; + } + } + } + } + + /// Prints a 2D boolean array to the debug log, using '#' for `true` and '-' for `false`. + #[allow(dead_code)] + pub fn debug_print(&self) { + if cfg!(debug_assertions) { + for row in self.data.rows() { + let row_str: String = row + .iter() + .map(|&cell| if cell { '#' } else { '-' }) + .collect(); + println!("{}", row_str); + } + } + } +} + +impl Index<(usize, usize)> for Shape { + type Output = bool; + + fn index(&self, index: (usize, usize)) -> &Self::Output { + &self.data[index] + } +} + +impl IndexMut<(usize, usize)> for Shape { + fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output { + &mut self.data[index] + } +} + +impl Display for Shape { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.data.fmt(f) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ShapeType { + #[default] + Square, + Triangle, + Hexagon, +} + +/// Represents the number of rows and columns removed from the sides of a 2D array. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct TrimSides { + /// The number of rows removed from the lower side of the x-axis. + pub lower_x: usize, + /// The number of rows removed from the higher side of the x-axis. + pub upper_x: usize, + /// The number of columns removed from the lower side of the y-axis. + pub lower_y: usize, + /// The number of columns removed from the higher side of the y-axis. + pub upper_y: usize, +} + +pub fn shape_square(data: &[[bool; N]]) -> Shape { + Shape::new(Square, arr2(data)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_rotate_clockwise_square_size_1() { + let mut shape = shape_square(&[[true]]); + shape.rotate_clockwise(); + let expected = shape_square(&[[true]]); + assert_eq!(expected, shape); + } + + #[test] + fn test_rotate_clockwise_square_size_2() { + let mut shape = shape_square(&[[true, false]]); + shape.rotate_clockwise(); + let expected = shape_square(&[[true], [false]]); + assert_eq!(expected, shape); + } + + #[test] + fn test_rotate_clockwise_square() { + let mut shape = shape_square(&[ + [true, false, false], + [true, true, true], + [true, false, true], + ]); + shape.rotate_clockwise(); + let expected = shape_square(&[ + [true, true, true], + [false, true, false], + [true, true, false], + ]); + assert_eq!(expected, shape); + } + // + // #[test] + // fn test_remove_true_rows_cols_from_sides_empty() { + // let mut array: Array2 = Array2::default((0, 0)); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected: Array2 = Array2::default((0, 0)); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 0, + // upper_y: 0, + // upper_x: 0, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_sides_true() { + // let mut array = shape_square(&[[true]]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected: Array2 = shape_square(&[[]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 1, + // upper_y: 0, + // upper_x: 0, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_sides_false() { + // let mut array = shape_square(&[[false]]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected: Array2 = shape_square(&[[false]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 0, + // upper_y: 0, + // upper_x: 0, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_sides_lower_y_upper_y() { + // let mut array = shape_square(&[ + // [true, true, false, true], + // [true, false, false, true], + // [true, true, false, true], + // ]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[[true, false], [false, false], [true, false]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 1, + // upper_y: 1, + // upper_x: 0, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_sides_lower_x_upper_x() { + // let mut array = shape_square(&[ + // [true, true, true, true], + // [false, true, false, false], + // [true, true, true, true], + // ]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[[false, true, false, false]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 1, + // lower_y: 0, + // upper_y: 0, + // upper_x: 1, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_sides_all_sides() { + // let mut array = shape_square(&[ + // [true, true, true, true, true], + // [true, true, false, false, true], + // [true, false, true, false, true], + // [true, true, true, true, true], + // ]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[[true, false, false], [false, true, false]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 1, + // lower_y: 1, + // upper_y: 1, + // upper_x: 1, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_lower_y() { + // let mut array = shape_square(&[[true, true, false, false], [true, false, true, false]]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[[true, false, false], [false, true, false]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 1, + // upper_y: 0, + // upper_x: 0, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_upper_y() { + // let mut array = shape_square(&[[false, false, true, true], [false, true, false, true]]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[[false, false, true], [false, true, false]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 0, + // upper_y: 1, + // upper_x: 0, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_lower_x() { + // let mut array = shape_square(&[ + // [true, true, true], + // [false, true, false], + // [true, false, true], + // ]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[[false, true, false], [true, false, true]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 1, + // lower_y: 0, + // upper_y: 0, + // upper_x: 0, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_from_upper_x() { + // let mut array = shape_square(&[ + // [false, true, false], + // [true, false, true], + // [true, true, true], + // ]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[[false, true, false], [true, false, true]]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 0, + // upper_y: 0, + // upper_x: 1, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_remove_true_rows_cols_test() { + // let mut array = shape_square(&[ + // [false, false, false, false], + // [false, false, false, false], + // [true, true, true, true], + // [false, true, false, true], + // [true, true, true, true], + // ]); + // let trim_sides = remove_true_rows_cols_from_sides(&mut array); + // let expected = shape_square(&[ + // [false, false, false, false], + // [false, false, false, false], + // [true, true, true, true], + // [false, true, false, true], + // ]); + // assert_eq!(expected, array); + // let expected_trim_sides = TrimSides { + // lower_x: 0, + // lower_y: 0, + // upper_y: 0, + // upper_x: 1, + // }; + // assert_eq!(expected_trim_sides, trim_sides); + // } + // + // #[test] + // fn test_or_arrays_at() { + // let parent = shape_square(&[ + // [false, false, false], + // [false, false, false], + // [false, false, false], + // ]); + // let child = shape_square(&[[true, false], [false, true]]); + // let result = or_arrays_at(&parent, &child, 1, 1); + // let expected = shape_square(&[ + // [false, false, false], + // [false, true, false], + // [false, false, true], + // ]); + // assert_eq!(expected, result); + // } + // + // #[test] + // fn test_or_arrays_at_empty_child() { + // let parent = shape_square(&[ + // [false, false, false], + // [false, false, false], + // [false, false, false], + // ]); + // let child = shape_square(&[[]]); + // let result = or_arrays_at(&parent, &child, 1, 1); + // let expected = shape_square(&[ + // [false, false, false], + // [false, false, false], + // [false, false, false], + // ]); + // assert_eq!(expected, result); + // } + // + // #[test] + // fn test_or_arrays_at_child_1x1() { + // let parent = shape_square(&[ + // [false, false, false], + // [false, false, false], + // [false, false, false], + // ]); + // let child = shape_square(&[[true]]); + // let result = or_arrays_at(&parent, &child, 1, 1); + // let expected = shape_square(&[ + // [false, false, false], + // [false, true, false], + // [false, false, false], + // ]); + // assert_eq!(expected, result); + // } + // + // #[test] + // fn test_or_arrays_at_child_off_parent() { + // let parent = shape_square(&[ + // [false, false, false], + // [false, false, false], + // [false, false, false], + // ]); + // let child = shape_square(&[[true, true], [true, true]]); + // let result = or_arrays_at(&parent, &child, 2, 2); + // let expected = shape_square(&[ + // [false, false, false], + // [false, false, false], + // [false, false, true], + // ]); + // assert_eq!(expected, result); + // } + // + // #[test] + // fn test_or_arrays_at_true_parent() { + // let parent = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + // let child = shape_square(&[[true, false], [true, false]]); + // let result = or_arrays_at(&parent, &child, 1, 1); + // let expected = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + // assert_eq!(expected, result); + // } + // + // #[test] + // fn test_or_arrays_at_smaller_parent() { + // let parent = shape_square(&[[false, false], [false, false]]); + // let child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + // let result = or_arrays_at(&parent, &child, 0, 0); + // let expected = shape_square(&[[true, true], [true, true]]); + // assert_eq!(expected, result); + // } + // + // #[test] + // fn test_place_on_all_positions() { + // let parent = shape_square(&[ + // [false, false, false], + // [false, false, false], + // [false, false, false], + // ]); + // let child = shape_square(&[[true, false], [false, true]]); + // let placements = place_on_all_positions(&parent, &child); + // assert_eq!(placements.len(), 4); + // assert!(placements.contains(&shape_square(&[ + // [true, false, false], + // [false, true, false], + // [false, false, false], + // ]))); + // assert!(placements.contains(&shape_square(&[ + // [false, true, false], + // [false, false, true], + // [false, false, false], + // ]))); + // assert!(placements.contains(&shape_square(&[ + // [false, false, false], + // [true, false, false], + // [false, true, false], + // ]))); + // assert!(placements.contains(&shape_square(&[ + // [false, false, false], + // [false, true, false], + // [false, false, true], + // ]))); + // } + // + // #[test] + // fn test_place_on_all_positions_same_size() { + // let parent = shape_square(&[[false, false], [false, false]]); + // let child = shape_square(&[[true, false], [false, true]]); + // let placements = place_on_all_positions(&parent, &child); + // assert_eq!(placements.len(), 1); + // assert!(placements.contains(&shape_square(&[[true, false], [false, true],]))); + // } + // + // #[test] + // fn test_place_on_all_positions_smaller_parent() { + // let parent = shape_square(&[[false, false], [false, false]]); + // let child = shape_square(&[[true, false, true], [false, true, false]]); + // let placements = place_on_all_positions(&parent, &child); + // assert_eq!(placements.len(), 0); + // } + // + // #[test] + // fn test_place_on_all_positions_with_blocking() { + // let parent = shape_square(&[ + // [false, false, false], + // [false, true, false], + // [false, false, false], + // ]); + // let child = shape_square(&[[true, false], [false, true]]); + // let placements = place_on_all_positions(&parent, &child); + // assert_eq!(placements.len(), 2); + // assert!(placements.contains(&shape_square(&[ + // [false, true, false], + // [false, true, true], + // [false, false, false], + // ]))); + // assert!(placements.contains(&shape_square(&[ + // [false, false, false], + // [true, true, false], + // [false, true, false], + // ]))); + // } + // + // #[test] + // fn test_remove_parent() { + // let parent = shape_square(&[ + // [true, false, true], + // [false, true, false], + // [true, true, true], + // ]); + // let mut child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + // remove_parent(&parent, &mut child); + // let expected = shape_square(&[ + // [false, true, false], + // [true, false, true], + // [false, false, false], + // ]); + // assert_eq!(expected, child); + // } + // + // #[test] + // fn test_remove_parent_smaller_parent() { + // let parent = shape_square(&[[true, false], [false, true]]); + // let mut child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + // remove_parent(&parent, &mut child); + // let expected = + // shape_square(&[[false, true, true], [true, false, true], [true, true, true]]); + // assert_eq!(expected, child); + // } + // + // #[test] + // #[should_panic] + // fn test_remove_parent_bigger_parent_panic() { + // let parent = shape_square(&[ + // [true, false, true], + // [false, true, false], + // [true, true, true], + // ]); + // let mut child = shape_square(&[[true, true], [true, true]]); + // remove_parent(&parent, &mut child); + // } + // + // #[test] + // fn test_count_biggest_connected_area_of_cells_matching() { + // let array = shape_square(&[ + // [true, false, true], + // [false, true, false], + // [true, true, true], + // ]); + // let count_true = count_biggest_connected_area_of_cells_matching(&array, true); + // let count_false = count_biggest_connected_area_of_cells_matching(&array, false); + // assert_eq!(count_true, 4); + // assert_eq!(count_false, 1); + // } + // + // #[test] + // fn test_tile_rotation_iterator() { + // let base = shape_square(&[[true, false], [false, false]]); + // let mut iter = TileRotationIterator::new(base); + // + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[true, false], [false, false]])) + // ); + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[false, true], [false, false]])) + // ); + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[false, false], [false, true]])) + // ); + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[false, false], [true, false]])) + // ); + // + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[true, false], [false, false]])) + // ); + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[false, true], [false, false]])) + // ); + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[false, false], [false, true]])) + // ); + // assert_eq!( + // iter.next(), + // Some(shape_square(&[[false, false], [true, false]])) + // ); + // assert_eq!(iter.next(), None); + // } +} From f005faa8b78ce3abe3f1e9208534bfe2e0bc1110 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Sun, 12 Apr 2026 16:38:02 +0200 Subject: [PATCH 4/9] Introduce Shape data structure to be able to represent tiles and boards that are not made out of squares Fixed tests TODO - clean up --- puzzle_solver/src/backtracking/mod.rs | 6 +- puzzle_solver/src/backtracking/positioned.rs | 4 +- .../src/backtracking/pruner/banned.rs | 14 +- puzzle_solver/src/board.rs | 19 +- puzzle_solver/src/lib.rs | 8 +- puzzle_solver/src/plausibility.rs | 2 +- puzzle_solver/src/tile.rs | 8 +- puzzled/src/solver/mod.rs | 2 +- puzzled_common/src/shape/iterators.rs | 5 +- puzzled_common/src/shape/mod.rs | 973 +++++++++--------- 10 files changed, 547 insertions(+), 494 deletions(-) diff --git a/puzzle_solver/src/backtracking/mod.rs b/puzzle_solver/src/backtracking/mod.rs index 20def7a..03ca27a 100644 --- a/puzzle_solver/src/backtracking/mod.rs +++ b/puzzle_solver/src/backtracking/mod.rs @@ -23,7 +23,7 @@ pub async fn solve_all_filling( let pruner = Pruner::new_for_filling(&board, &tiles); - let board_bitmask = Bitmask::from(board.get_array()); + let board_bitmask = Bitmask::from(board.get_shape()); let positioned_tiles: Vec = tiles .iter() .map(|tile| PositionedTile::new(tile, &board, &pruner)) @@ -39,7 +39,7 @@ pub async fn solve_all_filling( } let result = core::solve_filling( - board.get_array().dim().0 as i32, + board.get_shape().dim().0 as i32, &board_bitmask, &positioned_tiles, pruner, @@ -93,7 +93,7 @@ fn create_tile_placement( ) -> TilePlacement { let bitmask_placement = &positioned_tile.bitmasks()[placement_index]; let placement_board = - bitmask_placement.to_shape(board.get_array().dim().0, board.get_array().dim().1, Square); + bitmask_placement.to_shape(board.get_shape().dim().0, board.get_shape().dim().1, Square); let mut inverted_placement = placement_board.map(|v| !v); inverted_placement.trim_matching(true); let rotation = inverted_placement.map(|v| !v); diff --git a/puzzle_solver/src/backtracking/positioned.rs b/puzzle_solver/src/backtracking/positioned.rs index 564930a..4243aec 100644 --- a/puzzle_solver/src/backtracking/positioned.rs +++ b/puzzle_solver/src/backtracking/positioned.rs @@ -31,10 +31,10 @@ impl PositionedTile { let all_placements: Vec = tile .all_rotations .iter() - .flat_map(|rotation| board.get_array().place_on_all_positions(rotation)) + .flat_map(|rotation| board.get_shape().place_on_all_positions(rotation)) .map(|array| { let mut array = array.clone(); - array.remove_parent(board.get_array()); + array.remove_parent(board.get_shape()); array }) .collect(); diff --git a/puzzle_solver/src/backtracking/pruner/banned.rs b/puzzle_solver/src/backtracking/pruner/banned.rs index 5da046a..c82154a 100644 --- a/puzzle_solver/src/backtracking/pruner/banned.rs +++ b/puzzle_solver/src/backtracking/pruner/banned.rs @@ -30,11 +30,11 @@ pub fn create_banned_bitmasks_for_filling( .min() .unwrap_or(0); - let mut banned_bitmasks = Vec::with_capacity(board.get_array().len()); - for _ in 0..board.get_array().len() { + let mut banned_bitmasks = Vec::with_capacity(board.get_shape().len()); + for _ in 0..board.get_shape().len() { banned_bitmasks.push(Vec::with_capacity(0)); } - let (xs, ys) = board.get_array().dim(); + let (xs, ys) = board.get_shape().dim(); for x in 0..ys { for y in 0..xs { if !board[[y, x]] { @@ -281,7 +281,7 @@ fn create_banned_bitmask_for_pattern_at_if_possible( if area[(px, py)] && !pattern[(px, py)] && *board - .get_array() + .get_shape() .get((board_x as usize, board_y as usize)) .unwrap_or(&true) { @@ -313,13 +313,13 @@ fn create_banned_bitmask_for_pattern_at( y: isize, board: &Board, ) -> BannedBitmask { - let mut board_array = board.get_array().clone(); + let mut board_array = board.get_shape().clone(); board_array.fill(false); - let pattern_board = board_array.or_arrays_at(pattern, x, y); + let pattern_board = board_array.or_at(pattern, x, y); let pattern_bitmask = Bitmask::from(&pattern_board); - let area_board = board_array.or_arrays_at(area, x, y); + let area_board = board_array.or_at(area, x, y); let area_bitmask = Bitmask::from(&area_board); BannedBitmask { diff --git a/puzzle_solver/src/board.rs b/puzzle_solver/src/board.rs index f739e21..86bb04b 100644 --- a/puzzle_solver/src/board.rs +++ b/puzzle_solver/src/board.rs @@ -37,8 +37,8 @@ impl Board { /// use puzzle_solver::board::Board; /// /// let board = Board::new((3, 4)); - /// assert_eq!(board.get_array().shape(), &[3, 4]); - /// assert!(board.get_array().iter().all(|&b| b == false)); + /// assert_eq!(board.get_shape().dim(), (3, 4)); + /// assert!(board.get_shape().iter().all(|&b| b == false)); /// ``` pub fn new(dims: (usize, usize)) -> Self { Board(Shape::from_elem(dims, Square, false)) @@ -56,12 +56,13 @@ impl Board { /// /// ```rust /// use puzzle_solver::board::Board; - /// use ndarray::Array2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType::Square; /// /// let board = Board::new((3, 4)); - /// assert_eq!(board.get_array(), Array2::default((3, 4))); + /// assert_eq!(board.get_shape(), &Shape::from_elem((3, 4), Square, false)); /// ``` - pub fn get_array(&self) -> &Shape { + pub fn get_shape(&self) -> &Shape { &self.0 } @@ -108,14 +109,14 @@ mod tests { #[test] fn test_new_0_0() { let board = Board::new((0, 0)); - assert_eq!(board.get_array().dim(), (0, 0)); + assert_eq!(board.get_shape().dim(), (0, 0)); } #[test] fn test_new_3_4() { let board = Board::new((3, 4)); - assert_eq!(board.get_array().dim(), (3, 4)); - assert!(board.get_array().iter().all(|&b| b == false)); + assert_eq!(board.get_shape().dim(), (3, 4)); + assert!(board.get_shape().iter().all(|&b| b == false)); } #[test] @@ -137,7 +138,7 @@ mod tests { board.trim(); - assert_eq!(board.get_array().dim(), (3, 3)); + assert_eq!(board.get_shape().dim(), (3, 3)); assert_eq!(board[[0, 0]], false); assert_eq!(board[[0, 1]], false); assert_eq!(board[[0, 2]], false); diff --git a/puzzle_solver/src/lib.rs b/puzzle_solver/src/lib.rs index 6cc9e0f..983c0fa 100644 --- a/puzzle_solver/src/lib.rs +++ b/puzzle_solver/src/lib.rs @@ -34,17 +34,17 @@ pub mod tile; /// # Examples /// /// ``` -/// use ndarray::arr2; /// use puzzle_solver::board::Board; /// use puzzle_solver::tile::Tile; /// use puzzle_solver::solve_all_filling; /// use tokio_util::sync::CancellationToken; +/// use puzzled_common::shape::shape_square; /// /// let mut board = Board::new((3, 4)); /// board[[0, 0]] = true; /// let tiles = vec![ -/// Tile::new(arr2(&[[true, true, true], [true, true, true]])), -/// Tile::new(arr2(&[[true, true, true], [true, true, false]])), +/// Tile::new(shape_square(&[[true, true, true], [true, true, true]])), +/// Tile::new(shape_square(&[[true, true, true], [true, true, false]])), /// ]; /// let cancel_token = CancellationToken::new(); /// @@ -64,7 +64,7 @@ pub async fn solve_all_filling( let mut board = board; let trim_sides = board.trim(); - if board.get_array().iter().filter(|c| !*c).count() > Bitmask::max_bits() { + if board.get_shape().iter().filter(|c| !*c).count() > Bitmask::max_bits() { debug!("Board too large for bitmask representation."); return Err(UnsolvableReason::BoardTooLarge); } diff --git a/puzzle_solver/src/plausibility.rs b/puzzle_solver/src/plausibility.rs index 9179143..817cdee 100644 --- a/puzzle_solver/src/plausibility.rs +++ b/puzzle_solver/src/plausibility.rs @@ -17,7 +17,7 @@ use log::debug; /// /// returns: bool pub(crate) fn check(board: &Board, tiles: &[Tile]) -> bool { - let board_area = board.get_array().iter().filter(|&&cell| !cell).count(); + let board_area = board.get_shape().iter().filter(|&&cell| !cell).count(); let tiles_area: usize = tiles .iter() .map(|tile| tile.base.iter().filter(|&&cell| cell).count()) diff --git a/puzzle_solver/src/tile.rs b/puzzle_solver/src/tile.rs index 32caed7..85ed14c 100644 --- a/puzzle_solver/src/tile.rs +++ b/puzzle_solver/src/tile.rs @@ -30,9 +30,9 @@ impl Tile { /// /// ```rust /// use puzzle_solver::tile::Tile; - /// use ndarray::arr2; + /// use puzzled_common::shape::shape_square; /// - /// let base = arr2(&[[true, false], [true, true]]); + /// let base = shape_square(&[[true, false], [true, true]]); /// let tile = Tile::new(base); /// ``` pub fn new(base: Shape) -> Tile { @@ -60,9 +60,9 @@ impl Tile { /// /// ```rust /// use puzzle_solver::tile::Tile; - /// use ndarray::arr2; + /// use puzzled_common::shape::shape_square; /// - /// let base = arr2(&[[true, false], [true, true]]); + /// let base = shape_square(&[[true, false], [true, true]]); /// let tile = Tile::new(base.clone()); /// assert_eq!(tile.base(), &base); /// ``` diff --git a/puzzled/src/solver/mod.rs b/puzzled/src/solver/mod.rs index 4d7d46e..e1c90ae 100644 --- a/puzzled/src/solver/mod.rs +++ b/puzzled/src/solver/mod.rs @@ -157,7 +157,7 @@ impl Solver { /// returns: bool pub fn is_solved(&self, puzzle_state: &PuzzleState) -> bool { let board = self.create_board(puzzle_state); - board.get_array().iter().all(|cell| *cell) + board.get_shape().iter().all(|cell| *cell) } /// Creates a board representation from the given puzzle state and target to give to the solver. diff --git a/puzzled_common/src/shape/iterators.rs b/puzzled_common/src/shape/iterators.rs index ff48859..5623cf7 100644 --- a/puzzled_common/src/shape/iterators.rs +++ b/puzzled_common/src/shape/iterators.rs @@ -38,10 +38,11 @@ impl Iterator for TileRotationIterator { return None; } if self.iteration == 4 { - self.current.flip_default(); + self.current.transpose(); } + let current = self.current.clone(); self.current.rotate_clockwise(); self.iteration += 1; - Some(self.current.clone()) + Some(current) } } diff --git a/puzzled_common/src/shape/mod.rs b/puzzled_common/src/shape/mod.rs index 032d9cb..d1aa692 100644 --- a/puzzled_common/src/shape/mod.rs +++ b/puzzled_common/src/shape/mod.rs @@ -12,10 +12,60 @@ pub struct Shape { } impl Shape { + /// Creates a new `Shape` instance with the specified `shape_type` and 2D boolean array `data`. + /// + /// # Arguments + /// + /// * `shape_type`: the shape type. + /// * `data`: the data defining the shape + /// + /// returns: Shape + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let shape = Shape::new(ShapeType::Square, arr2(&[[true, false], [false, true]])); + /// + /// assert_eq!(shape.shape_type(), ShapeType::Square); + /// assert_eq!(shape.dim(), (2, 2)); + /// assert_eq!(shape.get((0, 0)), Some(&true)); + /// assert_eq!(shape.get((0, 1)), Some(&false)); + /// assert_eq!(shape.get((1, 0)), Some(&false)); + /// assert_eq!(shape.get((1, 1)), Some(&true)); + /// ``` pub fn new(shape_type: ShapeType, data: Array2) -> Self { Self { shape_type, data } } + /// Creates a new `Shape` instance with the specified dimensions, shape type, and initial value + /// for all cells. + /// + /// # Arguments + /// + /// * `(x, y)`: the dimensions of the shape + /// * `shape_type`: the shape type for the new shape + /// * `value`: the value to set all cells to + /// + /// returns: Shape + /// + /// # Examples + /// + /// ``` + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + ///let shape = Shape::from_elem((1, 2), ShapeType::Square, true); + /// + /// assert_eq!(shape.shape_type(), ShapeType::Square); + /// assert_eq!(shape.dim(), (1, 2)); + /// // FIXME remove some + /// assert_eq!(shape.get((0, 0)), Some(&true)); + /// assert_eq!(shape.get((0, 1)), Some(&true)); + /// ``` pub fn from_elem((x, y): (usize, usize), shape_type: ShapeType, value: bool) -> Self { Self { shape_type, @@ -109,43 +159,44 @@ impl Shape { /// /// returns: () pub fn trim_matching(&mut self, to_trim: bool) -> TrimSides { - let mut array = self.data.clone(); let mut trim_sides = TrimSides::default(); loop { - if array.nrows() == 0 || array.ncols() == 0 { + if self.data.nrows() == 0 || self.data.ncols() == 0 { break; } - let left_col_all_true = array.column(0).iter().all(|&cell| cell == to_trim); + let left_col_all_true = self.data.column(0).iter().all(|&cell| cell == to_trim); if left_col_all_true { - array = array.slice(s![.., 1..]).to_owned(); + self.data = self.data.slice(s![.., 1..]).to_owned(); trim_sides.lower_y += 1; continue; } - let right_col_all_true = array - .column(array.ncols() - 1) + let right_col_all_true = self + .data + .column(self.data.ncols() - 1) .iter() .all(|&cell| cell == to_trim); if right_col_all_true { - array = array.slice(s![.., ..array.ncols() - 1]).to_owned(); + self.data = self.data.slice(s![.., ..self.data.ncols() - 1]).to_owned(); trim_sides.upper_y += 1; continue; } - let top_row_all_true = array.row(0).iter().all(|&cell| cell == to_trim); + let top_row_all_true = self.data.row(0).iter().all(|&cell| cell == to_trim); if top_row_all_true { - array = array.slice(s![1.., ..]).to_owned(); + self.data = self.data.slice(s![1.., ..]).to_owned(); trim_sides.lower_x += 1; continue; } - let bottom_row_all_true = array - .row(array.nrows() - 1) + let bottom_row_all_true = self + .data + .row(self.data.nrows() - 1) .iter() .all(|&cell| cell == to_trim); if bottom_row_all_true { - array = array.slice(s![..array.nrows() - 1, ..]).to_owned(); + self.data = self.data.slice(s![..self.data.nrows() - 1, ..]).to_owned(); trim_sides.upper_x += 1; continue; } @@ -208,7 +259,7 @@ impl Shape { /// * `y_offset`: The y-axis offset for placing the child. /// /// returns: Shape - pub fn or_arrays_at(&self, child: &Shape, x_offset: isize, y_offset: isize) -> Shape { + pub fn or_at(&self, child: &Shape, x_offset: isize, y_offset: isize) -> Shape { let mut new_array = self.clone(); let child_xs = child.data.nrows(); let child_ys = child.data.ncols(); @@ -284,7 +335,7 @@ impl Shape { pub fn remove_parent(&mut self, parent: &Shape) { for row in 0..parent.data.nrows() { for col in 0..parent.data.ncols() { - if self[(row, col)] { + if parent[(row, col)] { self[(row, col)] = false; } } @@ -386,451 +437,451 @@ mod test { ]); assert_eq!(expected, shape); } - // - // #[test] - // fn test_remove_true_rows_cols_from_sides_empty() { - // let mut array: Array2 = Array2::default((0, 0)); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected: Array2 = Array2::default((0, 0)); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 0, - // upper_y: 0, - // upper_x: 0, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_sides_true() { - // let mut array = shape_square(&[[true]]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected: Array2 = shape_square(&[[]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 1, - // upper_y: 0, - // upper_x: 0, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_sides_false() { - // let mut array = shape_square(&[[false]]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected: Array2 = shape_square(&[[false]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 0, - // upper_y: 0, - // upper_x: 0, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_sides_lower_y_upper_y() { - // let mut array = shape_square(&[ - // [true, true, false, true], - // [true, false, false, true], - // [true, true, false, true], - // ]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[[true, false], [false, false], [true, false]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 1, - // upper_y: 1, - // upper_x: 0, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_sides_lower_x_upper_x() { - // let mut array = shape_square(&[ - // [true, true, true, true], - // [false, true, false, false], - // [true, true, true, true], - // ]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[[false, true, false, false]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 1, - // lower_y: 0, - // upper_y: 0, - // upper_x: 1, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_sides_all_sides() { - // let mut array = shape_square(&[ - // [true, true, true, true, true], - // [true, true, false, false, true], - // [true, false, true, false, true], - // [true, true, true, true, true], - // ]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[[true, false, false], [false, true, false]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 1, - // lower_y: 1, - // upper_y: 1, - // upper_x: 1, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_lower_y() { - // let mut array = shape_square(&[[true, true, false, false], [true, false, true, false]]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[[true, false, false], [false, true, false]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 1, - // upper_y: 0, - // upper_x: 0, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_upper_y() { - // let mut array = shape_square(&[[false, false, true, true], [false, true, false, true]]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[[false, false, true], [false, true, false]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 0, - // upper_y: 1, - // upper_x: 0, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_lower_x() { - // let mut array = shape_square(&[ - // [true, true, true], - // [false, true, false], - // [true, false, true], - // ]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[[false, true, false], [true, false, true]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 1, - // lower_y: 0, - // upper_y: 0, - // upper_x: 0, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_from_upper_x() { - // let mut array = shape_square(&[ - // [false, true, false], - // [true, false, true], - // [true, true, true], - // ]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[[false, true, false], [true, false, true]]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 0, - // upper_y: 0, - // upper_x: 1, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_remove_true_rows_cols_test() { - // let mut array = shape_square(&[ - // [false, false, false, false], - // [false, false, false, false], - // [true, true, true, true], - // [false, true, false, true], - // [true, true, true, true], - // ]); - // let trim_sides = remove_true_rows_cols_from_sides(&mut array); - // let expected = shape_square(&[ - // [false, false, false, false], - // [false, false, false, false], - // [true, true, true, true], - // [false, true, false, true], - // ]); - // assert_eq!(expected, array); - // let expected_trim_sides = TrimSides { - // lower_x: 0, - // lower_y: 0, - // upper_y: 0, - // upper_x: 1, - // }; - // assert_eq!(expected_trim_sides, trim_sides); - // } - // - // #[test] - // fn test_or_arrays_at() { - // let parent = shape_square(&[ - // [false, false, false], - // [false, false, false], - // [false, false, false], - // ]); - // let child = shape_square(&[[true, false], [false, true]]); - // let result = or_arrays_at(&parent, &child, 1, 1); - // let expected = shape_square(&[ - // [false, false, false], - // [false, true, false], - // [false, false, true], - // ]); - // assert_eq!(expected, result); - // } - // - // #[test] - // fn test_or_arrays_at_empty_child() { - // let parent = shape_square(&[ - // [false, false, false], - // [false, false, false], - // [false, false, false], - // ]); - // let child = shape_square(&[[]]); - // let result = or_arrays_at(&parent, &child, 1, 1); - // let expected = shape_square(&[ - // [false, false, false], - // [false, false, false], - // [false, false, false], - // ]); - // assert_eq!(expected, result); - // } - // - // #[test] - // fn test_or_arrays_at_child_1x1() { - // let parent = shape_square(&[ - // [false, false, false], - // [false, false, false], - // [false, false, false], - // ]); - // let child = shape_square(&[[true]]); - // let result = or_arrays_at(&parent, &child, 1, 1); - // let expected = shape_square(&[ - // [false, false, false], - // [false, true, false], - // [false, false, false], - // ]); - // assert_eq!(expected, result); - // } - // - // #[test] - // fn test_or_arrays_at_child_off_parent() { - // let parent = shape_square(&[ - // [false, false, false], - // [false, false, false], - // [false, false, false], - // ]); - // let child = shape_square(&[[true, true], [true, true]]); - // let result = or_arrays_at(&parent, &child, 2, 2); - // let expected = shape_square(&[ - // [false, false, false], - // [false, false, false], - // [false, false, true], - // ]); - // assert_eq!(expected, result); - // } - // - // #[test] - // fn test_or_arrays_at_true_parent() { - // let parent = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); - // let child = shape_square(&[[true, false], [true, false]]); - // let result = or_arrays_at(&parent, &child, 1, 1); - // let expected = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); - // assert_eq!(expected, result); - // } - // - // #[test] - // fn test_or_arrays_at_smaller_parent() { - // let parent = shape_square(&[[false, false], [false, false]]); - // let child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); - // let result = or_arrays_at(&parent, &child, 0, 0); - // let expected = shape_square(&[[true, true], [true, true]]); - // assert_eq!(expected, result); - // } - // - // #[test] - // fn test_place_on_all_positions() { - // let parent = shape_square(&[ - // [false, false, false], - // [false, false, false], - // [false, false, false], - // ]); - // let child = shape_square(&[[true, false], [false, true]]); - // let placements = place_on_all_positions(&parent, &child); - // assert_eq!(placements.len(), 4); - // assert!(placements.contains(&shape_square(&[ - // [true, false, false], - // [false, true, false], - // [false, false, false], - // ]))); - // assert!(placements.contains(&shape_square(&[ - // [false, true, false], - // [false, false, true], - // [false, false, false], - // ]))); - // assert!(placements.contains(&shape_square(&[ - // [false, false, false], - // [true, false, false], - // [false, true, false], - // ]))); - // assert!(placements.contains(&shape_square(&[ - // [false, false, false], - // [false, true, false], - // [false, false, true], - // ]))); - // } - // - // #[test] - // fn test_place_on_all_positions_same_size() { - // let parent = shape_square(&[[false, false], [false, false]]); - // let child = shape_square(&[[true, false], [false, true]]); - // let placements = place_on_all_positions(&parent, &child); - // assert_eq!(placements.len(), 1); - // assert!(placements.contains(&shape_square(&[[true, false], [false, true],]))); - // } - // - // #[test] - // fn test_place_on_all_positions_smaller_parent() { - // let parent = shape_square(&[[false, false], [false, false]]); - // let child = shape_square(&[[true, false, true], [false, true, false]]); - // let placements = place_on_all_positions(&parent, &child); - // assert_eq!(placements.len(), 0); - // } - // - // #[test] - // fn test_place_on_all_positions_with_blocking() { - // let parent = shape_square(&[ - // [false, false, false], - // [false, true, false], - // [false, false, false], - // ]); - // let child = shape_square(&[[true, false], [false, true]]); - // let placements = place_on_all_positions(&parent, &child); - // assert_eq!(placements.len(), 2); - // assert!(placements.contains(&shape_square(&[ - // [false, true, false], - // [false, true, true], - // [false, false, false], - // ]))); - // assert!(placements.contains(&shape_square(&[ - // [false, false, false], - // [true, true, false], - // [false, true, false], - // ]))); - // } - // - // #[test] - // fn test_remove_parent() { - // let parent = shape_square(&[ - // [true, false, true], - // [false, true, false], - // [true, true, true], - // ]); - // let mut child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); - // remove_parent(&parent, &mut child); - // let expected = shape_square(&[ - // [false, true, false], - // [true, false, true], - // [false, false, false], - // ]); - // assert_eq!(expected, child); - // } - // - // #[test] - // fn test_remove_parent_smaller_parent() { - // let parent = shape_square(&[[true, false], [false, true]]); - // let mut child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); - // remove_parent(&parent, &mut child); - // let expected = - // shape_square(&[[false, true, true], [true, false, true], [true, true, true]]); - // assert_eq!(expected, child); - // } - // - // #[test] - // #[should_panic] - // fn test_remove_parent_bigger_parent_panic() { - // let parent = shape_square(&[ - // [true, false, true], - // [false, true, false], - // [true, true, true], - // ]); - // let mut child = shape_square(&[[true, true], [true, true]]); - // remove_parent(&parent, &mut child); - // } - // - // #[test] - // fn test_count_biggest_connected_area_of_cells_matching() { - // let array = shape_square(&[ - // [true, false, true], - // [false, true, false], - // [true, true, true], - // ]); - // let count_true = count_biggest_connected_area_of_cells_matching(&array, true); - // let count_false = count_biggest_connected_area_of_cells_matching(&array, false); - // assert_eq!(count_true, 4); - // assert_eq!(count_false, 1); - // } - // - // #[test] - // fn test_tile_rotation_iterator() { - // let base = shape_square(&[[true, false], [false, false]]); - // let mut iter = TileRotationIterator::new(base); - // - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[true, false], [false, false]])) - // ); - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[false, true], [false, false]])) - // ); - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[false, false], [false, true]])) - // ); - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[false, false], [true, false]])) - // ); - // - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[true, false], [false, false]])) - // ); - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[false, true], [false, false]])) - // ); - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[false, false], [false, true]])) - // ); - // assert_eq!( - // iter.next(), - // Some(shape_square(&[[false, false], [true, false]])) - // ); - // assert_eq!(iter.next(), None); - // } + + #[test] + fn test_trim_sides_empty() { + let mut array = Shape::from_elem((0, 0), Square, true); + let trim_sides = array.trim_matching(true); + let expected = Shape::from_elem((0, 0), Square, true); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 0, + upper_y: 0, + upper_x: 0, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_true() { + let mut array = shape_square(&[[true]]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 1, + upper_y: 0, + upper_x: 0, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_false() { + let mut array = shape_square(&[[false]]); + let trim_sides = array.trim_matching(false); + let expected = shape_square(&[[]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 1, + upper_y: 0, + upper_x: 0, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_lower_y_upper_y() { + let mut array = shape_square(&[ + [true, true, false, true], + [true, false, false, true], + [true, true, false, true], + ]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[true, false], [false, false], [true, false]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 1, + upper_y: 1, + upper_x: 0, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_lower_x_upper_x() { + let mut array = shape_square(&[ + [true, true, true, true], + [false, true, false, false], + [true, true, true, true], + ]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[false, true, false, false]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 1, + lower_y: 0, + upper_y: 0, + upper_x: 1, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_all_sides() { + let mut array = shape_square(&[ + [true, true, true, true, true], + [true, true, false, false, true], + [true, false, true, false, true], + [true, true, true, true, true], + ]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[true, false, false], [false, true, false]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 1, + lower_y: 1, + upper_y: 1, + upper_x: 1, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_from_lower_y() { + let mut array = shape_square(&[[true, true, false, false], [true, false, true, false]]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[true, false, false], [false, true, false]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 1, + upper_y: 0, + upper_x: 0, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_from_upper_y() { + let mut array = shape_square(&[[false, false, true, true], [false, true, false, true]]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[false, false, true], [false, true, false]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 0, + upper_y: 1, + upper_x: 0, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_from_lower_x() { + let mut array = shape_square(&[ + [true, true, true], + [false, true, false], + [true, false, true], + ]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[false, true, false], [true, false, true]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 1, + lower_y: 0, + upper_y: 0, + upper_x: 0, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_from_upper_x() { + let mut array = shape_square(&[ + [false, true, false], + [true, false, true], + [true, true, true], + ]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[[false, true, false], [true, false, true]]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 0, + upper_y: 0, + upper_x: 1, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_trim_sides_rows_cols_test() { + let mut array = shape_square(&[ + [false, false, false, false], + [false, false, false, false], + [true, true, true, true], + [false, true, false, true], + [true, true, true, true], + ]); + let trim_sides = array.trim_matching(true); + let expected = shape_square(&[ + [false, false, false, false], + [false, false, false, false], + [true, true, true, true], + [false, true, false, true], + ]); + assert_eq!(expected, array); + let expected_trim_sides = TrimSides { + lower_x: 0, + lower_y: 0, + upper_y: 0, + upper_x: 1, + }; + assert_eq!(expected_trim_sides, trim_sides); + } + + #[test] + fn test_or_arrays_at() { + let parent = shape_square(&[ + [false, false, false], + [false, false, false], + [false, false, false], + ]); + let child = shape_square(&[[true, false], [false, true]]); + let result = parent.or_at(&child, 1, 1); + let expected = shape_square(&[ + [false, false, false], + [false, true, false], + [false, false, true], + ]); + assert_eq!(expected, result); + } + + #[test] + fn test_or_arrays_at_empty_child() { + let parent = shape_square(&[ + [false, false, false], + [false, false, false], + [false, false, false], + ]); + let child = shape_square(&[[]]); + let result = parent.or_at(&child, 1, 1); + let expected = shape_square(&[ + [false, false, false], + [false, false, false], + [false, false, false], + ]); + assert_eq!(expected, result); + } + + #[test] + fn test_or_arrays_at_child_1x1() { + let parent = shape_square(&[ + [false, false, false], + [false, false, false], + [false, false, false], + ]); + let child = shape_square(&[[true]]); + let result = parent.or_at(&child, 1, 1); + let expected = shape_square(&[ + [false, false, false], + [false, true, false], + [false, false, false], + ]); + assert_eq!(expected, result); + } + + #[test] + fn test_or_arrays_at_child_off_parent() { + let parent = shape_square(&[ + [false, false, false], + [false, false, false], + [false, false, false], + ]); + let child = shape_square(&[[true, true], [true, true]]); + let result = parent.or_at(&child, 2, 2); + let expected = shape_square(&[ + [false, false, false], + [false, false, false], + [false, false, true], + ]); + assert_eq!(expected, result); + } + + #[test] + fn test_or_arrays_at_true_parent() { + let parent = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + let child = shape_square(&[[true, false], [true, false]]); + let result = parent.or_at(&child, 1, 1); + let expected = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + assert_eq!(expected, result); + } + + #[test] + fn test_or_arrays_at_smaller_parent() { + let parent = shape_square(&[[false, false], [false, false]]); + let child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + let result = parent.or_at(&child, 0, 0); + let expected = shape_square(&[[true, true], [true, true]]); + assert_eq!(expected, result); + } + + #[test] + fn test_place_on_all_positions() { + let parent = shape_square(&[ + [false, false, false], + [false, false, false], + [false, false, false], + ]); + let child = shape_square(&[[true, false], [false, true]]); + let placements = parent.place_on_all_positions(&child); + assert_eq!(placements.len(), 4); + assert!(placements.contains(&shape_square(&[ + [true, false, false], + [false, true, false], + [false, false, false], + ]))); + assert!(placements.contains(&shape_square(&[ + [false, true, false], + [false, false, true], + [false, false, false], + ]))); + assert!(placements.contains(&shape_square(&[ + [false, false, false], + [true, false, false], + [false, true, false], + ]))); + assert!(placements.contains(&shape_square(&[ + [false, false, false], + [false, true, false], + [false, false, true], + ]))); + } + + #[test] + fn test_place_on_all_positions_same_size() { + let parent = shape_square(&[[false, false], [false, false]]); + let child = shape_square(&[[true, false], [false, true]]); + let placements = parent.place_on_all_positions(&child); + assert_eq!(placements.len(), 1); + assert!(placements.contains(&shape_square(&[[true, false], [false, true],]))); + } + + #[test] + fn test_place_on_all_positions_smaller_parent() { + let parent = shape_square(&[[false, false], [false, false]]); + let child = shape_square(&[[true, false, true], [false, true, false]]); + let placements = parent.place_on_all_positions(&child); + assert_eq!(placements.len(), 0); + } + + #[test] + fn test_place_on_all_positions_with_blocking() { + let parent = shape_square(&[ + [false, false, false], + [false, true, false], + [false, false, false], + ]); + let child = shape_square(&[[true, false], [false, true]]); + let placements = parent.place_on_all_positions(&child); + assert_eq!(placements.len(), 2); + assert!(placements.contains(&shape_square(&[ + [false, true, false], + [false, true, true], + [false, false, false], + ]))); + assert!(placements.contains(&shape_square(&[ + [false, false, false], + [true, true, false], + [false, true, false], + ]))); + } + + #[test] + fn test_remove_parent() { + let parent = shape_square(&[ + [true, false, true], + [false, true, false], + [true, true, true], + ]); + let mut child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + child.remove_parent(&parent); + let expected = shape_square(&[ + [false, true, false], + [true, false, true], + [false, false, false], + ]); + assert_eq!(expected, child); + } + + #[test] + fn test_remove_parent_smaller_parent() { + let parent = shape_square(&[[true, false], [false, true]]); + let mut child = shape_square(&[[true, true, true], [true, true, true], [true, true, true]]); + child.remove_parent(&parent); + let expected = + shape_square(&[[false, true, true], [true, false, true], [true, true, true]]); + assert_eq!(expected, child); + } + + #[test] + #[should_panic] + fn test_remove_parent_bigger_parent_panic() { + let parent = shape_square(&[ + [true, false, true], + [false, true, false], + [true, true, true], + ]); + let mut child = shape_square(&[[true, true], [true, true]]); + child.remove_parent(&parent); + } + + #[test] + fn test_count_biggest_connected_area_of_cells_matching() { + let array = shape_square(&[ + [true, false, true], + [false, true, false], + [true, true, true], + ]); + let count_true = array.count_biggest_connected_area_of_cells_matching(true); + let count_false = array.count_biggest_connected_area_of_cells_matching(false); + assert_eq!(count_true, 4); + assert_eq!(count_false, 1); + } + + #[test] + fn test_tile_rotation_iterator() { + let base = shape_square(&[[true, false], [false, false]]); + let mut iter = base.rotations_flips_iter(); + + assert_eq!( + iter.next(), + Some(shape_square(&[[true, false], [false, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, true], [false, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [false, true]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [true, false]])) + ); + + assert_eq!( + iter.next(), + Some(shape_square(&[[true, false], [false, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, true], [false, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [false, true]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [true, false]])) + ); + assert_eq!(iter.next(), None); + } } From 97531aed8a1ab1dee585de8f70f487d6e994c158 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Sun, 12 Apr 2026 17:02:43 +0200 Subject: [PATCH 5/9] Fixing rotation --- .../src/backtracking/pruner/banned.rs | 22 +++++----- puzzled_common/src/shape/iterators.rs | 2 +- puzzled_common/src/shape/mod.rs | 41 +++++++++++++------ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/puzzle_solver/src/backtracking/pruner/banned.rs b/puzzle_solver/src/backtracking/pruner/banned.rs index c82154a..76e57b8 100644 --- a/puzzle_solver/src/backtracking/pruner/banned.rs +++ b/puzzle_solver/src/backtracking/pruner/banned.rs @@ -117,8 +117,8 @@ fn banned_bitmasks_D2(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - pattern.rotate_clockwise(); - area.rotate_clockwise(); + pattern.rotate_counterclockwise(); + area.rotate_counterclockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -158,8 +158,8 @@ fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - pattern.rotate_clockwise(); - area.rotate_clockwise(); + pattern.rotate_counterclockwise(); + area.rotate_counterclockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -172,8 +172,8 @@ fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - pattern.rotate_clockwise(); - area.rotate_clockwise(); + pattern.rotate_counterclockwise(); + area.rotate_counterclockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -186,8 +186,8 @@ fn banned_bitmasks_L3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - pattern.rotate_clockwise(); - area.rotate_clockwise(); + pattern.rotate_counterclockwise(); + area.rotate_counterclockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -225,8 +225,8 @@ fn banned_bitmasks_I3(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V banned_bitmasks.push(banned_bitmask); } - pattern.rotate_clockwise(); - area.rotate_clockwise(); + pattern.rotate_counterclockwise(); + area.rotate_counterclockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, @@ -254,7 +254,7 @@ fn banned_bitmasks_O4(board: &Board, x: usize, y: usize, banned_bitmasks: &mut V [true, true, true, true], [false, true, true, false], ]); - area.rotate_clockwise(); + area.rotate_counterclockwise(); let opt_banned_bitmask = create_banned_bitmask_for_pattern_at_if_possible( &pattern, &area, diff --git a/puzzled_common/src/shape/iterators.rs b/puzzled_common/src/shape/iterators.rs index 5623cf7..f133f27 100644 --- a/puzzled_common/src/shape/iterators.rs +++ b/puzzled_common/src/shape/iterators.rs @@ -41,7 +41,7 @@ impl Iterator for TileRotationIterator { self.current.transpose(); } let current = self.current.clone(); - self.current.rotate_clockwise(); + self.current.rotate_counterclockwise(); self.iteration += 1; Some(current) } diff --git a/puzzled_common/src/shape/mod.rs b/puzzled_common/src/shape/mod.rs index d1aa692..debadad 100644 --- a/puzzled_common/src/shape/mod.rs +++ b/puzzled_common/src/shape/mod.rs @@ -1,6 +1,6 @@ mod iterators; -use crate::ShapeType::Square; +use crate::ShapeType::*; use ndarray::{arr2, s, Array2, Axis}; use std::fmt::{Display, Formatter}; use std::ops::{Index, IndexMut}; @@ -105,14 +105,29 @@ impl Shape { pub fn rotate_clockwise(&mut self) { match self.shape_type { - ShapeType::Square => { + Square => { + self.data.reverse_axes(); + self.data.invert_axis(Axis(0)); + } + Triangle => { + todo!() + } + Hexagon => { + todo!() + } + }; + } + + pub fn rotate_counterclockwise(&mut self) { + match self.shape_type { + Square => { self.data.reverse_axes(); self.data.invert_axis(Axis(1)); } - ShapeType::Triangle => { + Triangle => { todo!() } - ShapeType::Hexagon => { + Hexagon => { todo!() } }; @@ -127,13 +142,13 @@ impl Shape { pub fn flip_default(&mut self) { match self.shape_type { - ShapeType::Square => { + Square => { self.data.invert_axis(Axis(0)); } - ShapeType::Triangle => { + Triangle => { todo!() } - ShapeType::Hexagon => { + Hexagon => { todo!() } } @@ -407,29 +422,29 @@ mod test { use super::*; #[test] - fn test_rotate_clockwise_square_size_1() { + fn test_rotate_counterclockwise_square_size_1() { let mut shape = shape_square(&[[true]]); - shape.rotate_clockwise(); + shape.rotate_counterclockwise(); let expected = shape_square(&[[true]]); assert_eq!(expected, shape); } #[test] - fn test_rotate_clockwise_square_size_2() { + fn test_rotate_counterclockwise_square_size_2() { let mut shape = shape_square(&[[true, false]]); - shape.rotate_clockwise(); + shape.rotate_counterclockwise(); let expected = shape_square(&[[true], [false]]); assert_eq!(expected, shape); } #[test] - fn test_rotate_clockwise_square() { + fn test_rotate_counterclockwise_square() { let mut shape = shape_square(&[ [true, false, false], [true, true, true], [true, false, true], ]); - shape.rotate_clockwise(); + shape.rotate_counterclockwise(); let expected = shape_square(&[ [true, true, true], [false, true, false], From 39024ea56b372e78e9e63a344c6f6b734f638cb4 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Thu, 23 Apr 2026 11:03:48 +0200 Subject: [PATCH 6/9] More comments --- puzzle_solver/src/board.rs | 1 - puzzle_solver/src/tile.rs | 2 +- puzzled/src/application.rs | 1 - puzzled_common/src/shape/mod.rs | 97 ++++++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/puzzle_solver/src/board.rs b/puzzle_solver/src/board.rs index 86bb04b..beea4c6 100644 --- a/puzzle_solver/src/board.rs +++ b/puzzle_solver/src/board.rs @@ -1,5 +1,4 @@ use log::debug; -use ndarray::Array2; use puzzled_common::shape::TrimSides; use puzzled_common::Shape; use puzzled_common::ShapeType::Square; diff --git a/puzzle_solver/src/tile.rs b/puzzle_solver/src/tile.rs index 85ed14c..7d149dd 100644 --- a/puzzle_solver/src/tile.rs +++ b/puzzle_solver/src/tile.rs @@ -74,7 +74,7 @@ impl Tile { #[allow(dead_code)] pub(crate) fn debug_print(&self) { debug!("Tile Base: "); - &self.base.debug_print(); + self.base.debug_print(); debug!("All Rotations: "); for rotation in &self.all_rotations { debug!("Rotation:"); diff --git a/puzzled/src/application.rs b/puzzled/src/application.rs index a2cd9a2..035e3c0 100644 --- a/puzzled/src/application.rs +++ b/puzzled/src/application.rs @@ -29,7 +29,6 @@ use adw::subclass::prelude::*; use gettextrs::gettext; use gtk::{gio, glib, CssProvider, License, Settings, STYLE_PROVIDER_PRIORITY_APPLICATION}; use log::info; -use ndarray::array; use puzzle_config::ColorConfig; use puzzled_common::shape::shape_square; use std::fmt::Debug; diff --git a/puzzled_common/src/shape/mod.rs b/puzzled_common/src/shape/mod.rs index debadad..7cabc7f 100644 --- a/puzzled_common/src/shape/mod.rs +++ b/puzzled_common/src/shape/mod.rs @@ -62,7 +62,6 @@ impl Shape { /// /// assert_eq!(shape.shape_type(), ShapeType::Square); /// assert_eq!(shape.dim(), (1, 2)); - /// // FIXME remove some /// assert_eq!(shape.get((0, 0)), Some(&true)); /// assert_eq!(shape.get((0, 1)), Some(&true)); /// ``` @@ -73,22 +72,97 @@ impl Shape { } } + /// Returns the shape type of this shape + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let shape = Shape::new(ShapeType::Square, arr2(&[[true]])); + /// + /// assert_eq!(shape.shape_type(), ShapeType::Square); + /// ``` pub fn shape_type(&self) -> ShapeType { self.shape_type } + /// Returns the dimensions of this shape + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let shape = Shape::new(ShapeType::Square, arr2(&[[true, true]])); + /// + /// assert_eq!(shape.dim(), (1, 2)); + /// ``` pub fn dim(&self) -> (usize, usize) { self.data.dim() } + /// Returns the number of cells in the shape + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let shape = Shape::new(ShapeType::Square, arr2(&[[true, true], [false, true]])); + /// + /// assert_eq!(shape.len(), 4); + /// ``` pub fn len(&self) -> usize { self.data.len() } + /// Returns the value of the cell at the given position in the shape or none, if the index + /// is out of bounds. + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let shape = Shape::new(ShapeType::Square, arr2(&[[true, false]])); + /// + /// assert_eq!(shape.get((0, 0)), Some(&true)); + /// assert_eq!(shape.get((0, 1)), Some(&false)); + /// assert_eq!(shape.get((1, 1)), None); + /// ``` pub fn get(&self, index: (usize, usize)) -> Option<&bool> { self.data.get(index) } + /// Maps all values in the shape and returns a new shape. + /// + /// # Arguments + /// + /// * `f`: the mapping function + /// + /// returns: Shape + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let shape = Shape::new(ShapeType::Square, arr2(&[[true, false], [false, true]])); + /// let expected = Shape::new(ShapeType::Square, arr2(&[[false, true], [true, false]])); + /// + /// assert_eq!(shape.map(|v| !v), expected); + /// ``` pub fn map(&self, f: F) -> Self where F: FnMut(&bool) -> bool, @@ -99,6 +173,27 @@ impl Shape { } } + /// Fills the shape with the given value. + /// + /// # Arguments + /// + /// * `value`: the value to set all cells to + /// + /// returns: () + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let mut shape = Shape::new(ShapeType::Square, arr2(&[[true, false], [false, true]])); + /// shape.fill(false); + /// let expected = Shape::new(ShapeType::Square, arr2(&[[false, false], [false, false]])); + /// + /// assert_eq!(shape, expected); + /// ``` pub fn fill(&mut self, value: bool) { self.data.fill(value); } From 4648604e690eb640049434686c8818bfa04b32c3 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Sat, 25 Apr 2026 19:19:27 +0200 Subject: [PATCH 7/9] Phase out clockwise rotation --- puzzle_config/src/random/mod.rs | 14 +++++++------- puzzled/src/app/components/tile.rs | 3 ++- puzzled_common/src/shape/mod.rs | 31 ++++++++---------------------- 3 files changed, 17 insertions(+), 31 deletions(-) diff --git a/puzzle_config/src/random/mod.rs b/puzzle_config/src/random/mod.rs index 293915d..463d076 100644 --- a/puzzle_config/src/random/mod.rs +++ b/puzzle_config/src/random/mod.rs @@ -106,8 +106,8 @@ mod tests { assert_eq!( TileConfig::new( shape_square(&[ - [true, true, true, true, true], - [true, true, true, true, false] + [false, true, true, true, true], + [true, true, true, true, true] ]), ColorConfig::default_with_index(0), None @@ -117,11 +117,11 @@ mod tests { assert_eq!( TileConfig::new( shape_square(&[ - [false, false, true], + [true, true, false], + [true, false, false], + [true, false, false], [true, true, true], - [false, false, true], - [false, false, true], - [false, true, true] + [true, false, false] ]), ColorConfig::default_with_index(1), None @@ -138,7 +138,7 @@ mod tests { ); assert_eq!( TileConfig::new( - shape_square(&[[false, false, true], [true, true, true]]), + shape_square(&[[true, true, true], [true, false, false]]), ColorConfig::default_with_index(3), None ), diff --git a/puzzled/src/app/components/tile.rs b/puzzled/src/app/components/tile.rs index b519a0d..40469a3 100644 --- a/puzzled/src/app/components/tile.rs +++ b/puzzled/src/app/components/tile.rs @@ -212,7 +212,8 @@ impl TileView { /// Rotates the tile one step clockwise. pub fn rotate_clockwise(&self) { let mut layout = self.current_rotation().clone(); - layout.rotate_clockwise(); + // We are calling counterclockwise here, since the tile is drawn transposed. + layout.rotate_counterclockwise(); self.set_current_rotation(layout); } diff --git a/puzzled_common/src/shape/mod.rs b/puzzled_common/src/shape/mod.rs index 7cabc7f..145ece0 100644 --- a/puzzled_common/src/shape/mod.rs +++ b/puzzled_common/src/shape/mod.rs @@ -198,26 +198,11 @@ impl Shape { self.data.fill(value); } - pub fn rotate_clockwise(&mut self) { - match self.shape_type { - Square => { - self.data.reverse_axes(); - self.data.invert_axis(Axis(0)); - } - Triangle => { - todo!() - } - Hexagon => { - todo!() - } - }; - } - pub fn rotate_counterclockwise(&mut self) { match self.shape_type { Square => { self.data.reverse_axes(); - self.data.invert_axis(Axis(1)); + self.data.invert_axis(Axis(0)); } Triangle => { todo!() @@ -528,7 +513,7 @@ mod test { fn test_rotate_counterclockwise_square_size_2() { let mut shape = shape_square(&[[true, false]]); shape.rotate_counterclockwise(); - let expected = shape_square(&[[true], [false]]); + let expected = shape_square(&[[false], [true]]); assert_eq!(expected, shape); } @@ -541,9 +526,9 @@ mod test { ]); shape.rotate_counterclockwise(); let expected = shape_square(&[ - [true, true, true], + [false, true, true], [false, true, false], - [true, true, false], + [true, true, true], ]); assert_eq!(expected, shape); } @@ -965,7 +950,7 @@ mod test { ); assert_eq!( iter.next(), - Some(shape_square(&[[false, true], [false, false]])) + Some(shape_square(&[[false, false], [true, false]])) ); assert_eq!( iter.next(), @@ -973,7 +958,7 @@ mod test { ); assert_eq!( iter.next(), - Some(shape_square(&[[false, false], [true, false]])) + Some(shape_square(&[[false, true], [false, false]])) ); assert_eq!( @@ -982,7 +967,7 @@ mod test { ); assert_eq!( iter.next(), - Some(shape_square(&[[false, true], [false, false]])) + Some(shape_square(&[[false, false], [true, false]])) ); assert_eq!( iter.next(), @@ -990,7 +975,7 @@ mod test { ); assert_eq!( iter.next(), - Some(shape_square(&[[false, false], [true, false]])) + Some(shape_square(&[[false, true], [false, false]])) ); assert_eq!(iter.next(), None); } From 85297d99e36a3e3dddec6c0995b0cc9663b35265 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Sat, 25 Apr 2026 19:44:32 +0200 Subject: [PATCH 8/9] More tests for shape --- puzzled_common/src/shape/mod.rs | 147 +++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/puzzled_common/src/shape/mod.rs b/puzzled_common/src/shape/mod.rs index 145ece0..f5b1e04 100644 --- a/puzzled_common/src/shape/mod.rs +++ b/puzzled_common/src/shape/mod.rs @@ -198,6 +198,23 @@ impl Shape { self.data.fill(value); } + /// Rotates the shape counterclockwise. + /// This rotates by a different angle for each shape type, but is always the next viable + /// angle. + /// + /// # Examples + /// + /// ``` + /// use ndarray::arr2; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let mut shape = Shape::new(ShapeType::Square, arr2(&[[true, false], [false, true]])); + /// shape.rotate_counterclockwise(); + /// let expected = Shape::new(ShapeType::Square, arr2(&[[false, true], [true, false]])); + /// + /// assert_eq!(shape, expected); + /// ``` pub fn rotate_counterclockwise(&mut self) { match self.shape_type { Square => { @@ -213,6 +230,19 @@ impl Shape { }; } + /// Rotates the shape to landscape. + /// # Examples + /// + /// ``` + /// use puzzled_common::shape::shape_square; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let mut shape = shape_square(&[[true, false]]); + /// shape.rotate_to_landscape(); + /// let expected = shape_square(&[[true], [false]]); + /// assert_eq!(expected, shape); + /// ``` pub fn rotate_to_landscape(&mut self) { let dim = self.dim(); if dim.0 < dim.1 { @@ -220,6 +250,20 @@ impl Shape { } } + /// Flips the shape by a default axis for the shape type. + /// + /// # Examples + /// + /// ``` + /// use puzzled_common::shape::shape_square; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let mut shape = shape_square(&[[false, true], [false, false]]); + /// shape.flip_default(); + /// let expected = shape_square(&[[false, false], [false, true]]); + /// assert_eq!(expected, shape); + /// ``` pub fn flip_default(&mut self) { match self.shape_type { Square => { @@ -234,14 +278,38 @@ impl Shape { } } + /// Transposes the shape. + /// + /// # Examples + /// + /// ``` + /// use puzzled_common::shape::shape_square; + /// use puzzled_common::Shape; + /// use puzzled_common::ShapeType; + /// + /// let mut shape = shape_square(&[[false, true], [false, false]]); + /// shape.transpose(); + /// let expected = shape_square(&[[false, false], [true, false]]); + /// assert_eq!(expected, shape); + /// ``` pub fn transpose(&mut self) { - self.data.reverse_axes(); + match self.shape_type { + Square => { + self.data.reverse_axes(); + } + Triangle => { + todo!() + } + Hexagon => { + todo!() + } + } } pub fn transposed(&self) -> Self { Self { shape_type: self.shape_type, - data: self.data.t().into_owned(), + data: self.data.clone().reversed_axes(), } } @@ -533,6 +601,22 @@ mod test { assert_eq!(expected, shape); } + #[test] + fn test_rotate_to_landscape_empty() { + let mut shape = shape_square(&[[]]); + shape.rotate_to_landscape(); + let expected = shape_square(&[[]]); + assert_eq!(expected, shape); + } + + #[test] + fn test_rotate_to_landscape_one() { + let mut shape = shape_square(&[[true]]); + shape.rotate_to_landscape(); + let expected = shape_square(&[[true]]); + assert_eq!(expected, shape); + } + #[test] fn test_trim_sides_empty() { let mut array = Shape::from_elem((0, 0), Square, true); @@ -730,6 +814,65 @@ mod test { assert_eq!(expected_trim_sides, trim_sides); } + #[test] + fn test_count_biggest_connected_area_of_cells_matching_empty() { + let shape = shape_square(&[[]]); + + let count_true = shape.count_biggest_connected_area_of_cells_matching(true); + let count_false = shape.count_biggest_connected_area_of_cells_matching(false); + + assert_eq!(count_true, 0); + assert_eq!(count_false, 0); + } + + #[test] + fn test_count_biggest_connected_area_of_cells_matching_true() { + let shape = shape_square(&[[true]]); + + let count_true = shape.count_biggest_connected_area_of_cells_matching(true); + let count_false = shape.count_biggest_connected_area_of_cells_matching(false); + + assert_eq!(count_true, 1); + assert_eq!(count_false, 0); + } + + #[test] + fn test_count_biggest_connected_area_of_cells_matching_false() { + let shape = shape_square(&[[false]]); + + let count_true = shape.count_biggest_connected_area_of_cells_matching(true); + let count_false = shape.count_biggest_connected_area_of_cells_matching(false); + + assert_eq!(count_true, 0); + assert_eq!(count_false, 1); + } + + #[test] + fn test_count_biggest_connected_area_of_cells_matching_ring() { + let shape = shape_square(&[[true, true, true], [true, false, true], [true, true, true]]); + + let count_true = shape.count_biggest_connected_area_of_cells_matching(true); + let count_false = shape.count_biggest_connected_area_of_cells_matching(false); + + assert_eq!(count_true, 8); + assert_eq!(count_false, 1); + } + + #[test] + fn test_count_biggest_connected_area_of_cells_matching_complex() { + let shape = shape_square(&[ + [true, true, true, false, true], + [true, false, true, false, true], + [true, true, true, false, true], + ]); + + let count_true = shape.count_biggest_connected_area_of_cells_matching(true); + let count_false = shape.count_biggest_connected_area_of_cells_matching(false); + + assert_eq!(count_true, 8); + assert_eq!(count_false, 3); + } + #[test] fn test_or_arrays_at() { let parent = shape_square(&[ From dd80d5d5a848a1f1a72468d1bd3a7102c625a161 Mon Sep 17 00:00:00 2001 From: Tilman Holube Date: Sat, 25 Apr 2026 19:59:50 +0200 Subject: [PATCH 9/9] Move iterator tests and add comments --- puzzled_common/src/shape/iterators.rs | 64 ++++++++++++++++++++++++--- puzzled_common/src/shape/mod.rs | 41 ----------------- 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/puzzled_common/src/shape/iterators.rs b/puzzled_common/src/shape/iterators.rs index f133f27..d09c70e 100644 --- a/puzzled_common/src/shape/iterators.rs +++ b/puzzled_common/src/shape/iterators.rs @@ -3,34 +3,38 @@ use ndarray::iter::{IndexedIter, Iter}; use ndarray::Ix2; impl Shape { + /// Iterates over the values of the shape. pub fn iter(&self) -> Iter<'_, bool, Ix2> { self.data.iter() } + /// Iterates over the values of the shape while also being given the index of the value. pub fn indexed_iter(&self) -> IndexedIter<'_, bool, Ix2> { self.data.indexed_iter() } - pub fn rotations_flips_iter(&self) -> TileRotationIterator { - TileRotationIterator::new(self.clone()) + /// Iterates over all rotations of the shape. + /// Duplicates are not removed. + pub fn rotations_flips_iter(&self) -> ShapeRotationIterator { + ShapeRotationIterator::new(self.clone()) } } -pub struct TileRotationIterator { +pub struct ShapeRotationIterator { current: Shape, iteration: u8, } -impl TileRotationIterator { - fn new(tile: Shape) -> Self { +impl ShapeRotationIterator { + fn new(shape: Shape) -> Self { Self { - current: tile, + current: shape, iteration: 0, } } } -impl Iterator for TileRotationIterator { +impl Iterator for ShapeRotationIterator { type Item = Shape; fn next(&mut self) -> Option { @@ -46,3 +50,49 @@ impl Iterator for TileRotationIterator { Some(current) } } + +#[cfg(test)] +mod test { + use crate::shape::shape_square; + + #[test] + fn test_tile_rotation_iterator() { + let base = shape_square(&[[true, false], [false, false]]); + let mut iter = base.rotations_flips_iter(); + + assert_eq!( + iter.next(), + Some(shape_square(&[[true, false], [false, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [true, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [false, true]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, true], [false, false]])) + ); + + assert_eq!( + iter.next(), + Some(shape_square(&[[true, false], [false, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [true, false]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, false], [false, true]])) + ); + assert_eq!( + iter.next(), + Some(shape_square(&[[false, true], [false, false]])) + ); + assert_eq!(iter.next(), None); + } +} diff --git a/puzzled_common/src/shape/mod.rs b/puzzled_common/src/shape/mod.rs index f5b1e04..bdc01b1 100644 --- a/puzzled_common/src/shape/mod.rs +++ b/puzzled_common/src/shape/mod.rs @@ -1081,45 +1081,4 @@ mod test { assert_eq!(count_true, 4); assert_eq!(count_false, 1); } - - #[test] - fn test_tile_rotation_iterator() { - let base = shape_square(&[[true, false], [false, false]]); - let mut iter = base.rotations_flips_iter(); - - assert_eq!( - iter.next(), - Some(shape_square(&[[true, false], [false, false]])) - ); - assert_eq!( - iter.next(), - Some(shape_square(&[[false, false], [true, false]])) - ); - assert_eq!( - iter.next(), - Some(shape_square(&[[false, false], [false, true]])) - ); - assert_eq!( - iter.next(), - Some(shape_square(&[[false, true], [false, false]])) - ); - - assert_eq!( - iter.next(), - Some(shape_square(&[[true, false], [false, false]])) - ); - assert_eq!( - iter.next(), - Some(shape_square(&[[false, false], [true, false]])) - ); - assert_eq!( - iter.next(), - Some(shape_square(&[[false, false], [false, true]])) - ); - assert_eq!( - iter.next(), - Some(shape_square(&[[false, true], [false, false]])) - ); - assert_eq!(iter.next(), None); - } }