grid slots and spans
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
pub mod pattern;
|
||||||
|
pub mod slot;
|
||||||
|
pub mod symmetry;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Cell {
|
||||||
|
Black,
|
||||||
|
Empty,
|
||||||
|
Letter(char),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell {
|
||||||
|
pub fn is_black(&self) -> bool {
|
||||||
|
matches!(self, Cell::Black)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_white(&self) -> bool {
|
||||||
|
!self.is_black()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn letter(&self) -> Option<char> {
|
||||||
|
match self {
|
||||||
|
Cell::Letter(c) => Some(*c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Grid {
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
cells: Vec<Cell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
cells: vec![Cell::Empty; width * height],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_black(width: usize, height: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
cells: vec![Cell::Black; width * height],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, row: usize, col: usize) -> Cell {
|
||||||
|
self.cells[row * self.width + col]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, row: usize, col: usize, cell: Cell) {
|
||||||
|
self.cells[row * self.width + col] = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_bounds(&self, row: usize, col: usize) -> bool {
|
||||||
|
row < self.height && col < self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn black_count(&self) -> usize {
|
||||||
|
self.cells.iter().filter(|c| c.is_black()).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn white_count(&self) -> usize {
|
||||||
|
self.cells.iter().filter(|c| c.is_white()).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn density(&self) -> f64 {
|
||||||
|
self.black_count() as f64 / self.cells.len() as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_cells(&self) -> usize {
|
||||||
|
self.cells.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_grid_is_all_empty() {
|
||||||
|
let g = Grid::new(5, 5);
|
||||||
|
for r in 0..5 {
|
||||||
|
for c in 0..5 {
|
||||||
|
assert_eq!(g.get(r, c), Cell::Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_and_get() {
|
||||||
|
let mut g = Grid::new(3, 3);
|
||||||
|
g.set(1, 2, Cell::Black);
|
||||||
|
g.set(0, 0, Cell::Letter('A'));
|
||||||
|
assert_eq!(g.get(1, 2), Cell::Black);
|
||||||
|
assert_eq!(g.get(0, 0), Cell::Letter('A'));
|
||||||
|
assert_eq!(g.get(0, 1), Cell::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn density_calculation() {
|
||||||
|
let mut g = Grid::new(2, 2);
|
||||||
|
g.set(0, 0, Cell::Black);
|
||||||
|
assert!((g.density() - 0.25).abs() < f64::EPSILON);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::Grid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Direction {
|
||||||
|
Across,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Intersection {
|
||||||
|
pub other_slot: usize,
|
||||||
|
pub self_pos: usize,
|
||||||
|
pub other_pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Slot {
|
||||||
|
pub index: usize,
|
||||||
|
pub row: usize,
|
||||||
|
pub col: usize,
|
||||||
|
pub direction: Direction,
|
||||||
|
pub length: usize,
|
||||||
|
pub intersections: Vec<Intersection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Slot {
|
||||||
|
pub fn cells(&self) -> Vec<(usize, usize)> {
|
||||||
|
(0..self.length)
|
||||||
|
.map(|i| match self.direction {
|
||||||
|
Direction::Across => (self.row, self.col + i),
|
||||||
|
Direction::Down => (self.row + i, self.col),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_slots(grid: &Grid, min_length: usize) -> Vec<Slot> {
|
||||||
|
let mut slots = Vec::new();
|
||||||
|
|
||||||
|
// across slots
|
||||||
|
for r in 0..grid.height {
|
||||||
|
let mut c = 0;
|
||||||
|
while c < grid.width {
|
||||||
|
if grid.get(r, c).is_white() {
|
||||||
|
let start = c;
|
||||||
|
while c < grid.width && grid.get(r, c).is_white() {
|
||||||
|
c += 1;
|
||||||
|
}
|
||||||
|
let len = c - start;
|
||||||
|
if len >= min_length {
|
||||||
|
slots.push(Slot {
|
||||||
|
index: slots.len(),
|
||||||
|
row: r,
|
||||||
|
col: start,
|
||||||
|
direction: Direction::Across,
|
||||||
|
length: len,
|
||||||
|
intersections: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// down slots
|
||||||
|
for c in 0..grid.width {
|
||||||
|
let mut r = 0;
|
||||||
|
while r < grid.height {
|
||||||
|
if grid.get(r, c).is_white() {
|
||||||
|
let start = r;
|
||||||
|
while r < grid.height && grid.get(r, c).is_white() {
|
||||||
|
r += 1;
|
||||||
|
}
|
||||||
|
let len = r - start;
|
||||||
|
if len >= min_length {
|
||||||
|
slots.push(Slot {
|
||||||
|
index: slots.len(),
|
||||||
|
row: start,
|
||||||
|
col: c,
|
||||||
|
direction: Direction::Down,
|
||||||
|
length: len,
|
||||||
|
intersections: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_intersections(&mut slots);
|
||||||
|
slots
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_intersections(slots: &mut Vec<Slot>) {
|
||||||
|
let n = slots.len();
|
||||||
|
// build a map of (row, col) -> (slot_index, position_in_slot)
|
||||||
|
let mut cell_map: std::collections::HashMap<(usize, usize), Vec<(usize, usize)>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for slot in slots.iter() {
|
||||||
|
for (pos, (r, c)) in slot.cells().iter().enumerate() {
|
||||||
|
cell_map.entry((*r, *c)).or_default().push((slot.index, pos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut intersections: Vec<Vec<Intersection>> = vec![Vec::new(); n];
|
||||||
|
|
||||||
|
for entries in cell_map.values() {
|
||||||
|
if entries.len() == 2 {
|
||||||
|
let (s1, p1) = entries[0];
|
||||||
|
let (s2, p2) = entries[1];
|
||||||
|
intersections[s1].push(Intersection {
|
||||||
|
other_slot: s2,
|
||||||
|
self_pos: p1,
|
||||||
|
other_pos: p2,
|
||||||
|
});
|
||||||
|
intersections[s2].push(Intersection {
|
||||||
|
other_slot: s1,
|
||||||
|
self_pos: p2,
|
||||||
|
other_pos: p1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, slot) in slots.iter_mut().enumerate() {
|
||||||
|
slot.intersections = std::mem::take(&mut intersections[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::grid::Cell;
|
||||||
|
|
||||||
|
fn make_mini_grid() -> Grid {
|
||||||
|
// 3x3 grid with center black:
|
||||||
|
// . . .
|
||||||
|
// . # .
|
||||||
|
// . . .
|
||||||
|
let mut g = Grid::new(3, 3);
|
||||||
|
g.set(1, 1, Cell::Black);
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_across_slots() {
|
||||||
|
let g = make_mini_grid();
|
||||||
|
let slots = extract_slots(&g, 2);
|
||||||
|
let across: Vec<_> = slots
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.direction == Direction::Across)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(across.len(), 2); // top row (3) and no middle across (1+1 too short)
|
||||||
|
assert!(across.iter().any(|s| s.row == 0 && s.length == 3));
|
||||||
|
assert!(across.iter().any(|s| s.row == 2 && s.length == 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_down_slots() {
|
||||||
|
let g = make_mini_grid();
|
||||||
|
let slots = extract_slots(&g, 2);
|
||||||
|
let down: Vec<_> = slots
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.direction == Direction::Down)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(down.len(), 2); // left col (3) and right col (3)
|
||||||
|
assert!(down.iter().any(|s| s.col == 0 && s.length == 3));
|
||||||
|
assert!(down.iter().any(|s| s.col == 2 && s.length == 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intersections_found() {
|
||||||
|
let g = Grid::new(3, 3); // fully white
|
||||||
|
let slots = extract_slots(&g, 3);
|
||||||
|
// 3 across + 3 down = 6 slots, but only rows/cols of length 3
|
||||||
|
// actually: 3 across (one per row) + 3 down (one per col)
|
||||||
|
for slot in &slots {
|
||||||
|
assert!(!slot.intersections.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user