diff --git a/cli/src/console.rs b/cli/src/console.rs index 3d33b54..42fa4d0 100644 --- a/cli/src/console.rs +++ b/cli/src/console.rs @@ -101,6 +101,9 @@ fn run_command(command: &str, parameters: Vec<&str>, runtime: &mut Runtime) -> S "skip" | "s" => skip(runtime), "skip-all" => skip_all(runtime), "reset" => reset_command(parameters, runtime), + "rewind" => rewind_command(parameters, runtime), + "rewind_to_choice" => rewind_to_choice_command(runtime), + "rewind_to" => rewind_to(parameters, runtime), str => { if str.starts_with("->") { let substr: String = str.chars().skip(2).collect(); @@ -310,6 +313,59 @@ fn parameters_to_pattern(parameters: Vec<&str>) -> String { pattern.trim().to_string() } +fn rewind_command(parameters: Vec<&str>, runtime: &mut Runtime) -> String { + if parameters.len() > 1 { + return "Invalid parameters".to_string(); + } + + if !parameters.is_empty() { + if let Ok(rewind_count) = parameters[0].parse() { + for _i in 0..rewind_count { + runtime.rewind().unwrap(); + } + } else { + return "Invalid parameters".to_string(); + } + } else { + runtime.rewind().unwrap(); + } + + match runtime.current() { + Ok(current) => get_output_string(current, runtime), + Err(err) => get_runtime_error_string(err, runtime), + } +} + +fn rewind_to_choice_command(runtime: &mut Runtime) -> String { + runtime.rewind_to_choice().unwrap(); + + match runtime.current() { + Ok(current) => get_output_string(current, runtime), + Err(err) => get_runtime_error_string(err, runtime), + } +} + +fn rewind_to(parameters: Vec<&str>, runtime: &mut Runtime) -> String { + if parameters.len() > 1 { + return "Invalid parameters".to_string(); + } + + if !parameters.is_empty() { + if let Ok(index) = parameters[0].parse() { + runtime.rewind_to(index).unwrap(); + } else { + return "Invalid parameters".to_string(); + } + } else { + return "Missing parameter index".to_string(); + } + + match runtime.current() { + Ok(current) => get_output_string(current, runtime), + Err(err) => get_runtime_error_string(err, runtime), + } +} + fn reset_command(parameters: Vec<&str>, runtime: &mut Runtime) -> String { if parameters.is_empty() || parameters.contains(&"all") @@ -777,4 +833,57 @@ mod test { let str_found = Console::process_line(Ok("next".to_string()), &mut rl, &mut runtime).unwrap(); assert_eq!(expected_str, &str_found); } + + #[test] + fn rewind_command() { + let mut runtime = Console::load_runtime("./fixtures/script"); + runtime.database.config.keep_history = true; + runtime.next_block().unwrap(); + runtime.next_block().unwrap(); + + let mut rl = DefaultEditor::new().unwrap(); + let expected_str = "You've just arrived in the bustling city, full of excitement and anticipation for your new job."; + + let str_found = Console::process_line(Ok("rewind".to_string()), &mut rl, &mut runtime).unwrap(); + assert_eq!(expected_str, &str_found); + + runtime.next_block().unwrap(); + runtime.next_block().unwrap(); + + let str_found = + Console::process_line(Ok("rewind 2".to_string()), &mut rl, &mut runtime).unwrap(); + assert_eq!(expected_str, &str_found); + } + + #[test] + fn rewind_to_choice_command() { + let mut runtime = Console::load_runtime("./fixtures/script"); + runtime.database.config.keep_history = true; + runtime.skip().unwrap(); + runtime.pick_choice(0).unwrap(); + + let mut rl = DefaultEditor::new().unwrap(); + let expected_str = "As you take your first steps in this urban jungle, you feel a mix of emotions, hoping to find your place in this new environment.\n (1)I take a walk through a nearby park to relax and acclimate to the city.\n (2)I visit a popular street market to experience the city's unique flavors and energy.\n"; + + let str_found = + Console::process_line(Ok("rewind_to_choice".to_string()), &mut rl, &mut runtime).unwrap(); + assert_eq!(expected_str, &str_found); + } + + #[test] + fn rewind_to_command() { + let mut runtime = Console::load_runtime("./fixtures/script"); + runtime.database.config.keep_history = true; + runtime.next_block().unwrap(); + runtime.next_block().unwrap(); + runtime.next_block().unwrap(); + + let mut rl = DefaultEditor::new().unwrap(); + let expected_str = + "The skyline reaches for the clouds, and the sounds of traffic and people surround you."; + + let str_found = + Console::process_line(Ok("rewind_to 1".to_string()), &mut rl, &mut runtime).unwrap(); + assert_eq!(expected_str, &str_found); + } } diff --git a/common/src/config.rs b/common/src/config.rs index 19f6f9b..9cc8a29 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -18,6 +18,8 @@ pub struct Config { pub other_texts: HashMap, #[serde(default)] pub story_progress_style: StoryProgressStyle, + #[serde(default)] + pub keep_history: bool, } #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Eq)] diff --git a/examples/cuentitos.toml b/examples/cuentitos.toml index 47ae375..9a10f8b 100644 --- a/examples/cuentitos.toml +++ b/examples/cuentitos.toml @@ -1,6 +1,7 @@ locales = ['en'] default_locale = 'en' story_progress_style = 'next' +keep_history = true [variables] energy = "integer" diff --git a/runtime/src/error.rs b/runtime/src/error.rs index 07fbbee..05b82dc 100644 --- a/runtime/src/error.rs +++ b/runtime/src/error.rs @@ -10,6 +10,12 @@ type VariableName = String; #[derive(PartialEq, Eq)] pub enum RuntimeError { + MissingLocale(String), + RewindWithNoHistory(), + RewindWithToInvalidIndex { + index: usize, + current_index: usize, + }, InvalidBlockId(BlockId), WaitingForChoice(Vec), StoryFinished, @@ -131,6 +137,22 @@ impl Display for RuntimeError { RuntimeError::FrequencyOutOfBucket => { write!(f, "Frequencies are only allowed inside of buckets.") } + RuntimeError::RewindWithNoHistory() => { + write!(f, "Can't rewind when the history is empty.") + } + RuntimeError::MissingLocale(s) => { + write!(f, "Missing locale: {}", s) + } + RuntimeError::RewindWithToInvalidIndex { + index, + current_index, + } => { + write!( + f, + "Can't rewind to {} because the current index is {}.", + index, current_index + ) + } } } } diff --git a/runtime/src/runtime.rs b/runtime/src/runtime.rs index 0676952..0ca7ac8 100644 --- a/runtime/src/runtime.rs +++ b/runtime/src/runtime.rs @@ -160,6 +160,8 @@ pub struct Runtime { seed: u64, pub current_locale: LanguageId, pub section: Option
, + pub previous: Option>, + pub rewind_index: usize, } impl Runtime { @@ -178,6 +180,8 @@ impl Runtime { self.set_seed(self.seed); self.block_stack.clear(); self.section = None; + self.previous = None; + self.rewind_index = 0; self.choices.clear(); } pub fn reset_state(&mut self) { @@ -188,7 +192,7 @@ impl Runtime { self.reset_state(); } - pub fn set_locale(&mut self, locale: T) -> Result<(), String> + pub fn set_locale(&mut self, locale: T) -> Result<(), RuntimeError> where T: AsRef, { @@ -197,7 +201,7 @@ impl Runtime { self.current_locale = locale; Ok(()) } else { - Err("Missing Locale".to_string()) + Err(RuntimeError::MissingLocale(locale)) } } @@ -207,7 +211,10 @@ impl Runtime { } pub fn divert(&mut self, section: &Section) -> Result, RuntimeError> { + let history_entry = self.get_history_entry(); + let new_stack: Vec = self.get_section_block_ids(section)?; + self.block_stack.clear(); let mut blocks_added = Vec::default(); for block in new_stack { @@ -222,10 +229,12 @@ impl Runtime { blocks_added.push(Self::push_stack(self, block)?); } + self.add_to_history(history_entry); Ok(blocks_added) } pub fn boomerang_divert(&mut self, section: &Section) -> Result, RuntimeError> { + let history_entry = self.get_history_entry(); let new_stack: Vec = self.get_section_block_ids(section)?; let mut blocks_added = Vec::default(); @@ -241,6 +250,7 @@ impl Runtime { } blocks_added.push(Self::push_stack(self, block)?); } + self.add_to_history(history_entry); Ok(blocks_added) } @@ -255,11 +265,16 @@ impl Runtime { } pub fn next_block(&mut self) -> Result { + let history_entry = self.get_history_entry(); + if self.database.blocks.is_empty() { return Err(RuntimeError::EmptyDatabase); } let blocks = Self::update_stack(self)?; + + self.add_to_history(history_entry); + Output::from_blocks(blocks, self) } @@ -375,6 +390,8 @@ impl Runtime { } pub fn pick_choice(&mut self, choice: usize) -> Result { + let history_entry = self.get_history_entry(); + if self.database.blocks.is_empty() { return Err(RuntimeError::EmptyDatabase); } @@ -401,9 +418,55 @@ impl Runtime { blocks.append(&mut output.blocks); output.blocks = blocks; + self.add_to_history(history_entry); Ok(output) } + pub fn rewind(&mut self) -> Result<(), RuntimeError> { + if let Some(previous_state) = self.previous.clone() { + *self = *previous_state; + Ok(()) + } else { + Err(RuntimeError::RewindWithNoHistory()) + } + } + + pub fn rewind_to_choice(&mut self) -> Result<(), RuntimeError> { + if self.block_stack.is_empty() { + return Ok(()); + } + + if let Some(previous_state) = self.previous.clone() { + *self = *previous_state; + if self.choices.is_empty() { + self.rewind_to_choice() + } else { + Ok(()) + } + } else { + Err(RuntimeError::RewindWithNoHistory()) + } + } + + pub fn rewind_to(&mut self, index: usize) -> Result<(), RuntimeError> { + if !self.database.config.keep_history { + return Err(RuntimeError::RewindWithNoHistory()); + } + + if index >= self.rewind_index { + return Err(RuntimeError::RewindWithToInvalidIndex { + index, + current_index: self.rewind_index, + }); + } + + while self.rewind_index > index { + self.rewind()?; + } + + Ok(()) + } + pub fn set_variable(&mut self, variable: R, value: T) -> Result<(), RuntimeError> where T: Display + std::str::FromStr + Default, @@ -522,6 +585,23 @@ impl Runtime { Ok(choices_strings) } + fn get_history_entry(&self) -> Option { + if self.current().is_ok() && self.database.config.keep_history { + Some(self.clone()) + } else { + None + } + } + + fn add_to_history(&mut self, history_entry: Option) { + if let Some(history_entry) = history_entry { + if self.database.config.keep_history { + self.previous = Some(Box::new(history_entry)); + self.rewind_index += 1; + } + } + } + fn get_cuentitos_block(&self, id: BlockId) -> Result<&cuentitos_common::Block, RuntimeError> { if id < self.database.blocks.len() { Ok(&self.database.blocks[id]) @@ -4186,6 +4266,237 @@ mod test { }] ); } + + #[test] + fn rewind_story() { + let text_1 = Block::Text { + id: "text_1".to_string(), + settings: BlockSettings::default(), + }; + + let text_2 = Block::Text { + id: "text_2".to_string(), + settings: BlockSettings::default(), + }; + + let database = Database { + blocks: vec![text_1.clone(), text_2.clone()], + config: Config { + keep_history: false, + ..Default::default() + }, + ..Default::default() + }; + + let mut runtime = Runtime { + database, + ..Default::default() + }; + + runtime.progress_story().unwrap(); + let err = runtime.rewind().unwrap_err(); + assert_eq!(err, RuntimeError::RewindWithNoHistory()); + + runtime.database.config.keep_history = true; + + let initial_runtime = runtime.clone(); + runtime.progress_story().unwrap(); + + assert_ne!(runtime, initial_runtime); + + runtime.rewind().unwrap(); + + assert_eq!(runtime, initial_runtime); + } + + #[test] + fn rewind_to_choice() { + let text_1 = Block::Text { + id: "text_1".to_string(), + settings: BlockSettings { + children: vec![1], + ..Default::default() + }, + }; + + let choice = Block::Choice { + id: "0".to_string(), + settings: BlockSettings { + children: vec![2, 3], + ..Default::default() + }, + }; + + let text_2 = Block::Text { + id: "text_2".to_string(), + settings: BlockSettings::default(), + }; + + let text_3 = Block::Text { + id: "text_3".to_string(), + settings: BlockSettings::default(), + }; + + let database = Database { + blocks: vec![ + text_1.clone(), + choice.clone(), + text_2.clone(), + text_3.clone(), + ], + config: Config { + keep_history: true, + ..Default::default() + }, + ..Default::default() + }; + + let mut runtime = Runtime { + database, + ..Default::default() + }; + + runtime.progress_story().unwrap(); + + let initial_runtime = runtime.clone(); + + runtime.pick_choice(0).unwrap(); + runtime.progress_story().unwrap(); + + assert_ne!(runtime, initial_runtime); + + runtime.rewind_to_choice().unwrap(); + + assert_eq!(runtime, initial_runtime); + } + + #[test] + fn rewind_to() { + let text_1 = Block::Text { + id: "text_1".to_string(), + settings: BlockSettings { + children: vec![1], + ..Default::default() + }, + }; + + let text_2 = Block::Text { + id: "text_2".to_string(), + settings: BlockSettings::default(), + }; + + let text_3 = Block::Text { + id: "text_3".to_string(), + settings: BlockSettings::default(), + }; + + let database = Database { + blocks: vec![text_1.clone(), text_2.clone(), text_3.clone()], + config: Config { + keep_history: true, + ..Default::default() + }, + ..Default::default() + }; + + let mut runtime = Runtime { + database, + ..Default::default() + }; + + runtime.progress_story().unwrap(); + let initial_runtime = runtime.clone(); + runtime.progress_story().unwrap(); + runtime.progress_story().unwrap(); + + assert_ne!(runtime, initial_runtime); + + runtime.rewind_to(0).unwrap(); + + assert_eq!(runtime, initial_runtime); + + runtime.progress_story().unwrap(); + let initial_runtime = runtime.clone(); + runtime.progress_story().unwrap(); + runtime.progress_story().unwrap(); + runtime.rewind_to(1).unwrap(); + assert_eq!(runtime, initial_runtime); + } + + #[test] + fn missing_locale_throws_error() { + let mut strings: HashMap = HashMap::default(); + strings.insert("en".to_string(), HashMap::default()); + + let i18n = I18n { + locales: vec!["en".to_string()], + default_locale: "en".to_string(), + strings, + }; + + let database = Database { + i18n, + config: Config { + story_progress_style: cuentitos_common::StoryProgressStyle::Skip, + ..Default::default() + }, + ..Default::default() + }; + + let mut runtime = Runtime { + database, + ..Default::default() + }; + + let runtime_error = runtime.set_locale("es").unwrap_err(); + assert_eq!(runtime_error, RuntimeError::MissingLocale("es".to_string())); + } + + #[test] + fn rewind_to_invalid_index_throws_error() { + let text_1 = Block::Text { + id: "text_1".to_string(), + settings: BlockSettings { + children: vec![1], + ..Default::default() + }, + }; + + let text_2 = Block::Text { + id: "text_2".to_string(), + settings: BlockSettings::default(), + }; + + let text_3 = Block::Text { + id: "text_3".to_string(), + settings: BlockSettings::default(), + }; + + let database = Database { + blocks: vec![text_1.clone(), text_2.clone(), text_3.clone()], + config: Config { + keep_history: true, + ..Default::default() + }, + ..Default::default() + }; + + let mut runtime = Runtime { + database, + ..Default::default() + }; + + let err = runtime.rewind_to(1).unwrap_err(); + + assert_eq!( + RuntimeError::RewindWithToInvalidIndex { + index: 1, + current_index: 0 + }, + err + ); + } + #[derive(Debug, Default, PartialEq, Eq)] enum TimeOfDay { #[default]