use crate::composition::Interface;
use crate::placement::ir::Dir;
use crate::wfc::{E, N, S, W};
pub struct Faces<'a> {
faces: &'a [Option<Interface>],
wall: &'a str,
walkable: f32,
}
impl<'a> Faces<'a> {
pub fn new(faces: &'a [Option<Interface>], wall: &'a str, walkable: f32) -> Self {
Faces { faces, wall, walkable }
}
pub fn wall(&self, p: usize, dir: Dir) -> bool {
self.bands(p, dir).is_some_and(|b| b.iter().any(|b| b.token.as_deref() == Some(self.wall)))
}
pub fn doorway(&self, p: usize) -> bool {
[N, E, S, W].iter().any(|&d| {
let Some(bands) = self.bands(p, d) else {
return false;
};
let lowest = bands
.iter()
.filter(|b| b.token.as_deref() == Some(self.wall))
.fold(f32::INFINITY, |lo, b| lo.min(b.y.0));
lowest.is_finite() && lowest >= self.walkable
})
}
pub fn floor(&self, p: usize) -> bool {
p != 0 && p < self.faces.len()
}
fn bands(&self, p: usize, dir: Dir) -> Option<&'a Vec<crate::composition::Band>> {
match self.faces.get(p) {
Some(Some(i)) => i.faces.get(dir),
_ => None,
}
}
}
pub const RANGES: usize = 6;
pub const BINS: usize = RANGES * RANGES;
pub const OPENING_MAX: f32 = 4.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Measured {
pub enclosure: f32,
pub opening_density: Option<f32>,
pub regions: usize,
}
#[inline]
fn opposite(dir: Dir) -> Dir {
match dir {
N => S,
S => N,
E => W,
_ => E,
}
}
#[inline]
fn step(w: usize, h: usize, x: usize, z: usize, dir: Dir) -> Option<(usize, usize)> {
match dir {
N => z.checked_sub(1).map(|z| (x, z)),
E => (x + 1 < w).then_some((x + 1, z)),
S => (z + 1 < h).then_some((x, z + 1)),
W => x.checked_sub(1).map(|x| (x, z)),
_ => None,
}
}
#[inline]
fn blocked<F: Fn(usize, Dir) -> bool>(grid: &[usize], here: usize, there: usize, dir: Dir, walls: &F) -> bool {
walls(grid[here], dir) || walls(grid[there], opposite(dir))
}
fn reached_from_border<F: Fn(usize, Dir) -> bool>(
w: usize,
h: usize,
grid: &[usize],
walls: &F,
) -> Vec<bool> {
let mut seen = vec![false; w * h];
let mut stack: Vec<(usize, usize)> = Vec::new();
for z in 0..h {
for x in 0..w {
if x == 0 || z == 0 || x + 1 == w || z + 1 == h {
let i = z * w + x;
if !seen[i] {
seen[i] = true;
stack.push((x, z));
}
}
}
}
while let Some((x, z)) = stack.pop() {
let i = z * w + x;
for dir in [N, E, S, W] {
let Some((nx, nz)) = step(w, h, x, z, dir) else {
continue;
};
let j = nz * w + nx;
if seen[j] || blocked(grid, i, j, dir, walls) {
continue;
}
seen[j] = true;
stack.push((nx, nz));
}
}
seen
}
pub fn measure<Wf, Ff, Of>(
w: usize,
h: usize,
grid: &[usize],
walls: Wf,
floor: Ff,
opening: Of,
) -> Result<Measured, String>
where
Wf: Fn(usize, Dir) -> bool,
Ff: Fn(usize) -> bool,
Of: Fn(usize) -> bool,
{
if w == 0 || h == 0 {
return Err(format!("range: a {w} x {h} grid has no cells to measure"));
}
if grid.len() != w * h {
return Err(format!(
"range: a {w} x {h} grid holds {} cells, but {} were given",
w * h,
grid.len()
));
}
let outside = reached_from_border(w, h, grid, &walls);
let total_floor = (0..w * h).filter(|&i| floor(grid[i])).count();
let enclosed_floor = (0..w * h).filter(|&i| floor(grid[i]) && !outside[i]).count();
let enclosure = if total_floor == 0 {
0.0
} else {
enclosed_floor as f32 / total_floor as f32
};
let mut region_of: Vec<Option<usize>> = vec![None; w * h];
let mut regions = 0usize;
let mut doors_per_region: Vec<usize> = Vec::new();
for z0 in 0..h {
for x0 in 0..w {
let start = z0 * w + x0;
if outside[start] || !floor(grid[start]) || region_of[start].is_some() {
continue;
}
let id = regions;
regions += 1;
let mut doors: Vec<usize> = Vec::new();
let mut stack = vec![(x0, z0)];
region_of[start] = Some(id);
while let Some((x, z)) = stack.pop() {
let i = z * w + x;
for dir in [N, E, S, W] {
let Some((nx, nz)) = step(w, h, x, z, dir) else {
continue;
};
let j = nz * w + nx;
if opening(grid[j]) && !doors.contains(&j) {
doors.push(j);
}
if blocked(grid, i, j, dir, &walls) || outside[j] || !floor(grid[j]) {
continue;
}
if region_of[j].is_none() {
region_of[j] = Some(id);
stack.push((nx, nz));
}
}
}
doors_per_region.push(doors.len());
}
}
let opening_density = if regions == 0 {
None
} else {
Some(doors_per_region.iter().sum::<usize>() as f32 / regions as f32)
};
Ok(Measured { enclosure, opening_density, regions })
}
pub fn bin(enclosure: f32, opening: f32) -> (usize, usize) {
let clamp = |v: f32, hi: f32| -> usize {
if !v.is_finite() || v <= 0.0 {
return 0;
}
let ix = (v / hi * RANGES as f32).floor();
(ix.max(0.0) as usize).min(RANGES - 1)
};
(clamp(enclosure, 1.0), clamp(opening, OPENING_MAX))
}
pub fn max_bin_share(counts: &[u32]) -> f32 {
let total: u64 = counts.iter().map(|&c| c as u64).sum();
if total == 0 {
return 0.0;
}
let top = counts.iter().copied().max().unwrap_or(0) as f64;
(top / total as f64) as f32
}
pub fn normalised_entropy(counts: &[u32]) -> f32 {
let total: u64 = counts.iter().map(|&c| c as u64).sum();
if total == 0 || counts.len() < 2 {
return 0.0;
}
let total = total as f64;
let h: f64 = counts
.iter()
.filter(|&&c| c > 0)
.map(|&c| {
let p = c as f64 / total;
-p * p.ln()
})
.sum();
((h / (counts.len() as f64).ln()) as f32).max(0.0)
}
pub fn total_variation(a: &[u32], b: &[u32]) -> f32 {
let ta: u64 = a.iter().map(|&c| c as u64).sum();
let tb: u64 = b.iter().map(|&c| c as u64).sum();
if ta == 0 && tb == 0 {
return 0.0;
}
if ta == 0 || tb == 0 {
return 1.0;
}
let n = a.len().max(b.len());
let sum: f64 = (0..n)
.map(|i| {
let pa = *a.get(i).unwrap_or(&0) as f64 / ta as f64;
let pb = *b.get(i).unwrap_or(&0) as f64 / tb as f64;
(pa - pb).abs()
})
.sum();
(0.5 * sum) as f32
}
#[cfg(test)]
mod tests {
use super::*;
const OPEN: usize = 0;
const SOLID: usize = 1;
const DOOR: usize = 2;
const VOID: usize = 3;
const MASKED: usize = 16;
fn masked(dirs: &[Dir]) -> usize {
MASKED + dirs.iter().fold(0usize, |m, &d| m | 1 << d)
}
fn walls(p: usize, d: Dir) -> bool {
if p >= MASKED {
return (p - MASKED) >> d & 1 == 1;
}
p == SOLID || p == DOOR
}
fn floor(p: usize) -> bool {
p == OPEN
}
fn opening(p: usize) -> bool {
p == DOOR
}
fn score(w: usize, h: usize, g: &[usize]) -> Measured {
match measure(w, h, g, walls, floor, opening) {
Ok(m) => m,
Err(e) => panic!("{e}"),
}
}
fn sealed_room() -> Vec<usize> {
let mut g = vec![VOID; 25];
for z in 1..4 {
for x in 1..4 {
g[z * 5 + x] = SOLID;
}
}
g[2 * 5 + 2] = OPEN;
g
}
#[test]
fn a_sealed_room_encloses_its_floor_and_has_no_doors() {
let m = score(5, 5, &sealed_room());
assert_eq!(m.enclosure, 1.0, "the one floor cell is inside a closed boundary");
assert_eq!(m.regions, 1);
assert_eq!(m.opening_density, Some(0.0), "sealed is zero doors, not undefined");
}
#[test]
fn a_door_in_the_ring_is_counted_against_the_region_it_opens() {
let mut g = sealed_room();
g[5 + 2] = DOOR; let m = score(5, 5, &g);
assert_eq!(m.enclosure, 1.0, "a door is part of the boundary, so the room still closes");
assert_eq!(m.regions, 1);
assert_eq!(m.opening_density, Some(1.0));
}
#[test]
fn a_gap_in_the_ring_lets_the_fill_in_and_nothing_is_enclosed() {
let mut g = sealed_room();
g[5 + 2] = VOID; let m = score(5, 5, &g);
assert_eq!(m.enclosure, 0.0);
assert_eq!(m.regions, 0);
assert_eq!(m.opening_density, None, "no enclosed region is undefined, not zero");
}
#[test]
fn open_floor_everywhere_encloses_nothing() {
let m = score(4, 4, &[OPEN; 16]);
assert_eq!(m.enclosure, 0.0);
assert_eq!(m.regions, 0);
assert_eq!(m.opening_density, None);
}
#[test]
fn a_grid_with_no_floor_scores_zero_rather_than_dividing_by_zero() {
let m = score(4, 4, &[SOLID; 16]);
assert_eq!(m.enclosure, 0.0);
assert_eq!(m.regions, 0, "solid is not floor, so there is no room in there");
assert_eq!(m.opening_density, None);
}
#[test]
fn two_rooms_sharing_a_wall_stay_two_regions() {
let mut g = vec![VOID; 35];
for z in 1..4 {
for x in 1..6 {
g[z * 7 + x] = SOLID;
}
}
g[2 * 7 + 2] = OPEN;
g[2 * 7 + 4] = OPEN;
let m = score(7, 5, &g);
assert_eq!(m.enclosure, 1.0);
assert_eq!(m.regions, 2, "grouping every unreached cell would merge them through the ring");
assert_eq!(m.opening_density, Some(0.0));
}
#[test]
fn half_the_floor_enclosed_is_half() {
let mut g = sealed_room();
g[0] = OPEN; let m = score(5, 5, &g);
assert_eq!(m.enclosure, 0.5, "one of two floor cells is inside");
assert_eq!(m.regions, 1);
}
#[test]
fn a_ring_of_one_sided_walls_seals_from_every_side() {
let mut g = vec![VOID; 25];
g[5 + 1] = masked(&[N, W]);
g[5 + 2] = masked(&[N]);
g[5 + 3] = masked(&[N, E]);
g[2 * 5 + 1] = masked(&[W]);
g[2 * 5 + 3] = masked(&[E]);
g[3 * 5 + 1] = masked(&[W, S]);
g[3 * 5 + 2] = masked(&[S]);
g[3 * 5 + 3] = masked(&[E, S]);
g[2 * 5 + 2] = OPEN;
let m = score(5, 5, &g);
assert_eq!(m.enclosure, 1.0, "every approach is stopped by the far face of the seam");
assert_eq!(m.regions, 1);
assert_eq!(m.opening_density, Some(0.0));
let mut leaky = g.clone();
leaky[3 * 5 + 2] = masked(&[]);
assert_eq!(score(5, 5, &leaky).enclosure, 0.0);
}
#[test]
fn a_ring_of_inward_facing_walls_seals_from_the_near_side() {
let mut g = vec![VOID; 25];
g[5 + 2] = masked(&[S]); g[3 * 5 + 2] = masked(&[N]);
g[2 * 5 + 1] = masked(&[E]);
g[2 * 5 + 3] = masked(&[W]);
g[2 * 5 + 2] = OPEN;
let m = score(5, 5, &g);
assert_eq!(m.enclosure, 1.0, "each wall stops the fill as it leaves, not as it arrives");
assert_eq!(m.regions, 1);
let mut leaky = g.clone();
leaky[5 + 2] = masked(&[]);
assert_eq!(score(5, 5, &leaky).enclosure, 0.0, "one face is the whole seal");
}
#[test]
fn outside_can_be_two_places_and_both_are_outside() {
let mut g = vec![OPEN; 25];
for z in 0..5 {
g[z * 5 + 2] = SOLID; }
let m = score(5, 5, &g);
assert_eq!(m.enclosure, 0.0, "both halves reach a border of their own");
assert_eq!(m.regions, 0);
for (edge, cells) in [
("west", [(0, 2), (1, 2), (2, 2)]),
("east", [(4, 2), (3, 2), (2, 2)]),
("north", [(2, 0), (2, 1), (2, 2)]),
("south", [(2, 4), (2, 3), (2, 2)]),
] {
let mut pocket = vec![SOLID; 25];
for (x, z) in cells {
pocket[z * 5 + x] = OPEN;
}
let m = score(5, 5, &pocket);
assert_eq!(m.enclosure, 0.0, "a corridor open to the {edge} is not a room");
assert_eq!(m.regions, 0, "a corridor open to the {edge} is not a room");
}
}
#[test]
fn a_malformed_grid_is_named_rather_than_scored() {
let e = measure(3, 3, &[OPEN; 4], walls, floor, opening).unwrap_err();
assert!(e.contains("9 cells"), "{e}");
assert!(e.contains('4'), "{e}");
assert!(measure(0, 3, &[], walls, floor, opening).is_err());
}
#[test]
fn bins_span_the_pre_registered_domain_and_clamp_above_it() {
assert_eq!(bin(0.0, 0.0), (0, 0));
assert_eq!(bin(1.0, OPENING_MAX), (RANGES - 1, RANGES - 1), "the top edge is the top bin");
assert_eq!(bin(2.0, 99.0), (RANGES - 1, RANGES - 1), "above the domain clamps in");
assert_eq!(bin(0.5, 2.0), (3, 3));
assert_eq!(bin(0.16, 0.7), (0, 1), "a sixth of each axis is one range");
assert_eq!(bin(-1.0, -1.0), (0, 0));
assert_eq!(bin(f32::NAN, f32::NAN), (0, 0));
}
#[test]
fn max_bin_share_is_the_largest_share() {
assert_eq!(max_bin_share(&[]), 0.0);
assert_eq!(max_bin_share(&[0, 0]), 0.0);
assert_eq!(max_bin_share(&[10]), 1.0);
assert_eq!(max_bin_share(&[3, 1]), 0.75);
assert_eq!(max_bin_share(&[1, 1, 1, 1]), 0.25);
}
#[test]
fn entropy_is_zero_on_one_bin_and_one_on_a_uniform_spread() {
assert_eq!(normalised_entropy(&[0; 36]), 0.0);
let mut one = [0u32; 36];
one[7] = 200;
assert_eq!(normalised_entropy(&one), 0.0, "everything in one bin has no spread");
assert!((normalised_entropy(&[1; 36]) - 1.0).abs() < 1e-6);
}
#[test]
fn the_committed_floor_sits_exactly_between_two_and_three_uniform_bins() {
let uniform_over = |k: usize| -> f32 {
let mut c = [0u32; 36];
for slot in c.iter_mut().take(k) {
*slot = 10;
}
normalised_entropy(&c)
};
let two = uniform_over(2);
let three = uniform_over(3);
assert!(two < 0.25 && three > 0.25, "0.25 separates them: {two} / {three}");
assert!(
((0.25 - two) - (three - 0.25)).abs() < 1e-6,
"equidistant: {two} .. 0.25 .. {three}"
);
assert!((two + three - 0.5).abs() < 1e-6, "they sum to a half because ln6/ln36 = 1/2");
}
#[test]
fn total_variation_is_zero_on_identical_shapes_and_one_on_disjoint_ones() {
assert_eq!(total_variation(&[1, 1], &[5, 5]), 0.0, "same shape, different size");
assert_eq!(total_variation(&[1, 0], &[0, 1]), 1.0);
assert_eq!(total_variation(&[], &[]), 0.0);
assert_eq!(total_variation(&[1], &[0]), 1.0, "one side empty");
assert!((total_variation(&[3, 1], &[1, 1]) - 0.25).abs() < 1e-6);
}
#[test]
fn a_nested_block_reads_half_the_distance_of_a_disjoint_one() {
let a = [8u32, 0];
let fresh = [0u32, 8];
let nested = [a[0] + fresh[0], a[1] + fresh[1]];
let disjoint = total_variation(&a, &fresh);
let against_nested = total_variation(&a, &nested);
assert!((disjoint - 1.0).abs() < 1e-6);
assert!((against_nested - 0.5 * disjoint).abs() < 1e-6);
}
}