name.rs
24.3 kB · rust · 650 lines
1use crate::core::error::{value_error, Error, Result};2use crate::gen::recipe::{Design, Group, Source, Tile as Recipe};3use crate::math::name::{kind, Named};4use serde::{Deserialize, Serialize};56kind!("tile");78const CODES_2D: [(Design, u128); 10] = [9 (Design::Carpet, 7),10 (Design::Net, 14),11 (Design::Htree, 3),12 (Design::Vtree, 5),13 (Design::Void, 9),14 (Design::Point, 8),15 (Design::Dust, 1),16 (Design::Hline, 12),17 (Design::Vline, 10),18 (Design::Star, 6),19];2021const TOTAL_2D: u128 = 16;2223/// Returns the plane's bang code of a classic design, or None for one outside the plane.24pub fn classic_code(design: Design) -> Option<u128> {25 CODES_2D26 .iter()27 .find(|&&(d, _)| d == design)28 .map(|&(_, code)| code)29}3031fn code_of(source: Source) -> Result<u128> {32 match source {33 Source::Classic(design) => match classic_code(design) {34 Some(code) => Ok(code),35 None => value_error(format!(36 "design {} has no code in the plane.",37 design.name()38 )),39 },40 Source::Code(code) => Ok(code),41 }42}4344fn slot<T: Copy>(values: &[T], what: &str) -> Result<T> {45 match values.first() {46 Some(&value) => Ok(value),47 None => value_error(format!("a recipe with no {what} has no name.")),48 }49}5051fn is_false(flag: &bool) -> bool {52 !flag53}5455/// One value for every slot of a tile, or one value per slot.56#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]57#[serde(untagged)]58pub enum Slots {59 /// The one value the slots share.60 One(usize),61 /// One value per slot, in slot order.62 Each(Vec<usize>),63}6465impl Slots {66 fn is_still(&self) -> bool {67 match self {68 Slots::One(turn) => *turn == 0,69 Slots::Each(turns) => turns.iter().all(|&turn| turn == 0),70 }71 }72 fn one(&self, what: &str) -> Result<usize> {73 match self {74 Slots::One(value) => Ok(*value),75 Slots::Each(_) => value_error(format!("a one-slot tile wants one {what}, not a list.")),76 }77 }78 fn each(&self, count: usize, what: &str) -> Result<Vec<usize>> {79 match self {80 Slots::One(0) if what == "turn" => Ok(vec![0; count]),81 Slots::One(_) => value_error(format!("a {count}-slot tile wants a {what} per slot.")),82 Slots::Each(values) if values.len() == count => Ok(values.clone()),83 Slots::Each(values) => value_error(format!(84 "a {count}-slot tile wants {count} {what}s, not {}.",85 values.len()86 )),87 }88 }89}9091impl Default for Slots {92 fn default() -> Slots {93 Slots::One(0)94 }95}9697/// A tile recipe folded to its one canonical object.98///99/// The key that carries the codes says the group: `code` is one design flat or, with `level`, raised100/// to a power; `magic` is a list of letters; `special` is one mask code over a factor; `mosaic` is101/// three codes behind a tree mask. Classics fold to their codes, a lone anti folds into `invert`,102/// and a level of one folds away, so aliases that draw one picture share one name.103///104/// ```105/// use mrlyrs::gen::name::Tile;106/// use mrlyrs::math::name::Named;107/// let carpet = Tile::from_json(r#"{"kind":"tile","code":7,"side":3,"level":2}"#).unwrap();108/// assert_eq!(carpet.recipe().unwrap().width, 9);109/// assert_eq!(Tile::of(&carpet.recipe().unwrap()).unwrap(), carpet);110/// ```111#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]112#[serde(deny_unknown_fields)]113pub struct Tile {114 /// The kind word.115 pub kind: Kind,116 /// The one design of a flat or fractal tile.117 #[serde(default, skip_serializing_if = "Option::is_none")]118 pub code: Option<u128>,119 /// The mask code of a special tile.120 #[serde(default, skip_serializing_if = "Option::is_none")]121 pub special: Option<u128>,122 /// The letters of a magic tile, first letter outermost.123 #[serde(default, skip_serializing_if = "Vec::is_empty")]124 pub magic: Vec<u128>,125 /// The three codes of a mosaic tile.126 #[serde(default, skip_serializing_if = "Vec::is_empty")]127 pub mosaic: Vec<u128>,128 /// The side of the mask of a special or mosaic tile.129 #[serde(default, skip_serializing_if = "Option::is_none")]130 pub factor: Option<usize>,131 /// The side each slot renders at, one per letter for a magic tile.132 pub side: Slots,133 /// The power a fractal tile is raised to, absent at one.134 #[serde(default, skip_serializing_if = "Option::is_none")]135 pub level: Option<usize>,136 /// The quarter turns of each slot, absent when nothing turns.137 #[serde(default, skip_serializing_if = "Slots::is_still")]138 pub turn: Slots,139 /// Whether each slot swaps fill and void, absent when none does.140 #[serde(default, skip_serializing_if = "Vec::is_empty")]141 pub anti: Vec<bool>,142 /// Whether a special tile flips its mask.143 #[serde(default, skip_serializing_if = "is_false")]144 pub flip: bool,145 /// Whether the finished tile inverts.146 #[serde(default, skip_serializing_if = "is_false")]147 pub invert: bool,148}149150fn blank() -> Tile {151 Tile {152 kind: Kind,153 code: None,154 special: None,155 magic: Vec::new(),156 mosaic: Vec::new(),157 factor: None,158 side: Slots::One(0),159 level: None,160 turn: Slots::One(0),161 anti: Vec::new(),162 flip: false,163 invert: false,164 }165}166167fn turns(recipe: &Recipe) -> Slots {168 Slots::Each(recipe.rotations.iter().map(|r| r % 4).collect())169}170171fn plane(code: u128) -> Result<u128> {172 if code >= TOTAL_2D {173 return value_error(format!("code {code} is not in the plane (0..15)."));174 }175 Ok(code)176}177178impl Tile {179 /// Folds a recipe to its name.180 ///181 /// # Errors182 ///183 /// Errs when a source carries no code, or when the recipe's slot lists are ragged.184 pub fn of(recipe: &Recipe) -> Result<Tile> {185 let codes = recipe186 .sources187 .iter()188 .map(|&s| code_of(s))189 .collect::<Result<Vec<u128>>>()?;190 let mut name = blank();191 name.invert = recipe.invert;192 match recipe.group {193 Group::General | Group::Fractal => {194 name.code = Some(slot(&codes, "source")?);195 name.side = Slots::One(slot(&recipe.numbers, "number")?);196 if recipe.group == Group::Fractal {197 let level = slot(&recipe.levels, "level")?;198 name.level = (level != 1).then_some(level);199 }200 name.turn = Slots::One(slot(&recipe.rotations, "rotation")? % 4);201 name.invert = slot(&recipe.anti, "anti flag")? ^ recipe.invert;202 }203 Group::Magic => {204 name.magic = codes;205 name.side = Slots::Each(recipe.numbers.clone());206 name.turn = turns(recipe);207 name.anti = recipe.anti.clone();208 }209 Group::Special => {210 name.special = Some(slot(&codes, "source")?);211 name.factor = Some(recipe.factor);212 name.side = Slots::One(slot(&recipe.numbers, "number")?);213 name.turn = Slots::One(slot(&recipe.rotations, "rotation")? % 4);214 name.flip = recipe.flip;215 }216 Group::Mosaic => {217 name.mosaic = codes;218 name.factor = Some(recipe.factor);219 name.side = Slots::One(slot(&recipe.numbers, "number")?);220 name.turn = turns(recipe);221 name.anti = recipe.anti.clone();222 }223 }224 Ok(name.fold())225 }226 fn fold(mut self) -> Tile {227 if self.turn.is_still() {228 self.turn = Slots::One(0);229 }230 if self.anti.iter().all(|&a| !a) {231 self.anti.clear();232 }233 self234 }235 fn group(&self) -> Result<Group> {236 let carried = [237 (self.code.is_some(), Group::General),238 (self.special.is_some(), Group::Special),239 (!self.magic.is_empty(), Group::Magic),240 (!self.mosaic.is_empty(), Group::Mosaic),241 ];242 let mut groups = carried243 .iter()244 .filter(|(held, _)| *held)245 .map(|&(_, group)| group);246 let (Some(group), None) = (groups.next(), groups.next()) else {247 return value_error("a tile carries exactly one of code, special, magic or mosaic.");248 };249 if group == Group::General && self.level.is_some_and(|level| level != 1) {250 return Ok(Group::Fractal);251 }252 if group != Group::General && self.level.is_some() {253 return value_error("level is fractal only.");254 }255 Ok(group)256 }257 fn anti(&self, count: usize) -> Result<Vec<bool>> {258 match self.anti.len() {259 0 => Ok(vec![false; count]),260 n if n == count => Ok(self.anti.clone()),261 n => value_error(format!(262 "a {count}-slot tile wants {count} anti flags, not {n}."263 )),264 }265 }266 fn lone(&self, count: usize) -> Result<()> {267 if !self.anti.is_empty() {268 return value_error("anti folds into invert on a one-slot tile.");269 }270 if count == 1 && self.factor.is_some() {271 return value_error("factor is special or mosaic only.");272 }273 Ok(())274 }275 /// Builds the recipe the name folds, resized and checked.276 ///277 /// # Errors278 ///279 /// Errs when the keys do not say one group, a code is not in the plane, or the check fails.280 pub fn recipe(&self) -> Result<Recipe> {281 let group = self.group()?;282 let mut recipe = Recipe::new(group);283 recipe.invert = self.invert;284 match group {285 Group::General | Group::Fractal => {286 self.lone(1)?;287 if self.flip {288 return value_error("flip is special only.");289 }290 recipe.sources = vec![Source::Code(plane(self.code.expect("a code"))?)];291 recipe.numbers = vec![self.side.one("side")?];292 recipe.levels = vec![self.level.unwrap_or(1)];293 recipe.rotations = vec![self.turn.one("turn")?];294 recipe.anti = vec![false];295 }296 Group::Magic => {297 let count = self.magic.len();298 if self.flip || self.factor.is_some() {299 return value_error("a magic tile carries no flip or factor.");300 }301 recipe.sources = self302 .magic303 .iter()304 .map(|&code| Ok(Source::Code(plane(code)?)))305 .collect::<Result<Vec<Source>>>()?;306 recipe.numbers = self.side.each(count, "side")?;307 recipe.levels = vec![1; count];308 recipe.rotations = self.turn.each(count, "turn")?;309 recipe.anti = self.anti(count)?;310 }311 Group::Special => {312 if !self.anti.is_empty() {313 return value_error("anti is dead on a special tile.");314 }315 let Some(factor) = self.factor else {316 return value_error("a special tile wants its factor.");317 };318 recipe.sources = vec![Source::Code(plane(self.special.expect("a mask code"))?)];319 recipe.factor = factor;320 recipe.numbers = vec![self.side.one("side")?];321 recipe.levels = vec![1];322 recipe.rotations = vec![self.turn.one("turn")?];323 recipe.anti = vec![false];324 recipe.flip = self.flip;325 }326 Group::Mosaic => {327 if self.flip {328 return value_error("flip is special only.");329 }330 let Some(factor) = self.factor else {331 return value_error("a mosaic tile wants its factor.");332 };333 recipe.sources = self334 .mosaic335 .iter()336 .map(|&code| Ok(Source::Code(plane(code)?)))337 .collect::<Result<Vec<Source>>>()?;338 recipe.factor = factor;339 recipe.numbers = vec![self.side.one("side")?; 3];340 recipe.levels = vec![1; 3];341 recipe.rotations = self.turn.each(3, "turn")?;342 recipe.anti = self.anti(3)?;343 }344 }345 recipe.resize();346 recipe347 .check()348 .map_err(|note| Error::Value(format!("tile fails its check: {note}.")))?;349 Ok(recipe)350 }351}352353impl Named for Tile {354 const KIND: &'static str = "tile";355 const LISTS: &'static [&'static str] = &["magic", "mosaic", "anti"];356 fn checked(self) -> Result<Tile> {357 Tile::of(&self.recipe()?)358 }359}360361#[cfg(test)]362mod tests {363 use super::*;364 use crate::core::rng::Rng;365 use crate::gen::build::{build_2d, create_2d, Config2d};366 use crate::gen::recipe::{Catalog, Parity};367 use crate::math::bang::Code;368 use crate::math::two::designs;369370 const CARPET: &str = r#"{"kind":"tile","code":7,"side":3,"level":2}"#;371 const GENERAL: &str = r#"{"kind":"tile","code":3,"side":5,"turn":1,"invert":true}"#;372 const MAGIC: &str = r#"{"kind":"tile","magic":[7,14],"side":[3,5],"turn":[0,2],"anti":[false,true],"invert":true}"#;373 const SPECIAL: &str = r#"{"kind":"tile","special":5,"factor":3,"side":5,"flip":true}"#;374 const MOSAIC: &str = r#"{"kind":"tile","mosaic":[7,14,5],"factor":3,"side":3,"turn":[0,1,0],"anti":[false,false,true],"invert":true}"#;375376 fn built(recipe: &Recipe) -> crate::math::two::Cell2d {377 build_2d(recipe).unwrap()378 }379 fn fractal(design: Design) -> Recipe {380 let mut recipe = Recipe::new(Group::Fractal);381 recipe.sources = vec![Source::Classic(design)];382 recipe.numbers = vec![3];383 recipe.levels = vec![2];384 recipe.rotations = vec![0];385 recipe.anti = vec![false];386 recipe.resize();387 recipe388 }389390 #[test]391 fn classic_codes_match_their_renders() {392 for (design, code) in CODES_2D {393 let by_name = designs::create(Code::from(code), 3, 1, 0, 2).unwrap();394 let by_classic = match design {395 Design::Carpet => designs::carpet(3, 1).unwrap(),396 Design::Net => designs::net(3, 1).unwrap(),397 Design::Htree => designs::htree(3, 1).unwrap(),398 Design::Vtree => designs::vtree(3, 1).unwrap(),399 Design::Void => designs::void(3, 1).unwrap(),400 Design::Point => designs::point(3, 1).unwrap(),401 Design::Dust => designs::dust(3, 1).unwrap(),402 Design::Hline => designs::hline(3, 1).unwrap(),403 Design::Vline => designs::vline(3, 1).unwrap(),404 Design::Star => designs::star(3, 1).unwrap(),405 _ => unreachable!(),406 };407 assert_eq!(by_name, by_classic, "{}", design.name());408 }409 }410 #[test]411 fn example_names_hold_verbatim() {412 assert_eq!(413 Tile::of(&fractal(Design::Carpet)).unwrap().to_json(),414 CARPET415 );416 let mut general = Recipe::new(Group::General);417 general.sources = vec![Source::Code(3)];418 general.numbers = vec![5];419 general.levels = vec![1];420 general.rotations = vec![1];421 general.anti = vec![false];422 general.invert = true;423 general.resize();424 assert_eq!(Tile::of(&general).unwrap().to_json(), GENERAL);425 let mut magic = Recipe::new(Group::Magic);426 magic.sources = vec![Source::Classic(Design::Carpet), Source::Code(14)];427 magic.numbers = vec![3, 5];428 magic.levels = vec![1, 1];429 magic.rotations = vec![0, 2];430 magic.anti = vec![false, true];431 magic.invert = true;432 magic.resize();433 assert_eq!(Tile::of(&magic).unwrap().to_json(), MAGIC);434 let mut special = Recipe::new(Group::Special);435 special.sources = vec![Source::Classic(Design::Vtree)];436 special.factor = 3;437 special.numbers = vec![5];438 special.levels = vec![1];439 special.rotations = vec![0];440 special.anti = vec![true];441 special.flip = true;442 special.resize();443 assert_eq!(Tile::of(&special).unwrap().to_json(), SPECIAL);444 let mut mosaic = Recipe::new(Group::Mosaic);445 mosaic.sources = vec![Source::Code(7), Source::Code(14), Source::Code(5)];446 mosaic.factor = 3;447 mosaic.numbers = vec![3, 3, 3];448 mosaic.levels = vec![1, 1, 1];449 mosaic.rotations = vec![0, 1, 0];450 mosaic.anti = vec![false, false, true];451 mosaic.invert = true;452 mosaic.resize();453 assert_eq!(Tile::of(&mosaic).unwrap().to_json(), MOSAIC);454 }455 #[test]456 fn the_views_hold_verbatim() {457 let magic = Tile::from_json(MAGIC).unwrap();458 assert_eq!(459 magic.to_url().unwrap(),460 "/tile?magic=7,14&side=3,5&turn=0,2&anti=false,true&invert=true"461 );462 assert_eq!(463 magic.to_file().unwrap(),464 "tile_magic=[7,14]_side=[3,5]_turn=[0,2]_anti=[false,true]_invert=true"465 );466 assert_eq!(467 magic.to_mrly().unwrap(),468 "tile magic [7 14], side [3 5], turn [0 2], anti [false true], invert"469 );470 let carpet = Tile::from_json(CARPET).unwrap();471 assert_eq!(carpet.to_url().unwrap(), "/tile?code=7&side=3&level=2");472 assert_eq!(carpet.to_file().unwrap(), "tile_code=7_side=3_level=2");473 assert_eq!(carpet.to_mrly().unwrap(), "tile code 7, side 3, level 2");474 assert_eq!(475 Tile::from_json(SPECIAL).unwrap().to_mrly().unwrap(),476 "tile special 5, factor 3, side 5, flip"477 );478 }479 #[test]480 fn parsed_tiles_pass_check_and_build() {481 for name in [CARPET, GENERAL, MAGIC, SPECIAL, MOSAIC] {482 let tile = Tile::from_json(name).unwrap();483 let recipe = tile.recipe().unwrap();484 assert!(recipe.check().is_ok());485 assert_eq!(tile.to_json(), name);486 assert_eq!(487 Tile::from_url(&tile.to_url().unwrap()).unwrap(),488 tile,489 "{name}"490 );491 assert_eq!(492 Tile::from_file(&tile.to_file().unwrap()).unwrap(),493 tile,494 "{name}"495 );496 let cell = built(&recipe);497 assert_eq!(cell.width(), recipe.width);498 }499 }500 #[test]501 fn a_level_of_one_folds_to_the_flat_tile() {502 let mut flat = fractal(Design::Carpet);503 flat.levels = vec![1];504 flat.resize();505 let name = Tile::of(&flat).unwrap();506 assert_eq!(name.to_json(), r#"{"kind":"tile","code":7,"side":3}"#);507 let spelt = Tile::from_json(r#"{"kind":"tile","code":7,"side":3,"level":1}"#).unwrap();508 assert_eq!(spelt, name);509 let recipe = spelt.recipe().unwrap();510 assert_eq!(recipe.group, Group::General);511 assert_eq!(built(&recipe), built(&flat));512 }513 #[test]514 fn anti_invert_pairs_share_one_name_and_one_picture() {515 let mut plain = fractal(Design::Carpet);516 let mut folded = plain.clone();517 folded.anti = vec![true];518 folded.invert = true;519 assert_eq!(Tile::of(&plain).unwrap(), Tile::of(&folded).unwrap());520 assert_eq!(built(&plain), built(&folded));521 plain.invert = true;522 let mut alias = plain.clone();523 alias.anti = vec![true];524 alias.invert = false;525 assert_eq!(Tile::of(&plain).unwrap(), Tile::of(&alias).unwrap());526 assert_eq!(built(&plain), built(&alias));527 assert_eq!(528 Tile::of(&plain).unwrap().to_json(),529 r#"{"kind":"tile","code":7,"side":3,"level":2,"invert":true}"#530 );531 }532 #[test]533 fn classics_and_codes_share_one_name() {534 let by_classic = fractal(Design::Net);535 let mut by_code = by_classic.clone();536 by_code.sources = vec![Source::Code(14)];537 assert_eq!(Tile::of(&by_classic).unwrap(), Tile::of(&by_code).unwrap());538 assert_eq!(built(&by_classic), built(&by_code));539 }540 #[test]541 fn dead_special_anti_folds_away() {542 let mut special = Recipe::new(Group::Special);543 special.sources = vec![Source::Code(5)];544 special.factor = 3;545 special.numbers = vec![3];546 special.levels = vec![1];547 special.rotations = vec![0];548 special.anti = vec![true];549 special.resize();550 let parsed = Tile::from_json(&Tile::of(&special).unwrap().to_json())551 .unwrap()552 .recipe()553 .unwrap();554 assert_eq!(parsed.anti, vec![false]);555 assert_eq!(built(&special), built(&parsed));556 }557 #[test]558 fn a_spelt_default_folds_to_the_canonical_string() {559 let spelt = r#"{"kind":"tile","side":[3,5],"magic":[7,14],"turn":[0,0],"anti":[false,false],"invert":false}"#;560 let tile = Tile::from_json(spelt).unwrap();561 assert_eq!(562 tile.to_json(),563 r#"{"kind":"tile","magic":[7,14],"side":[3,5]}"#564 );565 assert_eq!(tile.turn, Slots::One(0));566 assert!(tile.anti.is_empty());567 }568 #[test]569 fn refuses_a_name_or_a_recipe_that_cannot_draw() {570 for bad in [571 r#"{"kind":"tile","code":7,"side":3,"turn":4}"#,572 r#"{"kind":"tile","code":16,"side":3}"#,573 r#"{"kind":"tile","code":7,"side":99}"#,574 r#"{"kind":"tile","code":7,"side":3,"flip":true}"#,575 r#"{"kind":"tile","code":7,"side":3,"anti":[true]}"#,576 r#"{"kind":"tile","code":7,"side":3,"factor":3}"#,577 r#"{"kind":"tile","code":7,"side":[3]}"#,578 r#"{"kind":"tile","code":7,"side":3,"level":7}"#,579 r#"{"kind":"tile","code":7,"magic":[7,14],"side":3}"#,580 r#"{"kind":"tile","side":3}"#,581 r#"{"kind":"tile","sparkle":7,"side":3}"#,582 r#"{"kind":"tile","magic":[7],"side":[3]}"#,583 r#"{"kind":"tile","magic":[7,14],"side":3}"#,584 r#"{"kind":"tile","magic":[7,14],"side":[3,5],"anti":[true]}"#,585 r#"{"kind":"tile","magic":[7,14],"side":[3,5],"level":2}"#,586 r#"{"kind":"tile","mosaic":[7,14,5],"factor":3,"side":[3,3,3]}"#,587 r#"{"kind":"tile","mosaic":[7,14],"factor":3,"side":3}"#,588 r#"{"kind":"tile","mosaic":[7,14,5],"side":3}"#,589 r#"{"kind":"tile","special":5,"side":5}"#,590 r#"{"kind":"tile","special":5,"factor":3,"side":5,"anti":[true]}"#,591 r#"{"kind":"bang","dim":2,"code":7}"#,592 "tile code 7, side 3, level 2",593 "tile_code=7_side=3_level=2",594 ] {595 assert!(Tile::from_json(bad).is_err(), "{bad}");596 }597 assert!(Tile::of(&Recipe::new(Group::General)).is_err());598 let mut cubic = fractal(Design::Xtree);599 cubic.resize();600 assert!(classic_code(Design::Xtree).is_none());601 assert!(Tile::of(&cubic).is_err());602 }603 #[test]604 fn seeded_tiles_round_trip() {605 let config = Config2d {606 catalog: Catalog::Universe,607 min_size: 2,608 max_size: 64,609 parity: Parity::Both,610 ..Config2d::default()611 };612 for s in 0..300 {613 let mut rng = Rng::new(s);614 let recipe = create_2d(&config, &mut rng).unwrap();615 let name = Tile::of(&recipe).unwrap();616 let text = name.to_json();617 let parsed = Tile::from_json(&text).unwrap();618 assert_eq!(parsed.to_json(), text, "seed {s}");619 assert_eq!(620 Tile::from_url(&name.to_url().unwrap()).unwrap(),621 name,622 "seed {s}"623 );624 assert_eq!(625 Tile::from_file(&name.to_file().unwrap()).unwrap(),626 name,627 "seed {s}"628 );629 let back = parsed.recipe().unwrap();630 assert!(back.check().is_ok(), "seed {s}");631 assert_eq!(built(&back), built(&recipe), "seed {s}");632 }633 }634 #[test]635 fn seeded_classic_tiles_round_trip() {636 let config = Config2d {637 min_size: 2,638 max_size: 64,639 parity: Parity::Both,640 ..Config2d::default()641 };642 for s in 0..300 {643 let mut rng = Rng::new(s);644 let recipe = create_2d(&config, &mut rng).unwrap();645 let text = Tile::of(&recipe).unwrap().to_json();646 let parsed = Tile::from_json(&text).unwrap();647 assert_eq!(built(&parsed.recipe().unwrap()), built(&recipe), "seed {s}");648 }649 }650}