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}