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}