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