rule.rs
14.5 kB · rust · 398 lines
1use super::{kind, Named};2use crate::life::{Boundary, Config, Counts};3use crate::two::Cell2d;4use mrlycore::errors::Result;5use serde::{Deserialize, Serialize};67kind!("rule");89fn is_false(flag: &bool) -> bool {10 !flag11}1213fn fold(counts: Counts) -> Counts {14 match counts {15 Counts::List(mut list) => {16 list.sort_unstable();17 list.dedup();18 Counts::List(list)19 }20 drawn => drawn,21 }22}2324mod counts {25 use crate::life::{Counts, Sequence};26 use serde::de::{Error, SeqAccess, Visitor};27 use serde::ser::SerializeSeq;28 use serde::{Deserializer, Serializer};29 use std::fmt;3031 pub fn spell(counts: &Counts) -> Option<String> {32 let Counts::Drawn { seq, zeros, ones } = counts else {33 return None;34 };35 let mut out = seq.name();36 if *zeros {37 out.push_str("_zeros");38 }39 if *ones {40 out.push_str("_ones");41 }42 Some(out)43 }4445 pub fn read(text: &str) -> Option<Counts> {46 let (seq, tail) = Sequence::read(text)?;47 let (zeros, tail) = match tail.strip_prefix("_zeros") {48 Some(rest) => (true, rest),49 None => (false, tail),50 };51 let (ones, tail) = match tail.strip_prefix("_ones") {52 Some(rest) => (true, rest),53 None => (false, tail),54 };55 tail.is_empty().then(|| Counts::drawn(seq, zeros, ones))56 }5758 pub fn serialize<S: Serializer>(counts: &Counts, serializer: S) -> Result<S::Ok, S::Error> {59 match counts {60 Counts::List(list) => {61 let folded = super::fold(Counts::List(list.clone()));62 let Counts::List(folded) = folded else {63 unreachable!()64 };65 let mut seq = serializer.serialize_seq(Some(folded.len()))?;66 for n in folded {67 seq.serialize_element(&n)?;68 }69 seq.end()70 }71 drawn => serializer.serialize_str(&spell(drawn).expect("a drawn side spells")),72 }73 }7475 struct Side;7677 impl<'de> Visitor<'de> for Side {78 type Value = Counts;79 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {80 f.write_str("a list of counts or a sequence word")81 }82 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Counts, A::Error> {83 let mut list = Vec::new();84 while let Some(n) = seq.next_element::<usize>()? {85 list.push(n);86 }87 Ok(Counts::List(list))88 }89 fn visit_str<E: Error>(self, text: &str) -> Result<Counts, E> {90 read(text).ok_or_else(|| E::custom(format!("sequence {text:?} is not known.")))91 }92 }9394 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Counts, D::Error> {95 deserializer.deserialize_any(Side)96 }97}9899/// A life rule: the birth and survival counts and whether the edge wraps.100///101/// ```102/// use mrlymath::name::{Named, Rule};103/// let conway = Rule::new(vec![3], vec![2, 3], false);104/// assert_eq!(conway.to_json(), r#"{"kind":"rule","birth":[3],"survive":[2,3]}"#);105/// assert_eq!(Rule::from_json(&conway.to_json()).unwrap(), conway);106/// ```107#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]108#[serde(deny_unknown_fields)]109pub struct Rule {110 /// The kind word.111 pub kind: Kind,112 /// The neighbor counts that create a cell, listed or drawn from a sequence.113 #[serde(with = "counts")]114 pub birth: Counts,115 /// The neighbor counts that keep a cell, listed or drawn from a sequence.116 #[serde(with = "counts")]117 pub survive: Counts,118 /// Whether the edge wraps, false unless said.119 #[serde(default, skip_serializing_if = "is_false")]120 pub wrap: bool,121}122123impl Rule {124 /// Builds a rule from its counts and edge policy, listed counts folded to a sorted set.125 pub fn new(birth: impl Into<Counts>, survive: impl Into<Counts>, wrap: bool) -> Rule {126 Rule {127 kind: Kind,128 birth: fold(birth.into()),129 survive: fold(survive.into()),130 wrap,131 }132 }133 /// Reads the rule out of a life config.134 pub fn of(config: &Config) -> Rule {135 Rule::new(136 config.birth.clone(),137 config.survive.clone(),138 config.boundary.wrap(),139 )140 }141 /// Returns the edge policy the rule runs under.142 pub fn boundary(&self) -> Boundary {143 if self.wrap {144 Boundary::Wrap145 } else {146 Boundary::Constant147 }148 }149 /// Builds a life config running this rule over a neighborhood mask.150 pub fn config(&self, mask: Cell2d) -> Config {151 let mut config = Config::new(mask, self.birth.clone(), self.survive.clone());152 config.boundary = self.boundary();153 config154 }155}156157impl Named for Rule {158 const KIND: &'static str = "rule";159 const LISTS: &'static [&'static str] = &["birth", "survive"];160 fn checked(self) -> Result<Rule> {161 Ok(Rule::new(self.birth, self.survive, self.wrap))162 }163}164165#[cfg(test)]166mod tests {167 use super::*;168 use crate::life::{moore, Sequence, Story};169 use mrlycore::rng::Rng;170 use mrlycore::tensor::Tensor;171172 const CONWAY: &str = r#"{"kind":"rule","birth":[3],"survive":[2,3]}"#;173174 fn wide_mask(side: usize) -> Cell2d {175 let mut mask = Tensor::full(vec![side, side], 1);176 mask.set(&[side / 2, side / 2], 0);177 Cell2d::new(mask)178 }179180 #[test]181 fn conway_holds_through_every_view() {182 let conway = Rule::new(vec![3], vec![2, 3], false);183 assert_eq!(conway.to_json(), CONWAY);184 assert_eq!(Rule::from_json(CONWAY).unwrap(), conway);185 assert_eq!(conway.to_url(), "/rule?birth=3&survive=2,3");186 assert_eq!(conway.to_file(), "rule_birth=[3]_survive=[2,3]");187 assert_eq!(conway.to_mrly(), "rule birth [3], survive [2 3]");188 assert_eq!(Rule::from_url(&conway.to_url()).unwrap(), conway);189 assert_eq!(Rule::from_file(&conway.to_file()).unwrap(), conway);190 assert_eq!(conway.to_id().len(), 8);191 let wrapped = Rule::new(vec![3], vec![2, 3], true);192 assert_eq!(193 wrapped.to_json(),194 r#"{"kind":"rule","birth":[3],"survive":[2,3],"wrap":true}"#195 );196 assert_eq!(wrapped.to_mrly(), "rule birth [3], survive [2 3], wrap");197 assert_ne!(wrapped.to_id(), conway.to_id());198 }199 #[test]200 fn the_wide_row_holds() {201 let wide = Rule::new(202 vec![12, 13],203 Counts::drawn(Sequence::Fibonacci, false, false),204 true,205 );206 let text = r#"{"kind":"rule","birth":[12,13],"survive":"fibonacci","wrap":true}"#;207 assert_eq!(wide.to_json(), text);208 assert_eq!(Rule::from_json(text).unwrap(), wide);209 assert_eq!(210 wide.to_url(),211 "/rule?birth=12,13&survive=fibonacci&wrap=true"212 );213 assert_eq!(214 wide.to_file(),215 "rule_birth=[12,13]_survive=fibonacci_wrap=true"216 );217 assert_eq!(218 wide.to_mrly(),219 "rule birth [12 13], survive fibonacci, wrap"220 );221 assert_eq!(Rule::from_url(&wide.to_url()).unwrap(), wide);222 assert_eq!(Rule::from_file(&wide.to_file()).unwrap(), wide);223 }224 #[test]225 fn to_json_folds_to_the_canonical_counts() {226 let messy = Rule::new(vec![3, 3, 1], vec![9, 2], false);227 assert_eq!(228 messy.to_json(),229 r#"{"kind":"rule","birth":[1,3],"survive":[2,9]}"#230 );231 let empty = Rule::new(Vec::new(), Vec::new(), false);232 assert_eq!(233 empty.to_json(),234 r#"{"kind":"rule","birth":[],"survive":[]}"#235 );236 assert_eq!(Rule::from_json(&empty.to_json()).unwrap(), empty);237 assert_eq!(Rule::from_url("/rule?birth=&survive=").unwrap(), empty);238 assert_eq!(Rule::from_file("rule_birth=[]_survive=[]").unwrap(), empty);239 let spelt =240 Rule::from_json(r#"{"kind":"rule","survive":[3,2,3],"birth":[3],"wrap":false}"#)241 .unwrap();242 assert_eq!(spelt, Rule::new(vec![3], vec![2, 3], false));243 assert_eq!(spelt.to_json(), CONWAY);244 }245 #[test]246 fn only_a_rule_parses() {247 for bad in [248 r#"{"kind":"bang","birth":[3],"survive":[2,3]}"#,249 r#"{"birth":[3],"survive":[2,3]}"#,250 r#"{"kind":"rule","birth":[3]}"#,251 r#"{"kind":"rule","birth":[3],"survive":[2,3],"wrap":1}"#,252 r#"{"kind":"rule","birth":[3],"survive":[2,3],"mask":7}"#,253 r#"{"kind":"rule","birth":"fib","survive":[3]}"#,254 r#"{"kind":"rule","birth":"random","survive":[3]}"#,255 r#"{"kind":"rule","birth":"random_007","survive":[3]}"#,256 r#"{"kind":"rule","birth":"fibonacci_zeros_zeros","survive":[3]}"#,257 r#"{"kind":"rule","birth":"fibonacci_ones_zeros","survive":[3]}"#,258 r#"{"kind":"rule","birth":"fibonacciq","survive":[3]}"#,259 r#"{"kind":"rule","birth":[-1],"survive":[3]}"#,260 r#"{"kind":"rule","birth":3,"survive":[3]}"#,261 "rule birth [3], survive [2 3]",262 "rule_birth=[3]_survive=[2,3]",263 ] {264 assert!(Rule::from_json(bad).is_err(), "{bad}");265 }266 }267 #[test]268 fn a_listed_count_above_nine_has_a_name() {269 let rule = Rule::new(vec![3, 12], vec![2, 3, 48], true);270 assert_eq!(271 rule.to_json(),272 r#"{"kind":"rule","birth":[3,12],"survive":[2,3,48],"wrap":true}"#273 );274 assert_eq!(Rule::from_json(&rule.to_json()).unwrap(), rule);275 let mut config = Config::new(moore(), vec![3], vec![2, 3]);276 config.survive = Counts::List(vec![48]);277 assert_eq!(Rule::of(&config).survive, Counts::List(vec![48]));278 }279 #[test]280 fn config_round_trips_through_the_rule() {281 let mask = crate::two::designs::ones(3, 1).unwrap();282 let rule = Rule::new(vec![3, 6], vec![2, 3], true);283 let config = rule.config(mask);284 assert_eq!(config.boundary, Boundary::Wrap);285 assert_eq!(Rule::of(&config), rule);286 assert_eq!(287 Rule::of(&config).to_json(),288 r#"{"kind":"rule","birth":[3,6],"survive":[2,3],"wrap":true}"#289 );290 }291 #[test]292 fn a_drawn_rule_names_its_sequence() {293 let rule = Rule::new(294 Counts::drawn(Sequence::Fibonacci, false, true),295 Counts::drawn(Sequence::GridSquares, false, false),296 true,297 );298 assert_eq!(299 rule.to_json(),300 r#"{"kind":"rule","birth":"fibonacci_ones","survive":"grid_squares","wrap":true}"#301 );302 assert_eq!(Rule::from_json(&rule.to_json()).unwrap(), rule);303 assert_eq!(Rule::from_url(&rule.to_url()).unwrap(), rule);304 assert_eq!(Rule::from_file(&rule.to_file()).unwrap(), rule);305 let seeded = Rule::new(306 Counts::drawn(Sequence::Random(4848495), true, false),307 vec![3],308 false,309 );310 assert_eq!(311 seeded.to_json(),312 r#"{"kind":"rule","birth":"random_4848495_zeros","survive":[3]}"#313 );314 assert_eq!(Rule::from_json(&seeded.to_json()).unwrap(), seeded);315 assert_eq!(Rule::from_file(&seeded.to_file()).unwrap(), seeded);316 assert_eq!(317 seeded.to_file(),318 "rule_birth=random_4848495_zeros_survive=[3]"319 );320 }321 #[test]322 fn a_wide_mask_run_replays_from_its_name() {323 let mask = wide_mask(7);324 let rule = Rule::new(325 Counts::drawn(Sequence::Fibonacci, false, false),326 Counts::drawn(Sequence::Primes, false, false),327 true,328 );329 let mut config = rule.config(mask.clone());330 config.max_generations = 12;331 assert_eq!(config.budget(), 48);332 let (birth, survive) = config.counts().unwrap();333 assert!(birth.iter().any(|&n| n > 9), "{birth:?}");334 assert!(survive.iter().any(|&n| n > 9), "{survive:?}");335 let mut seed = Tensor::new(vec![15, 15]);336 for (y, x) in [(6, 7), (7, 6), (7, 7), (7, 8), (8, 7)] {337 seed.set(&[y, x], 1);338 }339 let seed = Cell2d::new(seed);340 let mut story = Story::new();341 story.add(&seed, &config).unwrap();342 let back = Story::from_json(&story.to_json().unwrap()).unwrap();343 assert_eq!(Rule::of(&back.chapters[0].config), rule);344 assert_eq!(back.chapters[0].config.counts().unwrap(), (birth, survive));345 for (a, b) in story.grids().iter().zip(back.grids()) {346 assert_eq!(a.types(), b.types());347 }348 assert!(story.grids().len() > 1);349 }350 #[test]351 fn the_moore_budget_stays_in_the_digits() {352 let config = Rule::new(vec![3], vec![2, 3], false).config(moore());353 assert_eq!(config.budget(), 8);354 }355 #[test]356 fn seeded_values_round_trip() {357 let mut rng = Rng::new(5);358 for _ in 0..500 {359 let draw = |rng: &mut Rng| {360 let count = rng.below(5);361 (0..count).map(|_| rng.below(50)).collect::<Vec<usize>>()362 };363 let rule = Rule::new(draw(&mut rng), draw(&mut rng), rng.boolean());364 let text = rule.to_json();365 let back = Rule::from_json(&text).unwrap();366 assert_eq!(367 back.birth.values(49).unwrap(),368 rule.birth.values(49).unwrap()369 );370 assert_eq!(371 back.survive.values(49).unwrap(),372 rule.survive.values(49).unwrap()373 );374 assert_eq!(back.wrap, rule.wrap);375 assert_eq!(back.to_json(), text);376 assert_eq!(Rule::from_url(&rule.to_url()).unwrap(), rule);377 assert_eq!(Rule::from_file(&rule.to_file()).unwrap(), rule);378 }379 }380 #[test]381 fn seeded_sequences_round_trip() {382 let mut rng = Rng::new(11);383 let pool = Sequence::all();384 for _ in 0..200 {385 let pick = |rng: &mut Rng| match rng.below(3) {386 0 => Sequence::Random(rng.range(0, i64::MAX) as u64),387 1 => Sequence::CodeFills(rng.below(16) as u128),388 _ => *rng.choice(&pool),389 };390 let side = |rng: &mut Rng| Counts::drawn(pick(rng), rng.boolean(), rng.boolean());391 let rule = Rule::new(side(&mut rng), side(&mut rng), false);392 let text = rule.to_json();393 assert_eq!(Rule::from_json(&text).unwrap(), rule, "{text}");394 assert_eq!(Rule::from_url(&rule.to_url()).unwrap(), rule, "{text}");395 assert_eq!(Rule::from_file(&rule.to_file()).unwrap(), rule, "{text}");396 }397 }398}