use oxygengine_utils::{grid_2d::Grid2d, Scalar};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use std::{
collections::{HashSet, VecDeque},
iter::FromIterator,
};
const NEIGHBOR_COORD_DIRS: [Direction; 4] = [
Direction::Left,
Direction::Right,
Direction::Top,
Direction::Bottom,
];
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Direction {
Left,
Right,
Top,
Bottom,
}
#[derive(Debug, Clone, Copy)]
pub enum WaveFunctionCollapseError {
FoundPatternWithZeroFrequency(usize),
FoundEmptyPattern(usize),
SuperpositionCellHasNoPattern(usize, usize),
FoundUncollapsedCell,
FoundImpossibleInitialState,
BuilderInProgress,
}
#[derive(Debug, Clone)]
pub enum WaveFunctionCollapseResult<T> {
Incomplete,
Collapsed(Grid2d<T>),
Impossible,
}
#[derive(Debug, Default, Clone)]
pub struct WaveFunctionCollapseModel<T>
where
T: Clone + Send + Sync + PartialEq,
{
patterns: Vec<(Grid2d<T>, Scalar)>,
neighbors: Vec<HashSet<(usize, Direction)>>,
}
impl<T> WaveFunctionCollapseModel<T>
where
T: Clone + Send + Sync + PartialEq,
{
pub fn from_patterns(
patterns: Vec<(Grid2d<T>, usize)>,
) -> Result<Self, WaveFunctionCollapseError> {
for (i, (p, f)) in patterns.iter().enumerate() {
if *f == 0 {
return Err(WaveFunctionCollapseError::FoundPatternWithZeroFrequency(i));
} else if p.is_empty() {
return Err(WaveFunctionCollapseError::FoundEmptyPattern(i));
}
}
let total = patterns.iter().fold(0, |a, (_, f)| a + f) as Scalar;
let mut unique = Vec::with_capacity(patterns.len());
for (p, f) in patterns {
if let Some((_, f2)) = unique.iter_mut().find(|(p2, _)| &p == p2) {
*f2 += f;
} else {
unique.push((p, f));
}
}
let patterns = unique
.into_iter()
.map(|(p, f)| (p, f as Scalar / total))
.collect::<Vec<_>>();
let mut neighbors = vec![HashSet::default(); patterns.len()];
for (ai, (ap, _)) in patterns.iter().enumerate() {
let (ac, ar) = ap.size();
for (bi, (bp, _)) in patterns.iter().enumerate() {
let (bc, br) = bp.size();
if ar == br {
if bp.has_union_with(ap, 1, 0) {
neighbors[ai].insert((bi, Direction::Left));
neighbors[bi].insert((ai, Direction::Right));
}
if ap.has_union_with(bp, 1, 0) {
neighbors[ai].insert((bi, Direction::Right));
neighbors[bi].insert((ai, Direction::Left));
}
}
if ac == bc {
if bp.has_union_with(ap, 0, 1) {
neighbors[ai].insert((bi, Direction::Top));
neighbors[bi].insert((ai, Direction::Bottom));
}
if ap.has_union_with(bp, 0, 1) {
neighbors[ai].insert((bi, Direction::Bottom));
neighbors[bi].insert((ai, Direction::Top));
}
}
}
}
Ok(Self {
patterns,
neighbors,
})
}
pub fn from_views(
sample_size: (usize, usize),
seamless: bool,
views: Vec<Grid2d<Option<T>>>,
) -> Result<Self, WaveFunctionCollapseError> {
let f = |w: Grid2d<&Option<T>>| {
let items = w
.iter()
.filter_map(|c| {
if let Some(c) = c {
Some(c.clone())
} else {
None
}
})
.collect::<Vec<_>>();
if items.len() == w.len() {
Some((Grid2d::with_cells(w.cols(), items), 1))
} else {
None
}
};
let patterns = views
.into_iter()
.flat_map(|view| {
if seamless {
view.windows_seamless(sample_size)
.filter_map(f)
.collect::<Vec<_>>()
} else {
view.windows(sample_size).filter_map(f).collect::<Vec<_>>()
}
})
.collect();
Self::from_patterns(patterns)
}
pub fn patterns(&self) -> &[(Grid2d<T>, Scalar)] {
&self.patterns
}
pub fn neighbors(&self) -> &[HashSet<(usize, Direction)>] {
&self.neighbors
}
}
#[derive(Debug, Clone)]
struct Cell {
patterns: HashSet<usize>,
entropy: Scalar,
}
#[derive(Debug, Clone, Copy)]
enum BuilderPhase {
Process(usize),
Done,
Error(WaveFunctionCollapseError),
}
#[derive(Clone)]
pub struct WaveFunctionCollapseSolverBuilder<T>
where
T: Clone + Send + Sync + PartialEq,
{
model: WaveFunctionCollapseModel<T>,
superposition: [Grid2d<Cell>; 2],
current: usize,
phase: BuilderPhase,
cells_per_step: usize,
}
impl<T> WaveFunctionCollapseSolverBuilder<T>
where
T: Clone + Send + Sync + PartialEq,
{
fn new(
model: WaveFunctionCollapseModel<T>,
superposition: Grid2d<Vec<T>>,
cells_per_step: Option<usize>,
) -> Result<Self, WaveFunctionCollapseError> {
let (cols, rows) = superposition.size();
let cells = superposition
.iter_view((0, 0)..(cols, rows))
.map(|(col, row, cells)| {
let patterns = cells
.iter()
.flat_map(|cell| {
model
.patterns()
.iter()
.enumerate()
.filter_map(|(index, (pattern, _))| {
let pattern_cell = pattern.cell(0, 0).unwrap();
if cell == pattern_cell {
Some(index)
} else {
None
}
})
.collect::<HashSet<_>>()
})
.collect::<HashSet<_>>();
if patterns.is_empty() {
Err(WaveFunctionCollapseError::SuperpositionCellHasNoPattern(
col, row,
))
} else {
let entropy = calculate_entropy(&model, &patterns);
Ok(Cell { patterns, entropy })
}
})
.collect::<Result<Vec<_>, _>>()?;
let max_patterns = cells
.iter()
.map(|cell| cell.patterns.len())
.max_by(|a, b| a.cmp(&b))
.unwrap_or(1);
let cells_per_step = if let Some(cells_per_step) = cells_per_step {
cells_per_step
} else if max_patterns > 0 {
cells.len() / max_patterns
} else {
cells.len()
}
.max(1);
let superposition = Grid2d::with_cells(cols, cells);
Ok(Self {
model,
superposition: [superposition.clone(), superposition],
current: 0,
phase: BuilderPhase::Process(0),
cells_per_step,
})
}
pub fn process(&mut self) -> bool {
match self.phase {
BuilderPhase::Done | BuilderPhase::Error(_) => false,
BuilderPhase::Process(mut index) => {
let mut remaining = self.cells_per_step;
let mut reduced = false;
let cols = self.source().cols();
let rows = self.source().rows();
let count = self.source().len();
while index < count && remaining > 0 {
let col = index % cols;
let row = index / cols;
let patterns = &self.source().cell(col, row).unwrap().patterns;
let count = patterns.len();
match count {
0 | 1 => {
let cell = Cell {
patterns: patterns.clone(),
entropy: 0.0,
};
self.target().set(col, row, cell)
}
_ => {
let samples = [
self.source().cell((cols + col - 1) % cols, row).unwrap(),
self.source().cell((col + 1) % cols, row).unwrap(),
self.source().cell(col, (rows + row - 1) % rows).unwrap(),
self.source().cell(col, (row + 1) % rows).unwrap(),
];
#[cfg(not(feature = "parallel"))]
let patterns = patterns.iter();
#[cfg(feature = "parallel")]
let patterns = patterns.par_iter();
let patterns = patterns
.filter(|index| {
let neighbors = self.model.neighbors().get(**index).unwrap();
if neighbors.is_empty() {
return false;
}
NEIGHBOR_COORD_DIRS.iter().enumerate().all(|(i, d)| {
samples[i].patterns.iter().any(|n| {
neighbors.iter().any(|(neighbor, direction)| {
direction == d && neighbor == n
})
})
})
})
.cloned()
.collect::<HashSet<_>>();
if patterns.is_empty() {
self.phase = BuilderPhase::Error(
WaveFunctionCollapseError::FoundImpossibleInitialState,
);
return false;
} else if patterns.len() < count {
reduced = true;
}
let entropy = calculate_entropy(&self.model, &patterns);
self.target().set(col, row, Cell { patterns, entropy });
}
}
index += 1;
remaining -= 1;
}
if index == count {
if reduced {
self.phase = BuilderPhase::Process(0);
self.current = (self.current + 1) % 2;
true
} else {
self.phase = BuilderPhase::Done;
false
}
} else {
self.phase = BuilderPhase::Process(index);
true
}
}
}
}
pub fn progress(&self) -> (usize, usize) {
let count = self.source().len();
match self.phase {
BuilderPhase::Done | BuilderPhase::Error(_) => (count, count),
BuilderPhase::Process(index) => (index, count),
}
}
pub fn build(self) -> Result<WaveFunctionCollapseSolver<T>, WaveFunctionCollapseError> {
match self.phase {
BuilderPhase::Error(error) => Err(error),
BuilderPhase::Done => {
let count = self.source().len();
Ok(WaveFunctionCollapseSolver {
superposition: self.source().clone(),
model: self.model,
cached_progress: 0,
cached_open: VecDeque::with_capacity(count),
lately_updated: HashSet::with_capacity(count),
})
}
BuilderPhase::Process(_) => Err(WaveFunctionCollapseError::BuilderInProgress),
}
}
fn source(&self) -> &Grid2d<Cell> {
&self.superposition[self.current]
}
fn target(&mut self) -> &mut Grid2d<Cell> {
&mut self.superposition[(self.current + 1) % 2]
}
}
#[derive(Clone)]
pub struct WaveFunctionCollapseSolver<T>
where
T: Clone + Send + Sync + PartialEq,
{
model: WaveFunctionCollapseModel<T>,
superposition: Grid2d<Cell>,
cached_progress: usize,
cached_open: VecDeque<(usize, usize)>,
lately_updated: HashSet<(usize, usize)>,
}
impl<T> std::fmt::Debug for WaveFunctionCollapseSolver<T>
where
T: Clone + Send + Sync + PartialEq + std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WaveFunctionCollapseSolver")
.field("model", &self.model)
.field("superposition", &self.superposition)
.field("cached_progress", &self.cached_progress)
.field("cached_open", &self.cached_open)
.field("lately_updated", &self.lately_updated)
.finish()
}
}
impl<T> WaveFunctionCollapseSolver<T>
where
T: Clone + Send + Sync + PartialEq,
{
pub fn lately_updated(&self) -> &HashSet<(usize, usize)> {
&self.lately_updated
}
pub fn lately_updated_uncollapsed_cells<V>(&self) -> Vec<(usize, usize, V)>
where
V: FromIterator<T>,
{
self.lately_updated
.iter()
.filter_map(|(col, row)| {
self.superposition.cell(*col, *row).map(|cell| {
let items = cell
.patterns
.iter()
.map(|index| self.model.patterns()[*index].0.cell(0, 0).unwrap().clone())
.collect::<V>();
(*col, *row, items)
})
})
.collect()
}
pub fn lately_updated_collapsed_cells(&self) -> Vec<(usize, usize, T)> {
self.lately_updated
.iter()
.filter_map(|(col, row)| {
if let Some(cell) = self.superposition.cell(*col, *row) {
if cell.patterns.len() == 1 {
let index = *cell.patterns.iter().next().unwrap();
let item = self.model.patterns()[index].0.cell(0, 0).unwrap().clone();
Some((*col, *row, item))
} else {
None
}
} else {
None
}
})
.collect()
}
pub fn build(
model: WaveFunctionCollapseModel<T>,
superposition: Grid2d<Vec<T>>,
cells_per_step: Option<usize>,
) -> Result<WaveFunctionCollapseSolverBuilder<T>, WaveFunctionCollapseError> {
WaveFunctionCollapseSolverBuilder::new(model, superposition, cells_per_step)
}
pub fn new(
model: WaveFunctionCollapseModel<T>,
superposition: Grid2d<Vec<T>>,
) -> Result<Self, WaveFunctionCollapseError> {
let count = superposition.len();
let mut builder = Self::build(model, superposition, Some(count))?;
while builder.process() {}
builder.build()
}
pub fn new_inspect<F>(
model: WaveFunctionCollapseModel<T>,
superposition: Grid2d<Vec<T>>,
cells_per_step: Option<usize>,
mut f: F,
) -> Result<Self, WaveFunctionCollapseError>
where
F: FnMut(usize, usize),
{
let mut builder = Self::build(model, superposition, cells_per_step)?;
let (p, m) = builder.progress();
f(p, m);
while builder.process() {
let (p, m) = builder.progress();
f(p, m);
}
let (p, m) = builder.progress();
f(p, m);
builder.build()
}
pub fn collapse<R>(&mut self, gen_range: R) -> WaveFunctionCollapseResult<T>
where
R: FnMut(Scalar, Scalar) -> Scalar + Clone,
{
loop {
match self.collapse_step(gen_range.clone()) {
WaveFunctionCollapseResult::Incomplete => continue,
result => return result,
}
}
}
pub fn collapse_with_tries<R>(
&mut self,
mut tries: usize,
gen_range: R,
) -> WaveFunctionCollapseResult<T>
where
R: FnMut(Scalar, Scalar) -> Scalar + Clone,
{
while tries > 0 {
match self.collapse(gen_range.clone()) {
WaveFunctionCollapseResult::Impossible => {
tries -= 1;
continue;
}
result => return result,
}
}
WaveFunctionCollapseResult::Impossible
}
pub fn collapse_inspect<R, F>(
&mut self,
gen_range: R,
mut f: F,
) -> WaveFunctionCollapseResult<T>
where
F: FnMut(usize, usize, &Self),
R: FnMut(Scalar, Scalar) -> Scalar + Clone,
{
loop {
match self.collapse_step(gen_range.clone()) {
WaveFunctionCollapseResult::Incomplete => {
let (p, m) = self.progress();
f(p, m, self);
continue;
}
result => return result,
}
}
}
pub fn collapse_inspect_with_tries<R, F>(
&mut self,
mut tries: usize,
gen_range: R,
mut f: F,
) -> WaveFunctionCollapseResult<T>
where
F: FnMut() -> Box<dyn FnMut(usize, usize, &Self)>,
R: FnMut(Scalar, Scalar) -> Scalar + Clone,
{
while tries > 0 {
match self.collapse_inspect(gen_range.clone(), f()) {
WaveFunctionCollapseResult::Impossible => {
tries -= 1;
continue;
}
result => return result,
}
}
WaveFunctionCollapseResult::Impossible
}
pub fn collapse_step<R>(&mut self, gen_range: R) -> WaveFunctionCollapseResult<T>
where
R: FnMut(Scalar, Scalar) -> Scalar,
{
let coord = if let Ok(coord) = self.get_uncollapsed_coord() {
coord
} else {
return WaveFunctionCollapseResult::Impossible;
};
let (col, row) = if let Some(coord) = coord {
coord
} else if let Ok(collapsed) =
Self::superposition_to_collapsed_world(&self.model, &self.superposition)
{
return WaveFunctionCollapseResult::Collapsed(collapsed);
} else {
return WaveFunctionCollapseResult::Impossible;
};
self.lately_updated.clear();
if !self.collapse_cell(col, row, gen_range) {
return WaveFunctionCollapseResult::Impossible;
}
self.lately_updated.insert((col, row));
let (cols, rows) = self.superposition.size();
self.cached_open.push_back(((col + cols - 1) % cols, row));
self.cached_open.push_back(((col + 1) % cols, row));
self.cached_open.push_back((col, (row + rows - 1) % rows));
self.cached_open.push_back((col, (row + 1) % rows));
while !self.cached_open.is_empty() {
self.partially_reduce_superposition();
}
self.cached_progress =
self.superposition
.iter()
.fold(0, |a, c| if c.patterns.len() == 1 { a + 1 } else { a });
WaveFunctionCollapseResult::Incomplete
}
pub fn progress(&self) -> (usize, usize) {
(self.cached_progress, self.superposition.len())
}
fn collapse_cell<R>(&mut self, col: usize, row: usize, mut gen_range: R) -> bool
where
R: FnMut(Scalar, Scalar) -> Scalar,
{
let patterns = self.model.patterns();
let cell = self.superposition.cell(col, row).unwrap();
let total = cell
.patterns
.iter()
.fold(0.0, |accum, index| accum + patterns[*index].1);
let mut selected = gen_range(0.0, total);
for index in cell.patterns.iter() {
let weight = patterns[*index].1;
if selected <= weight {
let mut patterns = HashSet::with_capacity(1);
patterns.insert(*index);
self.superposition.set(
col,
row,
Cell {
patterns,
entropy: 0.0,
},
);
return true;
}
selected -= weight;
}
false
}
fn get_uncollapsed_coord(&self) -> Result<Option<(usize, usize)>, ()> {
if self
.superposition
.iter()
.any(|cell| cell.patterns.is_empty())
{
return Err(());
}
let cols = self.superposition.cols();
let result = {
let mut result = None;
for (index, cell) in self.superposition.iter().enumerate() {
let col = index % cols;
let row = index / cols;
if cell.patterns.len() > 1 {
if let Some((_, _, entropy)) = result {
if cell.entropy < entropy {
result = Some((col, row, cell.entropy));
}
} else {
result = Some((col, row, cell.entropy));
}
}
}
result
};
Ok(result.map(|(col, row, _)| (col, row)))
}
fn partially_reduce_superposition(&mut self) {
if self.cached_open.is_empty() {
return;
}
let (col, row) = self.cached_open.pop_front().unwrap();
let (cols, rows) = self.superposition.size();
let patterns = &self.superposition.cell(col, row).unwrap().patterns;
let count = patterns.len();
if count > 1 {
let samples = [
self.superposition
.cell((cols + col - 1) % cols, row)
.unwrap(),
self.superposition.cell((col + 1) % cols, row).unwrap(),
self.superposition
.cell(col, (rows + row - 1) % rows)
.unwrap(),
self.superposition.cell(col, (row + 1) % rows).unwrap(),
];
let neighbors = self.model.neighbors();
#[cfg(not(feature = "parallel"))]
let patterns = patterns.iter();
#[cfg(feature = "parallel")]
let patterns = patterns.par_iter();
let patterns = patterns
.filter(|index| {
let neighbors = neighbors.get(**index).unwrap();
if neighbors.is_empty() {
return false;
}
NEIGHBOR_COORD_DIRS.iter().enumerate().all(|(i, d)| {
samples[i].patterns.iter().any(|n| {
neighbors
.iter()
.any(|(neighbor, direction)| direction == d && neighbor == n)
})
})
})
.cloned()
.collect::<HashSet<_>>();
if patterns.len() < count {
self.lately_updated.insert((col, row));
let coord = ((col + cols - 1) % cols, row);
if samples[0].patterns.len() > 1 && !self.cached_open.contains(&coord) {
self.cached_open.push_back(coord);
}
let coord = ((col + 1) % cols, row);
if samples[1].patterns.len() > 1 && !self.cached_open.contains(&coord) {
self.cached_open.push_back(coord);
}
let coord = (col, (row + rows - 1) % rows);
if samples[2].patterns.len() > 1 && !self.cached_open.contains(&coord) {
self.cached_open.push_back(coord);
}
let coord = (col, (row + 1) % rows);
if samples[3].patterns.len() > 1 && !self.cached_open.contains(&coord) {
self.cached_open.push_back(coord);
}
let entropy = calculate_entropy(&self.model, &patterns);
self.superposition.set(col, row, Cell { patterns, entropy });
}
}
}
pub fn get_uncollapsed_world(&self) -> Grid2d<Vec<T>> {
let cols = self.superposition.cols();
let cells = self
.superposition
.iter()
.map(|cell| {
cell.patterns
.iter()
.map(|index| self.model.patterns()[*index].0.cell(0, 0).unwrap().clone())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
Grid2d::with_cells(cols, cells)
}
fn superposition_to_collapsed_world(
model: &WaveFunctionCollapseModel<T>,
superposition: &Grid2d<Cell>,
) -> Result<Grid2d<T>, WaveFunctionCollapseError> {
let cols = superposition.cols();
let cells = superposition
.iter()
.map(|cell| {
if cell.patterns.len() == 1 {
let index = cell.patterns.iter().next().unwrap();
Ok(model.patterns()[*index].0.cell(0, 0).unwrap().clone())
} else {
Err(WaveFunctionCollapseError::FoundUncollapsedCell)
}
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Grid2d::with_cells(cols, cells))
}
}
fn calculate_entropy<T>(model: &WaveFunctionCollapseModel<T>, patterns: &HashSet<usize>) -> Scalar
where
T: Clone + Send + Sync + PartialEq,
{
if patterns.is_empty() {
return 0.0;
}
let mut total_weight = 0.0;
let mut total_weight_log = 0.0;
for index in patterns {
let weight = model.patterns()[*index].1;
total_weight += weight;
total_weight_log += weight * weight.log2();
}
total_weight.log2() - (total_weight_log / total_weight)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{thread_rng, Rng};
use std::time::Instant;
#[allow(dead_code)]
fn parse_view(data: &str) -> Grid2d<Option<char>> {
let lines = data
.split(|c| c == '\n' || c == '\r')
.filter(|l| !l.is_empty())
.collect::<Vec<_>>();
let cols = lines.iter().fold(0, |a, l| a.max(l.len()));
let rows = lines.len();
let mut result = Grid2d::new(cols, rows, None);
for (row, line) in lines.into_iter().enumerate() {
for (col, character) in line.chars().enumerate() {
if !character.is_whitespace() {
result.set(col, row, Some(character));
}
}
}
print_view("= VIEW:", &result);
result
}
#[allow(dead_code)]
fn print_view(msg: &str, pattern: &Grid2d<Option<char>>) {
println!("{}", msg);
for row in 0..pattern.rows() {
for cell in pattern.get_row_cells(row).unwrap() {
if let Some(cell) = cell {
print!("{}", cell);
} else {
print!(" ");
}
}
println!();
}
}
#[allow(dead_code)]
fn print_collapsed_world(msg: &str, world: &Grid2d<char>) {
println!("{}", msg);
for row in 0..world.rows() {
for cell in world.get_row_cells(row).unwrap() {
print!("{}", cell);
}
println!();
}
}
#[allow(dead_code)]
fn print_uncollapsed_world(msg: &str, world: &Grid2d<Vec<char>>, uncertain: char) {
println!("{}", msg);
for row in 0..world.rows() {
for cell in world.get_row_cells(row).unwrap() {
if cell.len() == 1 {
print!("{}", cell[0]);
} else {
print!("{}", uncertain);
}
}
println!();
}
}
#[allow(dead_code)]
fn print_pattern(msg: &str, pattern: &Grid2d<char>) {
println!("{}", msg);
for row in 0..pattern.rows() {
for cell in pattern.get_row_cells(row).unwrap() {
print!("{}", cell);
}
println!();
}
}
#[test]
fn test_general() {
let view = parse_view(include_str!("../resources/view.txt"));
let values = {
let mut values = view.iter().filter_map(|c| c.clone()).collect::<Vec<_>>();
values.sort();
values.dedup();
values
};
println!("= VALUES: {:?}", values);
let model = WaveFunctionCollapseModel::from_views((3, 3), true, vec![view]).unwrap();
let world = Grid2d::new(75, 75, values);
let timer = Instant::now();
let mut timer2 = Instant::now();
let mut solver = WaveFunctionCollapseSolver::new_inspect(model, world, None, |p, m| {
if timer2.elapsed().as_millis() >= 400 {
timer2 = Instant::now();
println!(
"= INITIALIZE: {} / {} ({}%)",
p,
m,
100.0 * p as Scalar / m as Scalar
);
}
})
.unwrap();
println!("= INITIALIZED IN: {:?}", timer.elapsed());
let timer = Instant::now();
let mut timer2 = Instant::now();
let mut rng = thread_rng();
let mut max_changes = 0;
let result = solver.collapse_inspect(
move |f, t| rng.gen_range(f, t),
|p, m, s| {
max_changes = max_changes.max(s.lately_updated().len());
if timer2.elapsed().as_millis() >= 400 {
timer2 = Instant::now();
println!();
println!();
print_uncollapsed_world(
"= UNCOLLAPSED WORLD:",
&s.get_uncollapsed_world(),
' ',
);
println!(
"= PROGRESS: {} / {} ({}%)",
p,
m,
100.0 * p as Scalar / m as Scalar
)
}
},
);
match result {
WaveFunctionCollapseResult::Collapsed(world) => {
println!();
println!();
println!(
"= COLLAPSED IN: {:?} | MAX CHANGES: {}",
timer.elapsed(),
max_changes
);
print_collapsed_world("= COLLAPSED WORLD:", &world);
}
_ => panic!("= IMPOSSIBLE WORLD"),
}
}
}