rule.rs
14.5 kB · rust · 407 lines
1use crate::core::error::Result;2use crate::life::{Boundary, Config, Counts};3use crate::math::name::{kind, Named};4use crate::math::two::Cell2d;5use serde::{Deserialize, Serialize};67kind!("rule");89fn is_false(flag: &bool) -> bool {10 !flag11}1213fn sorted(mut list: Vec<usize>) -> Vec<usize> {14 list.sort_unstable();15 list.dedup();16 list17}1819fn fold(counts: Counts) -> Counts {20 match counts {21 Counts::List(list) => Counts::List(sorted(list)),22 drawn => drawn,23 }24}2526mod counts {27 use crate::life::{Counts, Source};28 use serde::de::{Error, SeqAccess, Visitor};29 use serde::ser::SerializeSeq;30 use serde::{Deserializer, Serializer};31 use std::fmt;3233 fn word(seq: Source, zeros: bool, ones: bool) -> String {34 let mut out = seq.name();35 if zeros {36 out.push_str("_zeros");37 }38 if ones {39 out.push_str("_ones");40 }41 out42 }4344 pub fn read(text: &str) -> Option<Counts> {45 let (seq, tail) = Source::read(text)?;46 let (zeros, tail) = match tail.strip_prefix("_zeros") {47 Some(rest) => (true, rest),48 None => (false, tail),49 };50 let (ones, tail) = match tail.strip_prefix("_ones") {51 Some(rest) => (true, rest),52 None => (false, tail),53 };54 tail.is_empty().then(|| Counts::drawn(seq, zeros, ones))55 }5657 pub fn serialize<S: Serializer>(counts: &Counts, serializer: S) -> Result<S::Ok, S::Error> {58 match counts {59 Counts::List(list) => {60 let folded = super::sorted(list.clone());61 let mut seq = serializer.serialize_seq(Some(folded.len()))?;62 for n in folded {63 seq.serialize_element(&n)?;64 }65 seq.end()66 }67 Counts::Drawn { seq, zeros, ones } => {68 serializer.serialize_str(&word(*seq, *zeros, *ones))69 }70 }71 }7273 struct Side;7475 impl<'de> Visitor<'de> for Side {76 type Value = Counts;77 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {78 f.write_str("a list of counts or a sequence word")79 }80 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Counts, A::Error> {81 let mut list = Vec::new();82 while let Some(n) = seq.next_element::<usize>()? {83 list.push(n);84 }85 Ok(Counts::List(list))86 }87 fn visit_str<E: Error>(self, text: &str) -> Result<Counts, E> {88 read(text).ok_or_else(|| E::custom(format!("sequence {text:?} is not known.")))89 }90 }9192 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Counts, D::Error> {93 deserializer.deserialize_any(Side)94 }95}9697/// A life rule: the birth and survival counts and whether the edge wraps.98///99/// ```100/// use mrlyrs::life::Rule;101/// use mrlyrs::math::name::Named;102/// let conway = Rule::new(vec![3], vec![2, 3], false);103/// assert_eq!(conway.to_json(), r#"{"kind":"rule","birth":[3],"survive":[2,3]}"#);104/// Rule::from_json(&conway.to_json())?;105/// # Ok::<(), mrlyrs::Error>(())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::core::rng::Rng;169 use crate::core::tensor::Tensor;170 use crate::life::{animate, moore, Source};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).unwrap();177 Cell2d::new(mask).unwrap()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().unwrap(), "/rule?birth=3&survive=2,3");186 assert_eq!(conway.to_file().unwrap(), "rule_birth=[3]_survive=[2,3]");187 assert_eq!(conway.to_mrly().unwrap(), "rule birth [3], survive [2 3]");188 assert_eq!(Rule::from_url(&conway.to_url().unwrap()).unwrap(), conway);189 assert_eq!(Rule::from_file(&conway.to_file().unwrap()).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!(197 wrapped.to_mrly().unwrap(),198 "rule birth [3], survive [2 3], wrap"199 );200 assert_ne!(wrapped.to_id(), conway.to_id());201 }202 #[test]203 fn the_wide_row_holds() {204 let wide = Rule::new(205 vec![12, 13],206 Counts::drawn(Source::Fibonacci, false, false),207 true,208 );209 let text = r#"{"kind":"rule","birth":[12,13],"survive":"fibonacci","wrap":true}"#;210 assert_eq!(wide.to_json(), text);211 assert_eq!(Rule::from_json(text).unwrap(), wide);212 assert_eq!(213 wide.to_url().unwrap(),214 "/rule?birth=12,13&survive=fibonacci&wrap=true"215 );216 assert_eq!(217 wide.to_file().unwrap(),218 "rule_birth=[12,13]_survive=fibonacci_wrap=true"219 );220 assert_eq!(221 wide.to_mrly().unwrap(),222 "rule birth [12 13], survive fibonacci, wrap"223 );224 assert_eq!(Rule::from_url(&wide.to_url().unwrap()).unwrap(), wide);225 assert_eq!(Rule::from_file(&wide.to_file().unwrap()).unwrap(), wide);226 }227 #[test]228 fn to_json_folds_to_the_canonical_counts() {229 let messy = Rule::new(vec![3, 3, 1], vec![9, 2], false);230 assert_eq!(231 messy.to_json(),232 r#"{"kind":"rule","birth":[1,3],"survive":[2,9]}"#233 );234 let empty = Rule::new(Vec::new(), Vec::new(), false);235 assert_eq!(236 empty.to_json(),237 r#"{"kind":"rule","birth":[],"survive":[]}"#238 );239 assert_eq!(Rule::from_url("/rule?birth=&survive=").unwrap(), empty);240 assert_eq!(Rule::from_file("rule_birth=[]_survive=[]").unwrap(), empty);241 let spelt =242 Rule::from_json(r#"{"kind":"rule","survive":[3,2,3],"birth":[3],"wrap":false}"#)243 .unwrap();244 assert_eq!(spelt, Rule::new(vec![3], vec![2, 3], false));245 assert_eq!(spelt.to_json(), CONWAY);246 }247 #[test]248 fn only_a_rule_parses() {249 for bad in [250 r#"{"kind":"bang","birth":[3],"survive":[2,3]}"#,251 r#"{"birth":[3],"survive":[2,3]}"#,252 r#"{"kind":"rule","birth":[3]}"#,253 r#"{"kind":"rule","birth":[3],"survive":[2,3],"wrap":1}"#,254 r#"{"kind":"rule","birth":[3],"survive":[2,3],"mask":7}"#,255 r#"{"kind":"rule","birth":"fib","survive":[3]}"#,256 r#"{"kind":"rule","birth":"random","survive":[3]}"#,257 r#"{"kind":"rule","birth":"random_007","survive":[3]}"#,258 r#"{"kind":"rule","birth":"fibonacci_zeros_zeros","survive":[3]}"#,259 r#"{"kind":"rule","birth":"fibonacci_ones_zeros","survive":[3]}"#,260 r#"{"kind":"rule","birth":"fibonacciq","survive":[3]}"#,261 r#"{"kind":"rule","birth":[-1],"survive":[3]}"#,262 r#"{"kind":"rule","birth":3,"survive":[3]}"#,263 "rule birth [3], survive [2 3]",264 "rule_birth=[3]_survive=[2,3]",265 ] {266 assert!(Rule::from_json(bad).is_err(), "{bad}");267 }268 }269 #[test]270 fn a_listed_count_above_nine_has_a_name() {271 let rule = Rule::new(vec![3, 12], vec![2, 3, 48], true);272 assert_eq!(273 rule.to_json(),274 r#"{"kind":"rule","birth":[3,12],"survive":[2,3,48],"wrap":true}"#275 );276 let mut config = Config::new(moore().unwrap(), vec![3], vec![2, 3]);277 config.survive = Counts::List(vec![48]);278 assert_eq!(Rule::of(&config).survive, Counts::List(vec![48]));279 }280 #[test]281 fn config_round_trips_through_the_rule() {282 let mask = crate::math::two::designs::ones(3, 1).unwrap();283 let rule = Rule::new(vec![3, 6], vec![2, 3], true);284 let config = rule.config(mask);285 assert_eq!(config.boundary, Boundary::Wrap);286 assert_eq!(Rule::of(&config), rule);287 assert_eq!(288 Rule::of(&config).to_json(),289 r#"{"kind":"rule","birth":[3,6],"survive":[2,3],"wrap":true}"#290 );291 }292 #[test]293 fn a_drawn_rule_names_its_sequence() {294 let rule = Rule::new(295 Counts::drawn(Source::Fibonacci, false, true),296 Counts::drawn(Source::GridSquares, false, false),297 true,298 );299 assert_eq!(300 rule.to_json(),301 r#"{"kind":"rule","birth":"fibonacci_ones","survive":"grid_squares","wrap":true}"#302 );303 assert_eq!(Rule::from_url(&rule.to_url().unwrap()).unwrap(), rule);304 assert_eq!(Rule::from_file(&rule.to_file().unwrap()).unwrap(), rule);305 let seeded = Rule::new(306 Counts::drawn(Source::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_file(&seeded.to_file().unwrap()).unwrap(), seeded);315 assert_eq!(316 seeded.to_file().unwrap(),317 "rule_birth=random_4848495_zeros_survive=[3]"318 );319 }320 #[test]321 fn a_wide_mask_run_replays_from_its_name() {322 let mask = wide_mask(7);323 let rule = Rule::new(324 Counts::drawn(Source::Fibonacci, false, false),325 Counts::drawn(Source::Primes, false, false),326 true,327 );328 let mut config = rule.config(mask.clone());329 config.max_generations = 12;330 assert_eq!(config.budget(), 48);331 let (birth, survive) = config.counts().unwrap();332 assert!(birth.iter().any(|&n| n > 9), "{birth:?}");333 assert!(survive.iter().any(|&n| n > 9), "{survive:?}");334 let mut seed = Tensor::new(vec![15, 15]);335 for (y, x) in [(6, 7), (7, 6), (7, 7), (7, 8), (8, 7)] {336 seed.set(&[y, x], 1).unwrap();337 }338 let seed = Cell2d::new(seed).unwrap();339 let back = Rule::from_json(&Rule::of(&config).to_json()).unwrap();340 assert_eq!(back, rule);341 let mut replay = back.config(mask);342 replay.max_generations = 12;343 assert_eq!(replay.counts().unwrap(), (birth, survive));344 let run = animate(&seed, &config).unwrap();345 let again = animate(&seed, &replay).unwrap();346 for (a, b) in run.grids.iter().zip(&again.grids) {347 assert_eq!(a.types(), b.types());348 }349 assert!(run.grids.len() > 1);350 }351 #[test]352 fn the_moore_budget_stays_in_the_digits() {353 let config = Rule::new(vec![3], vec![2, 3], false).config(moore().unwrap());354 assert_eq!(config.budget(), 8);355 }356 #[test]357 fn seeded_values_round_trip() {358 let mut rng = Rng::new(5);359 for _ in 0..500 {360 let draw = |rng: &mut Rng| {361 let count = rng.below(5);362 (0..count).map(|_| rng.below(50)).collect::<Vec<usize>>()363 };364 let rule = Rule::new(draw(&mut rng), draw(&mut rng), rng.boolean());365 let text = rule.to_json();366 let back = Rule::from_json(&text).unwrap();367 assert_eq!(368 back.birth.values(49).unwrap(),369 rule.birth.values(49).unwrap()370 );371 assert_eq!(372 back.survive.values(49).unwrap(),373 rule.survive.values(49).unwrap()374 );375 assert_eq!(back.wrap, rule.wrap);376 assert_eq!(back.to_json(), text);377 assert_eq!(Rule::from_url(&rule.to_url().unwrap()).unwrap(), rule);378 assert_eq!(Rule::from_file(&rule.to_file().unwrap()).unwrap(), rule);379 }380 }381 #[test]382 fn seeded_sequences_round_trip() {383 let mut rng = Rng::new(11);384 let pool = Source::all();385 for _ in 0..200 {386 let pick = |rng: &mut Rng| match rng.below(3) {387 0 => Source::Random(rng.range(0, i64::MAX) as u64),388 1 => Source::CodeFills(rng.below(16) as u128),389 _ => *rng.choice(&pool).unwrap(),390 };391 let side = |rng: &mut Rng| Counts::drawn(pick(rng), rng.boolean(), rng.boolean());392 let rule = Rule::new(side(&mut rng), side(&mut rng), false);393 let text = rule.to_json();394 assert_eq!(Rule::from_json(&text).unwrap(), rule, "{text}");395 assert_eq!(396 Rule::from_url(&rule.to_url().unwrap()).unwrap(),397 rule,398 "{text}"399 );400 assert_eq!(401 Rule::from_file(&rule.to_file().unwrap()).unwrap(),402 rule,403 "{text}"404 );405 }406 }407}