use crate::{
consts::{DONE_KNOT, END_KNOT},
error::{InklingError, ParseError},
follow::{FollowResult, LineDataBuffer, Next},
knot::Knot,
};
use std::collections::HashMap;
use super::{
parse::read_knots_from_string,
process::{fill_in_invalid_error, 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>;
#[derive(Debug)]
pub struct Story {
knots: HashMap<String, Knot>,
stack: Vec<String>,
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: String = self
.stack
.last()
.cloned()
.ok_or::<InklingError>(InklingError::NoKnotStack.into())?;
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;
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, &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<Next, InklingError>,
{
let mut internal_buffer = Vec::new();
let result = func(self, &mut internal_buffer)?;
process_buffer(line_buffer, internal_buffer);
match result {
Next::ChoiceSet(choice_set) => {
let user_choice_lines = prepare_choices_for_user(&choice_set, &self.knots)?;
Ok(Prompt::Choice(user_choice_lines))
}
Next::Done => Ok(Prompt::Done),
Next::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 Knot, &mut LineDataBuffer) -> FollowResult,
{
let knot_name = self.stack.last().unwrap();
let result = self
.knots
.get_mut(knot_name)
.ok_or(
InklingError::UnknownKnot {
knot_name: knot_name.clone(),
}
.into(),
)
.and_then(|knot| f(knot, buffer))?;
match result {
Next::Divert(destination) => self.divert_to_knot(&destination, buffer),
_ => Ok(result),
}
}
fn divert_to_knot(&mut self, destination: &str, buffer: &mut LineDataBuffer) -> FollowResult {
if destination == DONE_KNOT || destination == END_KNOT {
Ok(Next::Done)
} else {
self.increment_knot_visit_counter(destination)?;
self.stack
.last_mut()
.map(|knot_name| *knot_name = destination.to_string());
self.follow_knot(buffer)
}
}
fn increment_knot_visit_counter(&mut self, knot_name: &str) -> Result<(), InklingError> {
self.knots
.get_mut(knot_name)
.map(|knot| knot.num_visited += 1)
.ok_or(InklingError::UnknownKnot {
knot_name: knot_name.to_string(),
})
}
}
pub fn read_story_from_string(string: &str) -> Result<Story, ParseError> {
let (root, knots) = read_knots_from_string(string)?;
Ok(Story {
knots,
stack: vec![root],
in_progress: false,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn story_internally_follows_through_knots_when_diverts_are_found() {
let knot1_name = "back_in_london".to_string();
let knot2_name = "hurry_home".to_string();
let knot1_text = format!(
"\
We arrived into London at 9.45pm exactly.
-> {}\
",
knot2_name
);
let knot2_text = format!(
"\
We hurried home to Savile Row as fast as we could.\
"
);
let mut knots = HashMap::new();
knots.insert(knot1_name.clone(), Knot::from_str(&knot1_text).unwrap());
knots.insert(knot2_name, Knot::from_str(&knot2_text).unwrap());
let mut story = Story {
knots,
stack: vec![knot1_name],
in_progress: false,
};
let mut buffer = Vec::new();
story.follow_knot(&mut buffer).unwrap();
assert_eq!(&buffer.last().unwrap().text, &knot2_text);
}
#[test]
fn story_internally_resumes_from_the_new_knot_after_a_choice_is_made() {
let knot1_name = "back_in_london".to_string();
let knot2_name = "hurry_home".to_string();
let knot1_text = format!(
"\
We arrived into London at 9.45pm exactly.
-> {}\
",
knot2_name
);
let knot2_text = format!(
"\
\"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 mut knots = HashMap::new();
knots.insert(knot1_name.clone(), Knot::from_str(&knot1_text).unwrap());
knots.insert(knot2_name, Knot::from_str(&knot2_text).unwrap());
let mut story = Story {
knots,
stack: vec![knot1_name],
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 knot1_name = "back_in_london".to_string();
let knot2_name = "hurry_home".to_string();
let knot1_line = "We arrived into London at 9.45pm exactly.";
let knot1_text = format!(
"\
{}
-> {}\
",
knot1_line, knot2_name
);
let knot2_text = format!(
"\
* 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.
-> {}\
",
knot1_name
);
let mut knots = HashMap::new();
knots.insert(knot1_name.clone(), Knot::from_str(&knot1_text).unwrap());
knots.insert(knot2_name, Knot::from_str(&knot2_text).unwrap());
let mut story = Story {
knots,
stack: vec![knot1_name],
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, knot1_line);
assert_eq!(&buffer[5].text, knot1_line);
}
#[test]
fn divert_to_done_or_end_constant_knots_ends_story() {
let knot_done_text = "\
-> DONE
";
let knot_end_text = "\
-> END
";
let knot_done = Knot::from_str(&knot_done_text).unwrap();
let knot_end = Knot::from_str(&knot_end_text).unwrap();
let mut knots = HashMap::new();
knots.insert("knot_done".to_string(), knot_done);
knots.insert("knot_end".to_string(), knot_end);
let mut story = Story {
knots,
stack: vec!["knot_done".to_string()],
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!["knot_end".to_string()];
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 knot = Knot::from_str("").unwrap();
let mut knots = HashMap::new();
knots.insert("knot".to_string(), knot);
let mut buffer = Vec::new();
let mut story = Story {
knots,
stack: vec!["knot".to_string()],
in_progress: false,
};
assert_eq!(story.knots.get("knot").unwrap().num_visited, 0);
story.divert_to_knot("knot", &mut buffer).unwrap();
assert_eq!(story.knots.get("knot").unwrap().num_visited, 1);
}
#[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"),
}
}
}