From 0bc9e1a938e67c6526a0d90b85fed33a0153a13a Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Thu, 9 Dec 2021 10:31:43 -0500 Subject: [PATCH] Add a layout solver to the waveform_collapse map builder --- src/map_builders/waveform_collapse/mod.rs | 73 +++++- src/map_builders/waveform_collapse/solver.rs | 225 +++++++++++++++++++ 2 files changed, 291 insertions(+), 7 deletions(-) diff --git a/src/map_builders/waveform_collapse/mod.rs b/src/map_builders/waveform_collapse/mod.rs index f02b909..d148ee7 100644 --- a/src/map_builders/waveform_collapse/mod.rs +++ b/src/map_builders/waveform_collapse/mod.rs @@ -1,12 +1,18 @@ +mod common; +mod constraints; mod image_loader; -use image_loader::*; +mod solver; use super::common::{ generate_voronoi_spawn_regions, remove_unreachable_areas_returning_most_distant, }; use super::MapBuilder; use crate::{spawner, Map, Position, TileType, SHOW_MAPGEN_VISUALIZER}; +use common::*; +use constraints::*; +use image_loader::*; use rltk::RandomNumberGenerator; +use solver::*; use specs::prelude::*; use std::collections::HashMap; @@ -66,19 +72,42 @@ impl WaveformCollapseBuilder { fn build(&mut self) { let mut rng = RandomNumberGenerator::new(); - self.map = load_rex_map(self.depth, &rltk::XpFile::from_resource("../resources/wfc-demo1.xp").unwrap()); + const CHUNK_SIZE: i32 = 7; + + self.map = load_rex_map( + self.depth, + &rltk::XpFile::from_resource("../resources/wfc-demo2.xp").unwrap(), + ); self.take_snapshot(); + let patterns = build_patterns(&self.map, CHUNK_SIZE, true, true); + let constraints = patterns_to_constraints(patterns, CHUNK_SIZE); + self.render_tile_gallery(&constraints, CHUNK_SIZE); + + self.map = Map::new(self.depth); + loop { + let mut solver = Solver::new(constraints.clone(), CHUNK_SIZE, &self.map); + while !solver.iteration(&mut self.map, &mut rng) { + self.take_snapshot(); + } + self.take_snapshot(); + if solver.possible { + break; + } // If it has hit an impossible condition, try again + } + // Find a starting point; start at the middle and walk left until we find an open tile self.starting_position = Position { x: self.map.width / 2, y: self.map.height / 2, }; - let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y); - while self.map.tiles[start_idx] != TileType::Floor { - self.starting_position.x -= 1; - start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y); - } + let mut start_idx = self + .map + .xy_idx(self.starting_position.x, self.starting_position.y); + // while self.map.tiles[start_idx] != TileType::Floor { + // self.starting_position.x -= 1; + // start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y); + // } self.take_snapshot(); // Find all tiles we can reach from the starting point @@ -92,4 +121,34 @@ impl WaveformCollapseBuilder { // Now we build a noise map for use in spawning entities later self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng); } + + fn render_tile_gallery(&mut self, constraints: &Vec, chunk_size: i32) { + self.map = Map::new(0); + let mut counter = 0; + let mut x = 1; + let mut y = 1; + + while counter < constraints.len() { + render_pattern_to_map(&mut self.map, &constraints[counter], chunk_size, x, y); + + x += chunk_size + 1; + if x + chunk_size > self.map.width { + // Move to the next row + x = 1; + y += chunk_size + 1; + + if y + chunk_size > self.map.height { + // Move to the next page + self.take_snapshot(); + self.map = Map::new(0); + + x = 1; + y = 1; + } + } + + counter += 1; + } + self.take_snapshot(); + } } diff --git a/src/map_builders/waveform_collapse/solver.rs b/src/map_builders/waveform_collapse/solver.rs index e69de29..fcefabd 100644 --- a/src/map_builders/waveform_collapse/solver.rs +++ b/src/map_builders/waveform_collapse/solver.rs @@ -0,0 +1,225 @@ +use super::common::MapChunk; +use crate::Map; +use std::collections::HashSet; + +pub struct Solver { + constraints: Vec, + chunk_size: i32, + chunks: Vec>, + chunks_x: usize, + chunks_y: usize, + remaining: Vec<(usize, i32)>, // (index, # of neighbors) + pub possible: bool, +} + +impl Solver { + pub fn new(constraints: Vec, chunk_size: i32, map: &Map) -> Solver { + let chunks_x = (map.width / chunk_size) as usize; + let chunks_y = (map.height / chunk_size) as usize; + let mut remaining: Vec<(usize, i32)> = Vec::new(); + for i in 0..(chunks_x * chunks_y) { + remaining.push((i, 0)); + } + + Solver { + constraints, + chunk_size, + chunks: vec![None; chunks_x * chunks_y], + chunks_x, + chunks_y, + remaining, + possible: true, + } + } + + fn chunk_idx(&self, x: usize, y: usize) -> usize { + ((y * self.chunks_x) + x) as usize + } + + fn count_neighbors(&self, chunk_x: usize, chunk_y: usize) -> i32 { + let mut neighbors = 0; + + if chunk_x > 0 { + let left_idx = self.chunk_idx(chunk_x - 1, chunk_y); + if let Some(_) = self.chunks[left_idx] { + neighbors += 1; + } + } + + if chunk_x < self.chunks_x - 1 { + let right_idx = self.chunk_idx(chunk_x + 1, chunk_y); + if let Some(_) = self.chunks[right_idx] { + neighbors += 1; + } + } + + if chunk_y > 0 { + let up_idx = self.chunk_idx(chunk_x, chunk_y - 1); + if let Some(_) = self.chunks[up_idx] { + neighbors += 1; + } + } + + if chunk_y < self.chunks_y - 1 { + let down_idx = self.chunk_idx(chunk_x, chunk_y + 1); + if let Some(_) = self.chunks[down_idx] { + neighbors += 1; + } + } + + neighbors + } + + pub fn iteration(&mut self, map: &mut Map, rng: &mut rltk::RandomNumberGenerator) -> bool { + if self.remaining.is_empty() { + return true; + } + + // Populate the neighbor count of the remaining list + let mut remain_copy = self.remaining.clone(); + let mut neighbors_exist = false; + for r in remain_copy.iter_mut() { + let idx = r.0; + let chunk_x = idx % self.chunks_x; + let chunk_y = idx / self.chunks_x; + let neighbor_count = self.count_neighbors(chunk_x, chunk_y); + if neighbor_count > 0 { + neighbors_exist = true; + } + + *r = (r.0, neighbor_count); + } + remain_copy.sort_by(|a, b| b.1.cmp(&a.1)); + self.remaining = remain_copy; + + // Pick a random chunk we haven't dealt with yet and get its index, remove from remaining list + let remaining_index = if !neighbors_exist { + (rng.roll_dice(1, self.remaining.len() as i32) - 1) as usize + } else { + 0_usize + }; + let chunk_index = self.remaining[remaining_index].0; + self.remaining.remove(remaining_index); + + let chunk_x = chunk_index % self.chunks_x; + let chunk_y = chunk_index / self.chunks_x; + + let mut neighbors = 0; + let mut options: Vec> = Vec::new(); + + if chunk_x > 0 { + let left_idx = self.chunk_idx(chunk_x - 1, chunk_y); + match self.chunks[left_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[3].clone()); + } + } + } + + if chunk_x < self.chunks_x - 1 { + let right_idx = self.chunk_idx(chunk_x + 1, chunk_y); + match self.chunks[right_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[2].clone()); + } + } + } + + if chunk_y > 0 { + let up_idx = self.chunk_idx(chunk_x, chunk_y - 1); + match self.chunks[up_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[1].clone()); + } + } + } + + if chunk_y < self.chunks_y - 1 { + let down_idx = self.chunk_idx(chunk_x, chunk_y + 1); + match self.chunks[down_idx] { + None => {} + Some(nt) => { + neighbors += 1; + options.push(self.constraints[nt].compatible_with[0].clone()); + } + } + } + + if neighbors == 0 { + // There is nothing nearby, so we can have anything! + let new_chunk_idx = (rng.roll_dice(1, self.constraints.len() as i32) - 1) as usize; + self.chunks[chunk_index] = Some(new_chunk_idx); + let left_x = chunk_x as i32 * self.chunk_size; + let right_x = (chunk_x + 1) as i32 * self.chunk_size; + let top_y = chunk_y as i32 * self.chunk_size; + let bottom_y = (chunk_y + 1) as i32 * self.chunk_size; + + let mut i = 0_usize; + for y in top_y..bottom_y { + for x in left_x..right_x { + let mapidx = map.xy_idx(x, y); + let tile = self.constraints[new_chunk_idx].pattern[i]; + map.tiles[mapidx] = tile; + i += 1; + } + } + } else { + // There are neighbors, so we try to be compatible with them + let mut options_to_check: HashSet = HashSet::new(); + for o in options.iter() { + for i in o.iter() { + options_to_check.insert(*i); + } + } + + let mut possible_options: Vec = Vec::new(); + for new_chunk_idx in options_to_check.iter() { + let mut possible = true; + for o in options.iter() { + if !o.contains(new_chunk_idx) { + possible = false; + } + } + if possible { + possible_options.push(*new_chunk_idx); + } + } + + if possible_options.is_empty() { + rltk::console::log("Oh no! It's not possible!"); + self.possible = false; + + return true; + } + + let new_chunk_idx = if possible_options.len() == 1 { + 0 + } else { + rng.roll_dice(1, possible_options.len() as i32) - 1 + }; + self.chunks[chunk_index] = Some(new_chunk_idx as usize); + let left_x = chunk_x as i32 * self.chunk_size; + let right_x = (chunk_x + 1) as i32 * self.chunk_size; + let top_y = chunk_y as i32 * self.chunk_size; + let bottom_y = (chunk_y + 1) as i32 * self.chunk_size; + + let mut i = 0_usize; + for y in top_y..bottom_y { + for x in left_x..right_x { + let mapidx = map.xy_idx(x, y); + let tile = self.constraints[new_chunk_idx as usize].pattern[i]; + map.tiles[mapidx] = tile; + i += 1; + } + } + } + + false + } +}