rust/landgen/src/wavefront_collapse/wavefront_collapse.rs
author unC0Rr
Tue, 28 Jan 2025 15:49:45 +0100
changeset 16073 5d302b12d837
parent 16059 2acea266d297
child 16075 2c2b094e6bbe
permissions -rw-r--r--
- Update landgen to use the latest rand crate - Change Size width and height from usize to u32 for portability - Implement backtracking in wfc generator

use integral_geometry::Size;
use rand::distr::{weighted::WeightedIndex, Distribution};
use rand::prelude::IndexedRandom;
use rand::Rng;
use std::collections::HashSet;
use vec2d::Vec2D;

#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub enum Tile {
    Empty,
    OutsideBegin,
    OutsideFill,
    OutsideEnd,
    Numbered(usize),
}

#[derive(Debug)]
pub struct CollapseRule {
    pub weight: u32,
    pub tile: Tile,
    pub right: HashSet<Tile>,
    pub bottom: HashSet<Tile>,
    pub left: HashSet<Tile>,
    pub top: HashSet<Tile>,
}

pub struct WavefrontCollapse {
    rules: Vec<CollapseRule>,
    grid: Vec2D<Tile>,
    wrap: bool,
}

impl Default for WavefrontCollapse {
    fn default() -> Self {
        Self {
            rules: Vec::new(),
            grid: Vec2D::new(&Size::new(1, 1), Tile::Empty),
            wrap: false,
        }
    }
}

impl WavefrontCollapse {
    pub fn new(wrap: bool) -> Self {
        Self {
            rules: Vec::new(),
            grid: Vec2D::new(&Size::new(1, 1), Tile::Empty),
            wrap,
        }
    }

    pub fn generate_map<F: FnOnce(&mut Vec2D<Tile>)>(
        &mut self,
        map_size: &Size,
        seed_fn: F,
        random_numbers: &mut impl Rng,
    ) {
        self.grid = Vec2D::new(map_size, Tile::Empty);

        seed_fn(&mut self.grid);

        let mut backtracks = 0usize;
        while let Some(b) = self.collapse_step(random_numbers) {
            backtracks += b;

            if backtracks >= 1000 {
                println!("[WFC] Too much backtracking, stopping generation!");
                break;
            }
        }

        if backtracks > 0 {
            println!("[WFC] Had to backtrack {} times...", backtracks);
        }
    }

    pub fn set_rules(&mut self, rules: Vec<CollapseRule>) {
        self.rules = rules;
    }

    fn get_tile(&self, y: usize, x: usize) -> Tile {
        let x = if self.wrap {
            if x == usize::MAX {
                self.grid.width() - 1
            } else if x == self.grid.width() {
                0
            } else {
                x
            }
        } else {
            x
        };

        self.grid.get(y, x).copied().unwrap_or_else(|| {
            let x_out = x >= self.grid.width();

            if x_out {
                let y_at_begin = y == 0;
                let y_at_end = y.wrapping_add(1) == self.grid.height();
                if y_at_begin {
                    Tile::OutsideBegin
                } else if y_at_end {
                    Tile::OutsideEnd
                } else {
                    Tile::OutsideFill
                }
            } else {
                // if not x, then it is y

                let x_at_begin = x == 0;
                let x_at_end = x.wrapping_add(1) == self.grid.width();

                if x_at_begin {
                    Tile::OutsideBegin
                } else if x_at_end {
                    Tile::OutsideEnd
                } else {
                    Tile::OutsideFill
                }
            }
        })
    }

    fn collapse_step(&mut self, random_numbers: &mut impl Rng) -> Option<usize> {
        let mut tiles_to_collapse = (usize::MAX, Vec::new());

        // Iterate through the tiles in the land
        for x in 0..self.grid.width() {
            for y in 0..self.grid.height() {
                let current_tile = self.get_tile(y, x);

                if let Tile::Empty = current_tile {
                    let neighbors = [
                        (y, x.wrapping_add(1)),
                        (y.wrapping_add(1), x),
                        (y, x.wrapping_sub(1)),
                        (y.wrapping_sub(1), x),
                    ];

                    // calc entropy
                    let [right_tile, bottom_tile, left_tile, top_tile] =
                        neighbors.map(|(y, x)| self.get_tile(y, x));

                    let possibilities: Vec<(u32, Tile)> = self
                        .rules
                        .iter()
                        .filter_map(|rule| {
                            if rule.right.contains(&right_tile)
                                && rule.bottom.contains(&bottom_tile)
                                && rule.left.contains(&left_tile)
                                && rule.top.contains(&top_tile)
                            {
                                Some((rule.weight, rule.tile))
                            } else {
                                None
                            }
                        })
                        .collect();

                    let entropy = possibilities.len();
                    if entropy > 0 {
                        if entropy <= tiles_to_collapse.0 {
                            let weights = possibilities.iter().map(|(weight, _)| *weight);
                            let distribution = WeightedIndex::new(weights).unwrap();

                            let entry =
                                (y, x, possibilities[distribution.sample(random_numbers)].1);

                            if entropy < tiles_to_collapse.0 {
                                tiles_to_collapse = (entropy, vec![entry])
                            } else {
                                tiles_to_collapse.1.push(entry)
                            }
                        }
                    } else {
                        /*
                        println!("We're here: {}, {}", x, y);
                        println!(
                            "Neighbour tiles are: {:?} {:?} {:?} {:?}",
                            right_tile, bottom_tile, left_tile, top_tile
                        );
                        println!("Rules are: {:?}", self.rules);
                        */

                        let entries = neighbors
                            .iter()
                            .filter(|(y, x)| self.grid.get(*y, *x).is_some())
                            .map(|(y, x)| (*y, *x, Tile::Empty))
                            .collect::<Vec<_>>();

                        if entropy < tiles_to_collapse.0 {
                            tiles_to_collapse = (entropy, entries);
                        } else {
                            tiles_to_collapse.1.extend(entries);
                        }

                        //todo!("no collapse possible - what to do?")
                    }
                }
            }
        }

        if tiles_to_collapse.0 == 0 {
            // cannot collapse, we're clearing some tiles

            for (y, x, tile) in tiles_to_collapse.1 {
                *self
                    .grid
                    .get_mut(y, x)
                    .expect("correct iteration over grid") = tile;
            }

            Some(1)
        } else {
            if let Some(&(y, x, tile)) = tiles_to_collapse.1.as_slice().choose(random_numbers) {
                *self
                    .grid
                    .get_mut(y, x)
                    .expect("correct iteration over grid") = tile;

                Some(0)
            } else {
                None
            }
        }
    }

    pub fn grid(&self) -> &Vec2D<Tile> {
        &self.grid
    }
}

#[cfg(test)]
mod tests {
    use super::{Tile, WavefrontCollapse};
    use integral_geometry::Size;
    use vec2d::Vec2D;

    #[test]
    fn test_wavefront_collapse() {
        let size = Size::new(4, 4);
        let mut rnd = [0u32; 64].into_iter().cycle();
        let mut wfc = WavefrontCollapse::default();

        wfc.generate_map(&size, |_| {}, &mut rnd);

        let empty_land = Vec2D::new(&size, Tile::Empty);

        assert_eq!(empty_land.as_slice(), wfc.grid().as_slice());
    }
}