grid slots and spans

This commit is contained in:
2025-03-14 14:32:31 +02:00
parent 7c90380d14
commit b703612aba
2 changed files with 300 additions and 0 deletions
+114
View File
@@ -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);
}
}
+186
View File
@@ -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());
}
}
}