use crate::{
consts::{DONE_KNOT, END_KNOT},
error::{InklingError, ParseError, StackError},
follow::{ChoiceInfo, EncounteredEvent, FollowResult, LineDataBuffer},
knot::{Knot, Stitch},
};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::{
address::Address,
parse::read_knots_from_string,
process::{
fill_in_invalid_error, get_fallback_choices, prepare_choices_for_user, process_buffer,
},
};
#[derive(Clone, Debug, PartialEq)]
pub struct Line {
pub text: String,
pub tags: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Choice {
pub text: String,
pub tags: Vec<String>,
pub(crate) index: usize,
}
pub type LineBuffer = Vec<Line>;
pub type Knots = HashMap<String, Knot>;
#[derive(Debug)]
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
pub struct Story {
knots: Knots,
stack: Vec<Address>,
in_progress: bool,
}
#[derive(Debug)]
pub enum Prompt {
Done,
Choice(Vec<Choice>),
}
impl Prompt {
pub fn get_choices(&self) -> Option<Vec<Choice>> {
match self {
Prompt::Choice(choices) => Some(choices.clone()),
_ => None,
}
}
}
impl Story {
pub fn start(&mut self, line_buffer: &mut LineBuffer) -> Result<Prompt, InklingError> {
if self.in_progress {
return Err(InklingError::StartOnStoryInProgress);
}
self.in_progress = true;
let root_knot_name = self.stack.last().cloned().ok_or(StackError::NoStack)?;
self.increment_knot_visit_counter(&root_knot_name)?;
Self::follow_story_wrapper(
self,
|_self, buffer| Self::follow_knot(_self, buffer),
line_buffer,
)
}
pub fn resume_with_choice(
&mut self,
choice: &Choice,
line_buffer: &mut LineBuffer,
) -> Result<Prompt, InklingError> {
if !self.in_progress {
return Err(InklingError::ResumeBeforeStart);
}
let index = choice.index;
let current_address = self.stack.last().ok_or(StackError::NoStack)?.clone();
Self::follow_story_wrapper(
self,
|_self, buffer| Self::follow_knot_with_choice(_self, index, buffer),
line_buffer,
)
.map_err(|err| match err {
InklingError::InvalidChoice { .. } => {
fill_in_invalid_error(err, &choice, ¤t_address, &self.knots)
}
_ => err,
})
}
fn follow_story_wrapper<F>(
&mut self,
func: F,
line_buffer: &mut LineBuffer,
) -> Result<Prompt, InklingError>
where
F: FnOnce(&mut Self, &mut LineDataBuffer) -> Result<EncounteredEvent, InklingError>,
{
let mut internal_buffer = Vec::new();
let result = func(self, &mut internal_buffer)?;
process_buffer(line_buffer, internal_buffer);
match result {
EncounteredEvent::BranchingChoice(choice_set) => {
let current_address = self.stack.last().ok_or(StackError::NoStack)?;
let user_choice_lines =
prepare_choices_for_user(&choice_set, ¤t_address, &self.knots)?;
if !user_choice_lines.is_empty() {
Ok(Prompt::Choice(user_choice_lines))
} else {
let choice = get_fallback_choice(&choice_set, ¤t_address, &self.knots)?;
self.resume_with_choice(&choice, line_buffer)
}
}
EncounteredEvent::Done => Ok(Prompt::Done),
EncounteredEvent::Divert(..) => unreachable!("diverts are treated in the closure"),
}
}
fn follow_knot(&mut self, line_buffer: &mut LineDataBuffer) -> FollowResult {
self.follow_on_knot_wrapper(|knot, buffer| knot.follow(buffer), line_buffer)
}
fn follow_knot_with_choice(
&mut self,
choice_index: usize,
line_buffer: &mut LineDataBuffer,
) -> FollowResult {
self.follow_on_knot_wrapper(
|knot, buffer| knot.follow_with_choice(choice_index, buffer),
line_buffer,
)
}
fn follow_on_knot_wrapper<F>(&mut self, f: F, buffer: &mut LineDataBuffer) -> FollowResult
where
F: FnOnce(&mut Stitch, &mut LineDataBuffer) -> FollowResult,
{
let knot_name = self.stack.last().unwrap();
let result =
get_mut_stitch(knot_name, &mut self.knots).and_then(|stitch| f(stitch, buffer))?;
match result {
EncounteredEvent::Divert(destination) => self.divert_to_knot(&destination, buffer),
_ => Ok(result),
}
}
fn divert_to_knot(&mut self, to_address: &str, buffer: &mut LineDataBuffer) -> FollowResult {
if to_address == DONE_KNOT || to_address == END_KNOT {
Ok(EncounteredEvent::Done)
} else {
let current_address = self.stack.last().ok_or(StackError::NoStack)?;
let address = Address::from_target_address(to_address, current_address, &self.knots)?;
self.increment_knot_visit_counter(&address)?;
self.stack.last_mut().map(|knot_name| *knot_name = address);
self.follow_knot(buffer)
}
}
fn increment_knot_visit_counter(&mut self, address: &Address) -> Result<(), InklingError> {
get_mut_stitch(address, &mut self.knots)?.num_visited += 1;
Ok(())
}
}
pub fn read_story_from_string(string: &str) -> Result<Story, ParseError> {
let (root, knots) = read_knots_from_string(string)?;
let root_address = Address::from_root_knot(&root, &knots).expect(
"After successfully creating all knots, the root knot name that was returned from \
`read_knots_from_string` is not present in the set of created knots. \
This should never happen.",
);
Ok(Story {
knots,
stack: vec![root_address],
in_progress: false,
})
}
pub fn get_stitch<'a>(target: &Address, knots: &'a Knots) -> Result<&'a Stitch, InklingError> {
knots
.get(&target.knot)
.and_then(|knot| knot.stitches.get(&target.stitch))
.ok_or(
StackError::BadAddress {
address: target.clone(),
}
.into(),
)
}
pub fn get_mut_stitch<'a>(
target: &Address,
knots: &'a mut Knots,
) -> Result<&'a mut Stitch, InklingError> {
knots
.get_mut(&target.knot)
.and_then(|knot| knot.stitches.get_mut(&target.stitch))
.ok_or(
StackError::BadAddress {
address: target.clone(),
}
.into(),
)
}
fn get_fallback_choice(
choice_set: &[ChoiceInfo],
current_address: &Address,
knots: &Knots,
) -> Result<Choice, InklingError> {
get_fallback_choices(choice_set, current_address, knots).and_then(|choices| {
choices.first().cloned().ok_or(InklingError::OutOfChoices {
address: current_address.clone(),
})
})
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_choice(index: usize) -> Choice {
Choice {
text: String::new(),
tags: Vec::new(),
index,
}
}
#[test]
fn story_internally_follows_through_knots_when_diverts_are_found() {
let content = "
== back_in_london
We arrived into London at 9.45pm exactly.
-> hurry_home
== hurry_home
We hurried home to Savile Row as fast as we could.
";
let (head_knot, knots) = read_knots_from_string(content).unwrap();
let root_address = Address::from_root_knot(&head_knot, &knots).unwrap();
let mut story = Story {
knots,
stack: vec![root_address],
in_progress: false,
};
let mut buffer = Vec::new();
story.follow_knot(&mut buffer).unwrap();
assert_eq!(
&buffer.last().unwrap().text(),
"We hurried home to Savile Row as fast as we could."
);
}
#[test]
fn story_internally_resumes_from_the_new_knot_after_a_choice_is_made() {
let content = "
== back_in_london
We arrived into London at 9.45pm exactly.
-> hurry_home
== hurry_home
\"What's that?\" my master asked.
* \"I am somewhat tired[.\"],\" I repeated.
\"Really,\" he responded. \"How deleterious.\"
* \"Nothing, Monsieur!\"[] I replied.
\"Very good, then.\"
* \"I said, this journey is appalling[.\"] and I want no more of it.\"
\"Ah,\" he replied, not unkindly. \"I see you are feeling frustrated. Tomorrow, things will improve.\"
";
let (_, knots) = read_knots_from_string(content).unwrap();
let root_address = Address::from_root_knot("back_in_london", &knots).unwrap();
let mut story = Story {
knots,
stack: vec![root_address],
in_progress: false,
};
let mut buffer = Vec::new();
story.follow_knot(&mut buffer).unwrap();
story.follow_knot_with_choice(1, &mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text(), "\"Very good, then.\"");
}
#[test]
fn when_a_knot_is_returned_to_the_text_starts_from_the_beginning() {
let content = "
== back_in_london
We arrived into London at 9.45pm exactly.
-> hurry_home
== hurry_home
* We hurried home to Savile Row as fast as we could.
* But we decided our trip wasn't done and immediately left.
After a few days me returned again.
-> back_in_london
";
let (_, knots) = read_knots_from_string(content).unwrap();
let root_address = Address::from_root_knot("back_in_london", &knots).unwrap();
let mut story = Story {
knots,
stack: vec![root_address],
in_progress: false,
};
let mut buffer = Vec::new();
story.follow_knot(&mut buffer).unwrap();
story.follow_knot_with_choice(1, &mut buffer).unwrap();
assert_eq!(
&buffer[0].text(),
"We arrived into London at 9.45pm exactly."
);
assert_eq!(
&buffer[5].text(),
"We arrived into London at 9.45pm exactly."
);
}
#[test]
fn divert_to_done_or_end_constant_knots_ends_story() {
let content = "
== knot_done
-> DONE
== knot_end
-> END
";
let (_, knots) = read_knots_from_string(content).unwrap();
let done_address = Address::from_root_knot("knot_done", &knots).unwrap();
let end_address = Address::from_root_knot("knot_end", &knots).unwrap();
let mut story = Story {
knots,
stack: vec![done_address],
in_progress: false,
};
let mut buffer = Vec::new();
match story.start(&mut buffer).unwrap() {
Prompt::Done => (),
_ => panic!("story should be done when diverting to DONE knot"),
}
story.in_progress = false;
story.stack = vec![end_address];
match story.start(&mut buffer).unwrap() {
Prompt::Done => (),
_ => panic!("story should be done when diverting to END knot"),
}
}
#[test]
fn divert_to_knot_increments_visit_count() {
let content = "
== knot
Line one.
";
let (_, knots) = read_knots_from_string(content).unwrap();
let root_address = Address::from_root_knot("knot", &knots).unwrap();
let address = Address::from_target_address("knot", &root_address, &knots).unwrap();
let mut buffer = Vec::new();
let mut story = Story {
knots,
stack: vec![root_address.clone()],
in_progress: false,
};
assert_eq!(get_stitch(&address, &story.knots).unwrap().num_visited, 0);
story.divert_to_knot("knot", &mut buffer).unwrap();
assert_eq!(get_stitch(&address, &story.knots).unwrap().num_visited, 1);
}
#[test]
fn divert_to_specific_stitch_sets_stack_to_it() {
let content = "
== knot
Line one.
= stitch
Line two.
";
let (_, knots) = read_knots_from_string(content).unwrap();
let root_address = Address::from_root_knot("knot", &knots).unwrap();
let address = Address::from_target_address("knot.stitch", &root_address, &knots).unwrap();
let mut buffer = Vec::new();
let mut story = Story {
knots,
stack: vec![root_address.clone()],
in_progress: false,
};
story.divert_to_knot("knot.stitch", &mut buffer).unwrap();
assert_eq!(story.stack.last().unwrap(), &address);
}
#[test]
fn divert_to_stitch_inside_knot_with_internal_target_sets_full_destination_in_stack() {
let content = "
== knot
Line one.
= stitch
Line two.
";
let (_, knots) = read_knots_from_string(content).unwrap();
let root_address = Address::from_root_knot("knot", &knots).unwrap();
let address = Address::from_target_address("knot.stitch", &root_address, &knots).unwrap();
let mut buffer = Vec::new();
let mut story = Story {
knots,
stack: vec![root_address.clone()],
in_progress: false,
};
story.divert_to_knot("stitch", &mut buffer).unwrap();
assert_eq!(story.stack.last().unwrap(), &address);
}
#[test]
fn if_choice_list_returned_to_user_is_empty_follow_fallback_choice() {
let content = "
== knot
* Non-sticky choice -> knot
* ->
Fallback choice
";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
let choices = story.start(&mut buffer).unwrap().get_choices().unwrap();
assert_eq!(choices.len(), 1);
story
.resume_with_choice(&mock_choice(0), &mut buffer)
.unwrap();
assert_eq!(&buffer[1].text, "Fallback choice\n");
}
#[test]
fn if_no_fallback_choices_are_available_raise_error() {
let content = "
== knot
* Non-sticky choice -> knot
";
let mut story = read_story_from_string(content).unwrap();
let mut buffer = Vec::new();
story.start(&mut buffer).unwrap();
match story.resume_with_choice(&mock_choice(0), &mut buffer) {
Err(InklingError::OutOfChoices { .. }) => (),
Err(err) => panic!("expected `OutOfChoices` error but got {:?}", err),
Ok(_) => panic!("expected an error but got an Ok"),
}
}
#[test]
fn starting_a_story_is_only_allowed_once() {
let mut story = read_story_from_string("Line 1").unwrap();
let mut line_buffer = Vec::new();
assert!(story.start(&mut line_buffer).is_ok());
match story.start(&mut line_buffer) {
Err(InklingError::StartOnStoryInProgress) => (),
_ => panic!("did not raise `StartOnStoryInProgress` error"),
}
}
#[test]
fn cannot_resume_on_a_story_that_has_not_started() {
let mut story = read_story_from_string("* Choice 1").unwrap();
let mut line_buffer = Vec::new();
let choice = Choice {
index: 0,
text: "Choice 1".to_string(),
tags: Vec::new(),
};
match story.resume_with_choice(&choice, &mut line_buffer) {
Err(InklingError::ResumeBeforeStart) => (),
_ => panic!("did not raise `ResumeBeforeStart` error"),
}
}
}