story.rs

8.4 kB · rust · 253 lines

1use super::animate::animate;2use super::models::{Config, Life};3use super::Fate;4use crate::name::{Named, Rule};5use crate::two::{self, Cell2d};6use mrlycore::errors::{value_error, MrlyError, Result};7use mrlycore::{json, Json};8use serde::Deserialize;910/// One config's run inside a story.11#[derive(Clone, Debug)]12pub struct Chapter {13    /// The chapter's rulebook.14    pub config: Config,15    /// The chapter's recorded run.16    pub life: Life,17}1819impl Chapter {20    fn truncate(&mut self, length: usize) {21        if length > 0 && length < self.life.grids.len() {22            self.life.grids.truncate(length);23            self.life.count = self.life.grids.len();24        }25    }26    /// Encodes the chapter's rule, mask, seed, length and fate as a JSON object,27    /// as its canonical JSON.28    pub fn to_json(&self) -> Result<Json> {29        Ok(json!({30            "v": 1,31            "rule": Rule::of(&self.config).to_json(),32            "mask": two::to_strings(&self.config.mask),33            "seed": self.life.grids.first().map(two::to_strings),34            "length": self.life.grids.len(),35            "fate": self.life.fate.name(),36        }))37    }38    /// Decodes a chapter from its JSON object and replays it, or an error naming the broken field.39    pub fn from_json(value: &Json) -> Result<Chapter> {40        let parts = Parts::deserialize(value)?;41        let rule = Rule::from_json(&parts.rule)?;42        let mask = two::from_strings(&parts.mask)?;43        let seed = two::from_strings(&parts.seed)?;44        if parts.length == 0 {45            return value_error("field \"length\" must be positive.");46        }47        let fate = Fate::parse(&parts.fate)?;48        let mut config = rule.config(mask);49        config.max_generations = parts.length;50        let mut life = animate(&seed, &config)?;51        life.grids.truncate(parts.length);52        life.count = life.grids.len();53        life.fate = fate;54        Ok(Chapter { config, life })55    }56}5758#[derive(Deserialize)]59struct Parts {60    rule: String,61    mask: Vec<String>,62    seed: Vec<String>,63    length: usize,64    fate: String,65}6667/// A chain of life runs, each seeded by the last frame of the one before.68#[derive(Clone, Debug)]69pub struct Story {70    /// The chapters in order.71    pub chapters: Vec<Chapter>,72}7374impl Story {75    /// Builds an empty story.76    pub fn new() -> Story {77        Story {78            chapters: Vec::new(),79        }80    }81    /// Runs a chapter from a seed and returns its final grid.82    pub fn add(&mut self, seed: &Cell2d, config: &Config) -> Result<Cell2d> {83        let life = animate(seed, config)?;84        let last = life85            .last()86            .cloned()87            .ok_or_else(|| mrlycore::MrlyError::Value("chapter produced no grids.".into()))?;88        self.chapters.push(Chapter {89            config: config.clone(),90            life,91        });92        Ok(last)93    }94    /// Truncates the last chapter to a length and returns its new final grid.95    pub fn pivot(&mut self, length: usize) -> Result<Cell2d> {96        let chapter = self97            .chapters98            .last_mut()99            .ok_or_else(|| mrlycore::MrlyError::Value("no chapter to pivot.".into()))?;100        chapter.truncate(length);101        chapter102            .life103            .last()104            .cloned()105            .ok_or_else(|| mrlycore::MrlyError::Value("chapter empty after pivot.".into()))106    }107    /// Returns every grid of every chapter in order.108    pub fn grids(&self) -> Vec<Cell2d> {109        self.chapters110            .iter()111            .flat_map(|c| c.life.grids.iter().cloned())112            .collect()113    }114    /// Returns the frame count of each chapter.115    pub fn chapter_lengths(&self) -> Vec<usize> {116        self.chapters.iter().map(|c| c.life.grids.len()).collect()117    }118    /// Returns the total frame count across chapters.119    pub fn count(&self) -> usize {120        self.chapters.iter().map(|c| c.life.grids.len()).sum()121    }122    /// Returns the full-run index of a chapter's first frame.123    pub fn chapter_start(&self, i: usize) -> usize {124        self.chapters125            .iter()126            .take(i)127            .map(|c| c.life.grids.len())128            .sum()129    }130    /// Returns the full-run index one past a chapter's last frame.131    pub fn chapter_end(&self, i: usize) -> usize {132        self.chapter_start(i) + self.chapters.get(i).map_or(0, |c| c.life.grids.len())133    }134    /// Returns the index of the first frame.135    pub fn first_frame_idx(&self) -> usize {136        0137    }138    /// Returns the index of the last frame.139    pub fn last_frame_idx(&self) -> usize {140        self.count().saturating_sub(1)141    }142    /// Returns the last chapter's fate, or an error on an empty story.143    pub fn fate(&self) -> Result<Fate> {144        self.chapters145            .last()146            .map(|c| c.life.fate)147            .ok_or_else(|| mrlycore::MrlyError::Value("empty story.".into()))148    }149    /// Encodes the story chapter by chapter as a JSON object,150    /// or an error when a chapter carries no nameable rule.151    pub fn to_json(&self) -> Result<Json> {152        let chapters: Vec<Json> = self153            .chapters154            .iter()155            .map(Chapter::to_json)156            .collect::<Result<Vec<Json>>>()?;157        Ok(json!({158            "v": 1,159            "chapters": chapters,160        }))161    }162    /// Decodes a story from its JSON object and replays every chapter, or an error naming the broken field.163    pub fn from_json(value: &Json) -> Result<Story> {164        let chapters = value165            .get("chapters")166            .and_then(Json::as_array)167            .ok_or_else(|| MrlyError::Value("field \"chapters\" must be a list.".into()))?;168        let mut story = Story::new();169        for chapter in chapters {170            story.chapters.push(Chapter::from_json(chapter)?);171        }172        Ok(story)173    }174}175176impl Default for Story {177    fn default() -> Story {178        Story::new()179    }180}181182/// Runs a seed through each config in turn, pivoting between chapters when a length is given.183pub fn tell(seed: &Cell2d, configs: &[Config], pivot_at: Option<usize>) -> Result<Story> {184    if configs.is_empty() {185        return value_error("a story needs at least one chapter.");186    }187    let mut story = Story::new();188    let mut current = seed.clone();189    let last_index = configs.len() - 1;190    for (i, config) in configs.iter().enumerate() {191        current = story.add(&current, config)?;192        if i != last_index {193            if let Some(length) = pivot_at {194                current = story.pivot(length)?;195            }196        }197    }198    Ok(story)199}200201#[cfg(test)]202mod tests {203    use super::*;204    use crate::life::{moore, Boundary, Config};205    use mrlycore::tensor::Tensor;206    fn conway() -> Config {207        Config {208            boundary: Boundary::Constant,209            max_generations: 12,210            padding: 2,211            ..Config::new(moore(), vec![3], vec![2, 3])212        }213    }214    fn blinker() -> Cell2d {215        let mut t = Tensor::new(vec![5, 5]);216        t.set(&[1, 2], 1);217        t.set(&[2, 2], 1);218        t.set(&[3, 2], 1);219        Cell2d::new(t)220    }221    #[test]222    fn two_chapter_story_concatenates() {223        let story = tell(&blinker(), &[conway(), conway()], Some(2)).unwrap();224        assert_eq!(story.chapters.len(), 2);225        assert_eq!(story.chapter_lengths()[0], 2);226        assert_eq!(story.count(), story.grids().len());227    }228    #[test]229    fn story_json_replays_the_run() {230        let story = tell(&blinker(), &[conway(), conway()], Some(2)).unwrap();231        let back = Story::from_json(&story.to_json().unwrap()).unwrap();232        assert_eq!(back.chapter_lengths(), story.chapter_lengths());233        assert_eq!(back.fate().unwrap(), story.fate().unwrap());234        let (a, b) = (story.grids(), back.grids());235        assert_eq!(a.len(), b.len());236        for (x, y) in a.iter().zip(&b) {237            assert_eq!(x.types(), y.types());238        }239        for (c, d) in story.chapters.iter().zip(&back.chapters) {240            assert_eq!(Rule::of(&c.config), Rule::of(&d.config));241            assert_eq!(c.life.fate, d.life.fate);242        }243    }244    #[test]245    fn chapter_json_rejects_broken_fields() {246        let story = tell(&blinker(), &[conway()], None).unwrap();247        let mut value = story.chapters[0].to_json().unwrap();248        value["fate"] = json!("sparkle");249        assert!(Chapter::from_json(&value).is_err());250        assert!(Chapter::from_json(&json!({})).is_err());251        assert!(Story::from_json(&json!({ "chapters": 3 })).is_err());252    }253}