use crate::{
error::{InklingError, ParseError, StackError},
follow::{ChoiceInfo, EncounteredEvent, FollowData, LineDataBuffer},
knot::{get_empty_knot_counts, get_mut_stitch, validate_addresses_in_knots, Address, KnotSet},
process::{get_fallback_choices, prepare_choices_for_user, process_buffer},
story::read_knots_from_string,
};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq)]
pub struct Line {
pub text: String,
pub tags: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
pub struct Choice {
pub text: String,
pub tags: Vec<String>,
pub(crate) index: usize,
}
pub type LineBuffer = Vec<Line>;
#[derive(Debug)]
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
pub struct Story {
knots: KnotSet,
stack: Vec<Address>,
data: FollowData,
last_choices: Option<Vec<Choice>>,
requires_choice: bool,
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;
self.follow_story_wrapper(None, line_buffer)
}
pub fn resume_with_choice(
&mut self,
selection: usize,
line_buffer: &mut LineBuffer,
) -> Result<Prompt, InklingError> {
if !self.in_progress {
return Err(InklingError::ResumeBeforeStart);
}
if !self.requires_choice {
return Err(InklingError::ResumeWithoutChoice);
}
let index = self
.last_choices
.as_ref()
.ok_or(StackError::NoLastChoices.into())
.and_then(|last_choices| {
last_choices
.get(selection)
.ok_or(InklingError::InvalidChoice {
selection,
presented_choices: last_choices.clone(),
})
.map(|choice| choice.index)
})?;
self.requires_choice = false;
self.follow_story_wrapper(Some(index), line_buffer)
}
pub fn move_to(&mut self, knot: &str, stitch: Option<&str>) -> Result<(), InklingError> {
let to_address = Address::from_parts(knot, stitch, &self.knots).map_err(|_| {
InklingError::InvalidMove {
knot: knot.to_string(),
stitch: stitch.map(|s| s.to_string()),
}
})?;
self.update_last_stack(&to_address);
self.requires_choice = false;
self.last_choices = None;
Ok(())
}
pub fn resume(&mut self, line_buffer: &mut LineBuffer) -> Result<Prompt, InklingError> {
if !self.in_progress {
return Err(InklingError::ResumeBeforeStart);
}
self.follow_story_wrapper(None, line_buffer)
}
fn follow_story_wrapper(
&mut self,
selection: Option<usize>,
line_buffer: &mut LineBuffer,
) -> Result<Prompt, InklingError> {
let current_address = self.get_current_address()?;
let mut internal_buffer = Vec::new();
let (result, last_address) = follow_story(
¤t_address,
&mut internal_buffer,
selection,
&mut self.knots,
&mut self.data,
)?;
process_buffer(line_buffer, internal_buffer);
self.update_last_stack(&last_address);
match result {
Prompt::Choice(choices) => {
self.last_choices.replace(choices.clone());
self.requires_choice = true;
Ok(Prompt::Choice(choices))
}
other => Ok(other),
}
}
fn get_current_address(&self) -> Result<Address, InklingError> {
self.stack.last().cloned().ok_or(StackError::NoStack.into())
}
fn update_last_stack(&mut self, address: &Address) {
self.stack.push(address.clone());
}
}
pub fn read_story_from_string(string: &str) -> Result<Story, ParseError> {
let (root_knot_name, mut knots) = read_knots_from_string(string)?;
validate_addresses_in_knots(&mut knots)?;
let root_address = Address::from_root_knot(&root_knot_name, &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 simply should not be possible",
);
let data = FollowData {
knot_visit_counts: get_empty_knot_counts(&knots),
};
Ok(Story {
knots,
stack: vec![root_address],
data,
last_choices: None,
requires_choice: false,
in_progress: false,
})
}
fn follow_story(
current_address: &Address,
internal_buffer: &mut LineDataBuffer,
selection: Option<usize>,
knots: &mut KnotSet,
data: &mut FollowData,
) -> Result<(Prompt, Address), InklingError> {
let (last_address, event) =
follow_knot(current_address, internal_buffer, selection, knots, data)?;
match event {
EncounteredEvent::BranchingChoice(choice_set) => {
let user_choice_lines = prepare_choices_for_user(&choice_set, data)?;
if !user_choice_lines.is_empty() {
Ok((Prompt::Choice(user_choice_lines), last_address))
} else {
let choice = get_fallback_choice(&choice_set, &last_address, data)?;
follow_story(
&last_address,
internal_buffer,
Some(choice.index),
knots,
data,
)
}
}
EncounteredEvent::Done => Ok((Prompt::Done, last_address)),
EncounteredEvent::Divert(..) => unreachable!("diverts are treated in `follow_knot`"),
}
}
fn follow_knot(
address: &Address,
internal_buffer: &mut LineDataBuffer,
mut selection: Option<usize>,
knots: &mut KnotSet,
data: &mut FollowData,
) -> Result<(Address, EncounteredEvent), InklingError> {
let mut current_address = address.clone();
let event = loop {
let current_stitch = get_mut_stitch(¤t_address, knots)?;
let result = match selection.take() {
Some(i) => current_stitch.follow_with_choice(i, internal_buffer, data),
None => current_stitch.follow(internal_buffer, data),
}?;
match result {
EncounteredEvent::Divert(Address::End) => break EncounteredEvent::Done,
EncounteredEvent::Divert(to_address) => {
current_address = to_address;
}
_ => break result,
}
};
Ok((current_address, event))
}
fn get_fallback_choice(
choice_set: &[ChoiceInfo],
current_address: &Address,
data: &FollowData,
) -> Result<Choice, InklingError> {
get_fallback_choices(choice_set, data).and_then(|choices| {
choices.first().cloned().ok_or(InklingError::OutOfChoices {
address: current_address.clone(),
})
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
consts::ROOT_KNOT_NAME,
knot::{get_num_visited, ValidateAddresses},
};
fn mock_follow_data(knots: &KnotSet) -> FollowData {
FollowData {
knot_visit_counts: get_empty_knot_counts(knots),
}
}
#[test]
fn follow_knot_diverts_to_new_knots_when_encountered() {
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 (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let root_address = Address::from_root_knot("back_in_london", &knots).unwrap();
let mut buffer = Vec::new();
let mut data = mock_follow_data(&knots);
follow_knot(&root_address, &mut buffer, None, &mut knots, &mut data).unwrap();
assert_eq!(
&buffer.last().unwrap().text,
"We hurried home to Savile Row as fast as we could."
);
}
#[test]
fn follow_knot_does_not_return_divert_event_if_divert_is_encountered() {
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 (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let root_address = Address::from_root_knot("back_in_london", &knots).unwrap();
let mut buffer = Vec::new();
let mut data = mock_follow_data(&knots);
let (_, event) =
follow_knot(&root_address, &mut buffer, None, &mut knots, &mut data).unwrap();
match event {
EncounteredEvent::Done => (),
other => panic!("expected `EncounteredEvent::Done` but got {:?}", other),
}
}
#[test]
fn follow_knot_returns_choices_when_encountered() {
let content = "
== select_destination
* Tripoli
* Addis Ababa
* Rabat
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let root_address = Address::from_root_knot("select_destination", &knots).unwrap();
let mut buffer = Vec::new();
let mut data = mock_follow_data(&knots);
let (_, event) =
follow_knot(&root_address, &mut buffer, None, &mut knots, &mut data).unwrap();
match event {
EncounteredEvent::BranchingChoice(ref choices) => {
assert_eq!(choices.len(), 3);
}
other => panic!(
"expected `EncounteredEvent::BranchingChoice` but got {:?}",
other
),
}
}
#[test]
fn follow_knot_returns_the_last_active_knot() {
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 (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let root_address = Address::from_root_knot("back_in_london", &knots).unwrap();
let mut buffer = Vec::new();
let mut data = mock_follow_data(&knots);
let (last_address, _) =
follow_knot(&root_address, &mut buffer, None, &mut knots, &mut data).unwrap();
assert_eq!(
last_address,
Address::from_root_knot("hurry_home", &knots).unwrap()
);
}
#[test]
fn divert_to_done_or_end_constant_knots_ends_story() {
let content = "
== knot_done
-> DONE
== knot_end
-> END
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).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 buffer = Vec::new();
let mut data = mock_follow_data(&knots);
match follow_knot(&done_address, &mut buffer, None, &mut knots, &mut data).unwrap() {
(_, EncounteredEvent::Done) => (),
_ => panic!("story should be done when diverting to DONE knot"),
}
match follow_knot(&end_address, &mut buffer, None, &mut knots, &mut data).unwrap() {
(_, EncounteredEvent::Done) => (),
_ => panic!("story should be done when diverting to END knot"),
}
}
#[test]
fn divert_to_knot_increments_its_visit_count() {
let content = "
== addis_ababa
-> tripoli
== tripoli
-> DONE
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let current_address = Address::from_root_knot("addis_ababa", &knots).unwrap();
let divert_address = Address::from_root_knot("tripoli", &knots).unwrap();
let mut buffer = Vec::new();
let mut data = mock_follow_data(&knots);
follow_knot(¤t_address, &mut buffer, None, &mut knots, &mut data).unwrap();
assert_eq!(get_num_visited(&divert_address, &data).unwrap(), 1);
}
#[test]
fn knots_do_not_get_their_number_of_visits_incremented_when_resuming_a_choice() {
let content = "
== tripoli
* Cinema -> END
* Visit family -> END
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let current_address = Address::from_root_knot("tripoli", &knots).unwrap();
let mut buffer = Vec::new();
let mut data = mock_follow_data(&knots);
follow_knot(
¤t_address,
&mut buffer,
Some(1),
&mut knots,
&mut data,
)
.unwrap();
assert_eq!(get_num_visited(¤t_address, &data).unwrap(), 0);
}
#[test]
fn follow_story_returns_last_visited_address_when_reaching_end() {
let content = "
== addis_ababa
-> tripoli.cinema
== tripoli
= cinema
-> END
= visit_family
-> END
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let current_address = Address::from_root_knot("addis_ababa", &knots).unwrap();
let mut line_buffer = Vec::new();
let mut data = mock_follow_data(&knots);
let (_, last_address) = follow_story(
¤t_address,
&mut line_buffer,
None,
&mut knots,
&mut data,
)
.unwrap();
assert_eq!(
last_address,
Address::from_target_address("tripoli.cinema", ¤t_address, &knots).unwrap()
);
}
#[test]
fn follow_story_returns_last_visited_address_when_encountering_choices() {
let content = "
== addis_ababa
-> tripoli
== tripoli
* Cinema -> END
* Visit family -> END
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let current_address = Address::from_root_knot("addis_ababa", &knots).unwrap();
let mut line_buffer = Vec::new();
let mut data = mock_follow_data(&knots);
let (_, last_address) = follow_story(
¤t_address,
&mut line_buffer,
None,
&mut knots,
&mut data,
)
.unwrap();
assert_eq!(
last_address,
Address::from_target_address("tripoli", ¤t_address, &knots).unwrap()
);
}
#[test]
fn following_story_wrapper_updates_stack_to_last_address() {
let content = "
== addis_ababa
-> tripoli.cinema
== tripoli
= cinema
-> END
= visit_family
-> END
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.follow_story_wrapper(None, &mut line_buffer).unwrap();
let address = Address::from_target_address(
"tripoli.cinema",
&story.get_current_address().unwrap(),
&story.knots,
)
.unwrap();
assert_eq!(story.stack.last().unwrap(), &address);
}
#[test]
fn choice_index_is_used_to_resume_story_with() {
let content = "
== tripoli
* Cinema
You watched a horror movie in the cinema.
* Visit family
A visit to your family did you good.
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
story.resume_with_choice(1, &mut line_buffer).unwrap();
assert_eq!(
&line_buffer[1].text,
"A visit to your family did you good.\n"
);
}
#[test]
fn choice_index_is_converted_to_internal_branch_index_to_account_for_filtered_choices() {
let content = "
== tripoli
* Cinema
You watched a horror movie in the cinema.
* {addis_ababa} Call Kinfe.
* Visit family
A visit to your family did you good.
== addis_ababa
-> END
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
story.resume_with_choice(1, &mut line_buffer).unwrap();
assert_eq!(
&line_buffer[1].text,
"A visit to your family did you good.\n"
);
}
#[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(0, &mut buffer).unwrap();
assert_eq!(&buffer[1].text, "Fallback choice\n");
}
#[test]
fn fallback_choices_resume_from_the_knot_they_are_encountered_in() {
let content = "
== first
-> second
== second
+ ->
Fallback choice
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let current_address = Address::from_root_knot("first", &knots).unwrap();
let mut line_buffer = Vec::new();
let mut internal_buffer = Vec::new();
let mut data = mock_follow_data(&knots);
follow_story(
¤t_address,
&mut internal_buffer,
None,
&mut knots,
&mut data,
)
.unwrap();
process_buffer(&mut line_buffer, internal_buffer);
assert_eq!(&line_buffer[0].text, "Fallback choice\n");
}
#[test]
fn glue_is_followed_over_fallback_choices() {
let content = "
== tripoli
We decided to go to the <>
* [] Cinema.
";
let (_, mut knots) = read_knots_from_string(content).unwrap();
validate_addresses_in_knots(&mut knots).unwrap();
let current_address = Address::from_root_knot("tripoli", &knots).unwrap();
let mut line_buffer = Vec::new();
let mut internal_buffer = Vec::new();
let mut data = mock_follow_data(&knots);
follow_story(
¤t_address,
&mut internal_buffer,
None,
&mut knots,
&mut data,
)
.unwrap();
process_buffer(&mut line_buffer, internal_buffer);
assert_eq!(&line_buffer[0].text, "We decided to go to the ");
assert_eq!(&line_buffer[1].text, "Cinema.\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(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 last_set_of_presented_choices_are_stored() {
let content = "
== knot
* Choice 1
* Choice 2
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
assert!(story.last_choices.is_none());
story.start(&mut line_buffer).unwrap();
let last_choices = story.last_choices.as_ref().unwrap();
assert_eq!(last_choices.len(), 2);
assert_eq!(&last_choices[0].text, "Choice 1");
assert_eq!(&last_choices[1].text, "Choice 2");
}
#[test]
fn when_an_invalid_choices_is_made_to_resume_the_story_an_invalid_choice_error_is_yielded() {
let content = "
== knot
* Choice 1
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
match story.resume_with_choice(1, &mut line_buffer) {
Err(InklingError::InvalidChoice {
selection,
presented_choices,
}) => {
assert_eq!(selection, 1);
assert_eq!(presented_choices.len(), 1);
assert_eq!(&presented_choices[0].text, "Choice 1");
}
other => panic!("expected `InklingError::InvalidChoice` but got {:?}", other),
}
}
#[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();
match story.resume_with_choice(0, &mut line_buffer) {
Err(InklingError::ResumeBeforeStart) => (),
_ => panic!("did not raise `ResumeBeforeStart` error"),
}
}
#[test]
fn when_a_knot_is_returned_to_the_text_starts_from_the_beginning() {
let content = "
== back_in_almaty
We arrived into Almaty at 9.45pm exactly.
-> hurry_home
== hurry_home
* We hurried home as fast as we could. -> END
* But we decided our trip wasn't done and immediately left.
After a few days me returned again.
-> back_in_almaty
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.follow_story_wrapper(None, &mut line_buffer).unwrap();
story
.follow_story_wrapper(Some(1), &mut line_buffer)
.unwrap();
assert_eq!(
&line_buffer[0].text,
"We arrived into Almaty at 9.45pm exactly.\n"
);
assert_eq!(
&line_buffer[3].text,
"We arrived into Almaty at 9.45pm exactly.\n"
);
}
#[test]
fn when_the_story_begins_the_first_knot_gets_its_number_of_visits_set_to_one() {
let content = "
Hello, World!
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
let address = Address::from_root_knot("$ROOT$", &story.knots).unwrap();
assert_eq!(get_num_visited(&address, &story.data).unwrap(), 1);
}
#[test]
fn all_addresses_are_validated_in_knots_after_reading_story_from_lines() {
let content = "
== back_in_almaty
We arrived into Almaty at 9.45pm exactly.
-> hurry_home
== hurry_home
* We hurried home as fast as we could.
-> END
* But we decided our trip wasn't done yet.
* We immediately left the city.
After a few days me returned again.
-> back_in_almaty
* Still, we could not head out just yet. -> fin
== fin
-> END
";
let story = read_story_from_string(content).unwrap();
for knot in story.knots.values() {
for stitch in knot.stitches.values() {
assert!(stitch.root.all_addresses_are_valid());
}
}
}
#[test]
fn reading_story_from_string_initializes_all_knot_visit_counts_to_zero() {
let content = "
== back_in_almaty
-> END
== hurry_home
-> END
= at_home
-> END
";
let story = read_story_from_string(content).unwrap();
let back_in_almaty = Address::Validated {
knot: "back_in_almaty".into(),
stitch: "$ROOT$".into(),
};
let hurry_home = Address::Validated {
knot: "hurry_home".into(),
stitch: "$ROOT$".into(),
};
let at_home = Address::Validated {
knot: "hurry_home".into(),
stitch: "at_home".into(),
};
assert_eq!(get_num_visited(&back_in_almaty, &story.data).unwrap(), 0);
assert_eq!(get_num_visited(&hurry_home, &story.data).unwrap(), 0);
assert_eq!(get_num_visited(&at_home, &story.data).unwrap(), 0);
}
#[test]
fn knots_with_non_default_root_stitch_gets_validated_addresses_that_point_to_them() {
let content = "
-> almaty
== almaty
= back
We arrived into Almaty at 9.45pm exactly.
-> END
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
assert_eq!(
&line_buffer[0].text,
"We arrived into Almaty at 9.45pm exactly.\n"
);
}
#[test]
fn number_of_visits_in_a_story_is_consistent() {
let content = "
One
-> root
== root
+ {visit_twice < 2} -> visit_twice
+ {visit_twice >= 2} {visit_thrice < 3} -> visit_thrice
* [] -> END
== visit_twice
Two
-> root
== visit_thrice
Three
-> root
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
let knots = &story.knots;
let address_root = Address::from_root_knot("root", &knots).unwrap();
let address_twice = Address::from_root_knot("visit_twice", &knots).unwrap();
let address_thrice = Address::from_root_knot("visit_thrice", &knots).unwrap();
assert_eq!(get_num_visited(&address_twice, &story.data).unwrap(), 2);
assert_eq!(get_num_visited(&address_thrice, &story.data).unwrap(), 3);
assert_eq!(get_num_visited(&address_root, &story.data).unwrap(), 6);
}
#[test]
fn calling_resume_on_a_story_at_a_choice_returns_the_choice_again() {
let content = "
== back_in_almaty
After an arduous journey we arrived back in Almaty.
* We hurried home as fast as we could.
-> END
* But we decided our trip wasn't done yet.
We immediately left the city.
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
let choices = story
.start(&mut line_buffer)
.unwrap()
.get_choices()
.unwrap();
line_buffer.clear();
let resume_choices = story
.resume(&mut line_buffer)
.unwrap()
.get_choices()
.unwrap();
assert_eq!(choices, resume_choices);
assert!(line_buffer.is_empty());
}
#[test]
fn after_a_resume_is_made_the_choice_can_be_made_as_usual() {
let content = "
== back_in_almaty
After an arduous journey we arrived back in Almaty.
* We hurried home as fast as we could.
-> END
* But we decided our trip wasn't done yet.
We immediately left the city.
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story
.start(&mut line_buffer)
.unwrap()
.get_choices()
.unwrap();
line_buffer.clear();
story
.resume(&mut line_buffer)
.unwrap()
.get_choices()
.unwrap();
story
.resume(&mut line_buffer)
.unwrap()
.get_choices()
.unwrap();
story
.resume(&mut line_buffer)
.unwrap()
.get_choices()
.unwrap();
story
.resume(&mut line_buffer)
.unwrap()
.get_choices()
.unwrap();
story.resume_with_choice(1, &mut line_buffer).unwrap();
assert_eq!(line_buffer.len(), 2);
assert_eq!(
&line_buffer[0].text,
"But we decided our trip wasn't done yet.\n"
);
assert_eq!(&line_buffer[1].text, "We immediately left the city.\n");
}
#[test]
fn resume_cannot_be_called_on_an_unstarted_story() {
let content = "
== back_in_almaty
After an arduous journey we arrived back in Almaty.
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
match story.resume(&mut line_buffer) {
Err(InklingError::ResumeBeforeStart) => (),
other => panic!(
"expected `InklingError::ResumeBeforeStart` but got {:?}",
other
),
}
}
#[test]
fn story_can_be_moved_to_another_address() {
let content = "
We arrived into Almaty at 9.45pm exactly.
-> END
== hurry_home
We hurried home as fast as we could.
-> END
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
story.move_to("hurry_home", None).unwrap();
let address = story.stack.last().unwrap();
assert_eq!(address.get_knot().unwrap(), "hurry_home");
assert_eq!(address.get_stitch().unwrap(), ROOT_KNOT_NAME);
line_buffer.clear();
story.resume(&mut line_buffer).unwrap();
assert_eq!(
&line_buffer[0].text,
"We hurried home as fast as we could.\n"
);
}
#[test]
fn move_to_addresses_can_include_stitches() {
let content = "
We arrived into Almaty at 9.45pm exactly.
-> END
== hurry_home
We hurried home as fast as we could.
-> END
= at_home
Once back home we feasted on cheese.
-> END
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
story.move_to("hurry_home", Some("at_home")).unwrap();
let address = story.stack.last().unwrap();
assert_eq!(address.get_knot().unwrap(), "hurry_home");
assert_eq!(address.get_stitch().unwrap(), "at_home");
line_buffer.clear();
story.resume(&mut line_buffer).unwrap();
assert_eq!(
&line_buffer[0].text,
"Once back home we feasted on cheese.\n"
);
}
#[test]
fn move_to_can_be_called_on_a_story_before_the_start() {
let content = "
We arrived into Almaty at 9.45pm exactly.
-> END
== hurry_home
We hurried home as fast as we could.
-> END
= at_home
Once back home we feasted on cheese.
-> END
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.move_to("hurry_home", Some("at_home")).unwrap();
story.start(&mut line_buffer).unwrap();
assert_eq!(
&line_buffer[0].text,
"Once back home we feasted on cheese.\n"
);
}
#[test]
fn move_to_yields_error_if_knot_or_stitch_name_is_invalid() {
let content = "
We arrived into Almaty at 9.45pm exactly.
-> END
== hurry_home
We hurried home as fast as we could.
-> END
= at_home
Once back home we feasted on cheese.
-> END
";
let mut story = read_story_from_string(content).unwrap();
assert!(story.move_to("fin", None).is_err());
assert!(story.move_to("hurry_home", Some("not_at_home")).is_err());
}
#[test]
fn resume_with_choice_cannot_be_called_directly_after_a_move() {
let content = "
We arrived into Almaty at 9.45pm exactly.
* That was the end of our trip. -> fin
== hurry_home
We hurried home as fast as we could.
== fin
-> END
";
let mut story = read_story_from_string(content).unwrap();
let mut line_buffer = Vec::new();
story.start(&mut line_buffer).unwrap();
story.move_to("hurry_home", None).unwrap();
match story.resume_with_choice(0, &mut line_buffer) {
Err(InklingError::ResumeWithoutChoice) => (),
other => panic!(
"expected `InklingError::ResumeWithoutChoice` but got {:?}",
other
),
}
}
}