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}