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}