use rand_chacha::ChaCha8Rng;
use crate::placement::ir::{
Capabilities, Hardness, Locality, Outcome, Placement, PlacementProblem, Role, SolveError,
};
use crate::placement::solver::Solver;
use crate::rng::DetRng;
use crate::wfc::collapse_grid;
const MAX_RESTARTS: u32 = 12;
pub struct WfcSolver;
impl Solver for WfcSolver {
fn name(&self) -> &str {
"wfc"
}
fn handles(&self, role: &Role) -> bool {
matches!(role, Role::Tiled)
}
fn capabilities(&self) -> Capabilities {
Capabilities {
hardness: Hardness::Hard,
locality: Locality::Local,
cardinality: false,
deterministic: true,
needs_training_data: false,
}
}
fn solve(&self, problem: &PlacementProblem, rng: &mut ChaCha8Rng) -> Result<Outcome, SolveError> {
let tiled: Vec<usize> = problem
.candidates
.iter()
.enumerate()
.filter(|(_, c)| matches!(c.role, Role::Tiled))
.map(|(i, _)| i)
.collect();
if tiled.is_empty() {
return Ok(Outcome::Assignment(Vec::new()));
}
let region = problem.region;
let w = region.rect.width().max(0) as usize;
let h = region.rect.height().max(0) as usize;
if w == 0 || h == 0 {
return Ok(Outcome::Assignment(Vec::new()));
}
let n = tiled.len() + 1;
let mut weights = vec![1.0_f64; n];
weights[0] = 3.0 * tiled.len() as f64; let full: u32 = if n == 32 { u32::MAX } else { (1u32 << n) - 1 };
let support: [Vec<u32>; 4] = [
vec![full; n],
vec![full; n],
vec![full; n],
vec![full; n],
];
let initial = vec![full; w * h];
for _ in 0..MAX_RESTARTS {
let seed = rng.raw_u64();
if let Some(picks) = collapse_grid(w, h, &weights, &support, &initial, seed) {
let placed = picks
.iter()
.enumerate()
.filter_map(|(idx, &proto)| {
if proto == 0 {
return None; }
let candidate = tiled[proto - 1];
let cx = region.rect.min[0] + (idx % w) as i32;
let cy = region.rect.min[1] + (idx / w) as i32;
Some(Placement {
candidate,
pos: [cx as f32, 0.0, cy as f32],
yaw: 0.0,
})
})
.collect();
return Ok(Outcome::Assignment(placed));
}
}
let unsatisfied = problem.constraints.iter().map(|c| c.id).collect();
Ok(Outcome::Partial {
placed: Vec::new(),
unsatisfied,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::placement::ir::{Candidate, Dof, PropertyBag, Rect2, Region};
use crate::rng::seeded;
fn region() -> Region {
Region {
id: 0,
rect: Rect2 {
min: [3, 5],
max: [7, 9],
},
openings: Vec::new(),
adjacency: Vec::new(),
props: PropertyBag::default(),
}
}
fn tiled(asset: &str) -> Candidate {
Candidate {
asset: asset.to_string(),
role: Role::Tiled,
footprint: [1.0, 1.0],
dof: Dof::default(),
affordances: Vec::new(),
}
}
#[test]
fn placements_stay_inside_the_region_rect() {
let r = region();
let problem = PlacementProblem {
region: &r,
candidates: vec![tiled("a"), tiled("b")].into(),
constraints: Vec::new(),
};
let mut rng = seeded(7);
let Ok(Outcome::Assignment(ps)) = WfcSolver.solve(&problem, &mut rng) else {
panic!("expected an assignment from an all-compatible tiling");
};
assert!(!ps.is_empty(), "a weighted fill should place at least one tile");
for p in &ps {
assert!(
r.rect.contains([p.pos[0] as i32, p.pos[2] as i32]),
"placement {:?} escaped region {:?}",
p.pos,
r.rect
);
assert!(p.candidate < 2, "candidate index out of range");
}
}
#[test]
fn no_tiled_candidates_yields_empty_assignment() {
let r = region();
let mut freestanding = tiled("a");
freestanding.role = Role::Freestanding;
let problem = PlacementProblem {
region: &r,
candidates: vec![freestanding].into(),
constraints: Vec::new(),
};
let mut rng = seeded(7);
let out = WfcSolver.solve(&problem, &mut rng).expect("solve");
assert!(matches!(out, Outcome::Assignment(ref p) if p.is_empty()));
}
#[test]
fn deterministic_under_a_seed() {
let r = region();
let problem = PlacementProblem {
region: &r,
candidates: vec![tiled("a")].into(),
constraints: Vec::new(),
};
let run = || {
let mut rng = seeded(99);
match WfcSolver.solve(&problem, &mut rng).expect("solve") {
Outcome::Assignment(p) => p.iter().map(|p| (p.pos[0] as i32, p.pos[2] as i32)).collect::<Vec<_>>(),
_ => panic!("expected assignment"),
}
};
assert_eq!(run(), run(), "same seed must reproduce the same tiling");
}
}