recipe.rs

18.6 kB · rust · 563 lines

1use crate::core::error::{value_error, Result};2use crate::core::named_enum;3use serde::{Deserialize, Serialize};45pub use crate::math::bang::catalog::{6    antis, classics, Catalog, Design, Source, ANTIS_2D, ANTIS_3D, CLASSICS_2D, CLASSICS_3D,7};89/// The smallest side, number or factor a tile may take.10pub const MIN_SIDE: usize = 2;1112/// The largest side, number or factor a tile may take.13pub const MAX_SIDE: usize = 64;1415/// The deepest fractal level a tile may take.16pub const MAX_LEVEL: usize = 6;1718/// The most slots a magic tile may take.19pub const MAX_SLOTS: usize = 6;2021named_enum! {22    /// The five construction families a tile can belong to.23    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]24    pub enum Group {25        /// One source at one flat size.26        General => "General",27        /// One source raised to a power.28        Fractal => "Fractal",29        /// A magic-recipe construction.30        Magic => "Magic",31        /// A one-off special construction.32        Special => "Special",33        /// Sources nested as a product of factors.34        Mosaic => "Mosaic",35    }36}3738named_enum! {39    /// The parity filter over candidate sizes.40    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]41    pub enum Parity {42        /// Even sizes only.43        Evens => "Evens",44        /// Odd sizes only.45        Odds => "Odds",46        /// Every size.47        Both => "Both",48    }49}5051impl Parity {52    /// Returns true when the number passes the filter.53    pub fn keep(self, n: usize) -> bool {54        match self {55            Parity::Evens => n.is_multiple_of(2),56            Parity::Odds => !n.is_multiple_of(2),57            Parity::Both => true,58        }59    }60}6162/// A complete recipe for one tile.63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]64pub struct Tile {65    /// The construction family.66    pub group: Group,67    /// The base factor of the construction.68    pub factor: usize,69    /// The origin of each layer.70    pub sources: Vec<Source>,71    /// The grid size of each source.72    pub numbers: Vec<usize>,73    /// The fractal level of each source.74    pub levels: Vec<usize>,75    /// The quarter-turn rotation of each source.76    pub rotations: Vec<usize>,77    /// Whether each source swaps fill and void.78    pub anti: Vec<bool>,79    /// Whether the finished tile inverts.80    pub invert: bool,81    /// Whether the finished tile flips.82    pub flip: bool,83    /// The tile's width in cells.84    pub width: usize,85    /// The tile's height in cells.86    pub height: usize,87}8889impl Tile {90    /// Builds an empty tile in a group.91    ///92    /// ```93    /// use mrlyrs::gen::recipe::{Group, Tile};94    /// assert_eq!(Tile::new(Group::General).max_size(), 0);95    /// ```96    pub fn new(group: Group) -> Tile {97        Tile {98            group,99            factor: 0,100            sources: Vec::new(),101            numbers: Vec::new(),102            levels: Vec::new(),103            rotations: Vec::new(),104            anti: Vec::new(),105            invert: false,106            flip: false,107            width: 0,108            height: 0,109        }110    }111    /// Sets the tile's width and height.112    ///113    /// ```114    /// use mrlyrs::gen::recipe::{Group, Tile};115    /// assert_eq!(Tile::new(Group::General).size(3, 5).max_size(), 5);116    /// ```117    pub fn size(mut self, width: usize, height: usize) -> Tile {118        self.width = width;119        self.height = height;120        self121    }122    /// Returns the larger of width and height.123    pub fn max_size(&self) -> usize {124        self.width.max(self.height)125    }126    /// Returns whether the recipe is a magic tile of one repeated source at one repeated number,127    /// the shape a fractal tile of the same factor and level already draws.128    pub fn degenerate(&self) -> bool {129        self.group == Group::Magic130            && self.sources.len() > 1131            && uniform(&self.sources)132            && uniform(&self.numbers)133    }134    /// Recomputes the factor and side length the group and numbers imply, zero when they overflow.135    pub fn resize(&mut self) {136        let lead = self.numbers.first().copied().unwrap_or(0);137        if matches!(self.group, Group::General | Group::Fractal | Group::Magic) {138            self.factor = lead;139        }140        let size = match self.group {141            Group::General => lead,142            Group::Fractal => u32::try_from(self.levels.first().copied().unwrap_or(1))143                .ok()144                .and_then(|level| lead.checked_pow(level))145                .unwrap_or(0),146            Group::Magic => self147                .numbers148                .iter()149                .try_fold(1usize, |acc, &n| acc.checked_mul(n))150                .unwrap_or(0),151            Group::Special | Group::Mosaic => self.factor.checked_mul(lead).unwrap_or(0),152        };153        self.width = size;154        self.height = size;155    }156    /// Checks that the slots, numbers and sizes agree.157    ///158    /// ```159    /// use mrlyrs::gen::recipe::{Group, Tile};160    /// assert!(Tile::new(Group::General).check().is_err());161    /// ```162    ///163    /// # Errors164    ///165    /// Errs with a terse note for the first broken law: the slot count, a ragged slot list, a166    /// number, rotation, level, flip or factor out of range, or sizes the group does not imply.167    pub fn check(&self) -> Result<()> {168        let slots = self.sources.len();169        let wanted = match self.group {170            Group::Mosaic => slots == 3,171            Group::Magic => (2..=MAX_SLOTS).contains(&slots),172            _ => slots == 1,173        };174        if !wanted {175            return value_error("wrong slot count");176        }177        if self.numbers.len() != slots178            || self.levels.len() != slots179            || self.rotations.len() != slots180            || self.anti.len() != slots181        {182            return value_error("ragged slots");183        }184        if self185            .numbers186            .iter()187            .any(|&n| !(MIN_SIDE..=MAX_SIDE).contains(&n))188        {189            return value_error("numbers are 2 to 64");190        }191        if self.rotations.iter().any(|&r| r > 3) {192            return value_error("rotation is 0 to 3");193        }194        if self.flip && self.group != Group::Special {195            return value_error("flip is special only");196        }197        if self.group == Group::Fractal {198            if !(1..=MAX_LEVEL).contains(&self.levels[0]) {199                return value_error("level is 1 to 6");200            }201        } else if self.levels.iter().any(|&l| l != 1) {202            return value_error("level is fractal only");203        }204        if matches!(self.group, Group::Special | Group::Mosaic)205            && !(MIN_SIDE..=MAX_SIDE).contains(&self.factor)206        {207            return value_error("factor is 2 to 64");208        }209        if self.group == Group::Mosaic && self.numbers.iter().any(|&n| n != self.numbers[0]) {210            return value_error("mosaic shares one number");211        }212        let mut probe = self.clone();213        probe.resize();214        if probe.width != self.width || probe.height != self.height || probe.factor != self.factor {215            return value_error("sizes disagree");216        }217        if !(MIN_SIDE..=MAX_SIDE).contains(&self.max_size()) {218            return value_error("size is 2 to 64");219        }220        Ok(())221    }222}223224const MIN_FACTOR: usize = 2;225226/// Returns whether every item equals the first, vacuously true for an empty or single list.227///228/// ```229/// assert!(mrlyrs::gen::recipe::uniform(&[3, 3, 3]));230/// assert!(!mrlyrs::gen::recipe::uniform(&[3, 5, 3]));231/// ```232pub fn uniform<T: PartialEq>(items: &[T]) -> bool {233    items.windows(2).all(|pair| pair[0] == pair[1])234}235236fn factors(min_factor: usize, max_factor: usize, parity: Parity) -> Vec<usize> {237    (min_factor.max(MIN_FACTOR)..=max_factor)238        .filter(|&n| parity.keep(n))239        .collect()240}241242/// Returns every flat size in the range that passes the parity filter.243pub fn generals(min_size: usize, max_size: usize, parity: Parity) -> Vec<usize> {244    factors(min_size, max_size, parity)245}246247/// Returns every factor and level whose power lands in the size range.248pub fn powers(min_size: usize, max_size: usize, parity: Parity) -> Vec<(usize, usize)> {249    let mut out = Vec::new();250    for n in factors(MIN_FACTOR, max_size, parity) {251        let mut level = 2;252        loop {253            match n.checked_pow(level as u32) {254                Some(size) if size <= max_size => {255                    if size >= min_size {256                        out.push((n, level));257                    }258                    level += 1;259                }260                _ => break,261            }262        }263    }264    out265}266267/// Returns the side a factor raised to a level makes, or None when no usize holds it.268///269/// ```270/// assert_eq!(mrlyrs::gen::recipe::size(3, 3), Some(27));271/// assert_eq!(mrlyrs::gen::recipe::size(3, 4294967298), None);272/// ```273pub fn size(number: i64, level: i64) -> Option<usize> {274    let number = usize::try_from(number).ok()?;275    let level = u32::try_from(level).ok()?;276    number.checked_pow(level)277}278279/// Returns every count-long factor list whose product lands in the size range.280pub fn products(min_size: usize, max_size: usize, count: usize, parity: Parity) -> Vec<Vec<usize>> {281    if count < 1 {282        return Vec::new();283    }284    fn walk(285        min_size: usize,286        max_size: usize,287        remaining: usize,288        parity: Parity,289        out: &mut Vec<Vec<usize>>,290    ) {291        if remaining == 1 {292            for n in factors(min_size, max_size, parity) {293                out.push(vec![n]);294            }295            return;296        }297        for n in factors(MIN_FACTOR, max_size, parity) {298            let next_min = min_size.div_ceil(n);299            let next_max = max_size / n;300            if next_max < MIN_FACTOR {301                continue;302            }303            let mut tails = Vec::new();304            walk(next_min, next_max, remaining - 1, parity, &mut tails);305            for tail in tails {306                let mut item = vec![n];307                item.extend(tail);308                out.push(item);309            }310        }311    }312    let mut out = Vec::new();313    walk(min_size, max_size, count, parity, &mut out);314    out315}316317/// Returns every factor list of depth two and beyond whose product lands in the size range.318pub fn nestings(min_size: usize, max_size: usize, parity: Parity) -> Vec<Vec<usize>> {319    let mut out = Vec::new();320    let mut depth = 2;321    loop {322        let found = products(min_size, max_size, depth, parity);323        if found.is_empty() {324            if depth > 2 {325                break;326            }327            depth += 1;328            if depth > max_size {329                break;330            }331            continue;332        }333        out.extend(found);334        depth += 1;335    }336    out337}338339#[cfg(test)]340mod tests {341    use super::*;342    use crate::core::json;343    #[test]344    fn names_parse_back() {345        for group in Group::all() {346            assert_eq!(group, group.name().parse().unwrap());347        }348        for parity in Parity::all() {349            assert_eq!(parity, parity.name().parse().unwrap());350        }351    }352    #[test]353    fn parity_filters() {354        assert!(Parity::Odds.keep(3));355        assert!(!Parity::Odds.keep(4));356        assert!(Parity::Evens.keep(4));357        assert!(!Parity::Evens.keep(3));358        assert!(Parity::Both.keep(3));359        assert!(Parity::Both.keep(4));360    }361    #[test]362    fn generals_respects_parity_and_range() {363        assert_eq!(generals(3, 9, Parity::Odds), vec![3, 5, 7, 9]);364        assert_eq!(generals(3, 9, Parity::Evens), vec![4, 6, 8]);365        assert_eq!(generals(3, 9, Parity::Both), vec![3, 4, 5, 6, 7, 8, 9]);366    }367    #[test]368    fn powers_are_in_range() {369        for (n, level) in powers(3, 100, Parity::Odds) {370            let size = n.pow(level as u32);371            assert!((3..=100).contains(&size));372            assert!(level >= 2);373        }374        assert!(powers(3, 100, Parity::Odds).contains(&(3, 2)));375        assert!(powers(3, 100, Parity::Odds).contains(&(3, 4)));376    }377    #[test]378    fn products_multiply_into_range() {379        for option in products(3, 64, 2, Parity::Odds) {380            let size: usize = option.iter().product();381            assert!((3..=64).contains(&size));382            assert_eq!(option.len(), 2);383        }384    }385    #[test]386    fn nestings_go_deeper_than_two() {387        let deep = nestings(3, 300, Parity::Odds);388        assert!(deep.iter().any(|opt| opt.len() >= 3));389        for option in &deep {390            let size: usize = option.iter().product();391            assert!(size <= 300);392        }393    }394    #[test]395    fn tile_json_round_trips() {396        let mut tile = Tile::new(Group::Magic).size(45, 45);397        tile.sources = vec![Source::Classic(Design::Carpet), Source::Code(14)];398        tile.numbers = vec![5, 9];399        tile.levels = vec![1, 1];400        tile.rotations = vec![0, 0];401        tile.anti = vec![false, true];402        tile.factor = 5;403        let json = serde_json::to_value(&tile).unwrap();404        assert_eq!(json["group"], "Magic");405        assert_eq!(json["sources"][1], json!({ "code": "14" }));406        let back: Tile = serde_json::from_value(json).unwrap();407        assert_eq!(tile, back);408    }409    #[test]410    fn resize_follows_the_size_law() {411        let mut tile = Tile::new(Group::Fractal);412        tile.sources = vec![Source::Code(7)];413        tile.numbers = vec![3];414        tile.levels = vec![2];415        tile.rotations = vec![0];416        tile.anti = vec![false];417        tile.resize();418        assert_eq!((tile.factor, tile.width, tile.height), (3, 9, 9));419        tile.group = Group::Special;420        tile.factor = 5;421        tile.resize();422        assert_eq!((tile.width, tile.height), (15, 15));423        tile.group = Group::Magic;424        tile.numbers = vec![3, 5];425        tile.resize();426        assert_eq!((tile.factor, tile.width), (3, 15));427    }428    #[test]429    fn resize_survives_empty_and_huge_tiles() {430        let mut bare = Tile::new(Group::Magic);431        bare.resize();432        assert_eq!(bare.width, 1);433        let mut huge = Tile::new(Group::Fractal);434        huge.numbers = vec![3];435        huge.levels = vec![4_294_967_298];436        huge.resize();437        assert_eq!(huge.width, 0);438    }439    #[test]440    fn refuses_a_recipe_that_breaks_a_law() {441        let note = |tile: &Tile| tile.check().unwrap_err().to_string();442        let mut tile = Tile::new(Group::General);443        assert_eq!(note(&tile), "wrong slot count");444        tile.sources = vec![Source::Code(7)];445        assert_eq!(note(&tile), "ragged slots");446        tile.numbers = vec![3];447        tile.levels = vec![1];448        tile.rotations = vec![0];449        tile.anti = vec![false];450        tile.resize();451        assert!(tile.check().is_ok());452        let mut zero = tile.clone();453        zero.numbers = vec![0];454        zero.resize();455        assert_eq!(note(&zero), "numbers are 2 to 64");456        let mut wide = tile.clone();457        wide.numbers = vec![99];458        wide.resize();459        assert_eq!(note(&wide), "numbers are 2 to 64");460        let mut turned = tile.clone();461        turned.rotations = vec![4];462        assert_eq!(note(&turned), "rotation is 0 to 3");463        let mut flipped = tile.clone();464        flipped.flip = true;465        assert_eq!(note(&flipped), "flip is special only");466        let mut levelled = tile.clone();467        levelled.levels = vec![2];468        assert_eq!(note(&levelled), "level is fractal only");469        let mut deep = tile.clone();470        deep.group = Group::Fractal;471        deep.levels = vec![7];472        deep.resize();473        assert_eq!(note(&deep), "level is 1 to 6");474        assert_eq!(note(&tile.clone().size(5, 5)), "sizes disagree");475        let mut special = Tile::new(Group::Special);476        special.sources = vec![Source::Code(7)];477        special.numbers = vec![3];478        special.levels = vec![1];479        special.rotations = vec![0];480        special.anti = vec![false];481        special.factor = 1;482        special.resize();483        assert_eq!(note(&special), "factor is 2 to 64");484        let mut mosaic = Tile::new(Group::Mosaic);485        mosaic.sources = vec![Source::Code(7); 3];486        mosaic.numbers = vec![3, 3, 5];487        mosaic.levels = vec![1; 3];488        mosaic.rotations = vec![0; 3];489        mosaic.anti = vec![false; 3];490        mosaic.factor = 3;491        mosaic.resize();492        assert_eq!(note(&mosaic), "mosaic shares one number");493        let mut huge = tile.clone();494        huge.group = Group::Fractal;495        huge.numbers = vec![9];496        huge.levels = vec![3];497        huge.resize();498        assert_eq!(note(&huge), "size is 2 to 64");499    }500    #[test]501    fn powers_generalize_beyond_classic_bases() {502        let options = powers(3, 1000, Parity::Odds);503        assert!(options.contains(&(3, 2)));504        assert!(options.contains(&(5, 2)));505        assert!(options.contains(&(7, 2)));506        assert!(options.contains(&(9, 2)));507        assert!(options.contains(&(13, 2)));508    }509    #[test]510    fn size_refuses_what_it_cannot_hold() {511        assert_eq!(size(3, 3), Some(27));512        assert_eq!(size(3, 0), Some(1));513        assert_eq!(size(-1, 2), None);514        assert_eq!(size(3, -1), None);515        assert_eq!(size(3, 64), None);516        assert_eq!(size(3, 4294967296), None);517        assert_eq!(size(3, 4294967298), None);518    }519    #[test]520    fn degenerate_marks_the_magic_tiles_a_fractal_already_draws() {521        let mut tile = Tile::new(Group::Magic);522        tile.sources = vec![Source::Classic(Design::Carpet); 2];523        tile.numbers = vec![3, 3];524        tile.levels = vec![1, 1];525        tile.rotations = vec![0, 0];526        tile.anti = vec![false, false];527        tile.resize();528        assert!(tile.degenerate());529        tile.numbers = vec![3, 5];530        tile.resize();531        assert!(!tile.degenerate());532        tile.numbers = vec![3, 3];533        tile.sources = vec![Source::Classic(Design::Carpet), Source::Code(7)];534        tile.resize();535        assert!(!tile.degenerate());536    }537    #[test]538    fn degenerate_is_a_magic_law_only() {539        let mut tile = Tile::new(Group::Mosaic);540        tile.sources = vec![Source::Classic(Design::Carpet); 3];541        tile.numbers = vec![3, 3, 3];542        assert!(!tile.degenerate());543        tile.group = Group::General;544        tile.sources = vec![Source::Classic(Design::Carpet)];545        tile.numbers = vec![3];546        assert!(!tile.degenerate());547    }548    #[test]549    fn uniform_holds_for_short_lists() {550        assert!(uniform::<usize>(&[]));551        assert!(uniform(&[3]));552        assert!(uniform(&[3, 3, 3]));553        assert!(!uniform(&[3, 3, 5]));554    }555    #[test]556    fn evens_factors_work() {557        assert!(powers(4, 1000, Parity::Evens)558            .iter()559            .all(|(n, _)| n % 2 == 0));560        assert!(powers(4, 1000, Parity::Evens).contains(&(4, 2)));561        assert!(powers(4, 1000, Parity::Evens).contains(&(6, 2)));562    }563}