variations.rs

6.2 kB · rust · 221 lines

1use crate::config::{Config, LADDER};2use crate::Path;3use mrlycore::errors::Result;4use mrlycore::state::{boolean, choice};5use mrlycore::tile::{Design, Group, Source, Tile, CLASSICS_2D};6use mrlymath::two::tile as tile2d;7use mrlymath::two::{build, Cell2d};89/// A drawn board: the seed cell and its climb from tile to canvas.10#[derive(Clone, Debug)]11pub struct Board {12    /// The built seed cell.13    pub cell: Cell2d,14    /// The tile side in cells.15    pub unit: usize,16    /// The tile to grid multiplier.17    pub grid: usize,18    /// The grid to canvas multiplier.19    pub canvas: usize,20}2122impl Board {23    /// Wraps a bare grid as a board with no climb.24    pub fn of(cell: Cell2d) -> Board {25        let unit = cell.width().max(cell.height());26        Board {27            cell,28            unit,29            grid: 1,30            canvas: 1,31        }32    }33    /// Returns the grid side in cells.34    pub fn grid_unit(&self) -> usize {35        self.unit * self.grid36    }37    /// Returns the canvas side in cells.38    pub fn canvas_unit(&self) -> usize {39        self.grid_unit() * self.canvas40    }41    /// Returns the dead border between grid and canvas.42    pub fn padding(&self) -> usize {43        (self.canvas_unit() - self.grid_unit()) / 244    }45}4647fn ladder_climb(base: usize, max_canvas: usize) -> usize {48    let options: Vec<usize> = LADDER49        .iter()50        .copied()51        .filter(|i| base * i <= max_canvas)52        .collect();53    if options.is_empty() {54        return 1;55    }56    choice(&options)57}5859fn draw_config(min_size: usize, max_size: usize) -> tile2d::Config {60    tile2d::Config {61        groups: vec![Group::General, Group::Fractal, Group::Magic],62        min_size,63        max_size,64        ..tile2d::Config::default()65    }66}6768/// Draws a seeded board: a random tile climbed up the grid and canvas ladders.69pub fn board(config: &Config) -> Result<Board> {70    let tile = tile2d::create(&tile2d::Config {71        min_size: config.min_tile,72        max_size: config.max_tile,73        ..tile2d::Config::default()74    })?;75    let cell = build(&tile)?;76    let unit = tile.max_size();77    let grid = ladder_climb(unit, config.max_canvas);78    let canvas = ladder_climb(unit * grid, config.max_canvas);79    Ok(Board {80        cell,81        unit,82        grid,83        canvas,84    })85}8687fn pop_center(cell: &mut Cell2d) {88    let center = cell.width() / 2;89    cell.cell.types.set(&[center, center], 0);90}9192fn simple_mask() -> Result<Cell2d> {93    let design = choice(&CLASSICS_2D);94    let mut tile = Tile::new(Group::General).size(3, 3);95    tile.sources = vec![Source::Classic(design)];96    tile.numbers = vec![3];97    tile.levels = vec![1];98    tile.rotations = vec![0];99    tile.anti = vec![false];100    tile.factor = 3;101    tile.invert = matches!(design, Design::Htree | Design::Vtree) && boolean();102    build(&tile)103}104105fn basic_mask(config: &Config) -> Result<Cell2d> {106    let tile = tile2d::create(&draw_config(config.min_mask, config.max_mask))?;107    build(&tile)108}109110fn copy_mask(board: &Board) -> Cell2d {111    let cell = board.cell.clone();112    if boolean() {113        cell.invert()114    } else {115        cell116    }117}118119/// Draws a segment's mask down one of the three paths, center popped.120pub fn mask(board: &Board, config: &Config) -> Result<(Path, Cell2d)> {121    let mut paths = vec![Path::Simple, Path::Basic];122    if (config.min_mask..=config.max_mask).contains(&board.unit) {123        paths.push(Path::Copy);124    }125    let path = choice(&paths);126    let mut cell = match path {127        Path::Simple => simple_mask()?,128        Path::Basic => basic_mask(config)?,129        Path::Copy => copy_mask(board),130    };131    pop_center(&mut cell);132    Ok((path, cell))133}134135#[cfg(test)]136mod tests {137    use super::*;138    use mrlycore::state::{guard, seed};139    fn tiny() -> Config {140        Config {141            max_canvas: 15,142            min_tile: 3,143            max_tile: 5,144            min_mask: 3,145            max_mask: 5,146            ..Config::default()147        }148    }149    #[test]150    fn boards_climb_within_the_canvas_cap() {151        let _g = guard();152        let config = tiny();153        for s in 0..50 {154            seed(s);155            let b = board(&config).unwrap();156            assert!(b.unit >= 3 && b.unit <= 5);157            assert!(LADDER.contains(&b.grid));158            assert!(LADDER.contains(&b.canvas));159            assert!(b.grid_unit() <= config.max_canvas);160            assert!(b.canvas_unit() <= config.max_canvas);161            assert_eq!(b.grid_unit() + 2 * b.padding(), b.canvas_unit());162            assert_eq!(b.cell.width(), b.unit);163        }164    }165    #[test]166    fn masks_come_odd_square_and_popped() {167        let _g = guard();168        let config = tiny();169        for s in 0..50 {170            seed(s);171            let b = board(&config).unwrap();172            let (path, cell) = mask(&b, &config).unwrap();173            let side = cell.width();174            assert_eq!(side, cell.height(), "path {path:?} seed {s}");175            assert_eq!(side % 2, 1, "path {path:?} seed {s}");176            assert_eq!(cell.types().get(&[side / 2, side / 2]), 0);177            if path == Path::Simple {178                assert_eq!(side, 3);179            }180        }181    }182    #[test]183    fn copy_masks_mirror_the_board() {184        let _g = guard();185        let config = tiny();186        for s in 0..200 {187            seed(s);188            let b = board(&config).unwrap();189            let (path, cell) = mask(&b, &config).unwrap();190            if path == Path::Copy {191                assert_eq!(cell.width(), b.cell.width());192                return;193            }194        }195        panic!("no seed took the copy path");196    }197    #[test]198    fn oversized_boards_lose_the_copy_path() {199        let _g = guard();200        let config = Config {201            max_mask: 3,202            ..tiny()203        };204        for s in 0..100 {205            seed(s);206            let b = board(&config).unwrap();207            if b.unit > 3 {208                let (path, _) = mask(&b, &config).unwrap();209                assert_ne!(path, Path::Copy, "seed {s}");210            }211        }212    }213    #[test]214    fn wrapped_boards_carry_no_climb() {215        let b = Board::of(mrlymath::life::moore());216        assert_eq!(b.unit, 3);217        assert_eq!(b.grid_unit(), 3);218        assert_eq!(b.canvas_unit(), 3);219        assert_eq!(b.padding(), 0);220    }221}