tile.rs
8.1 kB · rust · 267 lines
1use mrlycore::errors::{value_error, Result};2use mrlycore::state::choice;3use mrlycore::tensor::Tensor;4use mrlycore::tile::{Group, Source, Tile};56use super::designs;7use super::geometry;8use super::models::Cell2d;9use crate::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/// Returns whether the tile passes its check and builds to its declared size.124pub fn probe(tile: &Tile) -> bool {125 tile.check().is_ok()126 && build(tile)127 .map(|c| c.width() == tile.width && c.height() == tile.height)128 .unwrap_or(false)129}130131/// Returns a k by k tensor of types sampled evenly across the cell.132pub fn sample_types(cell: &Cell2d, k: usize) -> Tensor {133 let (w, h) = (cell.width(), cell.height());134 let mut out = Tensor::new(vec![k, k]);135 for y in 0..k {136 for x in 0..k {137 out.set(&[y, x], cell.types().get(&[y * h / k, x * w / k]));138 }139 }140 out141}142143#[cfg(test)]144mod tests {145 use super::*;146 use mrlycore::state::{guard as rng_lock, seed};147 use mrlycore::tile::{Catalog, Design, Parity};148 #[test]149 fn random_tile_respects_max() {150 let _guard = rng_lock();151 for s in 0..50 {152 seed(s);153 let tile = random_tile(30).unwrap();154 assert!(tile.max_size() <= 30);155 }156 }157 #[test]158 fn magic_can_nest_deeper_than_two() {159 let _guard = rng_lock();160 let config = Config {161 min_size: 3,162 max_size: 300,163 groups: vec![Group::Magic],164 anti: Some(false),165 ..Config::default()166 };167 let mut deep = false;168 for s in 0..200 {169 seed(s);170 if let Ok(tile) = create(&config) {171 if tile.sources.len() >= 3 {172 deep = true;173 let cell = build(&tile).unwrap();174 assert_eq!(cell.width(), tile.width);175 }176 }177 }178 assert!(deep, "expected at least one magic tile nested 3+ deep");179 }180 #[test]181 fn magic_rolls_never_repeat_a_fractal() {182 let _guard = rng_lock();183 let config = Config {184 catalog: Catalog::Codes(vec![7]),185 min_size: 3,186 max_size: 64,187 groups: vec![Group::Magic],188 anti: Some(false),189 ..Config::default()190 };191 for s in 0..200 {192 seed(s);193 let tile = create(&config).unwrap();194 assert!(tile.sources.len() >= 2, "seed {s} rolled one slot");195 assert!(!tile.degenerate(), "seed {s} rolled a fractal twin");196 let cell = build(&tile).unwrap();197 assert_eq!(cell.width(), tile.width, "seed {s}");198 }199 }200 #[test]201 fn a_magic_roll_keeps_its_twin_when_nothing_else_fits() {202 let _guard = rng_lock();203 let config = Config {204 catalog: Catalog::Codes(vec![7]),205 min_size: 9,206 max_size: 9,207 groups: vec![Group::Magic],208 anti: Some(false),209 ..Config::default()210 };211 for s in 0..20 {212 seed(s);213 let tile = create(&config).unwrap();214 assert_eq!(tile.numbers, vec![3, 3], "seed {s}");215 assert!(tile.degenerate(), "seed {s}");216 assert_eq!(build(&tile).unwrap().width(), 9, "seed {s}");217 }218 }219 #[test]220 fn build_errors_on_a_ragged_tile() {221 use mrlycore::json;222 let parsed: Tile = serde_json::from_value(json!({223 "group": "General", "factor": 0,224 "sources": [{ "design": "Carpet" }],225 "numbers": [], "levels": [], "rotations": [], "anti": [],226 "invert": false, "flip": false, "base": "Two", "width": 0, "height": 0,227 }))228 .unwrap();229 assert!(build(&parsed).is_err());230 assert!(!probe(&parsed));231 let mut bare = Tile::new(Group::Mosaic);232 bare.sources = vec![Source::Classic(Design::Carpet)];233 assert!(build(&bare).is_err());234 }235 #[test]236 fn probe_delegates_to_the_check_law() {237 let mut tile = Tile::new(Group::General);238 tile.sources = vec![Source::Classic(Design::Carpet)];239 tile.numbers = vec![3];240 tile.levels = vec![1];241 tile.rotations = vec![0];242 tile.anti = vec![false];243 tile.resize();244 assert!(probe(&tile));245 tile.anti = Vec::new();246 assert!(!probe(&tile));247 }248 #[test]249 fn evens_parity_builds() {250 let _guard = rng_lock();251 let config = Config {252 min_size: 4,253 max_size: 64,254 parity: Parity::Evens,255 groups: vec![Group::General],256 anti: Some(false),257 ..Config::default()258 };259 for s in 0..50 {260 seed(s);261 let tile = create(&config).unwrap();262 assert_eq!(tile.numbers[0] % 2, 0);263 let cell = build(&tile).unwrap();264 assert_eq!(cell.width(), tile.width);265 }266 }267}