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}