From 950c2772cd015de60eecff4627e75fa7d8892719 Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 24 Mar 2025 19:50:43 +0200 Subject: [PATCH] grid symmetry helpers --- cruciverb-core/src/grid/symmetry.rs | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 cruciverb-core/src/grid/symmetry.rs diff --git a/cruciverb-core/src/grid/symmetry.rs b/cruciverb-core/src/grid/symmetry.rs new file mode 100644 index 0000000..823867e --- /dev/null +++ b/cruciverb-core/src/grid/symmetry.rs @@ -0,0 +1,110 @@ +use crate::config::SymmetryType; + +pub fn mirror_coords( + row: usize, + col: usize, + width: usize, + height: usize, + symmetry: SymmetryType, +) -> Vec<(usize, usize)> { + let mut coords = vec![(row, col)]; + + let mirror_row = |i: usize| height.checked_sub(1).and_then(|h| h.checked_sub(i)); + let mirror_col = |i: usize| width.checked_sub(1).and_then(|w| w.checked_sub(i)); + + match symmetry { + SymmetryType::Rotational180 => { + if let (Some(mr), Some(mc)) = (mirror_row(row), mirror_col(col)) { + coords.push((mr, mc)); + } + } + SymmetryType::Rotational90 => { + // each 90-degree image is kept only when it lands inside the grid, + // so non-square grids no longer underflow or go out of bounds + if let Some(mc) = mirror_col(row) { + if col < height { + coords.push((col, mc)); + } + } + if let (Some(mr), Some(mc)) = (mirror_row(row), mirror_col(col)) { + coords.push((mr, mc)); + } + if let Some(mr) = mirror_row(col) { + if row < width { + coords.push((mr, row)); + } + } + } + SymmetryType::Diagonal => { + if width == height { + coords.push((col, row)); + } + } + SymmetryType::Mirror => { + if let Some(mc) = mirror_col(col) { + coords.push((row, mc)); + } + } + SymmetryType::None => {} + } + coords.sort(); + coords.dedup(); + coords +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rotational_180() { + let coords = mirror_coords(0, 0, 5, 5, SymmetryType::Rotational180); + assert_eq!(coords, vec![(0, 0), (4, 4)]); + } + + #[test] + fn rotational_180_center() { + let coords = mirror_coords(2, 2, 5, 5, SymmetryType::Rotational180); + assert_eq!(coords, vec![(2, 2)]); + } + + #[test] + fn rotational_90() { + let coords = mirror_coords(0, 1, 5, 5, SymmetryType::Rotational90); + assert!(coords.contains(&(0, 1))); + assert!(coords.contains(&(1, 4))); + assert!(coords.contains(&(4, 3))); + assert!(coords.contains(&(3, 0))); + } + + #[test] + fn mirror_lr() { + let coords = mirror_coords(1, 0, 5, 5, SymmetryType::Mirror); + assert_eq!(coords, vec![(1, 0), (1, 4)]); + } + + #[test] + fn diagonal() { + let coords = mirror_coords(1, 3, 5, 5, SymmetryType::Diagonal); + assert_eq!(coords, vec![(1, 3), (3, 1)]); + } + + #[test] + fn rotational_90_non_square_stays_in_bounds() { + let coords = mirror_coords(0, 3, 5, 7, SymmetryType::Rotational90); + for (r, c) in &coords { + assert!(*r < 7 && *c < 5, "point ({}, {}) out of bounds", r, c); + } + assert!(coords.contains(&(0, 3))); + let coords2 = mirror_coords(6, 4, 5, 7, SymmetryType::Rotational90); + for (r, c) in &coords2 { + assert!(*r < 7 && *c < 5, "point ({}, {}) out of bounds", r, c); + } + } + + #[test] + fn no_symmetry() { + let coords = mirror_coords(1, 2, 5, 5, SymmetryType::None); + assert_eq!(coords, vec![(1, 2)]); + } +}