tile.rs

23.8 kB · rust · 762 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 numeral base tile codes are read in.43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]44pub enum Base {45    /// Base two.46    #[default]47    Two,48}4950/// The pool of sources a tile may draw from.51#[derive(Clone, Debug, PartialEq, Eq)]52pub enum Catalog {53    /// The classic designs only.54    Classics,55    /// The canonical codes, one per symmetry orbit.56    Universe,57    /// An explicit list of codes.58    Codes(Vec<u128>),59}6061impl Group {62    /// Returns the group's display name.63    pub fn name(self) -> &'static str {64        match self {65            Group::General => "General",66            Group::Fractal => "Fractal",67            Group::Magic => "Magic",68            Group::Special => "Special",69            Group::Mosaic => "Mosaic",70        }71    }72    /// Parses a display name back into its group, or an error for an unknown name.73    pub fn parse(name: &str) -> Result<Group> {74        match name {75            "General" => Ok(Group::General),76            "Fractal" => Ok(Group::Fractal),77            "Magic" => Ok(Group::Magic),78            "Special" => Ok(Group::Special),79            "Mosaic" => Ok(Group::Mosaic),80            other => value_error(format!("unknown group {other:?}.")),81        }82    }83    /// Returns every group in canonical order.84    pub fn all() -> [Group; 5] {85        [86            Group::General,87            Group::Fractal,88            Group::Magic,89            Group::Special,90            Group::Mosaic,91        ]92    }93}9495impl Parity {96    /// Returns true when the number passes the filter.97    pub fn keep(self, n: usize) -> bool {98        match self {99            Parity::Evens => n.is_multiple_of(2),100            Parity::Odds => !n.is_multiple_of(2),101            Parity::Both => true,102        }103    }104    /// Returns the parity's display name.105    pub fn name(self) -> &'static str {106        match self {107            Parity::Evens => "Evens",108            Parity::Odds => "Odds",109            Parity::Both => "Both",110        }111    }112    /// Parses a display name back into its parity, or an error for an unknown name.113    pub fn parse(name: &str) -> Result<Parity> {114        match name {115            "Evens" => Ok(Parity::Evens),116            "Odds" => Ok(Parity::Odds),117            "Both" => Ok(Parity::Both),118            other => value_error(format!("unknown parity {other:?}.")),119        }120    }121}122123impl Base {124    /// Returns the base as a number.125    pub fn value(self) -> usize {126        match self {127            Base::Two => 2,128        }129    }130}131132/// The named designs a source can point at: the four classics and their four antis.133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]134pub enum Design {135    /// The carpet with a lattice of holes.136    Carpet,137    /// The net of crossing lines.138    Net,139    /// The stripes along the even rows.140    Htree,141    /// The stripes along the even columns.142    Vtree,143    /// The checkerboard lattice.144    Void,145    /// The beams along the x axis.146    Xtree,147    /// The beams along the y axis.148    Ytree,149    /// The beams along the z axis.150    Ztree,151    /// The points at the odd-odd sites.152    Point,153    /// The dust at the even-even sites.154    Dust,155    /// The lines along the odd rows.156    Hline,157    /// The lines along the odd columns.158    Vline,159    /// The star of sites with exactly one odd coordinate.160    Star,161    /// The rods along the x axis.162    Xline,163    /// The rods along the y axis.164    Yline,165    /// The rods along the z axis.166    Zline,167}168169impl Design {170    /// Returns the design's display name.171    pub fn name(self) -> &'static str {172        match self {173            Design::Carpet => "Carpet",174            Design::Net => "Net",175            Design::Htree => "Htree",176            Design::Vtree => "Vtree",177            Design::Void => "Void",178            Design::Xtree => "Xtree",179            Design::Ytree => "Ytree",180            Design::Ztree => "Ztree",181            Design::Point => "Point",182            Design::Dust => "Dust",183            Design::Hline => "Hline",184            Design::Vline => "Vline",185            Design::Star => "Star",186            Design::Xline => "Xline",187            Design::Yline => "Yline",188            Design::Zline => "Zline",189        }190    }191    /// Parses a display name back into its design, or an error for an unknown name.192    pub fn parse(name: &str) -> Result<Design> {193        match name {194            "Carpet" => Ok(Design::Carpet),195            "Net" => Ok(Design::Net),196            "Htree" => Ok(Design::Htree),197            "Vtree" => Ok(Design::Vtree),198            "Void" => Ok(Design::Void),199            "Xtree" => Ok(Design::Xtree),200            "Ytree" => Ok(Design::Ytree),201            "Ztree" => Ok(Design::Ztree),202            "Point" => Ok(Design::Point),203            "Dust" => Ok(Design::Dust),204            "Hline" => Ok(Design::Hline),205            "Vline" => Ok(Design::Vline),206            "Star" => Ok(Design::Star),207            "Xline" => Ok(Design::Xline),208            "Yline" => Ok(Design::Yline),209            "Zline" => Ok(Design::Zline),210            other => value_error(format!("unknown design {other:?}.")),211        }212    }213}214215/// The five classic designs of the plane.216pub const CLASSICS_2D: [Design; 5] = [217    Design::Carpet,218    Design::Net,219    Design::Htree,220    Design::Vtree,221    Design::Void,222];223224/// The six classic designs of the cube.225pub const CLASSICS_3D: [Design; 6] = [226    Design::Carpet,227    Design::Net,228    Design::Xtree,229    Design::Ytree,230    Design::Ztree,231    Design::Void,232];233234/// The five antis of the plane, the complements of the five classics in order.235pub const ANTIS_2D: [Design; 5] = [236    Design::Point,237    Design::Dust,238    Design::Hline,239    Design::Vline,240    Design::Star,241];242243/// The six antis of the cube: point, dust, the three lines and the star.244pub const ANTIS_3D: [Design; 6] = [245    Design::Point,246    Design::Dust,247    Design::Xline,248    Design::Yline,249    Design::Zline,250    Design::Star,251];252253/// The origin of one tile layer, a one-field json object.254#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]255pub enum Source {256    /// A classic named design.257    #[serde(rename = "design")]258    Classic(Design),259    /// A numbered rule code, spelled as a decimal string.260    #[serde(rename = "code")]261    Code(#[serde(with = "decimal")] u128),262}263264mod decimal {265    use serde::{Deserialize, Deserializer, Serializer};266267    pub fn serialize<S: Serializer>(code: &u128, serializer: S) -> Result<S::Ok, S::Error> {268        serializer.collect_str(code)269    }270271    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u128, D::Error> {272        #[derive(Deserialize)]273        #[serde(untagged)]274        enum Code {275            Text(String),276            Number(u64),277        }278        match Code::deserialize(deserializer)? {279            Code::Text(text) => text.parse().map_err(serde::de::Error::custom),280            Code::Number(code) => Ok(code.into()),281        }282    }283}284285/// Returns the classic designs for a dimension.286pub fn classics(dimension: usize) -> Vec<Design> {287    match dimension {288        3 => CLASSICS_3D.to_vec(),289        _ => CLASSICS_2D.to_vec(),290    }291}292293/// Returns the anti designs for a dimension.294pub fn antis(dimension: usize) -> Vec<Design> {295    match dimension {296        3 => ANTIS_3D.to_vec(),297        _ => ANTIS_2D.to_vec(),298    }299}300301/// A complete recipe for one tile.302#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]303pub struct Tile {304    /// The construction family.305    pub group: Group,306    /// The base factor of the construction.307    pub factor: usize,308    /// The origin of each layer.309    pub sources: Vec<Source>,310    /// The grid size of each source.311    pub numbers: Vec<usize>,312    /// The fractal level of each source.313    pub levels: Vec<usize>,314    /// The quarter-turn rotation of each source.315    pub rotations: Vec<usize>,316    /// Whether each source swaps fill and void.317    pub anti: Vec<bool>,318    /// Whether the finished tile inverts.319    pub invert: bool,320    /// Whether the finished tile flips.321    pub flip: bool,322    /// The numeral base of the codes.323    #[serde(default)]324    pub base: Base,325    /// The tile's width in cells.326    pub width: usize,327    /// The tile's height in cells.328    pub height: usize,329}330331impl Tile {332    /// Builds an empty tile in a group.333    pub fn new(group: Group) -> Tile {334        Tile {335            group,336            factor: 0,337            sources: Vec::new(),338            numbers: Vec::new(),339            levels: Vec::new(),340            rotations: Vec::new(),341            anti: Vec::new(),342            invert: false,343            flip: false,344            base: Base::Two,345            width: 0,346            height: 0,347        }348    }349    /// Sets the tile's width and height.350    pub fn size(mut self, width: usize, height: usize) -> Tile {351        self.width = width;352        self.height = height;353        self354    }355    /// Returns the larger of width and height.356    pub fn max_size(&self) -> usize {357        self.width.max(self.height)358    }359    /// Returns whether the recipe is a magic tile of one repeated source at one repeated number,360    /// the shape a fractal tile of the same factor and level already draws.361    pub fn degenerate(&self) -> bool {362        self.group == Group::Magic363            && self.sources.len() > 1364            && uniform(&self.sources)365            && uniform(&self.numbers)366    }367    /// Recomputes the factor and side length the group and numbers imply, zero when they overflow.368    pub fn resize(&mut self) {369        let lead = self.numbers.first().copied().unwrap_or(0);370        if matches!(self.group, Group::General | Group::Fractal | Group::Magic) {371            self.factor = lead;372        }373        let size = match self.group {374            Group::General => lead,375            Group::Fractal => u32::try_from(self.levels.first().copied().unwrap_or(1))376                .ok()377                .and_then(|level| lead.checked_pow(level))378                .unwrap_or(0),379            Group::Magic => self380                .numbers381                .iter()382                .try_fold(1usize, |acc, &n| acc.checked_mul(n))383                .unwrap_or(0),384            Group::Special | Group::Mosaic => self.factor.checked_mul(lead).unwrap_or(0),385        };386        self.width = size;387        self.height = size;388    }389    /// Checks that the slots, numbers and sizes agree, or a terse note for the first broken law.390    pub fn check(&self) -> std::result::Result<(), &'static str> {391        let slots = self.sources.len();392        let wanted = match self.group {393            Group::Mosaic => slots == 3,394            Group::Magic => (2..=MAX_SLOTS).contains(&slots),395            _ => slots == 1,396        };397        if !wanted {398            return Err("wrong slot count");399        }400        if self.numbers.len() != slots401            || self.levels.len() != slots402            || self.rotations.len() != slots403            || self.anti.len() != slots404        {405            return Err("ragged slots");406        }407        if self408            .numbers409            .iter()410            .any(|&n| !(MIN_SIDE..=MAX_SIDE).contains(&n))411        {412            return Err("numbers are 2 to 64");413        }414        if self.rotations.iter().any(|&r| r > 3) {415            return Err("rotation is 0 to 3");416        }417        if self.flip && self.group != Group::Special {418            return Err("flip is special only");419        }420        if self.group == Group::Fractal {421            if !(1..=MAX_LEVEL).contains(&self.levels[0]) {422                return Err("level is 1 to 6");423            }424        } else if self.levels.iter().any(|&l| l != 1) {425            return Err("level is fractal only");426        }427        if matches!(self.group, Group::Special | Group::Mosaic)428            && !(MIN_SIDE..=MAX_SIDE).contains(&self.factor)429        {430            return Err("factor is 2 to 64");431        }432        if self.group == Group::Mosaic && self.numbers.iter().any(|&n| n != self.numbers[0]) {433            return Err("mosaic shares one number");434        }435        let mut probe = self.clone();436        probe.resize();437        if probe.width != self.width || probe.height != self.height || probe.factor != self.factor {438            return Err("sizes disagree");439        }440        if !(MIN_SIDE..=MAX_SIDE).contains(&self.max_size()) {441            return Err("size is 2 to 64");442        }443        Ok(())444    }445}446447const MIN_FACTOR: usize = 2;448449/// Returns whether every item equals the first, vacuously true for an empty or single list.450///451/// ```452/// assert!(mrlycore::tile::uniform(&[3, 3, 3]));453/// assert!(!mrlycore::tile::uniform(&[3, 5, 3]));454/// ```455pub fn uniform<T: PartialEq>(items: &[T]) -> bool {456    items.windows(2).all(|pair| pair[0] == pair[1])457}458459fn factors(min_factor: usize, max_factor: usize, parity: Parity) -> Vec<usize> {460    (min_factor.max(MIN_FACTOR)..=max_factor)461        .filter(|&n| parity.keep(n))462        .collect()463}464465/// Returns every flat size in the range that passes the parity filter.466pub fn generals(min_size: usize, max_size: usize, parity: Parity) -> Vec<usize> {467    factors(min_size, max_size, parity)468}469470/// Returns every factor and level whose power lands in the size range.471pub fn powers(min_size: usize, max_size: usize, parity: Parity) -> Vec<(usize, usize)> {472    let mut out = Vec::new();473    for n in factors(MIN_FACTOR, max_size, parity) {474        let mut level = 2;475        loop {476            match n.checked_pow(level as u32) {477                Some(size) if size <= max_size => {478                    if size >= min_size {479                        out.push((n, level));480                    }481                    level += 1;482                }483                _ => break,484            }485        }486    }487    out488}489490/// Returns the side a factor raised to a level makes, or None when no usize holds it.491///492/// ```493/// assert_eq!(mrlycore::tile::size(3, 3), Some(27));494/// assert_eq!(mrlycore::tile::size(3, 4294967298), None);495/// ```496pub fn size(number: i64, level: i64) -> Option<usize> {497    let number = usize::try_from(number).ok()?;498    let level = u32::try_from(level).ok()?;499    number.checked_pow(level)500}501502/// Returns every count-long factor list whose product lands in the size range.503pub fn products(min_size: usize, max_size: usize, count: usize, parity: Parity) -> Vec<Vec<usize>> {504    if count < 1 {505        return Vec::new();506    }507    fn walk(508        min_size: usize,509        max_size: usize,510        remaining: usize,511        parity: Parity,512        out: &mut Vec<Vec<usize>>,513    ) {514        if remaining == 1 {515            for n in factors(min_size, max_size, parity) {516                out.push(vec![n]);517            }518            return;519        }520        for n in factors(MIN_FACTOR, max_size, parity) {521            let next_min = min_size.div_ceil(n);522            let next_max = max_size / n;523            if next_max < MIN_FACTOR {524                continue;525            }526            let mut tails = Vec::new();527            walk(next_min, next_max, remaining - 1, parity, &mut tails);528            for tail in tails {529                let mut item = vec![n];530                item.extend(tail);531                out.push(item);532            }533        }534    }535    let mut out = Vec::new();536    walk(min_size, max_size, count, parity, &mut out);537    out538}539540/// Returns every factor list of depth two and beyond whose product lands in the size range.541pub fn nestings(min_size: usize, max_size: usize, parity: Parity) -> Vec<Vec<usize>> {542    let mut out = Vec::new();543    let mut depth = 2;544    loop {545        let found = products(min_size, max_size, depth, parity);546        if found.is_empty() {547            if depth > 2 {548                break;549            }550            depth += 1;551            if depth > max_size {552                break;553            }554            continue;555        }556        out.extend(found);557        depth += 1;558    }559    out560}561562#[cfg(test)]563mod tests {564    use super::*;565    use crate::json;566    #[test]567    fn parity_filters() {568        assert!(Parity::Odds.keep(3));569        assert!(!Parity::Odds.keep(4));570        assert!(Parity::Evens.keep(4));571        assert!(!Parity::Evens.keep(3));572        assert!(Parity::Both.keep(3));573        assert!(Parity::Both.keep(4));574    }575    #[test]576    fn generals_respects_parity_and_range() {577        assert_eq!(generals(3, 9, Parity::Odds), vec![3, 5, 7, 9]);578        assert_eq!(generals(3, 9, Parity::Evens), vec![4, 6, 8]);579        assert_eq!(generals(3, 9, Parity::Both), vec![3, 4, 5, 6, 7, 8, 9]);580    }581    #[test]582    fn powers_are_in_range() {583        for (n, level) in powers(3, 100, Parity::Odds) {584            let size = n.pow(level as u32);585            assert!((3..=100).contains(&size));586            assert!(level >= 2);587        }588        assert!(powers(3, 100, Parity::Odds).contains(&(3, 2)));589        assert!(powers(3, 100, Parity::Odds).contains(&(3, 4)));590    }591    #[test]592    fn products_multiply_into_range() {593        for option in products(3, 64, 2, Parity::Odds) {594            let size: usize = option.iter().product();595            assert!((3..=64).contains(&size));596            assert_eq!(option.len(), 2);597        }598    }599    #[test]600    fn nestings_go_deeper_than_two() {601        let deep = nestings(3, 300, Parity::Odds);602        assert!(deep.iter().any(|opt| opt.len() >= 3));603        for option in &deep {604            let size: usize = option.iter().product();605            assert!(size <= 300);606        }607    }608    #[test]609    fn tile_json_round_trips() {610        let mut tile = Tile::new(Group::Magic).size(45, 45);611        tile.sources = vec![Source::Classic(Design::Carpet), Source::Code(14)];612        tile.numbers = vec![5, 9];613        tile.levels = vec![1, 1];614        tile.rotations = vec![0, 0];615        tile.anti = vec![false, true];616        tile.factor = 5;617        let json = serde_json::to_value(&tile).unwrap();618        assert_eq!(json["group"], "Magic");619        assert_eq!(json["sources"][1], json!({ "code": "14" }));620        let back: Tile = serde_json::from_value(json).unwrap();621        assert_eq!(tile, back);622    }623    #[test]624    fn source_json_round_trips() {625        for source in [Source::Classic(Design::Vtree), Source::Code(232)] {626            let json = serde_json::to_value(source).unwrap();627            let back: Source = serde_json::from_value(json).unwrap();628            assert_eq!(source, back);629        }630    }631    #[test]632    fn source_json_spells_codes_as_strings() {633        let wide = u128::MAX - 1;634        let json = serde_json::to_value(Source::Code(wide)).unwrap();635        assert_eq!(json, json!({ "code": wide.to_string() }));636        let back: Source = serde_json::from_value(json).unwrap();637        assert_eq!(back, Source::Code(wide));638    }639    #[test]640    fn source_json_reads_bare_int_codes() {641        let read = |value| serde_json::from_value::<Source>(value);642        assert_eq!(read(json!({ "code": 7 })).unwrap(), Source::Code(7));643        assert!(read(json!({ "code": "soup" })).is_err());644        assert!(read(json!({ "code": true })).is_err());645        assert!(read(json!({ "design": "Soup" })).is_err());646    }647    #[test]648    fn resize_follows_the_size_law() {649        let mut tile = Tile::new(Group::Fractal);650        tile.sources = vec![Source::Code(7)];651        tile.numbers = vec![3];652        tile.levels = vec![2];653        tile.rotations = vec![0];654        tile.anti = vec![false];655        tile.resize();656        assert_eq!((tile.factor, tile.width, tile.height), (3, 9, 9));657        tile.group = Group::Special;658        tile.factor = 5;659        tile.resize();660        assert_eq!((tile.width, tile.height), (15, 15));661        tile.group = Group::Magic;662        tile.numbers = vec![3, 5];663        tile.resize();664        assert_eq!((tile.factor, tile.width), (3, 15));665    }666    #[test]667    fn resize_survives_empty_and_huge_tiles() {668        let mut bare = Tile::new(Group::Magic);669        bare.resize();670        assert_eq!(bare.width, 1);671        let mut huge = Tile::new(Group::Fractal);672        huge.numbers = vec![3];673        huge.levels = vec![4_294_967_298];674        huge.resize();675        assert_eq!(huge.width, 0);676    }677    #[test]678    fn check_names_the_first_broken_law() {679        let mut tile = Tile::new(Group::General);680        assert_eq!(tile.check(), Err("wrong slot count"));681        tile.sources = vec![Source::Code(7)];682        assert_eq!(tile.check(), Err("ragged slots"));683        tile.numbers = vec![3];684        tile.levels = vec![1];685        tile.rotations = vec![0];686        tile.anti = vec![false];687        tile.resize();688        assert_eq!(tile.check(), Ok(()));689        tile.rotations = vec![4];690        assert_eq!(tile.check(), Err("rotation is 0 to 3"));691        tile.rotations = vec![0];692        tile.flip = true;693        assert_eq!(tile.check(), Err("flip is special only"));694        tile.flip = false;695        tile.width = 5;696        assert_eq!(tile.check(), Err("sizes disagree"));697    }698    #[test]699    fn powers_generalize_beyond_classic_bases() {700        let options = powers(3, 1000, Parity::Odds);701        assert!(options.contains(&(3, 2)));702        assert!(options.contains(&(5, 2)));703        assert!(options.contains(&(7, 2)));704        assert!(options.contains(&(9, 2)));705        assert!(options.contains(&(13, 2)));706    }707    #[test]708    fn size_refuses_what_it_cannot_hold() {709        assert_eq!(size(3, 3), Some(27));710        assert_eq!(size(3, 0), Some(1));711        assert_eq!(size(-1, 2), None);712        assert_eq!(size(3, -1), None);713        assert_eq!(size(3, 64), None);714        assert_eq!(size(3, 4294967296), None);715        assert_eq!(size(3, 4294967298), None);716    }717    #[test]718    fn degenerate_marks_the_magic_tiles_a_fractal_already_draws() {719        let mut tile = Tile::new(Group::Magic);720        tile.sources = vec![Source::Classic(Design::Carpet); 2];721        tile.numbers = vec![3, 3];722        tile.levels = vec![1, 1];723        tile.rotations = vec![0, 0];724        tile.anti = vec![false, false];725        tile.resize();726        assert_eq!(tile.check(), Ok(()));727        assert!(tile.degenerate());728        tile.numbers = vec![3, 5];729        tile.resize();730        assert!(!tile.degenerate());731        tile.numbers = vec![3, 3];732        tile.sources = vec![Source::Classic(Design::Carpet), Source::Code(7)];733        tile.resize();734        assert!(!tile.degenerate());735    }736    #[test]737    fn degenerate_is_a_magic_law_only() {738        let mut tile = Tile::new(Group::Mosaic);739        tile.sources = vec![Source::Classic(Design::Carpet); 3];740        tile.numbers = vec![3, 3, 3];741        assert!(!tile.degenerate());742        tile.group = Group::General;743        tile.sources = vec![Source::Classic(Design::Carpet)];744        tile.numbers = vec![3];745        assert!(!tile.degenerate());746    }747    #[test]748    fn uniform_holds_for_short_lists() {749        assert!(uniform::<usize>(&[]));750        assert!(uniform(&[3]));751        assert!(uniform(&[3, 3, 3]));752        assert!(!uniform(&[3, 3, 5]));753    }754    #[test]755    fn evens_factors_work() {756        assert!(powers(4, 1000, Parity::Evens)757            .iter()758            .all(|(n, _)| n % 2 == 0));759        assert!(powers(4, 1000, Parity::Evens).contains(&(4, 2)));760        assert!(powers(4, 1000, Parity::Evens).contains(&(6, 2)));761    }762}