tile.rs

23.4 kB · rust · 741 lines

1use super::errors::{value_error, Result};2use serde::{Deserialize, Serialize};34/// The smallest side, number or factor a tile may take.5pub const MIN_SIDE: usize = 2;67/// The largest side, number or factor a tile may take.8pub const MAX_SIDE: usize = 64;910/// The deepest fractal level a tile may take.11pub const MAX_LEVEL: usize = 6;1213/// The most slots a magic tile may take.14pub const MAX_SLOTS: usize = 6;1516/// The five construction families a tile can belong to.17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]18pub enum Group {19    /// One source at one flat size.20    General,21    /// One source raised to a power.22    Fractal,23    /// A magic-recipe construction.24    Magic,25    /// A one-off special construction.26    Special,27    /// Sources nested as a product of factors.28    Mosaic,29}3031/// The parity filter over candidate sizes.32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]33pub enum Parity {34    /// Even sizes only.35    Evens,36    /// Odd sizes only.37    Odds,38    /// Every size.39    Both,40}4142/// The pool of sources a tile may draw from.43#[derive(Clone, Debug, PartialEq, Eq)]44pub enum Catalog {45    /// The classic designs only.46    Classics,47    /// The canonical codes, one per symmetry orbit.48    Universe,49    /// An explicit list of codes.50    Codes(Vec<u128>),51}5253impl Group {54    /// Returns the group's display name.55    pub fn name(self) -> &'static str {56        match self {57            Group::General => "General",58            Group::Fractal => "Fractal",59            Group::Magic => "Magic",60            Group::Special => "Special",61            Group::Mosaic => "Mosaic",62        }63    }64    /// Parses a display name back into its group, or an error for an unknown name.65    pub fn parse(name: &str) -> Result<Group> {66        match name {67            "General" => Ok(Group::General),68            "Fractal" => Ok(Group::Fractal),69            "Magic" => Ok(Group::Magic),70            "Special" => Ok(Group::Special),71            "Mosaic" => Ok(Group::Mosaic),72            other => value_error(format!("unknown group {other:?}.")),73        }74    }75    /// Returns every group in canonical order.76    pub fn all() -> [Group; 5] {77        [78            Group::General,79            Group::Fractal,80            Group::Magic,81            Group::Special,82            Group::Mosaic,83        ]84    }85}8687impl Parity {88    /// Returns true when the number passes the filter.89    pub fn keep(self, n: usize) -> bool {90        match self {91            Parity::Evens => n.is_multiple_of(2),92            Parity::Odds => !n.is_multiple_of(2),93            Parity::Both => true,94        }95    }96    /// Returns the parity's display name.97    pub fn name(self) -> &'static str {98        match self {99            Parity::Evens => "Evens",100            Parity::Odds => "Odds",101            Parity::Both => "Both",102        }103    }104    /// Parses a display name back into its parity, or an error for an unknown name.105    pub fn parse(name: &str) -> Result<Parity> {106        match name {107            "Evens" => Ok(Parity::Evens),108            "Odds" => Ok(Parity::Odds),109            "Both" => Ok(Parity::Both),110            other => value_error(format!("unknown parity {other:?}.")),111        }112    }113}114115/// The named designs a source can point at: the four classics and their four antis.116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]117pub enum Design {118    /// The carpet with a lattice of holes.119    Carpet,120    /// The net of crossing lines.121    Net,122    /// The stripes along the even rows.123    Htree,124    /// The stripes along the even columns.125    Vtree,126    /// The checkerboard lattice.127    Void,128    /// The beams along the x axis.129    Xtree,130    /// The beams along the y axis.131    Ytree,132    /// The beams along the z axis.133    Ztree,134    /// The points at the odd-odd sites.135    Point,136    /// The dust at the even-even sites.137    Dust,138    /// The lines along the odd rows.139    Hline,140    /// The lines along the odd columns.141    Vline,142    /// The star of sites with exactly one odd coordinate.143    Star,144    /// The rods along the x axis.145    Xline,146    /// The rods along the y axis.147    Yline,148    /// The rods along the z axis.149    Zline,150}151152impl Design {153    /// Returns the design's display name.154    pub fn name(self) -> &'static str {155        match self {156            Design::Carpet => "Carpet",157            Design::Net => "Net",158            Design::Htree => "Htree",159            Design::Vtree => "Vtree",160            Design::Void => "Void",161            Design::Xtree => "Xtree",162            Design::Ytree => "Ytree",163            Design::Ztree => "Ztree",164            Design::Point => "Point",165            Design::Dust => "Dust",166            Design::Hline => "Hline",167            Design::Vline => "Vline",168            Design::Star => "Star",169            Design::Xline => "Xline",170            Design::Yline => "Yline",171            Design::Zline => "Zline",172        }173    }174    /// Parses a display name back into its design, or an error for an unknown name.175    pub fn parse(name: &str) -> Result<Design> {176        match name {177            "Carpet" => Ok(Design::Carpet),178            "Net" => Ok(Design::Net),179            "Htree" => Ok(Design::Htree),180            "Vtree" => Ok(Design::Vtree),181            "Void" => Ok(Design::Void),182            "Xtree" => Ok(Design::Xtree),183            "Ytree" => Ok(Design::Ytree),184            "Ztree" => Ok(Design::Ztree),185            "Point" => Ok(Design::Point),186            "Dust" => Ok(Design::Dust),187            "Hline" => Ok(Design::Hline),188            "Vline" => Ok(Design::Vline),189            "Star" => Ok(Design::Star),190            "Xline" => Ok(Design::Xline),191            "Yline" => Ok(Design::Yline),192            "Zline" => Ok(Design::Zline),193            other => value_error(format!("unknown design {other:?}.")),194        }195    }196}197198/// The five classic designs of the plane.199pub const CLASSICS_2D: [Design; 5] = [200    Design::Carpet,201    Design::Net,202    Design::Htree,203    Design::Vtree,204    Design::Void,205];206207/// The six classic designs of the cube.208pub const CLASSICS_3D: [Design; 6] = [209    Design::Carpet,210    Design::Net,211    Design::Xtree,212    Design::Ytree,213    Design::Ztree,214    Design::Void,215];216217/// The five antis of the plane, the complements of the five classics in order.218pub const ANTIS_2D: [Design; 5] = [219    Design::Point,220    Design::Dust,221    Design::Hline,222    Design::Vline,223    Design::Star,224];225226/// The six antis of the cube: point, dust, the three lines and the star.227pub const ANTIS_3D: [Design; 6] = [228    Design::Point,229    Design::Dust,230    Design::Xline,231    Design::Yline,232    Design::Zline,233    Design::Star,234];235236/// The origin of one tile layer, a one-field json object.237#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]238pub enum Source {239    /// A classic named design.240    #[serde(rename = "design")]241    Classic(Design),242    /// A numbered rule code, spelled as a decimal string.243    #[serde(rename = "code")]244    Code(#[serde(with = "decimal")] u128),245}246247mod decimal {248    use serde::{Deserialize, Deserializer, Serializer};249250    pub fn serialize<S: Serializer>(code: &u128, serializer: S) -> Result<S::Ok, S::Error> {251        serializer.collect_str(code)252    }253254    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u128, D::Error> {255        #[derive(Deserialize)]256        #[serde(untagged)]257        enum Code {258            Text(String),259            Number(u64),260        }261        match Code::deserialize(deserializer)? {262            Code::Text(text) => text.parse().map_err(serde::de::Error::custom),263            Code::Number(code) => Ok(code.into()),264        }265    }266}267268/// Returns the classic designs for a dimension.269pub fn classics(dimension: usize) -> Vec<Design> {270    match dimension {271        3 => CLASSICS_3D.to_vec(),272        _ => CLASSICS_2D.to_vec(),273    }274}275276/// Returns the anti designs for a dimension.277pub fn antis(dimension: usize) -> Vec<Design> {278    match dimension {279        3 => ANTIS_3D.to_vec(),280        _ => ANTIS_2D.to_vec(),281    }282}283284/// A complete recipe for one tile.285#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]286pub struct Tile {287    /// The construction family.288    pub group: Group,289    /// The base factor of the construction.290    pub factor: usize,291    /// The origin of each layer.292    pub sources: Vec<Source>,293    /// The grid size of each source.294    pub numbers: Vec<usize>,295    /// The fractal level of each source.296    pub levels: Vec<usize>,297    /// The quarter-turn rotation of each source.298    pub rotations: Vec<usize>,299    /// Whether each source swaps fill and void.300    pub anti: Vec<bool>,301    /// Whether the finished tile inverts.302    pub invert: bool,303    /// Whether the finished tile flips.304    pub flip: bool,305    /// The tile's width in cells.306    pub width: usize,307    /// The tile's height in cells.308    pub height: usize,309}310311impl Tile {312    /// Builds an empty tile in a group.313    pub fn new(group: Group) -> Tile {314        Tile {315            group,316            factor: 0,317            sources: Vec::new(),318            numbers: Vec::new(),319            levels: Vec::new(),320            rotations: Vec::new(),321            anti: Vec::new(),322            invert: false,323            flip: false,324            width: 0,325            height: 0,326        }327    }328    /// Sets the tile's width and height.329    pub fn size(mut self, width: usize, height: usize) -> Tile {330        self.width = width;331        self.height = height;332        self333    }334    /// Returns the larger of width and height.335    pub fn max_size(&self) -> usize {336        self.width.max(self.height)337    }338    /// Returns whether the recipe is a magic tile of one repeated source at one repeated number,339    /// the shape a fractal tile of the same factor and level already draws.340    pub fn degenerate(&self) -> bool {341        self.group == Group::Magic342            && self.sources.len() > 1343            && uniform(&self.sources)344            && uniform(&self.numbers)345    }346    /// Recomputes the factor and side length the group and numbers imply, zero when they overflow.347    pub fn resize(&mut self) {348        let lead = self.numbers.first().copied().unwrap_or(0);349        if matches!(self.group, Group::General | Group::Fractal | Group::Magic) {350            self.factor = lead;351        }352        let size = match self.group {353            Group::General => lead,354            Group::Fractal => u32::try_from(self.levels.first().copied().unwrap_or(1))355                .ok()356                .and_then(|level| lead.checked_pow(level))357                .unwrap_or(0),358            Group::Magic => self359                .numbers360                .iter()361                .try_fold(1usize, |acc, &n| acc.checked_mul(n))362                .unwrap_or(0),363            Group::Special | Group::Mosaic => self.factor.checked_mul(lead).unwrap_or(0),364        };365        self.width = size;366        self.height = size;367    }368    /// Checks that the slots, numbers and sizes agree, or a terse note for the first broken law.369    pub fn check(&self) -> std::result::Result<(), &'static str> {370        let slots = self.sources.len();371        let wanted = match self.group {372            Group::Mosaic => slots == 3,373            Group::Magic => (2..=MAX_SLOTS).contains(&slots),374            _ => slots == 1,375        };376        if !wanted {377            return Err("wrong slot count");378        }379        if self.numbers.len() != slots380            || self.levels.len() != slots381            || self.rotations.len() != slots382            || self.anti.len() != slots383        {384            return Err("ragged slots");385        }386        if self387            .numbers388            .iter()389            .any(|&n| !(MIN_SIDE..=MAX_SIDE).contains(&n))390        {391            return Err("numbers are 2 to 64");392        }393        if self.rotations.iter().any(|&r| r > 3) {394            return Err("rotation is 0 to 3");395        }396        if self.flip && self.group != Group::Special {397            return Err("flip is special only");398        }399        if self.group == Group::Fractal {400            if !(1..=MAX_LEVEL).contains(&self.levels[0]) {401                return Err("level is 1 to 6");402            }403        } else if self.levels.iter().any(|&l| l != 1) {404            return Err("level is fractal only");405        }406        if matches!(self.group, Group::Special | Group::Mosaic)407            && !(MIN_SIDE..=MAX_SIDE).contains(&self.factor)408        {409            return Err("factor is 2 to 64");410        }411        if self.group == Group::Mosaic && self.numbers.iter().any(|&n| n != self.numbers[0]) {412            return Err("mosaic shares one number");413        }414        let mut probe = self.clone();415        probe.resize();416        if probe.width != self.width || probe.height != self.height || probe.factor != self.factor {417            return Err("sizes disagree");418        }419        if !(MIN_SIDE..=MAX_SIDE).contains(&self.max_size()) {420            return Err("size is 2 to 64");421        }422        Ok(())423    }424}425426const MIN_FACTOR: usize = 2;427428/// Returns whether every item equals the first, vacuously true for an empty or single list.429///430/// ```431/// assert!(mrlyrs::core::tile::uniform(&[3, 3, 3]));432/// assert!(!mrlyrs::core::tile::uniform(&[3, 5, 3]));433/// ```434pub fn uniform<T: PartialEq>(items: &[T]) -> bool {435    items.windows(2).all(|pair| pair[0] == pair[1])436}437438fn factors(min_factor: usize, max_factor: usize, parity: Parity) -> Vec<usize> {439    (min_factor.max(MIN_FACTOR)..=max_factor)440        .filter(|&n| parity.keep(n))441        .collect()442}443444/// Returns every flat size in the range that passes the parity filter.445pub fn generals(min_size: usize, max_size: usize, parity: Parity) -> Vec<usize> {446    factors(min_size, max_size, parity)447}448449/// Returns every factor and level whose power lands in the size range.450pub fn powers(min_size: usize, max_size: usize, parity: Parity) -> Vec<(usize, usize)> {451    let mut out = Vec::new();452    for n in factors(MIN_FACTOR, max_size, parity) {453        let mut level = 2;454        loop {455            match n.checked_pow(level as u32) {456                Some(size) if size <= max_size => {457                    if size >= min_size {458                        out.push((n, level));459                    }460                    level += 1;461                }462                _ => break,463            }464        }465    }466    out467}468469/// Returns the side a factor raised to a level makes, or None when no usize holds it.470///471/// ```472/// assert_eq!(mrlyrs::core::tile::size(3, 3), Some(27));473/// assert_eq!(mrlyrs::core::tile::size(3, 4294967298), None);474/// ```475pub fn size(number: i64, level: i64) -> Option<usize> {476    let number = usize::try_from(number).ok()?;477    let level = u32::try_from(level).ok()?;478    number.checked_pow(level)479}480481/// Returns every count-long factor list whose product lands in the size range.482pub fn products(min_size: usize, max_size: usize, count: usize, parity: Parity) -> Vec<Vec<usize>> {483    if count < 1 {484        return Vec::new();485    }486    fn walk(487        min_size: usize,488        max_size: usize,489        remaining: usize,490        parity: Parity,491        out: &mut Vec<Vec<usize>>,492    ) {493        if remaining == 1 {494            for n in factors(min_size, max_size, parity) {495                out.push(vec![n]);496            }497            return;498        }499        for n in factors(MIN_FACTOR, max_size, parity) {500            let next_min = min_size.div_ceil(n);501            let next_max = max_size / n;502            if next_max < MIN_FACTOR {503                continue;504            }505            let mut tails = Vec::new();506            walk(next_min, next_max, remaining - 1, parity, &mut tails);507            for tail in tails {508                let mut item = vec![n];509                item.extend(tail);510                out.push(item);511            }512        }513    }514    let mut out = Vec::new();515    walk(min_size, max_size, count, parity, &mut out);516    out517}518519/// Returns every factor list of depth two and beyond whose product lands in the size range.520pub fn nestings(min_size: usize, max_size: usize, parity: Parity) -> Vec<Vec<usize>> {521    let mut out = Vec::new();522    let mut depth = 2;523    loop {524        let found = products(min_size, max_size, depth, parity);525        if found.is_empty() {526            if depth > 2 {527                break;528            }529            depth += 1;530            if depth > max_size {531                break;532            }533            continue;534        }535        out.extend(found);536        depth += 1;537    }538    out539}540541#[cfg(test)]542mod tests {543    use super::*;544    use crate::core::json;545    #[test]546    fn parity_filters() {547        assert!(Parity::Odds.keep(3));548        assert!(!Parity::Odds.keep(4));549        assert!(Parity::Evens.keep(4));550        assert!(!Parity::Evens.keep(3));551        assert!(Parity::Both.keep(3));552        assert!(Parity::Both.keep(4));553    }554    #[test]555    fn generals_respects_parity_and_range() {556        assert_eq!(generals(3, 9, Parity::Odds), vec![3, 5, 7, 9]);557        assert_eq!(generals(3, 9, Parity::Evens), vec![4, 6, 8]);558        assert_eq!(generals(3, 9, Parity::Both), vec![3, 4, 5, 6, 7, 8, 9]);559    }560    #[test]561    fn powers_are_in_range() {562        for (n, level) in powers(3, 100, Parity::Odds) {563            let size = n.pow(level as u32);564            assert!((3..=100).contains(&size));565            assert!(level >= 2);566        }567        assert!(powers(3, 100, Parity::Odds).contains(&(3, 2)));568        assert!(powers(3, 100, Parity::Odds).contains(&(3, 4)));569    }570    #[test]571    fn products_multiply_into_range() {572        for option in products(3, 64, 2, Parity::Odds) {573            let size: usize = option.iter().product();574            assert!((3..=64).contains(&size));575            assert_eq!(option.len(), 2);576        }577    }578    #[test]579    fn nestings_go_deeper_than_two() {580        let deep = nestings(3, 300, Parity::Odds);581        assert!(deep.iter().any(|opt| opt.len() >= 3));582        for option in &deep {583            let size: usize = option.iter().product();584            assert!(size <= 300);585        }586    }587    #[test]588    fn tile_json_round_trips() {589        let mut tile = Tile::new(Group::Magic).size(45, 45);590        tile.sources = vec![Source::Classic(Design::Carpet), Source::Code(14)];591        tile.numbers = vec![5, 9];592        tile.levels = vec![1, 1];593        tile.rotations = vec![0, 0];594        tile.anti = vec![false, true];595        tile.factor = 5;596        let json = serde_json::to_value(&tile).unwrap();597        assert_eq!(json["group"], "Magic");598        assert_eq!(json["sources"][1], json!({ "code": "14" }));599        let back: Tile = serde_json::from_value(json).unwrap();600        assert_eq!(tile, back);601    }602    #[test]603    fn source_json_round_trips() {604        for source in [Source::Classic(Design::Vtree), Source::Code(232)] {605            let json = serde_json::to_value(source).unwrap();606            let back: Source = serde_json::from_value(json).unwrap();607            assert_eq!(source, back);608        }609    }610    #[test]611    fn source_json_spells_codes_as_strings() {612        let wide = u128::MAX - 1;613        let json = serde_json::to_value(Source::Code(wide)).unwrap();614        assert_eq!(json, json!({ "code": wide.to_string() }));615        let back: Source = serde_json::from_value(json).unwrap();616        assert_eq!(back, Source::Code(wide));617    }618    #[test]619    fn source_json_reads_bare_int_codes() {620        let read = |value| serde_json::from_value::<Source>(value);621        assert_eq!(read(json!({ "code": 7 })).unwrap(), Source::Code(7));622        assert!(read(json!({ "code": "soup" })).is_err());623        assert!(read(json!({ "code": true })).is_err());624        assert!(read(json!({ "design": "Soup" })).is_err());625    }626    #[test]627    fn resize_follows_the_size_law() {628        let mut tile = Tile::new(Group::Fractal);629        tile.sources = vec![Source::Code(7)];630        tile.numbers = vec![3];631        tile.levels = vec![2];632        tile.rotations = vec![0];633        tile.anti = vec![false];634        tile.resize();635        assert_eq!((tile.factor, tile.width, tile.height), (3, 9, 9));636        tile.group = Group::Special;637        tile.factor = 5;638        tile.resize();639        assert_eq!((tile.width, tile.height), (15, 15));640        tile.group = Group::Magic;641        tile.numbers = vec![3, 5];642        tile.resize();643        assert_eq!((tile.factor, tile.width), (3, 15));644    }645    #[test]646    fn resize_survives_empty_and_huge_tiles() {647        let mut bare = Tile::new(Group::Magic);648        bare.resize();649        assert_eq!(bare.width, 1);650        let mut huge = Tile::new(Group::Fractal);651        huge.numbers = vec![3];652        huge.levels = vec![4_294_967_298];653        huge.resize();654        assert_eq!(huge.width, 0);655    }656    #[test]657    fn check_names_the_first_broken_law() {658        let mut tile = Tile::new(Group::General);659        assert_eq!(tile.check(), Err("wrong slot count"));660        tile.sources = vec![Source::Code(7)];661        assert_eq!(tile.check(), Err("ragged slots"));662        tile.numbers = vec![3];663        tile.levels = vec![1];664        tile.rotations = vec![0];665        tile.anti = vec![false];666        tile.resize();667        assert_eq!(tile.check(), Ok(()));668        tile.rotations = vec![4];669        assert_eq!(tile.check(), Err("rotation is 0 to 3"));670        tile.rotations = vec![0];671        tile.flip = true;672        assert_eq!(tile.check(), Err("flip is special only"));673        tile.flip = false;674        tile.width = 5;675        assert_eq!(tile.check(), Err("sizes disagree"));676    }677    #[test]678    fn powers_generalize_beyond_classic_bases() {679        let options = powers(3, 1000, Parity::Odds);680        assert!(options.contains(&(3, 2)));681        assert!(options.contains(&(5, 2)));682        assert!(options.contains(&(7, 2)));683        assert!(options.contains(&(9, 2)));684        assert!(options.contains(&(13, 2)));685    }686    #[test]687    fn size_refuses_what_it_cannot_hold() {688        assert_eq!(size(3, 3), Some(27));689        assert_eq!(size(3, 0), Some(1));690        assert_eq!(size(-1, 2), None);691        assert_eq!(size(3, -1), None);692        assert_eq!(size(3, 64), None);693        assert_eq!(size(3, 4294967296), None);694        assert_eq!(size(3, 4294967298), None);695    }696    #[test]697    fn degenerate_marks_the_magic_tiles_a_fractal_already_draws() {698        let mut tile = Tile::new(Group::Magic);699        tile.sources = vec![Source::Classic(Design::Carpet); 2];700        tile.numbers = vec![3, 3];701        tile.levels = vec![1, 1];702        tile.rotations = vec![0, 0];703        tile.anti = vec![false, false];704        tile.resize();705        assert_eq!(tile.check(), Ok(()));706        assert!(tile.degenerate());707        tile.numbers = vec![3, 5];708        tile.resize();709        assert!(!tile.degenerate());710        tile.numbers = vec![3, 3];711        tile.sources = vec![Source::Classic(Design::Carpet), Source::Code(7)];712        tile.resize();713        assert!(!tile.degenerate());714    }715    #[test]716    fn degenerate_is_a_magic_law_only() {717        let mut tile = Tile::new(Group::Mosaic);718        tile.sources = vec![Source::Classic(Design::Carpet); 3];719        tile.numbers = vec![3, 3, 3];720        assert!(!tile.degenerate());721        tile.group = Group::General;722        tile.sources = vec![Source::Classic(Design::Carpet)];723        tile.numbers = vec![3];724        assert!(!tile.degenerate());725    }726    #[test]727    fn uniform_holds_for_short_lists() {728        assert!(uniform::<usize>(&[]));729        assert!(uniform(&[3]));730        assert!(uniform(&[3, 3, 3]));731        assert!(!uniform(&[3, 3, 5]));732    }733    #[test]734    fn evens_factors_work() {735        assert!(powers(4, 1000, Parity::Evens)736            .iter()737            .all(|(n, _)| n % 2 == 0));738        assert!(powers(4, 1000, Parity::Evens).contains(&(4, 2)));739        assert!(powers(4, 1000, Parity::Evens).contains(&(6, 2)));740    }741}