randscape 0.4.1

PCG toolkit for grids processing
Documentation
use crate::{
    generators::GridGenetator,
    grid::{FixedGrid, Grid},
};
use rand::Rng;
use vek::Vec2;

#[derive(Debug)]
pub struct AutoTileRule<const N: usize, S: Copy + PartialEq, O: Copy> {
    pub pattern: FixedGrid<N, N, Option<S>>,
    pub tiles: Vec<O>,
    pub probability: f32,
    pub stop_on_match: bool,
}

impl<const N: usize, S: Copy + PartialEq, O: Copy> AutoTileRule<N, S, O> {
    pub fn new(pattern: FixedGrid<N, N, Option<S>>, tiles: impl IntoIterator<Item = O>) -> Self {
        Self {
            pattern,
            tiles: tiles.into_iter().collect(),
            probability: 1.0,
            stop_on_match: true,
        }
    }

    pub fn tile(mut self, tile: O) -> Self {
        self.tiles.push(tile);
        self
    }

    pub fn tiles(mut self, tiles: impl IntoIterator<Item = O>) -> Self {
        self.tiles.extend(tiles);
        self
    }

    pub fn probability(mut self, probability: f32) -> Self {
        self.probability = probability;
        self
    }

    pub fn stop_on_match(mut self, stop_on_match: bool) -> Self {
        self.stop_on_match = stop_on_match;
        self
    }

    pub fn does_match(&self, grid: &Grid<S>, location: Vec2<usize>) -> bool {
        for y_pattern in 0..N {
            let y_grid = location.y as isize + y_pattern as isize - (N as isize / 2);
            for x_pattern in 0..N {
                let x_grid = location.x as isize + x_pattern as isize - (N as isize / 2);
                let pattern_cell = self.pattern.get((x_pattern, y_pattern)).flatten();
                if let Some(pattern_cell) = pattern_cell {
                    let grid_cell = if y_grid < 0 || x_grid < 0 {
                        None
                    } else {
                        grid.get((x_grid as usize, y_grid as usize))
                    };
                    if Some(pattern_cell) != grid_cell {
                        return false;
                    }
                }
            }
        }
        true
    }
}

#[derive(Debug)]
pub struct AutoTileModel<const N: usize, S: Copy + PartialEq, O: Copy> {
    pub rules: Vec<AutoTileRule<N, S, O>>,
    pub default_tile: O,
}

impl<const N: usize, S: Copy + PartialEq, O: Copy> Default for AutoTileModel<N, S, O>
where
    O: Default,
{
    fn default() -> Self {
        Self {
            rules: Vec::new(),
            default_tile: O::default(),
        }
    }
}

impl<const N: usize, S: Copy + PartialEq, O: Copy> AutoTileModel<N, S, O> {
    pub fn new(rules: impl IntoIterator<Item = AutoTileRule<N, S, O>>, default_tile: O) -> Self {
        Self {
            rules: rules.into_iter().collect(),
            default_tile,
        }
    }

    pub fn rule(mut self, rule: AutoTileRule<N, S, O>) -> Self {
        self.rules.push(rule);
        self
    }

    pub fn rules(mut self, rules: impl IntoIterator<Item = AutoTileRule<N, S, O>>) -> Self {
        self.rules.extend(rules);
        self
    }

    pub fn default_tile(mut self, default_tile: O) -> Self {
        self.default_tile = default_tile;
        self
    }

    pub fn get_matching_rules<'a>(
        &'a self,
        grid: &Grid<S>,
        location: Vec2<usize>,
        output: &mut Vec<&'a AutoTileRule<N, S, O>>,
    ) {
        for rule in &self.rules {
            if rule.does_match(grid, location) {
                output.push(rule);
                if rule.stop_on_match {
                    return;
                }
            }
        }
    }

    pub fn select_tile<'a>(
        &'a self,
        grid: &Grid<S>,
        location: Vec2<usize>,
        rng: &mut impl Rng,
        matching_rules_cache: &mut Vec<&'a AutoTileRule<N, S, O>>,
    ) -> O {
        matching_rules_cache.clear();
        self.get_matching_rules(grid, location, matching_rules_cache);
        if matching_rules_cache.is_empty() {
            return self.default_tile;
        }
        let total_probability = matching_rules_cache
            .iter()
            .map(|r| r.probability)
            .sum::<f32>();
        let mut random_value = rng.random_range(0.0..total_probability);
        for rule in matching_rules_cache {
            if random_value < rule.probability {
                let tile_index = rng.random_range(0..rule.tiles.len());
                return rule.tiles[tile_index];
            }
            random_value -= rule.probability;
        }
        self.default_tile
    }
}

pub struct AutoTilingGenerator<'a, const N: usize, S: Copy + PartialEq, O: Copy, R: Rng> {
    pub model: &'a AutoTileModel<N, S, O>,
    pub source: &'a Grid<S>,
    pub rng: R,
    matching_rules_cache: Vec<&'a AutoTileRule<N, S, O>>,
}

impl<'a, const N: usize, S: Copy + PartialEq, O: Copy, R: Rng> AutoTilingGenerator<'a, N, S, O, R> {
    pub fn new(model: &'a AutoTileModel<N, S, O>, source: &'a Grid<S>, rng: R) -> Self {
        Self {
            model,
            source,
            rng,
            matching_rules_cache: Vec::new(),
        }
    }
}

impl<const N: usize, S: Copy + PartialEq, O: Copy, R: Rng> GridGenetator<O>
    for AutoTilingGenerator<'_, N, S, O, R>
{
    fn generate(&mut self, location: Vec2<usize>, _: Vec2<usize>, _: O, _: &Grid<O>) -> O {
        self.model.select_tile(
            self.source,
            location,
            &mut self.rng,
            &mut self.matching_rules_cache,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_auto_tile_rule_does_match() {
        let rule = AutoTileRule::new(
            FixedGrid::<3, 3, Option<u8>>::with_buffer([
                [None, Some(1), None],
                [Some(1), None, Some(1)],
                [None, Some(1), None],
            ]),
            [42],
        );

        let grid = Grid::with_buffer(
            (3, 3),
            vec![
                0, 1, 0, //
                1, 2, 1, //
                0, 1, 0, //
            ],
        )
        .unwrap();

        assert!(!rule.does_match(&grid, Vec2::new(0, 0)));
        assert!(rule.does_match(&grid, Vec2::new(1, 1)));
        assert!(!rule.does_match(&grid, Vec2::new(2, 2)));
    }

    #[test]
    fn test_auto_tiling() {
        let model = AutoTileModel::new(
            [
                AutoTileRule::new(
                    FixedGrid::<3, 3, Option<u8>>::with_buffer([
                        [None, None, None],
                        [None, Some(0), Some(1)],
                        [None, Some(1), None],
                    ]),
                    [1],
                ),
                AutoTileRule::new(
                    FixedGrid::<3, 3, Option<u8>>::with_buffer([
                        [None, None, None],
                        [Some(0), Some(1), None],
                        [None, Some(0), None],
                    ]),
                    [2],
                ),
                AutoTileRule::new(
                    FixedGrid::<3, 3, Option<u8>>::with_buffer([
                        [None, Some(0), None],
                        [None, Some(1), Some(0)],
                        [None, None, None],
                    ]),
                    [3],
                ),
                AutoTileRule::new(
                    FixedGrid::<3, 3, Option<u8>>::with_buffer([
                        [None, Some(1), None],
                        [Some(1), Some(0), None],
                        [None, None, None],
                    ]),
                    [4],
                ),
            ],
            0,
        );

        let source = Grid::with_buffer(
            (2, 2),
            vec![
                0, 1, //
                1, 0, //
            ],
        )
        .unwrap();

        let output = Grid::generate(
            (2, 2),
            AutoTilingGenerator::new(&model, &source, rand::rng()),
        );
        assert_eq!(output.buffer(), &[1, 2, 3, 4]);
    }
}