use std::{
fmt::{Display, Formatter},
str::FromStr,
};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
use crate::vessel::Article;
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
EnumIter,
EnumString,
Display,
)]
#[strum(ascii_case_insensitive)]
pub enum Verb {
Learn,
Create,
Become,
Enter,
Leave,
Take,
Drop,
Move,
Look,
Transform,
Note,
Warp,
Program,
Use,
Cast,
Exit,
Save,
Load,
Debug,
}
impl Verb {
pub fn short_help_text(&self) -> String {
match self {
Verb::Learn => r#"Learn [about] [action]: Display general information on Parade, or information on a specific action or topic."#,
Verb::Create => r#"Create <vessel>: Create a new vessel adjacent to yours."#,
Verb::Become => r#"Become <vessel>: Take control of an adjacent vessel."#,
Verb::Enter => r#"Enter <vessel>: Move your vessel into an adjacent vessel."#,
Verb::Leave => r#"Leave: Move your vessel out of the vessel it is in (unless it is inside itself)."#,
Verb::Take => r#"Take <vessel>: Move an adjacent vessel inside yours."#,
Verb::Drop => r#"Drop <vessel>: Move a vessel inside yours outside, so it is adjacent to yours."#,
Verb::Move => r#"Move <vessel> <to/into> <vessel>: Move an adjacent vessel into another adjacent vessel."#,
Verb::Look => r#"Look [[at/in] <vessel>]: See a list of the adjacent vessels, or of the vessels inside an adjacent vessel."#,
Verb::Transform => r#"Transform [to/into] <vessel>: Change your vessel's name."#,
Verb::Note => r#"Note [text]: Assign a description to the vessel you are inside."#,
Verb::Warp => r#"Warp [to/into] <vessel>: Move your vessel into another vessel, anywhere."#,
Verb::Program => r#"Program [vessel]: Begin programming a task for a vessel, which can be activated with Use or Cast."#,
Verb::Use => r#"Use <vessel>: Execute an adjacent vessel's program."#,
Verb::Cast => r#"Cast <vessel> <at/on> <vessel>: Executes a distant vessel's program as another vessel."#,
Verb::Exit => r#"Exit: Exit parade, or stop programming a vessel if you're doing so."#,
Verb::Save => r#"Save [filename]: Save the state of all vessels to a file."#,
Verb::Load => r#"Load [filename]: Load the state of all vessels from a file."#,
Verb::Debug => r#"Debug: Display the state of all vessels as raw json data."#,
}.to_string()
}
pub fn help_text(&self) -> String {
match self {
Verb::Learn => r#"Learn [about] [Action]: Learn about Parade.
If used on its own, explains what Parade is, how to use it, and lists the available actions.
If used with an action, displays more in-depth information on that action.
Action descriptions begin with their parameters.
Parameters in square brackets (e.g. "[Action]") are optional, whereas those in angle brackets ("<noun>") are required."#,
Verb::Create => r#"Create [article] <noun>: Create a new vessel adjacent to yours.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if there is already a vessel in your world with the same name as the vessel you are trying to create. This applies regardless of where in your world the existing vessel is.
Vessel names can contain any number of words: You can create a vessel named "pink pony", and it will be distinct from a vessel named just "pony".
If an article is provided, parade will refer to the vessel using that article: e.g. "the library", "a ghost", etc.
If no article is provided, parade will refer to the vessel without an article: e.g. "Parade", "Jack the Ripper", etc."#,
Verb::Become => r#"Become [article] <noun>: Take control of an adjacent vessel.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if the selected vessel does not exist, or is not adjacent to yours.
This does not change your vessel or the selected vessel in any way, aside from changing which one you are controlling."#,
Verb::Enter => r#"Enter [article] <noun>: Move your vessel into an adjacent vessel.
"Adjacent", here, means that it is inside the same parent vessel as yours.
This action will not work if the selected vessel does not exist, or is not adjacent to yours."#,
Verb::Leave => r#"Leave: Move your vessel out of the vessel it is in.
If your vessel is inside itself, this will do nothing."#,
Verb::Take => r#"Take [article] <noun>: Move an adjacent vessel inside yours.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if the selected vessel does not exist, or is not adjacent to yours."#,
Verb::Drop => r#"Drop [article] <noun>: Move a vessel inside yours outside, so it is adjacent to yours.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if the selected vessel does not exist, or is not inside yours."#,
Verb::Move => r#"Move [article] <noun> <preposition> [article] <noun>: Move an adjacent vessel into another adjacent vessel.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if either of the selected vessels do not exist, or are not adjacent to yours.
Because vessel names can contain any number of words, you must separate the two vessel names a preposition.
The prepositions available here are "to" and "into"."#,
Verb::Look => r#"Look [[preposition] [article] <noun>]: See a list of the vessels adjacent to yours or inside the selected adjacent vessel.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if the selected vessel does not exist, or is not adjacent to yours.
If used on its own, this will display a list of the adjacent vessels.
If used with a noun, this will display a list of the vessels inside it.
The prepositions available here are "at" and "in"."#,
Verb::Transform => r#"Transform [preposition] [article] <noun>: Change your vessel's name to the provided name.
This action will not work if there is already a vessel in your world with the same name as the one you are trying to take. This applies regardless of where in your world the existing vessel is.
The prepositions available here are "into" and "to"."#,
Verb::Note => r#"Note [text]: Assign a description to the vessel you are inside.
The description will be displayed to anyone inside the vessel, or who Looks at it."#,
Verb::Warp => r#"Warp [preposition] [article] <noun>: Move your vessel into another vessel, anywhere.
This can be used to move your vessel inside itself.
This action will not work if the selected vessel does not exist.
The prepositions available here are "into" and "to"."#,
Verb::Program => r#"Program [article] [noun]: Begin programming a task for a vessel, which can be activated with Use.
If a vessel is provided, it must be adjacent. If no vessel is provided, the parent vessel will be selected.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if the selected vessel does not exist, or is not adjacent to yours.
While programming a vessel, all actions will be added to its "program", instead of being executed normally.
The only exception to this is Exit, which will cause you to stop programming the vessel - Unless you're currently programming the vessel to program another vessel. You can nest programs as much as you like.
Nouns mentioned in actions you add to a program are evaluated when you run the program, instead of when you add them to it.
This means that you can reference vessels which do not currently exist or which are not yet accessible."#,
Verb::Use => r#"Use [article] <noun>: Execute an adjacent vessel's program.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if the selected vessel does not exist, if it is not adjacent to yours, or it if does not have a program.
No actions used by the program will display information to you, and the Save and Load actions will do nothing.
When the program executes, it executes with control of the program's vessel, not yours. It can still, however, Become other vessels."#,
Verb::Cast => r#"Cast [article] <noun> <preposition> [article] <noun>: Execute a vessel's program as an adjacent vessel.
"Adjacent", here, means that it is inside the same parent vessel.
This action will not work if either of the selected vessels do not exist, if the first vessel does not have a program, or if the second vessel is not adjacent to yours.
The first named vessel is the vessel whose program will be cast, and the second named vessel is the vessel which will have the program cast on/as it.
This action otherwise functions as Use.
The prepositions available here are "at" and "on"."#,
Verb::Exit => r#"Exit: Exit parade, or stop programming a vessel if you're doing so.
Exits parade if you are outside of the Program interface.
If you are inside the Program interface, exits it.
If you are programming a vessel to program another vessel, this will add "Exit" to the former vessel's program.
Parade-rs supports unlimited nesting of programs."#,
Verb::Save => r#"Save [text]: Save the state of all vessels to a file.
If a name is provided, this will save to the file with that name.
Otherwise, the name "parade_save.json" will be used."#,
Verb::Load => r#"Load [text]: Load the state of all vessels from a file.
If a name is provided, this will save to the file with that name.
Otherwise, the name "parade_save.json" will be used."#,
Verb::Debug => r#"Debug: Display the state of all vessels as raw json data."#,
}.to_string()
}
pub fn help_list_text() -> String {
Verb::iter().fold(r#"This is Parade (or, specifically, parade-rs).
Parade is an experimental interactive-fiction playground, or maybe a filesystem or something.
Parade-rs is based on paradise (https://wiki.xxiivv.com/site/paradise.html) and parade (https://wiki.xxiivv.com/site/parade.html).
You interact with parade using the following actions:
"#.to_string(), |acc, topic| {
format!("{acc}- {}\n", topic.short_help_text())
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Action {
Create(VesselRequest),
Become(VesselRequest),
Enter(VesselRequest),
Leave,
Take(VesselRequest),
Drop(VesselRequest),
Move {
object_article: Option<Article>,
object_name: String,
preposition: IntoPrep,
target_article: Option<Article>,
target_name: String,
},
Look {
preposition: Option<AtPrep>,
basic: Option<VesselRequest>,
},
Transform {
preposition: Option<IntoPrep>,
basic: VesselRequest,
},
Note {
text: Option<String>,
},
Warp {
preposition: Option<IntoPrep>,
basic: VesselRequest,
},
Program(Option<VesselRequest>),
Use(VesselRequest),
Cast {
spell: VesselRequest,
preposition: OnPrep,
target: VesselRequest,
},
Save {
filename: Option<String>,
},
Load {
filename: Option<String>,
},
Debug,
Exit,
Learn {
preposition: Option<AboutPrep>,
topic: Option<Verb>,
},
}
impl Action {
pub fn from_vessel_request(verb: Verb, basic: VesselRequest) -> Result<Self, Error> {
match verb {
Verb::Create => Ok(Action::Create(basic)),
Verb::Become => Ok(Action::Become(basic)),
Verb::Enter => Ok(Action::Enter(basic)),
Verb::Take => Ok(Action::Take(basic)),
Verb::Drop => Ok(Action::Drop(basic)),
Verb::Program => Ok(Action::Program(Some(basic))),
Verb::Use => Ok(Action::Use(basic)),
Verb::Learn
| Verb::Leave
| Verb::Move
| Verb::Look
| Verb::Transform
| Verb::Note
| Verb::Warp
| Verb::Cast
| Verb::Exit
| Verb::Save
| Verb::Load
| Verb::Debug => Err(Error::NonBasicAction(verb)),
}
}
pub fn parse(action_string: &str) -> Result<Option<Action>, Error> {
let mut words = action_string.split_whitespace();
match words.next() {
Some(first_word) => match Verb::from_str(first_word) {
Ok(verb) => match verb {
Verb::Learn => match words.next() {
Some(second_word) => {
let preposition = AboutPrep::from_str(second_word);
let topic = if preposition.is_ok() {
match words.next() {
Some(word) => Verb::from_str(word),
None => {
return Ok(Some(Action::Learn {
preposition: preposition.ok(),
topic: None,
}));
}
}
} else {
Verb::from_str(second_word)
};
match topic {
Ok(help_topic) => Ok(Some(Action::Learn {
preposition: preposition.ok(),
topic: Some(help_topic),
})),
Err(_) => Err(Error::UnknownVerb(second_word.to_string())),
}
}
None => Ok(Some(Action::Learn {
preposition: None,
topic: None,
})),
},
Verb::Create
| Verb::Become
| Verb::Enter
| Verb::Take
| Verb::Drop
| Verb::Use => Ok(Some(Action::from_vessel_request(
verb,
VesselRequest::parse(words.next().ok_or(Error::MissingNoun)?, &mut words)?,
)?)),
Verb::Leave => Ok(Some(Action::Leave)),
Verb::Move => match words.next() {
Some(second_word) => {
let VesselRequest {
article: object_article,
name: object_name,
} = VesselRequest::parse(
second_word,
&mut words.take_while_ref(|word| IntoPrep::from_str(word).is_err()),
)?;
let preposition = words
.next()
.and_then(|prep_word| IntoPrep::from_str(prep_word).ok())
.ok_or(Error::MissingSecondNoun)?;
let VesselRequest {
article: target_article,
name: target_name,
} = VesselRequest::parse(
words.next().ok_or(Error::MissingSecondNoun)?,
&mut words,
)?;
Ok(Some(Action::Move {
object_article,
object_name,
preposition,
target_article,
target_name,
}))
}
None => Err(Error::MissingNoun),
},
Verb::Look => match words.next() {
Some(second_word) => {
let (preposition, basic) = VesselRequest::parse_with_prep::<AtPrep, _>(
second_word,
&mut words,
)?;
Ok(Some(Action::Look {
preposition,
basic: Some(basic),
}))
}
None => Ok(Some(Action::Look {
preposition: None,
basic: None,
})),
},
Verb::Transform => {
let (preposition, basic) = VesselRequest::parse_with_prep::<IntoPrep, _>(
words.next().ok_or(Error::MissingNoun)?,
&mut words,
)?;
Ok(Some(Action::Transform { preposition, basic }))
}
Verb::Note => Ok(Some(Action::Note {
text: join_or_none(&mut words, " "),
})),
Verb::Warp => {
let (preposition, basic) = VesselRequest::parse_with_prep::<IntoPrep, _>(
words.next().ok_or(Error::MissingNoun)?,
&mut words,
)?;
Ok(Some(Action::Warp { preposition, basic }))
}
Verb::Program => {
match words
.next()
.ok_or(Error::MissingNoun)
.and_then(|second_word| VesselRequest::parse(second_word, &mut words))
{
Ok(vessel_request) => {
Ok(Some(Action::from_vessel_request(verb, vessel_request)?))
}
Err(Error::MissingNoun) => Ok(Some(Action::Program(None))),
Err(e) => Err(e),
}
}
Verb::Cast => match words.next() {
Some(second_word) => {
let spell = VesselRequest::parse(
second_word,
&mut words.take_while_ref(|word| OnPrep::from_str(word).is_err()),
)?;
let preposition = words
.next()
.and_then(|prep_word| OnPrep::from_str(prep_word).ok())
.ok_or(Error::MissingSecondNoun)?;
let target = VesselRequest::parse(
words.next().ok_or(Error::MissingSecondNoun)?,
&mut words,
)?;
Ok(Some(Action::Cast {
spell,
preposition,
target,
}))
}
None => Err(Error::MissingNoun),
},
Verb::Exit => Ok(Some(Action::Exit)),
Verb::Save => Ok(Some(Action::Save {
filename: join_or_none(&mut words, " "),
})),
Verb::Load => Ok(Some(Action::Load {
filename: join_or_none(&mut words, " "),
})),
Verb::Debug => Ok(Some(Action::Debug)),
},
Err(_) => Err(Error::UnknownVerb(first_word.to_string())),
},
None => Ok(None),
}
}
}
#[derive(Debug)]
pub enum Error {
MissingNoun,
MissingSecondNoun,
UnknownVerb(String),
NonBasicAction(Verb),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingNoun => write!(f, "This action requires a noun."),
Self::MissingSecondNoun => write!(
f,
"This action requires two nouns. Did you remember to delimit them?"
),
Self::UnknownVerb(verb_string) => {
write!(f, "Unrecognized action: \"{verb_string}\".")
}
Self::NonBasicAction(verb) => write!(
f,
"Program tried to parse non-basic action \"{verb}\" as basic."
),
}
}
}
impl std::error::Error for Error {}
pub trait Preposition: FromStr {}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EnumString)]
#[strum(ascii_case_insensitive)]
pub enum IntoPrep {
Into,
To,
}
impl Preposition for IntoPrep {}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EnumString)]
#[strum(ascii_case_insensitive)]
pub enum AtPrep {
At,
In,
}
impl Preposition for AtPrep {}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EnumString)]
#[strum(ascii_case_insensitive)]
pub enum OnPrep {
At,
On,
}
impl Preposition for OnPrep {}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EnumString)]
#[strum(ascii_case_insensitive)]
pub enum AboutPrep {
About,
}
impl Preposition for AboutPrep {}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct VesselRequest {
pub article: Option<Article>,
pub name: String,
}
impl VesselRequest {
pub fn parse<'a, T: Iterator<Item = &'a str>>(
second_word: &'a str,
words: &mut T,
) -> Result<Self, Error> {
let article = Article::from_str(second_word).ok();
let name = join_or_none(
&mut if article.is_some() {
None
} else {
Some(second_word)
}
.into_iter()
.chain(words),
" ",
)
.ok_or(Error::MissingNoun)?;
Ok(Self { article, name })
}
pub fn parse_with_prep<'a, P: Preposition, T: Iterator<Item = &'a str>>(
second_word: &'a str,
words: &mut T,
) -> Result<(Option<P>, Self), Error> {
let preposition = P::from_str(second_word).ok();
let third_word = if preposition.is_none() {
second_word
} else {
words.next().ok_or(Error::MissingNoun)?
};
Ok((preposition, Self::parse(third_word, words)?))
}
}
fn join_or_none<'a, T: Iterator<Item = &'a str>>(words: &mut T, sep: &str) -> Option<String> {
let string = words.join(sep);
if string.is_empty() {
None
} else {
Some(string)
}
}