use std::collections::HashMap;
use crate::composition::{Composition, Envelope, Interface, Stamped};
use crate::library::Library;
use crate::map::{Map, Placed};
use crate::placement::ir::Dir;
use crate::wfc;
use crate::wfc::{E, N, S, W};
#[derive(Clone, Debug, PartialEq)]
pub enum Prototype {
Empty,
Piece { descriptor: String, yaw: f32 },
Composed { composition: String, yaw: f32 },
}
pub const CELL_EPSILON: f32 = 1e-4;
pub const MAX_PROTOTYPES: usize = 32;
#[derive(Debug)]
pub struct Grammar {
pub prototypes: Vec<Prototype>,
pub weights: Vec<f64>,
pub support: [Vec<u32>; 4],
}
impl Grammar {
pub fn len(&self) -> usize {
self.prototypes.len()
}
pub fn is_empty(&self) -> bool {
self.prototypes.is_empty()
}
}
fn to_cell(map: &Map, cell: f32, at: (f32, f32)) -> (i64, i64) {
let (min_x, min_z, _, _) = map.floor_rect();
(
((at.0 - min_x) / cell).floor() as i64,
((at.1 - min_z) / cell).floor() as i64,
)
}
fn cell_centre(map: &Map, cell: f32, c: (i64, i64)) -> (f32, f32) {
let (min_x, min_z, _, _) = map.floor_rect();
(
min_x + (c.0 as f32 + 0.5) * cell,
min_z + (c.1 as f32 + 0.5) * cell,
)
}
fn dimensions(map: &Map, cell: f32) -> (usize, usize) {
(
(map.bounds.0 / cell).round().max(1.0) as usize,
(map.bounds.2 / cell).round().max(1.0) as usize,
)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Area {
pub from: (f32, f32),
pub to: (f32, f32),
}
impl Area {
pub fn holds(&self, map: &Map, cell: f32, at: (f32, f32)) -> bool {
let Ok(win) = window(map, cell, Some(self)) else {
return false;
};
let c = to_cell(map, cell, at);
let (x0, z0, x1, z1) = win.area;
c.0 >= x0 as i64 && c.0 < x1 as i64 && c.1 >= z0 as i64 && c.1 < z1 as i64
}
}
struct Window {
grid: (usize, usize, usize, usize),
area: (usize, usize, usize, usize),
}
impl Window {
fn width(&self) -> usize {
self.grid.2 - self.grid.0
}
fn height(&self) -> usize {
self.grid.3 - self.grid.1
}
fn local(&self, c: (i64, i64)) -> Option<usize> {
let (x0, z0, x1, z1) = self.grid;
let (x, z) = (usize::try_from(c.0).ok()?, usize::try_from(c.1).ok()?);
(x >= x0 && x < x1 && z >= z0 && z < z1).then(|| (z - z0) * self.width() + (x - x0))
}
fn lays(&self, x: usize, z: usize) -> bool {
let (x, z) = (x + self.grid.0, z + self.grid.1);
let (x0, z0, x1, z1) = self.area;
x >= x0 && x < x1 && z >= z0 && z < z1
}
fn beside(&self, x: usize, z: usize) -> bool {
let (x, z) = (x + self.grid.0, z + self.grid.1);
let (x0, z0, x1, z1) = self.area;
(x >= x0 && x < x1) || (z >= z0 && z < z1)
}
}
fn window(map: &Map, cell: f32, area: Option<&Area>) -> Result<Window, String> {
let (w, h) = dimensions(map, cell);
let Some(a) = area else {
return Ok(Window {
grid: (0, 0, w, h),
area: (0, 0, w, h),
});
};
let (min_x, min_z, max_x, max_z) = map.floor_rect();
let corners = [a.from.0, a.from.1, a.to.0, a.to.1];
if !corners.iter().all(|v| v.is_finite()) {
return Err(format!(
"grammar: the area from {:?} to {:?} is not a place on the map",
a.from, a.to
));
}
let span = |lo: f32, hi: f32, min: f32, n: usize| -> (usize, usize) {
let (lo, hi) = (lo.min(hi), lo.max(hi));
let first = ((lo - min) / cell - 0.5 - CELL_EPSILON).ceil().max(0.0) as usize;
let past = (((hi - min) / cell - 0.5 + CELL_EPSILON).floor() + 1.0).clamp(0.0, n as f32);
(first.min(n), past as usize)
};
let (x0, x1) = span(a.from.0, a.to.0, min_x, w);
let (z0, z1) = span(a.from.1, a.to.1, min_z, h);
if x0 >= x1 || z0 >= z1 {
return Err(format!(
"grammar: the area from {:?} to {:?} holds no cell of the map, whose floor runs from \
({min_x}, {min_z}) to ({max_x}, {max_z}); give an area over the floor",
a.from, a.to
));
}
Ok(Window {
grid: (
x0.saturating_sub(1),
z0.saturating_sub(1),
(x1 + 1).min(w),
(z1 + 1).min(h),
),
area: (x0, z0, x1, z1),
})
}
struct Part {
win: Window,
pins: Vec<u32>,
domains: Vec<u32>,
}
impl Part {
fn of(win: &Window, initial: &[u32], grammar: &Grammar) -> Self {
let (x0, z0, x1, z1) = win.area;
let part = Window {
grid: win.area,
area: win.area,
};
let (w, h) = (part.width(), part.height());
let mut pins = Vec::with_capacity(w * h);
for z in z0..z1 {
for x in x0..x1 {
let at = win.local((x as i64, z as i64));
pins.push(at.and_then(|i| initial.get(i)).copied().unwrap_or(0));
}
}
let mut domains = pins.clone();
let steps: [(i64, i64); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
for z in z0..z1 {
for x in x0..x1 {
let here = (z - z0) * w + (x - x0);
for (dir, (dx, dz)) in steps.iter().enumerate() {
let c = (x as i64 + dx, z as i64 + dz);
let (cx, cz) = (c.0, c.1);
let inside = cx >= x0 as i64 && cx < x1 as i64 && cz >= z0 as i64 && cz < z1 as i64;
if inside {
continue;
}
let Some(ring) = win.local(c).and_then(|i| initial.get(i)).copied() else {
continue; };
let opposite = (dir + 2) % 4;
let mut allowed = 0u32;
for (q, row) in grammar.support[opposite].iter().enumerate() {
if ring & (1 << q) != 0 {
allowed |= row;
}
}
domains[here] &= allowed;
}
}
}
Part { win: part, pins, domains }
}
fn beyond(&self, win: &Window) -> [bool; 4] {
let (x0, z0, x1, z1) = win.area;
[z0 > win.grid.1, x1 < win.grid.2, z1 < win.grid.3, x0 > win.grid.0]
}
fn width(&self) -> usize {
self.win.width()
}
fn height(&self) -> usize {
self.win.height()
}
fn with_ring(&self, win: &Window, initial: &[u32], solved: &[usize]) -> Vec<usize> {
let (w, h) = (win.width(), win.height());
let (x0, z0, ..) = win.area;
let aw = self.width();
let mut grid = vec![0usize; w * h];
for z in 0..h {
for x in 0..w {
let i = z * w + x;
grid[i] = if win.lays(x, z) {
let (ax, az) = (x + win.grid.0 - x0, z + win.grid.1 - z0);
solved.get(az * aw + ax).copied().unwrap_or(0)
} else {
match initial.get(i) {
Some(&mask) if mask.count_ones() == 1 => mask.trailing_zeros() as usize,
_ => 0,
}
};
}
}
grid
}
}
pub fn learn(map: &Map, cell: f32) -> Result<Grammar, String> {
if !(cell.is_finite() && cell > 0.0) {
return Err(format!("grammar: cell size {cell} is not a usable step"));
}
let (w, h) = dimensions(map, cell);
let mut prototypes = vec![Prototype::Empty];
let mut counts: Vec<f64> = vec![0.0];
let mut grid: Vec<usize> = vec![0; w * h];
let mut placed_at: HashMap<(i64, i64), usize> = HashMap::new();
for p in &map.placements {
let c = to_cell(map, cell, p.at);
if c.0 < 0 || c.1 < 0 || c.0 as usize >= w || c.1 as usize >= h {
continue;
}
if placed_at.contains_key(&c) {
continue;
}
let proto = Prototype::Piece {
descriptor: p.descriptor.clone(),
yaw: (p.yaw / 15.0).round() * 15.0,
};
let ix = match prototypes.iter().position(|q| *q == proto) {
Some(ix) => ix,
None => {
if prototypes.len() >= MAX_PROTOTYPES {
return Err(format!(
"grammar: this map uses more than {MAX_PROTOTYPES} distinct piece-and-yaw \
combinations, which is more than the solver's domain can hold. Learn from a \
smaller region, or place fewer kinds of thing in the example."
));
}
prototypes.push(proto);
counts.push(0.0);
prototypes.len() - 1
}
};
placed_at.insert(c, ix);
grid[c.1 as usize * w + c.0 as usize] = ix;
}
for &ix in &grid {
counts[ix] += 1.0;
}
let n = prototypes.len();
let mut support: [Vec<u32>; 4] = [vec![0; n], vec![0; n], vec![0; n], vec![0; n]];
let steps: [(i64, i64); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)];
for y in 0..h {
for x in 0..w {
let a = grid[y * w + x];
for (dir, (dx, dy)) in steps.iter().enumerate() {
let (nx, ny) = (x as i64 + dx, y as i64 + dy);
if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= h {
continue;
}
let b = grid[ny as usize * w + nx as usize];
support[dir][a] |= 1 << b;
}
}
}
for row in &mut support {
row[0] |= 1;
}
Ok(Grammar {
prototypes,
weights: counts,
support,
})
}
#[derive(Debug)]
pub struct Solved {
pub placements: Vec<Placed>,
pub stamps: Vec<Stamped>,
pub free_cells: usize,
pub owned_cells: usize,
pub grid: Vec<usize>,
pub width: usize,
pub height: usize,
pub unmet: u64,
}
pub struct Wishes {
pub enclosure: f32,
pub enclosure_weight: Option<u32>,
}
pub fn solve(
map: &Map,
grammar: &Grammar,
cell: f32,
seed: u64,
next_id: impl FnMut() -> String,
) -> Result<Solved, String> {
solve_window(map, grammar, cell, None, seed, next_id)
}
pub fn solve_in(
map: &Map,
grammar: &Grammar,
cell: f32,
area: Area,
seed: u64,
next_id: impl FnMut() -> String,
) -> Result<Solved, String> {
solve_window(map, grammar, cell, Some(&area), seed, next_id)
}
fn solve_window(
map: &Map,
grammar: &Grammar,
cell: f32,
area: Option<&Area>,
seed: u64,
mut next_id: impl FnMut() -> String,
) -> Result<Solved, String> {
if grammar.len() < 2 {
return Err(
"grammar: nothing has been placed yet, so there is no arrangement to continue. Place a \
few pieces the way you want them first."
.to_owned(),
);
}
let win = window(map, cell, area)?;
let (w, h) = (win.width(), win.height());
let full: u32 = if grammar.len() == 32 {
u32::MAX
} else {
(1u32 << grammar.len()) - 1
};
let (initial, owned_cells) = pinned_domains(map, grammar, cell, &win, full)?;
let part = Part::of(&win, &initial, grammar);
let solved = wfc::collapse_grid(
part.width(),
part.height(),
&grammar.weights,
&grammar.support,
&part.domains,
seed,
)
.ok_or_else(|| {
"grammar: no arrangement satisfies what you have pinned. The learned rules cannot tile \
this region around those cells — free some of them, or extend the example so the solver \
has more ways to join things up."
.to_owned()
})?;
let (placements, stamps, free_cells) =
harvest(map, grammar, cell, &solved, &part.pins, &part.win, &mut next_id);
Ok(Solved {
placements,
stamps,
free_cells,
owned_cells,
grid: part.with_ring(&win, &initial, &solved),
width: w,
height: h,
unmet: 0,
})
}
const SOLVE_BUDGET: u64 = 200_000;
pub fn solve_constrained(
map: &Map,
grammar: &Grammar,
faces: &[Option<Interface>],
wishes: &Wishes,
cell: f32,
seed: u64,
next_id: impl FnMut() -> String,
) -> Result<Solved, String> {
solve_constrained_window(map, grammar, faces, wishes, cell, None, seed, next_id)
}
#[allow(clippy::too_many_arguments)]
pub fn solve_constrained_in(
map: &Map,
grammar: &Grammar,
faces: &[Option<Interface>],
wishes: &Wishes,
cell: f32,
area: Area,
seed: u64,
next_id: impl FnMut() -> String,
) -> Result<Solved, String> {
solve_constrained_window(map, grammar, faces, wishes, cell, Some(&area), seed, next_id)
}
#[allow(clippy::too_many_arguments)]
fn solve_constrained_window(
map: &Map,
grammar: &Grammar,
faces: &[Option<Interface>],
wishes: &Wishes,
cell: f32,
area: Option<&Area>,
seed: u64,
mut next_id: impl FnMut() -> String,
) -> Result<Solved, String> {
if grammar.len() < 2 {
return Err(
"grammar: nothing has been placed yet, so there is no arrangement to continue. Place a \
few pieces the way you want them first."
.to_owned(),
);
}
if faces.len() != grammar.len() {
return Err(format!(
"grammar: {} interfaces for {} prototypes — they must be the pair `from_compositions` \
returned, or the constraint and the metric are reading different kits.",
faces.len(),
grammar.len()
));
}
let win = window(map, cell, area)?;
let (w, h) = (win.width(), win.height());
let full: u32 = if grammar.len() == 32 {
u32::MAX
} else {
(1u32 << grammar.len()) - 1
};
let (initial, owned_cells) = pinned_domains(map, grammar, cell, &win, full)?;
let part = Part::of(&win, &initial, grammar);
let (aw, ah) = (part.width(), part.height());
let mut gp = crate::constraints::GridProblem::encode(
&grammar.support,
&part.domains,
aw,
ah,
grammar.len(),
)?;
crate::constraints::domain_rules(&mut gp.problem, &gp.place, &part.domains);
crate::constraints::preference_rules(&mut gp.problem, &gp.place, &grammar.weights, seed)?;
if wishes.enclosure > 0.0 {
let read = crate::range::Faces::new(faces, WALL_TOKEN, WALKABLE_GAP);
crate::constraints::enclosure_rules_beyond(
&mut gp.problem,
&gp.place,
&read,
aw,
ah,
part.beyond(&win),
wishes.enclosure,
wishes.enclosure_weight,
)?;
}
let solution = gp.problem.solve(SOLVE_BUDGET)?;
let chosen = gp.read(&solution)?;
let (placements, stamps, free_cells) =
harvest(map, grammar, cell, &chosen, &part.pins, &part.win, &mut next_id);
Ok(Solved {
placements,
stamps,
free_cells,
owned_cells,
grid: part.with_ring(&win, &initial, &chosen),
width: w,
height: h,
unmet: solution.unmet(),
})
}
pub fn interfaces(faces: &[Option<crate::composition::Interface>]) -> crate::range::Faces<'_> {
crate::range::Faces::new(faces, WALL_TOKEN, WALKABLE_GAP)
}
const WALL_TOKEN: &str = "wall";
const WALKABLE_GAP: f32 = 0.5;
fn pinned_domains(
map: &Map,
grammar: &Grammar,
cell: f32,
win: &Window,
full: u32,
) -> Result<(Vec<u32>, usize), String> {
let w = win.width();
let mut initial = vec![full; w * win.height()];
let mut owned_cells = 0usize;
let quarter = |yaw: f32| (yaw / 15.0).round() * 15.0;
let known = |p: &Prototype| {
grammar
.prototypes
.iter()
.position(|q| q == p)
.or_else(|| same_tile_turned(grammar, p))
};
let mut ring: HashMap<usize, Option<usize>> = HashMap::new();
let mut stands = |i: usize, proto: Prototype| {
let ix = known(&proto);
match ring.get_mut(&i) {
None => {
ring.insert(i, ix);
}
Some(slot @ None) => *slot = ix,
Some(Some(_)) => {}
}
};
for p in &map.placements {
if let Some(i) = win.local(to_cell(map, cell, p.at))
&& !win.lays(i % w, i / w)
{
stands(
i,
Prototype::Piece {
descriptor: p.descriptor.clone(),
yaw: quarter(p.yaw),
},
);
}
}
for s in &map.stamps {
if let Some(i) = win.local(to_cell(map, cell, s.at))
&& !win.lays(i % w, i / w)
{
stands(
i,
Prototype::Composed {
composition: s.of.clone(),
yaw: quarter(s.yaw),
},
);
}
}
for (i, mask) in initial.iter_mut().enumerate() {
let (x, z) = (i % w, i / w);
if win.lays(x, z) || !win.beside(x, z) {
continue;
}
*mask = match ring.get(&i) {
None => 1 << 0, Some(Some(ix)) => 1 << ix,
Some(None) => full,
};
}
for p in map.placements.iter().filter(|p| p.owned) {
let Some(i) = win.local(to_cell(map, cell, p.at)) else {
continue;
};
if !win.lays(i % w, i / w) {
continue;
}
let proto = Prototype::Piece {
descriptor: p.descriptor.clone(),
yaw: quarter(p.yaw),
};
let Some(ix) = known(&proto) else {
return Err(format!(
"grammar: owned placement `{}` is a `{}` at {}deg, which is not in the learned \
grammar — re-learn from a map that contains it.",
p.id, p.descriptor, p.yaw
));
};
initial[i] = 1 << ix;
owned_cells += 1;
}
Ok((initial, owned_cells))
}
fn same_tile_turned(grammar: &Grammar, proto: &Prototype) -> Option<usize> {
let Prototype::Composed { composition, yaw } = proto else {
return None;
};
let turns = |p: &Prototype| match p {
Prototype::Composed { composition: c, yaw } if c == composition => Some(*yaw),
_ => None,
};
let kept = grammar.prototypes.iter().filter_map(turns).count();
if !matches!(kept, 1 | 2 | 4) {
return None;
}
let period = 90.0 * kept as f32;
let want = yaw.rem_euclid(period);
grammar.prototypes.iter().position(|p| {
turns(p).is_some_and(|y| {
let y = y.rem_euclid(360.0);
(y - want).abs() < 1.0 && y < period
})
})
}
fn harvest(
map: &Map,
grammar: &Grammar,
cell: f32,
chosen: &[usize],
initial: &[u32],
win: &Window,
next_id: &mut impl FnMut() -> String,
) -> (Vec<Placed>, Vec<Stamped>, usize) {
let mut placements = Vec::new();
let mut stamps = Vec::new();
let mut free_cells = 0usize;
let w = win.width();
for y in 0..win.height() {
for x in 0..w {
if !win.lays(x, y) {
continue; }
let c = ((x + win.grid.0) as i64, (y + win.grid.1) as i64);
let (Some(&ix), Some(&mask)) = (chosen.get(y * w + x), initial.get(y * w + x)) else {
continue;
};
if mask.count_ones() == 1 {
continue; }
free_cells += 1;
match grammar.prototypes.get(ix) {
None | Some(Prototype::Empty) => {}
Some(Prototype::Piece { descriptor, yaw }) => placements.push(Placed {
id: next_id(),
descriptor: descriptor.clone(),
at: cell_centre(map, cell, c),
yaw: *yaw,
..Placed::default()
}),
Some(Prototype::Composed { composition, yaw }) => stamps.push(Stamped {
id: next_id(),
of: composition.clone(),
at: cell_centre(map, cell, c),
yaw: *yaw,
..Stamped::default()
}),
}
}
}
(placements, stamps, free_cells)
}
#[cfg(test)]
mod declared_tests {
use super::*;
use crate::descriptor::{Descriptor, Extent, SubCell, Subgrid};
use crate::library::LIBRARY_VERSION;
fn post(id: &str, token: &str) -> Descriptor {
let mut d = Descriptor {
id: id.to_owned(),
extent: Extent { footprint: Some((0.5, 0.5)), height: Some(0.5) },
..Default::default()
};
let div = crate::descriptor::divisions(&d, 1).expect("measured");
let mut cells = Vec::new();
for x in 0..div.0 {
for y in 0..div.1 {
for z in 0..div.2 {
cells.push(SubCell {
at: (x, y, z),
solid: true,
edge: Some(token.to_owned()),
});
}
}
}
d.subgrid = Some(Subgrid { cells });
d
}
fn lib(ds: Vec<Descriptor>) -> Library {
Library { version: LIBRARY_VERSION, note: None, descriptors: ds }
}
#[test]
fn a_library_with_no_tokens_refuses_rather_than_generating_noise() {
let plain = Descriptor {
id: "crate_a".to_owned(),
extent: Extent { footprint: Some((0.5, 0.5)), height: Some(0.5) },
..Default::default()
};
let err = declared(&lib(vec![plain]), 1, 0.5).expect_err("must refuse");
assert!(err.contains("no descriptor"), "{err}");
assert!(err.contains("Author tokens"), "{err}");
}
#[test]
fn only_matching_tokens_may_abut() {
let g = declared(&lib(vec![post("stone", "stone"), post("glass", "glass")]), 1, 0.5)
.expect("builds");
let ix = |id: &str| {
g.prototypes
.iter()
.position(|p| matches!(p, Prototype::Piece { descriptor, .. } if descriptor == id))
.unwrap_or_else(|| panic!("no {id}"))
};
let (stone, glass) = (ix("stone"), ix("glass"));
assert!(g.support[N][stone] & (1 << stone) != 0, "stone may meet stone");
assert!(g.support[N][stone] & (1 << glass) == 0, "stone must not meet glass");
assert!(g.support[N][stone] & 1 != 0);
assert!(g.support[N][0] & (1 << stone) != 0);
}
#[test]
fn a_symmetric_tile_is_one_prototype_not_four() {
let g = declared(&lib(vec![post("stone", "stone")]), 1, 0.5).expect("builds");
assert_eq!(g.prototypes.len(), 2, "Empty plus one: {:?}", g.prototypes);
}
#[test]
fn two_tiles_presenting_the_same_faces_are_still_two_tiles() {
let g = declared(&lib(vec![post("wall", "stone"), post("column", "stone")]), 1, 0.5)
.expect("builds");
let ids: Vec<&str> = g
.prototypes
.iter()
.filter_map(|p| match p {
Prototype::Piece { descriptor, .. } => Some(descriptor.as_str()),
Prototype::Composed { composition, .. } => Some(composition.as_str()),
Prototype::Empty => None,
})
.collect();
assert!(ids.contains(&"wall") && ids.contains(&"column"), "{ids:?}");
}
#[test]
fn tiles_that_are_not_the_grids_size_refuse_and_name_their_sizes() {
let err = declared(&lib(vec![post("stone", "stone")]), 1, 1.0).expect_err("must refuse");
assert!(err.contains("wrong size"), "{err}");
assert!(err.contains("`stone` is 0.5 x 0.5 m"), "{err}");
assert!(err.contains("generate from the map instead"), "{err}");
}
#[test]
fn more_prototypes_than_the_domain_holds_refuses_with_the_count() {
let many: Vec<Descriptor> = (0..MAX_PROTOTYPES + 2)
.map(|i| post(&format!("p{i:03}"), &format!("t{i:03}")))
.collect();
let err = declared(&lib(many), 1, 0.5).expect_err("must refuse");
assert!(err.contains(&MAX_PROTOTYPES.to_string()), "{err}");
assert!(err.contains("narrow the kit"), "{err}");
}
#[test]
fn a_declared_grammar_drives_the_shared_collapser() {
let g = declared(&lib(vec![post("stone", "stone"), post("glass", "glass")]), 1, 0.5)
.expect("builds");
let n = g.prototypes.len();
let initial = vec![(1u32 << n) - 1; 4 * 4];
let out = crate::wfc::collapse_grid(4, 4, &g.weights, &g.support, &initial, 7)
.unwrap_or_else(|| panic!("a fully permissive start must collapse"));
assert_eq!(out.len(), 16);
for z in 0..4usize {
for x in 0..4usize {
let p = out[z * 4 + x];
if x + 1 < 4 {
let q = out[z * 4 + x + 1];
assert!(g.support[E][p] & (1 << q) != 0, "({x},{z}) east pair is not allowed");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::{Descriptor, Extent, SubCell, Subgrid};
fn iface(token: Option<&str>) -> crate::composition::Interface {
use crate::composition::Band;
let band = Band {
y: (0.0, 1.0),
lat: (-0.5, 0.5),
token: token.map(str::to_owned),
};
crate::composition::Interface {
faces: [
vec![band.clone()],
vec![band.clone()],
vec![band.clone()],
vec![band],
],
faults: Vec::new(),
}
}
#[test]
fn the_shipped_site_kit_learns_a_grammar_and_solves() {
let Composed { grammar: g, skipped, faces } = super::tests::site_kit();
assert!(skipped.is_empty(), "every authored tile is one cell and bounded: {skipped:?}");
assert_eq!(faces.len(), g.len(), "one interface slot per prototype");
assert!(faces[0].is_none(), "Empty presents nothing");
assert!(faces[1..].iter().all(|f| f.is_some()), "every tile prototype presents something");
let turns_of = |id: &str| {
g.prototypes
.iter()
.filter(|p| matches!(p, Prototype::Composed { composition, .. } if composition == id))
.count()
};
assert_eq!(turns_of("site/tile_floor"), 1, "a symmetric tile is one prototype, not four");
assert_eq!(turns_of("site/tile_wall_n"), 4, "an asymmetric tile is four");
let wall = g
.prototypes
.iter()
.position(|p| matches!(p, Prototype::Composed { composition, yaw }
if composition == "site/tile_wall_n" && *yaw == 0.0))
.expect("the kit has a north wall tile");
assert!(
g.support[N][wall] & !1 != 0,
"a wall tile whose only northern neighbour is Empty cannot make a room; support is \
{:#b}",
g.support[N][wall]
);
let weight_of = |id: &str| -> f64 {
g.prototypes
.iter()
.zip(g.weights.iter())
.filter(|(p, _)| matches!(p, Prototype::Composed { composition, .. } if composition == id))
.map(|(_, w)| *w)
.sum()
};
let floor = weight_of("site/tile_floor");
let wall = weight_of("site/tile_wall_n");
assert!(
(floor - wall).abs() < 1e-9,
"a symmetric tile and an asymmetric one must carry the same total weight — floor \
{floor}, wall {wall}"
);
assert!(
(floor - 1.0).abs() < 1e-9,
"and that total is one tile's worth, not one prototype's: {floor}"
);
let map = crate::map::Map {
name: "probe".into(),
bounds: (6.0, 3.0, 6.0),
..crate::map::Map::default()
};
let mut n = 0;
let solved = solve(&map, &g, 1.0, 42, || {
n += 1;
format!("g@{n}")
})
.expect("the shipped kit solves a 6x6 grid");
assert_eq!(solved.free_cells, 36);
assert!(
solved.placements.is_empty(),
"a grammar over compositions places no descriptors: {:?}",
solved.placements
);
assert!(
solved.stamps.len() > 20,
"a solve that fills almost nothing is a grammar that cannot express a room: {} stamps",
solved.stamps.len()
);
assert!(
solved.stamps.iter().all(|s| {
g.prototypes.iter().any(
|p| matches!(p, Prototype::Composed { composition, .. } if *composition == s.of),
)
}),
"every stamp names a composition the kit actually has"
);
}
#[test]
fn the_kit_can_build_a_room_the_metric_calls_enclosed() {
let Composed { grammar: g, faces, .. } = super::tests::site_kit();
let ix = |id: &str, yaw: f32| -> usize {
g.prototypes
.iter()
.position(|p| matches!(p, Prototype::Composed { composition, yaw: y }
if composition == id && (*y - yaw).abs() < 1e-6))
.unwrap_or_else(|| panic!("the kit has {id} at {yaw} degrees"))
};
let (nw, ne, sw, se) = (
ix("site/tile_corner_nw", 0.0),
ix("site/tile_corner_nw", 270.0),
ix("site/tile_corner_nw", 90.0),
ix("site/tile_corner_nw", 180.0),
);
let (n_, e_, s_, w_) = (
ix("site/tile_wall_n", 0.0),
ix("site/tile_wall_n", 270.0),
ix("site/tile_wall_n", 180.0),
ix("site/tile_wall_n", 90.0),
);
let floor_ix = ix("site/tile_floor", 0.0);
const D: usize = 9;
let (lo, hi) = (2usize, 6usize);
let mut grid = vec![0usize; D * D];
for z in lo..=hi {
for x in lo..=hi {
let on_top = z == lo;
let on_bottom = z == hi;
let on_left = x == lo;
let on_right = x == hi;
grid[z * D + x] = match (on_top, on_bottom, on_left, on_right) {
(true, _, true, _) => nw,
(true, _, _, true) => ne,
(_, true, true, _) => sw,
(_, true, _, true) => se,
(true, ..) => n_,
(_, true, ..) => s_,
(_, _, true, _) => w_,
(_, _, _, true) => e_,
_ => floor_ix,
};
}
}
let mut illegal = Vec::new();
for z in 0..D {
for x in 0..D {
let p = grid[z * D + x];
for (dir, nx, nz) in [
(N, x as i64, z as i64 - 1),
(E, x as i64 + 1, z as i64),
(S, x as i64, z as i64 + 1),
(W, x as i64 - 1, z as i64),
] {
if nx < 0 || nz < 0 || nx as usize >= D || nz as usize >= D {
continue;
}
let q = grid[nz as usize * D + nx as usize];
if g.support[dir][p] & (1 << q) == 0 {
illegal.push(format!(
"{:?} may not sit {} of {:?}",
g.prototypes[q],
["north", "east", "south", "west"][dir],
g.prototypes[p]
));
}
}
}
}
assert!(
illegal.is_empty(),
"the kit's own room is not a legal arrangement under its own grammar, so no solve could \
ever produce one: {illegal:#?}"
);
let f = crate::range::Faces::new(&faces, "wall", 0.5);
let sealed = crate::range::measure(D, D, &grid, |p, d| f.wall(p, d), |p| f.floor(p), |p| {
f.doorway(p)
})
.expect("a well-formed grid");
assert_eq!(sealed.enclosure, 1.0, "every cell of a walled room is inside it");
assert_eq!(sealed.regions, 1);
assert_eq!(sealed.opening_density, Some(0.0), "no doors yet");
grid[lo * D + (lo + 2)] = ix("site/tile_doorway_n", 0.0);
let with_door =
crate::range::measure(D, D, &grid, |p, d| f.wall(p, d), |p| f.floor(p), |p| f.doorway(p))
.expect("a well-formed grid");
assert_eq!(with_door.enclosure, 1.0, "a doorway is part of the boundary, not a hole in it");
assert_eq!(with_door.regions, 1);
assert_eq!(with_door.opening_density, Some(1.0));
grid[lo * D + (lo + 1)] = 0;
let breached =
crate::range::measure(D, D, &grid, |p, d| f.wall(p, d), |p| f.floor(p), |p| f.doorway(p))
.expect("a well-formed grid");
assert_eq!(breached.enclosure, 0.0);
assert_eq!(breached.regions, 0);
}
fn empty_tile(id: &str) -> Composition {
Composition {
id: id.to_owned(),
envelope: Envelope::Bounded { size: (1.0, 1.0, 1.0) },
members: Vec::new(),
locations: Vec::new(),
note: None,
}
}
#[test]
fn a_slot_costs_the_solver_nothing() {
use crate::composition::{Body, Member};
let library = Library {
version: crate::library::LIBRARY_VERSION,
note: None,
descriptors: Vec::new(),
};
let plain = empty_tile("tile_a");
let mut holed = empty_tile("tile_a");
holed.members = ["a", "b", "c", "d"]
.iter()
.enumerate()
.map(|(i, id)| Member {
id: (*id).to_owned(),
body: Body::Slot { accepts: format!("thing-{i}") },
at: (0.0, 0.0),
yaw: 0.0,
lift: 0.1 * i as f32,
paint: 0,
of_fingerprint: None,
note: None,
})
.collect();
let learn = |c: &Composition| {
from_compositions(
std::slice::from_ref(c),
&library,
1,
1.0,
crate::composition::agrees,
)
.expect("learns")
};
let (a, b) = (learn(&plain), learn(&holed));
assert!(b.skipped.is_empty(), "a holed tile is still a tile: {:?}", b.skipped);
assert_eq!(
a.grammar.len(),
b.grammar.len(),
"four holes changed the alphabet from {} to {} prototypes",
a.grammar.len(),
b.grammar.len()
);
assert_eq!(a.grammar.support, b.grammar.support, "four holes moved the adjacency table");
}
pub(super) fn site_kit() -> Composed {
let plate = |id: &str| Descriptor {
id: id.to_owned(),
extent: Extent { footprint: Some((1.0, 1.0)), height: Some(0.1) },
..Default::default()
};
let mut wall = Descriptor {
id: "wall".to_owned(),
extent: Extent { footprint: Some((0.1, 1.0)), height: Some(2.4) },
..Default::default()
};
let div = crate::descriptor::divisions(&wall, 1).expect("measured");
let mut cells = Vec::new();
for x in 0..div.0 {
for y in 0..div.1 {
for z in 0..div.2 {
cells.push(SubCell {
at: (x, y, z),
solid: true,
edge: Some(WALL_TOKEN.to_owned()),
});
}
}
}
wall.subgrid = Some(Subgrid { cells });
let mut header = Descriptor {
id: "header".to_owned(),
extent: Extent { footprint: Some((0.1, 1.0)), height: Some(0.4) },
..Default::default()
};
let div = crate::descriptor::divisions(&header, 1).expect("measured");
let mut cells = Vec::new();
for x in 0..div.0 {
for y in 0..div.1 {
for z in 0..div.2 {
cells.push(SubCell {
at: (x, y, z),
solid: true,
edge: Some(WALL_TOKEN.to_owned()),
});
}
}
}
header.subgrid = Some(Subgrid { cells });
let library = Library {
version: crate::library::LIBRARY_VERSION,
note: None,
descriptors: vec![plate("floor"), wall, header],
};
use crate::composition::{Body, Envelope, Member};
let member = |id: &str, at: (f32, f32)| Member {
id: id.to_owned(),
body: Body::Descriptor { id: id.to_owned(), tip: (0, 0), on: None, patch: None },
at,
yaw: 0.0,
lift: 0.0,
paint: 0,
of_fingerprint: None,
note: None,
};
let tile = |id: &str, members: Vec<Member>| Composition {
id: id.to_owned(),
envelope: Envelope::Bounded { size: (1.0, 2.4, 1.0) },
members,
locations: Vec::new(),
note: None,
};
let north = || Member { at: (0.0, -0.45), yaw: 90.0, ..member("wall", (0.0, -0.45)) };
let west = || Member {
id: "wall_2".to_owned(),
at: (-0.45, 0.0),
..member("wall", (-0.45, 0.0))
};
let lintel = || Member {
id: "header".to_owned(),
at: (0.0, -0.45),
yaw: 90.0,
lift: 2.0,
..member("header", (0.0, -0.45))
};
let comps = vec![
tile("site/tile_corner_nw", vec![member("floor", (0.0, 0.0)), north(), west()]),
tile("site/tile_doorway_n", vec![member("floor", (0.0, 0.0)), lintel()]),
tile("site/tile_floor", vec![member("floor", (0.0, 0.0))]),
tile("site/tile_wall_n", vec![member("floor", (0.0, 0.0)), north()]),
];
from_compositions(&comps, &library, 1, 1.0, crate::composition::agrees)
.expect("the fixture kit learns")
}
fn region(n: usize) -> Map {
Map {
name: "constrained".into(),
bounds: (n as f32, 3.0, n as f32),
..Map::default()
}
}
fn measured(faces: &[Option<Interface>], solved: &Solved) -> crate::range::Measured {
let f = || crate::range::Faces::new(faces, WALL_TOKEN, WALKABLE_GAP);
crate::range::measure(
solved.width,
solved.height,
&solved.grid,
|p, d| f().wall(p, d),
|p| f().floor(p),
|p| f().doorway(p),
)
.expect("a solved grid measures")
}
#[test]
fn a_hard_wish_makes_rooms_the_metric_agrees_are_rooms() {
let Composed { grammar: g, faces, .. } = site_kit();
let want = 0.25;
let wishes = Wishes { enclosure: want, enclosure_weight: None };
let solved = solve_constrained(®ion(12), &g, &faces, &wishes, 1.0, 7, ids())
.expect("a hard, reachable wish must be granted");
let m = measured(&faces, &solved);
assert!(m.enclosure >= want, "asked {want}, the metric reports {:.3}", m.enclosure);
assert!(m.regions > 0, "enclosure above zero means at least one enclosed region");
assert_eq!(solved.unmet, 0, "a hard wish is met or refused, never charged for");
assert_eq!(solved.grid.len(), 144, "the grid comes back whole");
assert!(!solved.stamps.is_empty(), "a composition grammar writes stamps");
assert!(solved.placements.is_empty(), "and no loose placements");
}
#[test]
fn without_a_wish_the_same_region_stays_open() {
let Composed { grammar: g, faces, .. } = site_kit();
let wishes = Wishes { enclosure: 0.0, enclosure_weight: None };
let solved = solve_constrained(®ion(12), &g, &faces, &wishes, 1.0, 7, ids())
.expect("solves");
assert_eq!(measured(&faces, &solved).regions, 0, "nothing asked for, nothing closed");
}
#[test]
fn an_impossible_wish_is_charged_for_rather_than_refused_when_soft() {
let Composed { grammar: g, faces, .. } = site_kit();
let wishes = Wishes { enclosure: 1.0, enclosure_weight: Some(200) };
let solved = solve_constrained(®ion(12), &g, &faces, &wishes, 1.0, 3, ids())
.expect("a soft wish must not refuse");
assert_eq!(solved.unmet, 200, "the wish it could not grant costs exactly its weight");
assert_eq!(solved.grid.len(), 144);
}
#[test]
fn an_impossible_wish_refuses_by_name_when_hard() {
let Composed { grammar: g, faces, .. } = site_kit();
let wishes = Wishes { enclosure: 1.0, enclosure_weight: None };
let err = solve_constrained(®ion(12), &g, &faces, &wishes, 1.0, 3, ids())
.expect_err("total enclosure is unreachable");
assert!(err.contains("no arrangement"), "the refusal must say what happened: {err}");
}
#[test]
fn a_constrained_solve_is_reproducible_for_a_seed() {
let Composed { grammar: g, faces, .. } = site_kit();
let wishes = Wishes { enclosure: 0.2, enclosure_weight: Some(200) };
let once = || {
solve_constrained(®ion(10), &g, &faces, &wishes, 1.0, 4242, ids())
.expect("solves")
.grid
};
let first = once();
for round in 1..4 {
assert_eq!(once(), first, "round {round} produced a different world from one seed");
}
}
#[test]
fn different_seeds_give_different_worlds() {
let Composed { grammar: g, faces, .. } = site_kit();
let wishes = Wishes { enclosure: 0.2, enclosure_weight: Some(200) };
let grids: Vec<Vec<usize>> = (1..=5u64)
.map(|s| {
solve_constrained(®ion(10), &g, &faces, &wishes, 1.0, s, ids())
.expect("solves")
.grid
})
.collect();
let distinct: std::collections::BTreeSet<&Vec<usize>> = grids.iter().collect();
assert_eq!(distinct.len(), 5, "five seeds must give five arrangements");
}
#[test]
fn interfaces_that_do_not_match_the_grammar_are_refused() {
let Composed { grammar: g, mut faces, .. } = site_kit();
faces.pop();
let wishes = Wishes { enclosure: 0.2, enclosure_weight: None };
let err = solve_constrained(®ion(8), &g, &faces, &wishes, 1.0, 1, ids())
.expect_err("a short interface list must be refused");
assert!(err.contains("interfaces"), "the refusal must name the mismatch: {err}");
}
#[test]
fn swapping_the_rule_changes_the_learned_grammar() {
let comps = vec![empty_tile("tile_a"), empty_tile("tile_b")];
let library = Library {
version: crate::library::LIBRARY_VERSION,
note: None,
descriptors: Vec::new(),
};
let Composed { grammar: permissive, skipped, .. } =
from_compositions(&comps, &library, 1, 1.0, |_, _, _| true).expect("learns");
assert!(skipped.is_empty(), "both tiles are one cell and bounded: {skipped:?}");
assert_eq!(permissive.len(), 3, "Empty plus the two tiles");
let refusing = from_compositions(&comps, &library, 1, 1.0, |_, _, _| false)
.expect("learns")
.grammar;
assert_eq!(
permissive.support[N][0], refusing.support[N][0],
"Empty's row must not depend on the adjacency rule"
);
assert_ne!(
permissive.support[N][1], refusing.support[N][1],
"the rule is a parameter and this proves it: an inline comparison would give one answer"
);
assert_eq!(
refusing.support[N][1] & 0b110,
0,
"a rule that refuses everything must leave no tile-to-tile support"
);
}
#[test]
fn the_adjacency_rule_is_a_parameter_and_swapping_it_changes_the_grammar() {
let wall = iface(Some("wall"));
let floor = iface(None);
assert!(crate::composition::agrees(&wall, &wall, N));
assert!(crate::composition::agrees(&floor, &floor, N));
assert!(
!crate::composition::agrees(&wall, &floor, N),
"`None` is a token in its own right and matches only `None`"
);
let blank = crate::composition::Interface { faces: Default::default(), faults: Vec::new() };
assert!(crate::composition::agrees(&blank, &wall, N));
}
fn map_with(bounds: (f32, f32, f32), pieces: &[(&str, f32, f32, f32, bool)]) -> Map {
let mut m = Map {
name: "example".into(),
bounds,
..Map::default()
};
for (i, (d, x, z, yaw, owned)) in pieces.iter().enumerate() {
m.placements.push(Placed {
id: format!("p{i}"),
descriptor: (*d).to_owned(),
at: (*x, *z),
yaw: *yaw,
owned: *owned,
owned_because: owned.then(|| "test".to_owned()),
..Placed::default()
});
}
m
}
fn ids() -> impl FnMut() -> String {
let mut n = 0;
move || {
n += 1;
format!("g{n}")
}
}
#[test]
fn an_empty_map_teaches_only_emptiness() {
let g = learn(&map_with((4.0, 3.0, 4.0), &[]), 1.0).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(g.prototypes, vec![Prototype::Empty]);
let err = solve(&map_with((4.0, 3.0, 4.0), &[]), &g, 1.0, 1, ids())
.err()
.unwrap_or_default();
assert!(err.contains("nothing has been placed"), "{err}");
}
#[test]
fn the_same_piece_at_two_yaws_is_two_prototypes() {
let m = map_with(
(4.0, 3.0, 4.0),
&[("wall", -1.5, -1.5, 0.0, false), ("wall", -0.5, -1.5, 90.0, false)],
);
let g = learn(&m, 1.0).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(g.len(), 3, "empty + two yaws: {:?}", g.prototypes);
}
#[test]
fn yaw_is_quantised_so_float_noise_is_not_a_new_prototype() {
let m = map_with(
(4.0, 3.0, 4.0),
&[("wall", -1.5, -1.5, 90.0, false), ("wall", -0.5, -1.5, 90.0001, false)],
);
assert_eq!(learn(&m, 1.0).unwrap_or_else(|e| panic!("{e}")).len(), 2);
}
#[test]
fn an_owned_cell_survives_the_solve() {
let m = map_with(
(4.0, 3.0, 4.0),
&[
("floor", -1.5, -1.5, 0.0, true),
("floor", -0.5, -1.5, 0.0, false),
("floor", -1.5, -0.5, 0.0, false),
],
);
let g = learn(&m, 1.0).unwrap_or_else(|e| panic!("{e}"));
let s = solve(&m, &g, 1.0, 7, ids()).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(s.owned_cells, 1);
assert!(
!s.placements.iter().any(|p| p.at == (-1.5, -1.5)),
"the owned cell was overwritten"
);
assert_eq!(s.free_cells, 15, "4x4 grid minus the one pinned cell");
}
#[test]
fn the_solver_only_places_what_the_example_contains() {
let m = map_with(
(4.0, 3.0, 4.0),
&[("floor", -1.5, -1.5, 0.0, false), ("floor", -0.5, -1.5, 0.0, false)],
);
let g = learn(&m, 1.0).unwrap_or_else(|e| panic!("{e}"));
let s = solve(&m, &g, 1.0, 3, ids()).unwrap_or_else(|e| panic!("{e}"));
for p in &s.placements {
assert_eq!(p.descriptor, "floor", "invented a piece nobody placed");
}
}
#[test]
fn empty_can_meet_empty_so_a_sparse_map_stays_sparse() {
let m = map_with((6.0, 3.0, 6.0), &[("crate", 0.5, 0.5, 0.0, true)]);
let g = learn(&m, 1.0).unwrap_or_else(|e| panic!("{e}"));
let s = solve(&m, &g, 1.0, 11, ids()).unwrap_or_else(|e| panic!("{e}"));
assert!(
s.placements.len() < s.free_cells,
"every free cell was filled ({} of {}) — the grammar cannot express empty space",
s.placements.len(),
s.free_cells
);
}
#[test]
fn a_solve_is_reproducible_for_a_seed() {
let m = map_with(
(5.0, 3.0, 5.0),
&[("floor", -1.5, -1.5, 0.0, false), ("wall", -0.5, -1.5, 90.0, false)],
);
let g = learn(&m, 1.0).unwrap_or_else(|e| panic!("{e}"));
let a = solve(&m, &g, 1.0, 42, ids()).unwrap_or_else(|e| panic!("{e}"));
let b = solve(&m, &g, 1.0, 42, ids()).unwrap_or_else(|e| panic!("{e}"));
let key = |s: &Solved| -> Vec<(String, i64, i64)> {
s.placements
.iter()
.map(|p| (p.descriptor.clone(), (p.at.0 * 100.0) as i64, (p.at.1 * 100.0) as i64))
.collect()
};
assert_eq!(key(&a), key(&b));
}
#[test]
fn an_owned_piece_outside_the_grammar_is_refused() {
let example = map_with((4.0, 3.0, 4.0), &[("floor", -1.5, -1.5, 0.0, false)]);
let g = learn(&example, 1.0).unwrap_or_else(|e| panic!("{e}"));
let other = map_with((4.0, 3.0, 4.0), &[("statue", -0.5, -0.5, 0.0, true)]);
let err = solve(&other, &g, 1.0, 1, ids()).err().unwrap_or_default();
assert!(err.contains("not in the learned grammar"), "{err}");
}
#[test]
fn a_zero_cell_is_refused_rather_than_looping() {
assert!(learn(&map_with((4.0, 3.0, 4.0), &[]), 0.0).is_err());
assert!(learn(&map_with((4.0, 3.0, 4.0), &[]), f32::NAN).is_err());
}
#[test]
fn too_many_prototypes_is_a_named_error() {
let pieces: Vec<(String, f32, f32, f32, bool)> = (0..MAX_PROTOTYPES + 4)
.map(|i| {
(
format!("piece_{i}"),
-19.5 + i as f32,
-19.5,
0.0,
false,
)
})
.collect();
let refs: Vec<(&str, f32, f32, f32, bool)> = pieces
.iter()
.map(|(d, x, z, y, o)| (d.as_str(), *x, *z, *y, *o))
.collect();
let err = learn(&map_with((40.0, 3.0, 40.0), &refs), 1.0)
.err()
.unwrap_or_default();
assert!(err.contains(&MAX_PROTOTYPES.to_string()), "{err}");
}
}
fn turn_composition(c: &Composition, yaw: f32) -> Composition {
let mut out = c.clone();
if let Envelope::Bounded { size } = out.envelope {
let quarter = ((yaw / 90.0).round() as i32).rem_euclid(4);
if quarter % 2 == 1 {
out.envelope = Envelope::Bounded { size: (size.2, size.1, size.0) };
}
}
for m in &mut out.members {
m.at = crate::composition::rotate_xz(m.at, yaw);
m.yaw = crate::composition::add_yaw(m.yaw, yaw);
}
out
}
#[derive(Debug)]
pub struct Composed {
pub grammar: Grammar,
pub skipped: Vec<String>,
pub faces: Vec<Option<Interface>>,
}
pub fn from_compositions(
compositions: &[Composition],
library: &Library,
per_tile: u32,
cell: f32,
agrees: impl Fn(&Interface, &Interface, Dir) -> bool,
) -> Result<Composed, String> {
if per_tile == 0 {
return Err("composition grammar: the project divides each tile 0 ways".to_owned());
}
if !(cell.is_finite() && cell > 0.0) {
return Err(format!("composition grammar: a cell of {cell} m is not a grid"));
}
let mut prototypes = vec![Prototype::Empty];
let mut interfaces: Vec<Option<Interface>> = vec![None];
let mut weights: Vec<f64> = vec![1.0];
let mut skipped: Vec<String> = Vec::new();
for c in compositions {
let size = match c.envelope {
Envelope::Bounded { size } => size,
Envelope::Anchored => {
skipped.push(format!("`{}` is anchored, so it presents no edge to match", c.id));
continue;
}
};
if (size.0 - cell).abs() > CELL_EPSILON || (size.2 - cell).abs() > CELL_EPSILON {
skipped.push(format!(
"`{}` is {:.2} x {:.2} m and the grid is {cell:.2} m, so it cannot be a tile",
c.id, size.0, size.2
));
continue;
}
let mut seen_faces: Vec<[Vec<crate::composition::Band>; 4]> = Vec::new();
let first_proto = prototypes.len();
for quarter in 0..4u8 {
let yaw = quarter as f32 * 90.0;
let turned = turn_composition(c, yaw);
let iface = match crate::composition::interface(&turned, compositions, library, per_tile)
{
Ok(Some(i)) => i,
Ok(None) => {
if quarter == 0 {
skipped.push(format!("`{}` derives no interface", c.id));
}
continue;
}
Err(e) => return Err(format!("composition grammar: `{}`: {e}", c.id)),
};
if seen_faces.contains(&iface.faces) {
continue;
}
seen_faces.push(iface.faces.clone());
prototypes.push(Prototype::Composed { composition: c.id.clone(), yaw });
interfaces.push(Some(iface));
}
let turns = prototypes.len() - first_proto;
if turns > 0 {
let share = 1.0 / turns as f64;
for _ in 0..turns {
weights.push(share);
}
}
}
if prototypes.len() == 1 {
const SHOWN: usize = 5;
let mut why = skipped.iter().take(SHOWN).cloned().collect::<Vec<_>>().join("; ");
if skipped.len() > SHOWN {
why.push_str(&format!("; and {} more", skipped.len() - SHOWN));
}
return Err(if compositions.is_empty() {
"composition grammar: the project has no compositions, so there is no tile to solve \
with. A tile is a composition with a `Bounded` envelope one grid cell across."
.to_owned()
} else {
format!(
"composition grammar: none of the {} composition(s) is a tile, so there is nothing \
to solve with: {why}. A tile is a composition with a `Bounded` envelope {cell:.2} \
m across; an `Anchored` group is placed whole and is never one.",
compositions.len()
)
});
}
if prototypes.len() > MAX_PROTOTYPES {
return Err(format!(
"composition grammar: {} prototypes, over the {MAX_PROTOTYPES} the solver's domain \
holds. That is {} distinct turned tiles from {} composition(s) — narrow the selection \
rather than raising the cap, which is a `u32` the dungeon generator shares.",
prototypes.len(),
prototypes.len() - 1,
compositions
.iter()
.filter(|c| matches!(c.envelope, crate::composition::Envelope::Bounded { .. }))
.count()
));
}
let n = prototypes.len();
let mut support: [Vec<u32>; 4] = [vec![0; n], vec![0; n], vec![0; n], vec![0; n]];
for (p, pi) in interfaces.iter().enumerate() {
for (q, qi) in interfaces.iter().enumerate() {
for dir in [N, E, S, W] {
let ok = match (pi, qi) {
(None, _) | (_, None) => true,
(Some(a), Some(b)) => agrees(a, b, dir),
};
if ok {
support[dir][p] |= 1 << q;
}
}
}
}
Ok(Composed {
grammar: Grammar {
prototypes,
weights,
support,
},
skipped,
faces: interfaces,
})
}
pub fn declared(library: &Library, per_tile: u32, cell: f32) -> Result<Grammar, String> {
if per_tile == 0 {
return Err("declared grammar: the project divides each tile 0 ways".to_owned());
}
if !(cell.is_finite() && cell > 0.0) {
return Err(format!("declared grammar: a cell of {cell} m is not a grid"));
}
let mut wrong_size: Vec<String> = Vec::new();
let mut prototypes = vec![Prototype::Empty];
let mut faces: Vec<[Vec<Option<String>>; 4]> = vec![Default::default()];
for d in &library.descriptors {
let Some(grid) = &d.subgrid else { continue };
if !grid.cells.iter().any(|c| c.edge.is_some()) {
continue;
}
let fits = crate::descriptor::placed_footprint(d).is_some_and(|(w, dep)| {
(w - cell).abs() <= CELL_EPSILON && (dep - cell).abs() <= CELL_EPSILON
});
if !fits {
let size = crate::descriptor::placed_footprint(d)
.map(|(w, dep)| format!("{w} x {dep} m"))
.unwrap_or_else(|| "unmeasured".to_owned());
wrong_size.push(format!("`{}` is {size}", d.id));
continue;
}
let div = crate::descriptor::divisions(d, per_tile)?;
let mine = faces.len();
for quarter in 0..4u8 {
let turned = grid.rotated(quarter, div);
let tdiv = crate::descriptor::rotate_div(div, quarter);
let read = |dir: usize| -> Vec<Option<String>> {
crate::adjacency::face(&turned, dir, tdiv)
.into_iter()
.map(|t| t.map(str::to_owned))
.collect()
};
let sig = [read(N), read(E), read(S), read(W)];
if faces[mine..].contains(&sig) {
continue;
}
prototypes.push(Prototype::Piece {
descriptor: d.id.clone(),
yaw: quarter as f32 * 90.0,
});
faces.push(sig);
}
}
if prototypes.len() == 1 {
if !wrong_size.is_empty() {
wrong_size.sort_unstable();
return Err(format!(
"declared grammar: every tokened piece is the wrong size for a {cell} m cell, so a \
solve would place them at a spacing unrelated to their extents — {}. A tile grammar \
needs tiles of the grid's size; generate from the map instead, or author a kit on \
the cell.",
wrong_size.join(", ")
));
}
return Err(
"declared grammar: no descriptor in this library carries an edge token, so there is \
nothing to build a grammar from. Author tokens on the Tiles tab, or generate from the \
map instead."
.to_owned(),
);
}
if prototypes.len() > MAX_PROTOTYPES {
return Err(format!(
"declared grammar: {} prototypes, over the {MAX_PROTOTYPES} the solver's domain holds. \
That is {} distinct turned tiles from {} tokened descriptor(s) — narrow the kit rather \
than raising the cap, which is a `u32` the dungeon generator shares.",
prototypes.len(),
prototypes.len() - 1,
library
.descriptors
.iter()
.filter(|d| d
.subgrid
.as_ref()
.is_some_and(|g| g.cells.iter().any(|c| c.edge.is_some())))
.count()
));
}
let n = prototypes.len();
let weights = vec![1.0f64; n];
let mut support: [Vec<u32>; 4] = [vec![0; n], vec![0; n], vec![0; n], vec![0; n]];
for (p, pf) in faces.iter().enumerate() {
for (q, qf) in faces.iter().enumerate() {
for (dir, opposite) in [(N, S), (E, W), (S, N), (W, E)] {
let ok = p == 0 || q == 0 || pf[dir] == qf[opposite];
if ok {
support[dir][p] |= 1 << q;
}
}
}
}
Ok(Grammar {
prototypes,
weights,
support,
})
}