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(¤t, 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}