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}