tile.rs
7.0 kB · rust · 233 lines
1use crate::core::errors::{value_error, Result};2use crate::core::state::choice;3use crate::core::tensor::Tensor;4use crate::core::tile::{Group, Source, Tile};56use super::designs;7use super::geometry;8use super::Cell2d;9use crate::math::dim::tile as spec;1011/// The constraints a random 2d tile is drawn under.12pub type Config = spec::ConfigNd<2>;1314fn rotation(_source: Source) -> usize {15 choice(&[0, 1, 2, 3])16}1718/// Draws a random tile satisfying the config, rotations drawn from the four quarter-turns.19pub fn create(config: &Config) -> Result<Tile> {20 spec::create(config, rotation)21}2223/// Draws a random tile up to the given size under the default config.24pub fn random_tile(max_size: usize) -> Result<Tile> {25 spec::random_tile::<2>(max_size, rotation)26}2728fn source_cell(source: Source, number: usize, level: usize, rotation: usize) -> Result<Cell2d> {29 match source {30 Source::Classic(design) => designs::named(design, number, level, rotation),31 Source::Code(code) => designs::create(code, number, level, rotation, 2),32 }33}3435fn cell(tile: &Tile, i: usize, level: usize) -> Result<Cell2d> {36 let mut c = source_cell(tile.sources[i], tile.numbers[i], level, tile.rotations[i])?;37 if tile.anti.get(i).copied().unwrap_or(false) {38 c = c.anti();39 }40 Ok(c)41}4243fn tree_mask(n: usize) -> Result<Tensor> {44 let vertical = designs::vtree(n, 1)?;45 let horizontal = vertical.clone().rotate(1);46 let v = vertical.types();47 let h = horizontal.types();48 let mut data = vec![0u8; v.size()];49 for (flat, item) in data.iter_mut().enumerate() {50 let a = v.bytes()[flat];51 let b = h.bytes()[flat];52 *item = match (a, b) {53 (1, 1) => 2,54 (1, _) | (_, 1) => 1,55 _ => 0,56 };57 }58 Ok(Tensor::of(data, v.shape.clone()))59}6061fn build_general(tile: &Tile) -> Result<Cell2d> {62 cell(tile, 0, 1)63}6465fn build_fractal(tile: &Tile) -> Result<Cell2d> {66 cell(tile, 0, tile.levels[0])67}6869fn build_magic(tile: &Tile) -> Result<Cell2d> {70 let cells: Result<Vec<Cell2d>> = (0..tile.sources.len()).map(|i| cell(tile, i, 1)).collect();71 geometry::magic(&cells?)72}7374fn build_special(tile: &Tile) -> Result<Cell2d> {75 let cell = designs::vtree(tile.numbers[0], 1)?;76 let mut mask = source_cell(tile.sources[0], tile.factor, 1, tile.rotations[0])?;77 if tile.flip {78 mask = mask.invert();79 }80 geometry::special(mask.types(), &cell)81}8283fn build_mosaic(tile: &Tile) -> Result<Cell2d> {84 let mask = tree_mask(tile.factor)?;85 let cells: Result<Vec<Cell2d>> = (0..3).map(|i| cell(tile, i, 1)).collect();86 geometry::mosaic(&mask, &cells?)87}8889fn builder(group: Group) -> fn(&Tile) -> Result<Cell2d> {90 match group {91 Group::General => build_general,92 Group::Fractal => build_fractal,93 Group::Magic => build_magic,94 Group::Special => build_special,95 Group::Mosaic => build_mosaic,96 }97}9899fn ragged(tile: &Tile) -> bool {100 let slots = tile.sources.len();101 let wanted = match tile.group {102 Group::Mosaic => 3,103 _ => 1,104 };105 slots < wanted106 || tile.numbers.len() < slots107 || tile.levels.len() < slots108 || tile.rotations.len() < slots109}110111/// Builds the cell the tile describes, or an error when the tile is ragged or will not render.112pub fn build(tile: &Tile) -> Result<Cell2d> {113 if ragged(tile) {114 return value_error("tile slots are ragged.");115 }116 let mut c = builder(tile.group)(tile)?;117 if tile.invert {118 c = c.invert();119 }120 Ok(c)121}122123#[cfg(test)]124mod tests {125 use super::*;126 use crate::core::state::{guard as rng_lock, seed};127 use crate::core::tile::{Catalog, Design, Parity};128 #[test]129 fn random_tile_respects_max() {130 let _guard = rng_lock();131 for s in 0..50 {132 seed(s);133 let tile = random_tile(30).unwrap();134 assert!(tile.max_size() <= 30);135 }136 }137 #[test]138 fn magic_can_nest_deeper_than_two() {139 let _guard = rng_lock();140 let config = Config {141 min_size: 3,142 max_size: 300,143 groups: vec![Group::Magic],144 anti: Some(false),145 ..Config::default()146 };147 let mut deep = false;148 for s in 0..200 {149 seed(s);150 if let Ok(tile) = create(&config) {151 if tile.sources.len() >= 3 {152 deep = true;153 let cell = build(&tile).unwrap();154 assert_eq!(cell.width(), tile.width);155 }156 }157 }158 assert!(deep, "expected at least one magic tile nested 3+ deep");159 }160 #[test]161 fn magic_rolls_never_repeat_a_fractal() {162 let _guard = rng_lock();163 let config = Config {164 catalog: Catalog::Codes(vec![7]),165 min_size: 3,166 max_size: 64,167 groups: vec![Group::Magic],168 anti: Some(false),169 ..Config::default()170 };171 for s in 0..200 {172 seed(s);173 let tile = create(&config).unwrap();174 assert!(tile.sources.len() >= 2, "seed {s} rolled one slot");175 assert!(!tile.degenerate(), "seed {s} rolled a fractal twin");176 let cell = build(&tile).unwrap();177 assert_eq!(cell.width(), tile.width, "seed {s}");178 }179 }180 #[test]181 fn a_magic_roll_keeps_its_twin_when_nothing_else_fits() {182 let _guard = rng_lock();183 let config = Config {184 catalog: Catalog::Codes(vec![7]),185 min_size: 9,186 max_size: 9,187 groups: vec![Group::Magic],188 anti: Some(false),189 ..Config::default()190 };191 for s in 0..20 {192 seed(s);193 let tile = create(&config).unwrap();194 assert_eq!(tile.numbers, vec![3, 3], "seed {s}");195 assert!(tile.degenerate(), "seed {s}");196 assert_eq!(build(&tile).unwrap().width(), 9, "seed {s}");197 }198 }199 #[test]200 fn build_errors_on_a_ragged_tile() {201 use crate::core::json;202 let parsed: Tile = serde_json::from_value(json!({203 "group": "General", "factor": 0,204 "sources": [{ "design": "Carpet" }],205 "numbers": [], "levels": [], "rotations": [], "anti": [],206 "invert": false, "flip": false, "width": 0, "height": 0,207 }))208 .unwrap();209 assert!(build(&parsed).is_err());210 let mut bare = Tile::new(Group::Mosaic);211 bare.sources = vec![Source::Classic(Design::Carpet)];212 assert!(build(&bare).is_err());213 }214 #[test]215 fn evens_parity_builds() {216 let _guard = rng_lock();217 let config = Config {218 min_size: 4,219 max_size: 64,220 parity: Parity::Evens,221 groups: vec![Group::General],222 anti: Some(false),223 ..Config::default()224 };225 for s in 0..50 {226 seed(s);227 let tile = create(&config).unwrap();228 assert_eq!(tile.numbers[0] % 2, 0);229 let cell = build(&tile).unwrap();230 assert_eq!(cell.width(), tile.width);231 }232 }233}