Initial commit
This commit is contained in:
commit
10b7a15c23
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "midnight"
|
||||
version = "1.0.0"
|
||||
authors = ["Ben Bridle <bridle.benjamin@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
switches = { git = "https://tildegit.org/toast/switches", tag = "v1.0" }
|
||||
markdown_parser = { git = "https://tildegit.org/toast/markdown_parser", tag = "v1.0" }
|
||||
vagabond = { git = "https://tildegit.org/toast/vagabond", tag = "v1.0" }
|
||||
wetstring_http = { git = "https://tildegit.org/toast/wetstring_http", tag = "v1.0" }
|
||||
env_logger = "0.8.4"
|
||||
log = "0.4.14"
|
|
@ -0,0 +1,18 @@
|
|||
use crate::{Clause, Input, Pattern};
|
||||
|
||||
pub struct Action {
|
||||
pub name: String,
|
||||
pub patterns: Vec<Pattern>,
|
||||
pub clauses: Vec<Clause>,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn matches(&self, input: &Input) -> bool {
|
||||
for pattern in &self.patterns {
|
||||
if pattern.matches(input) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use super::Word;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
pub struct Input {
|
||||
words: Vec<Word>,
|
||||
}
|
||||
impl Input {
|
||||
pub fn parse(input: &str) -> Self {
|
||||
Self {
|
||||
words: Word::parse_many(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn words(&self) -> &[Word] {
|
||||
&self.words
|
||||
}
|
||||
}
|
||||
impl Display for Input {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||
let string = self
|
||||
.words
|
||||
.iter()
|
||||
.map(|word| word.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
write!(f, "{}", string)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
mod word;
|
||||
pub use word::Word;
|
||||
|
||||
mod input;
|
||||
pub use input::Input;
|
||||
|
||||
mod pattern;
|
||||
pub use pattern::Pattern;
|
||||
|
||||
fn sanitise_input(input: &str) -> String {
|
||||
input
|
||||
.to_ascii_uppercase()
|
||||
.replace('\n', " ")
|
||||
.replace('\r', " ")
|
||||
.replace('\t', " ")
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
mod pattern;
|
||||
mod pattern_parse_error;
|
||||
mod pattern_token;
|
||||
|
||||
pub use pattern::Pattern;
|
||||
use pattern_parse_error::PatternParseError;
|
||||
use pattern_token::PatternToken;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
|
@ -0,0 +1,222 @@
|
|||
use super::super::{Input, Word};
|
||||
use super::{PatternParseError, PatternToken};
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
enum BracketType {
|
||||
Optional,
|
||||
Choice,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Pattern {
|
||||
Word(Word),
|
||||
Choice(Vec<Pattern>),
|
||||
Sequence(Vec<Pattern>),
|
||||
Optional(Box<Pattern>),
|
||||
}
|
||||
impl Pattern {
|
||||
pub fn matches(&self, input: &Input) -> bool {
|
||||
match self.consume_words(input.words()) {
|
||||
Ok(words) => words.is_empty(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_words<'a>(&self, words: &'a [Word]) -> Result<&'a [Word], ()> {
|
||||
match self {
|
||||
Pattern::Sequence(patterns) => {
|
||||
let mut words = words.clone();
|
||||
for pattern in patterns {
|
||||
words = pattern.consume_words(words)?;
|
||||
}
|
||||
return Ok(words);
|
||||
}
|
||||
Pattern::Choice(patterns) => {
|
||||
for pattern in patterns {
|
||||
if let Ok(words) = pattern.consume_words(words) {
|
||||
return Ok(words);
|
||||
}
|
||||
}
|
||||
return Err(());
|
||||
}
|
||||
Pattern::Optional(pattern) => match pattern.consume_words(words) {
|
||||
Ok(words) => return Ok(words),
|
||||
Err(_) => return Ok(words),
|
||||
},
|
||||
Pattern::Word(pattern_word) => {
|
||||
let first_word = match words.get(0) {
|
||||
Some(word) => word,
|
||||
None => return Err(()),
|
||||
};
|
||||
if pattern_word == first_word {
|
||||
return Ok(&words[1..]);
|
||||
} else {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(pattern_string: &str) -> Result<Self, PatternParseError> {
|
||||
let tokens = PatternToken::tokenise(pattern_string);
|
||||
Self::parse_tokens(&tokens)
|
||||
}
|
||||
|
||||
fn parse_tokens(tokens: &[PatternToken]) -> Result<Self, PatternParseError> {
|
||||
let mut choice = Vec::new();
|
||||
let mut sequence = Vec::new();
|
||||
|
||||
let mut bracket_stack = Vec::new();
|
||||
let mut outer_bracket_type = None;
|
||||
let mut bracket_start_index = 0;
|
||||
|
||||
for (index, token) in tokens.iter().enumerate() {
|
||||
if bracket_stack.is_empty() {
|
||||
bracket_start_index = index;
|
||||
}
|
||||
match token {
|
||||
PatternToken::ChoiceStart => bracket_stack.push(BracketType::Choice),
|
||||
PatternToken::ChoiceEnd => match bracket_stack.pop() {
|
||||
Some(BracketType::Choice) => outer_bracket_type = Some(BracketType::Choice),
|
||||
Some(_) => return Err(PatternParseError::UnmatchedChoiceStart),
|
||||
None => return Err(PatternParseError::UnmatchedChoiceEnd),
|
||||
},
|
||||
PatternToken::OptionalStart => bracket_stack.push(BracketType::Optional),
|
||||
PatternToken::OptionalEnd => match bracket_stack.pop() {
|
||||
Some(BracketType::Optional) => outer_bracket_type = Some(BracketType::Optional),
|
||||
Some(_) => return Err(PatternParseError::UnmatchedOptionalEnd),
|
||||
None => return Err(PatternParseError::UnmatchedOptionalEnd),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
if bracket_stack.is_empty() {
|
||||
if let Some(ref bracket_type) = outer_bracket_type {
|
||||
let inner_tokens = &tokens[bracket_start_index + 1..index];
|
||||
if inner_tokens.len() > 0 {
|
||||
let pattern = Self::parse_tokens(inner_tokens)?;
|
||||
sequence.push(match bracket_type {
|
||||
BracketType::Optional => Pattern::Optional(Box::new(pattern)),
|
||||
BracketType::Choice => pattern,
|
||||
});
|
||||
} else {
|
||||
return Err(match bracket_type {
|
||||
BracketType::Optional => PatternParseError::EmptyOptional,
|
||||
BracketType::Choice => PatternParseError::EmptyChoice,
|
||||
});
|
||||
}
|
||||
outer_bracket_type = None;
|
||||
} else {
|
||||
match token {
|
||||
PatternToken::Word(word) => sequence.push(Pattern::Word(word.clone())),
|
||||
PatternToken::ChoiceSeparator => {
|
||||
if !sequence.is_empty() {
|
||||
let pattern = Pattern::Sequence(sequence.clone()).clean()?;
|
||||
if let Pattern::Optional(_) = pattern {
|
||||
return Err(PatternParseError::OptionalInChoice);
|
||||
};
|
||||
choice.push(pattern);
|
||||
sequence.clear();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !bracket_stack.is_empty() {
|
||||
return Err(match bracket_stack[0] {
|
||||
BracketType::Optional => PatternParseError::UnmatchedOptionalStart,
|
||||
BracketType::Choice => PatternParseError::UnmatchedChoiceStart,
|
||||
});
|
||||
}
|
||||
if !choice.is_empty() && !sequence.is_empty() {
|
||||
// Flush the final choice
|
||||
let pattern = Pattern::Sequence(sequence.clone()).clean()?;
|
||||
if let Pattern::Optional(_) = pattern {
|
||||
return Err(PatternParseError::OptionalInChoice);
|
||||
};
|
||||
choice.push(pattern);
|
||||
}
|
||||
if !choice.is_empty() {
|
||||
Ok(Pattern::Choice(choice).clean()?)
|
||||
} else if !sequence.is_empty() {
|
||||
Ok(Pattern::Sequence(sequence).clean()?)
|
||||
} else {
|
||||
Err(PatternParseError::EmptySequence)
|
||||
}
|
||||
}
|
||||
|
||||
fn clean(self) -> Result<Self, PatternParseError> {
|
||||
match self {
|
||||
Pattern::Choice(ref patterns) => {
|
||||
if patterns.len() == 0 {
|
||||
Err(PatternParseError::EmptyChoice)
|
||||
} else if patterns.len() == 1 {
|
||||
Ok(patterns[0].clone())
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
Pattern::Sequence(ref patterns) => {
|
||||
if patterns.len() == 0 {
|
||||
Err(PatternParseError::EmptySequence)
|
||||
} else if patterns.len() == 1 {
|
||||
Ok(patterns[0].clone())
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
_ => Ok(self),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_display_string(&self) -> String {
|
||||
let patterns_to_string = |v: &[Pattern], sep: &str| {
|
||||
v.iter()
|
||||
.map(|p| p.to_display_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(sep)
|
||||
};
|
||||
|
||||
match self {
|
||||
Pattern::Choice(patterns) => {
|
||||
format!("({})", patterns_to_string(patterns, "|"))
|
||||
}
|
||||
Pattern::Sequence(patterns) => {
|
||||
format!("{}", patterns_to_string(patterns, " "))
|
||||
}
|
||||
Pattern::Optional(pattern) => format!("[{}]", pattern.to_display_string()),
|
||||
Pattern::Word(word) => format!("{}", word),
|
||||
}
|
||||
}
|
||||
fn to_debug_string(&self) -> String {
|
||||
let patterns_to_string = |v: &[Pattern]| {
|
||||
v.iter()
|
||||
.map(|p| p.to_debug_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
};
|
||||
|
||||
match self {
|
||||
Pattern::Choice(patterns) => {
|
||||
format!("Choice({})", patterns_to_string(patterns))
|
||||
}
|
||||
Pattern::Sequence(patterns) => {
|
||||
format!("Sequence({})", patterns_to_string(patterns))
|
||||
}
|
||||
Pattern::Optional(pattern) => format!("Optional({})", pattern.to_debug_string()),
|
||||
Pattern::Word(word) => format!("Word({})", word),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Pattern {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "{}", self.to_display_string())
|
||||
}
|
||||
}
|
||||
impl Debug for Pattern {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "{}", self.to_debug_string())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
pub enum PatternParseError {
|
||||
UnmatchedOptionalStart,
|
||||
UnmatchedOptionalEnd,
|
||||
UnmatchedChoiceStart,
|
||||
UnmatchedChoiceEnd,
|
||||
OptionalInChoice,
|
||||
EmptyOptional,
|
||||
EmptySequence,
|
||||
EmptyChoice,
|
||||
}
|
||||
impl Debug for PatternParseError {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||
let string = match self {
|
||||
PatternParseError::UnmatchedOptionalStart => "Missing a ] character",
|
||||
PatternParseError::UnmatchedOptionalEnd => "Missing a [ character",
|
||||
PatternParseError::UnmatchedChoiceStart => "Missing a ) character",
|
||||
PatternParseError::UnmatchedChoiceEnd => "Missing a ( character",
|
||||
PatternParseError::OptionalInChoice => "Choice contains an optional pattern",
|
||||
PatternParseError::EmptySequence => "Sequence contains no patterns",
|
||||
PatternParseError::EmptyOptional => "Optional doesn't contain a pattern",
|
||||
PatternParseError::EmptyChoice => "Choice contains no patterns",
|
||||
};
|
||||
write!(f, "{}", string)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
use super::super::{sanitise_input, Word};
|
||||
|
||||
pub enum PatternToken {
|
||||
Word(Word),
|
||||
ChoiceStart,
|
||||
ChoiceEnd,
|
||||
OptionalStart,
|
||||
OptionalEnd,
|
||||
ChoiceSeparator,
|
||||
}
|
||||
impl PatternToken {
|
||||
pub fn tokenise(pattern_string: &str) -> Vec<PatternToken> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut word = String::new();
|
||||
|
||||
for c in sanitise_input(pattern_string).chars() {
|
||||
if "[]()| ".contains(c) && word.len() > 0 {
|
||||
tokens.push(PatternToken::Word(Word {
|
||||
word: std::mem::take(&mut word),
|
||||
}));
|
||||
}
|
||||
match c {
|
||||
'[' => tokens.push(PatternToken::OptionalStart),
|
||||
']' => tokens.push(PatternToken::OptionalEnd),
|
||||
'(' => tokens.push(PatternToken::ChoiceStart),
|
||||
')' => tokens.push(PatternToken::ChoiceEnd),
|
||||
'|' => tokens.push(PatternToken::ChoiceSeparator),
|
||||
' ' => (),
|
||||
_ => word.push(c),
|
||||
}
|
||||
}
|
||||
if word.len() > 0 {
|
||||
tokens.push(PatternToken::Word(Word { word }));
|
||||
}
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PatternToken {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
let string = match self {
|
||||
PatternToken::Word(word) => word.as_str(),
|
||||
PatternToken::ChoiceStart => "(",
|
||||
PatternToken::ChoiceEnd => ")",
|
||||
PatternToken::OptionalStart => "[",
|
||||
PatternToken::OptionalEnd => "]",
|
||||
PatternToken::ChoiceSeparator => "|",
|
||||
};
|
||||
write!(f, "{}", string)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
use super::*;
|
||||
use crate::Input;
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_parse_failure() {
|
||||
assert!(matches!(Pattern::parse("("), Err(PatternParseError::UnmatchedChoiceStart)));
|
||||
assert!(matches!(Pattern::parse(")"), Err(PatternParseError::UnmatchedChoiceEnd)));
|
||||
assert!(matches!(Pattern::parse("["), Err(PatternParseError::UnmatchedOptionalStart)));
|
||||
assert!(matches!(Pattern::parse("]"), Err(PatternParseError::UnmatchedOptionalEnd)));
|
||||
assert!(matches!(Pattern::parse("(WALK"),Err(PatternParseError::UnmatchedChoiceStart)));
|
||||
assert!(matches!(Pattern::parse("WALK)"), Err(PatternParseError::UnmatchedChoiceEnd)));
|
||||
assert!(matches!(Pattern::parse("[WALK"), Err(PatternParseError::UnmatchedOptionalStart)));
|
||||
assert!(matches!(Pattern::parse("WALK]"), Err(PatternParseError::UnmatchedOptionalEnd)));
|
||||
assert!(matches!(Pattern::parse("()"), Err(PatternParseError::EmptyChoice)));
|
||||
assert!(matches!(Pattern::parse(""), Err(PatternParseError::EmptySequence)));
|
||||
assert!(matches!(Pattern::parse("|"), Err(PatternParseError::EmptySequence)));
|
||||
assert!(matches!(Pattern::parse("OPEN (A|[THE]) DOOR"), Err(PatternParseError::OptionalInChoice)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_parse_success() {
|
||||
assert!(Pattern::parse("DOOR").unwrap().to_string() == "DOOR");
|
||||
assert!(Pattern::parse("WALK TO DOOR").unwrap().to_string() == "WALK TO DOOR");
|
||||
assert!(Pattern::parse("WALK TO [THE] DOOR").unwrap().to_string() == "WALK TO [THE] DOOR");
|
||||
assert!(Pattern::parse("(RUN|WALK) TO [THE] DOOR").unwrap().to_string() == "(RUN|WALK) TO [THE] DOOR");
|
||||
assert!(Pattern::parse("(|RUN|WALK|) TO [THE] DOOR").unwrap().to_string() == "(RUN|WALK) TO [THE] DOOR");
|
||||
assert!(Pattern::parse("(RUN [QUICKLY]|WALK) (AT|TO) [THE] DOOR").unwrap().to_string() == "(RUN [QUICKLY]|WALK) (AT|TO) [THE] DOOR");
|
||||
assert!(Pattern::parse("Sprint ( towards|to) [the] door ").unwrap().to_string() == "SPRINT (TOWARDS|TO) [THE] DOOR");
|
||||
assert!(Pattern::parse("WALK|RUN").unwrap().to_string() == "(WALK|RUN)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_parse_success_precise() {
|
||||
assert!(format!("{:?}", Pattern::parse("DOOR").unwrap()) == "Word(DOOR)");
|
||||
assert!(format!("{:?}", Pattern::parse("OPEN DOOR").unwrap()) == "Sequence(Word(OPEN), Word(DOOR))");
|
||||
assert!(format!("{:?}", Pattern::parse("OPEN [THE] DOOR").unwrap()) == "Sequence(Word(OPEN), Optional(Word(THE)), Word(DOOR))");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_match_failure() {
|
||||
assert!(!Pattern::parse("DOOR").unwrap().matches(&Input::parse("DOORS")));
|
||||
assert!(!Pattern::parse("DOOR").unwrap().matches(&Input::parse("DOOR KNOB")));
|
||||
assert!(!Pattern::parse("OPEN DOOR").unwrap().matches(&Input::parse("OPEN THE DOOR")));
|
||||
assert!(!Pattern::parse("OPEN [THE] DOOR").unwrap().matches(&Input::parse("OPEN A DOOR")));
|
||||
assert!(!Pattern::parse("OPEN (A|THE) DOOR").unwrap().matches(&Input::parse("OPEN YOUR DOOR")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_match_success() {
|
||||
assert!(Pattern::parse("DOOR").unwrap().matches(&Input::parse("DOOR")));
|
||||
assert!(Pattern::parse("OPEN DOOR").unwrap().matches(&Input::parse("OPEN DOOR")));
|
||||
assert!(Pattern::parse("OPEN THE DOOR").unwrap().matches(&Input::parse("OPEN THE DOOR")));
|
||||
assert!(Pattern::parse("OPEN [THE] DOOR").unwrap().matches(&Input::parse("OPEN THE DOOR")));
|
||||
assert!(Pattern::parse("OPEN [THE] DOOR").unwrap().matches(&Input::parse("OPEN DOOR")));
|
||||
assert!(Pattern::parse("OPEN (A|THE) DOOR").unwrap().matches(&Input::parse("OPEN A DOOR")));
|
||||
assert!(Pattern::parse("OPEN (A|THE) DOOR").unwrap().matches(&Input::parse("OPEN THE DOOR")));
|
||||
assert!(Pattern::parse("OPEN [(A|THE)] DOOR").unwrap().matches(&Input::parse("OPEN DOOR")));
|
||||
assert!(Pattern::parse("(RUN FORWARD|(GO|WALK) THROUGH [THE] DOOR)").unwrap().matches(&Input::parse("RUN FORWARD")));
|
||||
assert!(Pattern::parse("(RUN FORWARD|(GO|WALK) THROUGH [THE] DOOR)").unwrap().matches(&Input::parse("GO THROUGH THE DOOR")));
|
||||
assert!(Pattern::parse("(RUN FORWARD|(GO|WALK) THROUGH [THE] DOOR)").unwrap().matches(&Input::parse("WALK THROUGH DOOR")));
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
use super::sanitise_input;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Clone, Eq)]
|
||||
pub struct Word {
|
||||
pub(crate) word: String,
|
||||
}
|
||||
impl Word {
|
||||
pub fn parse_many(input: &str) -> Vec<Self> {
|
||||
sanitise_input(input)
|
||||
.split(' ')
|
||||
.filter(|word| *word != "")
|
||||
.map(|word| Word {
|
||||
word: word.to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.word
|
||||
}
|
||||
}
|
||||
impl Display for Word {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "{}", self.word)
|
||||
}
|
||||
}
|
||||
impl PartialEq for Word {
|
||||
fn eq(&self, other: &Word) -> bool {
|
||||
self.word == other.word
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
use crate::{Attributes, Condition, Mutation};
|
||||
use markdown_parser as md;
|
||||
|
||||
/// A clause contains prose, which is text to print to the screen, and conditions
|
||||
/// that need to be fulfilled before the text is allowed to be shown
|
||||
pub struct Clause {
|
||||
pub prose: String,
|
||||
pub conditions: Vec<Condition>,
|
||||
pub mutations: Vec<Mutation>,
|
||||
}
|
||||
|
||||
impl Clause {
|
||||
/// Parse the content of a markdown quote block into a clause
|
||||
pub fn parse(lines: &[md::Line]) -> Self {
|
||||
let mut prose = String::new();
|
||||
let mut conditions = Vec::new();
|
||||
let mut mutations = Vec::new();
|
||||
for line in lines {
|
||||
for text in line {
|
||||
match text {
|
||||
md::Text::Code(content) => {
|
||||
let mut chars = content.chars();
|
||||
let first_char = match chars.next() {
|
||||
Some(v) => v,
|
||||
None => panic!("Empty code block"),
|
||||
};
|
||||
let content: String = chars.collect();
|
||||
match first_char {
|
||||
'?' => conditions.push(Condition::parse(&content)),
|
||||
'!' => mutations.push(Mutation::parse(&content)),
|
||||
other => panic!("Unexpected starting char '{}'", other),
|
||||
}
|
||||
}
|
||||
md::Text::Normal(string) => {
|
||||
prose.push_str(string.trim());
|
||||
prose.push(' ');
|
||||
}
|
||||
other => panic!("Unsupported text type in clause: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
prose,
|
||||
conditions,
|
||||
mutations,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_conditions(&self, attributes: &mut Attributes) -> bool {
|
||||
for condition in &self.conditions {
|
||||
if !condition.evaluate(attributes) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn apply_mutations(&self, attributes: &mut Attributes) {
|
||||
for mutation in &self.mutations {
|
||||
mutation.apply(attributes);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
use crate::Attributes;
|
||||
|
||||
pub struct Condition {
|
||||
attribute: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
pub fn parse(content: &str) -> Self {
|
||||
if let Some((key, value)) = content.split_once('=') {
|
||||
Self {
|
||||
attribute: key.trim().to_string(),
|
||||
value: value.trim().to_string(),
|
||||
}
|
||||
} else {
|
||||
panic!("Can't parse condition: {}", content);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, attributes: &mut Attributes) -> bool {
|
||||
attributes.get(&self.attribute) == &self.value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
use crate::*;
|
||||
use action_parser::Input;
|
||||
use log::{debug, error};
|
||||
use vagabond as vg;
|
||||
use wetstring_http::*;
|
||||
|
||||
pub struct Game {
|
||||
locations: Vec<Location>,
|
||||
saves_directory: vg::Entry,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(game_directory: vg::Entry, saves_directory: vg::Entry) -> Self {
|
||||
let mut locations = Vec::new();
|
||||
for entry in vg::traverse_directory(game_directory).unwrap() {
|
||||
if entry.extension == "location" {
|
||||
locations.push(Location::parse(entry).unwrap());
|
||||
}
|
||||
}
|
||||
Game {
|
||||
locations,
|
||||
saves_directory,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_location(&self, location: &str) -> &Location {
|
||||
match self.locations.iter().find(|l| &l.identifier == location) {
|
||||
Some(v) => v,
|
||||
None => unreachable!("Player is in unknown location: {}", location),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestProcessor for Game {
|
||||
type Req = HttpRequest;
|
||||
type Res = HttpResponse;
|
||||
|
||||
fn process_request(&self, request: &Self::Req) -> HttpResponse {
|
||||
let username = "alex";
|
||||
let mut save_file = match SaveFile::load(&self.saves_directory, username) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
error!("Save file not found for user '{}'", username);
|
||||
return HttpResponse::client_error();
|
||||
}
|
||||
};
|
||||
|
||||
// Set dynamic attributes
|
||||
save_file.attributes.set("$time_of_day", "day");
|
||||
|
||||
let input = match request.target.query_parameters.get("action") {
|
||||
Some(v) => Input::parse(&v.replace('+', " ")),
|
||||
None => Input::parse(""),
|
||||
};
|
||||
let location_name = save_file.attributes.get_with_default("location", "start");
|
||||
let mut location = self.get_location(location_name);
|
||||
|
||||
let mut action_prose = Vec::new();
|
||||
|
||||
// Apply actions and mutations
|
||||
if let Some(action) = location.actions.iter().find(|a| a.matches(&input)) {
|
||||
debug!("Matched action: {}", action.name);
|
||||
for clause in &action.clauses {
|
||||
if clause.check_conditions(&mut save_file.attributes) {
|
||||
clause.apply_mutations(&mut save_file.attributes);
|
||||
action_prose.push(clause.prose.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("No match: {}", input);
|
||||
}
|
||||
|
||||
let mut body_text = String::new();
|
||||
body_text.push_str(HTML_HEAD);
|
||||
|
||||
if !action_prose.is_empty() {
|
||||
body_text.push_str(r#"<div style="height: 30px"></div>"#);
|
||||
for line in &action_prose {
|
||||
body_text.push_str(&format!("<p>{}</p>", line));
|
||||
}
|
||||
body_text.push_str(r#"<div style="height: 5px"></div>"#);
|
||||
|
||||
let location_name = save_file.attributes.get("location");
|
||||
location = self.get_location(location_name);
|
||||
};
|
||||
|
||||
// Print description for current location
|
||||
body_text.push_str(&format!("<h1>{}</h1>", location.name));
|
||||
for clause in &location.description {
|
||||
if clause.check_conditions(&mut save_file.attributes) {
|
||||
body_text.push_str(&format!("<p>{}</p>", clause.prose));
|
||||
}
|
||||
}
|
||||
|
||||
body_text.push_str(HTML_TAIL);
|
||||
|
||||
save_file.save();
|
||||
HttpResponse::new(Status::Success).with_utf8_body(&body_text)
|
||||
}
|
||||
}
|
||||
|
||||
const HTML_HEAD: &str = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@600&family=Vollkorn:wght@600&family=PT+Serif&family=Merriweather&display=swap" rel="stylesheet">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>An Ode to the Ever-blowing Wind</title>
|
||||
<style>
|
||||
* {
|
||||
color: #022;
|
||||
}
|
||||
body {
|
||||
background: #ed2;
|
||||
}
|
||||
h1.title {
|
||||
color: #552;
|
||||
font-size: 36pt;
|
||||
font-family: 'PT Serif', serif;
|
||||
text-orientation: sideways;
|
||||
writing-mode: vertical-rl;
|
||||
white-space: nowrap;
|
||||
transform: rotate(180deg) translateX(100%);
|
||||
transform-origin: center;
|
||||
position: absolute;
|
||||
border-left: 2px solid #444;
|
||||
padding-left: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
font-family: Merriweather, serif;
|
||||
font-size: 11pt;
|
||||
}
|
||||
div.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding-left: 60px;
|
||||
height: 90%;
|
||||
}
|
||||
div.content {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
div.content > h1 {
|
||||
font-family: Vollkorn, serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
input {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-weight: 600;
|
||||
font-size: 12pt;
|
||||
background-color: #ed6;
|
||||
border: none;
|
||||
}
|
||||
form.action {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border: 2px solid #022;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input#action {
|
||||
padding: 2px 6px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
input#action:focus {
|
||||
outline: none;
|
||||
}
|
||||
input#submit {
|
||||
padding: 2px 6px;
|
||||
margin: 0;
|
||||
border-left: 2px solid #022;
|
||||
}
|
||||
input#submit:focus {
|
||||
outline: none;
|
||||
background-color: #eda;
|
||||
}
|
||||
|
||||
/* Hide title on small screens */
|
||||
@media (max-width: 700px) {
|
||||
h1.title {
|
||||
display: none;
|
||||
}
|
||||
div.container {
|
||||
padding-left: 0px;
|
||||
}
|
||||
div.content {
|
||||
padding-left: 10px;
|
||||
}
|
||||
div.content > h1 {
|
||||
font-size: 20pt;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="title">An Ode to the Ever-blowing Wind</h1>
|
||||
|
||||
<div class="content">
|
||||
"#;
|
||||
|
||||
const HTML_TAIL: &str = r#"
|
||||
<div style="height: 50px"></div>
|
||||
<form class="action" action="">
|
||||
<input id="action" name="action" type="text">
|
||||
<input id="submit" type="submit" value="->">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 10%"></div>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
|
@ -0,0 +1,17 @@
|
|||
mod action;
|
||||
mod action_parser;
|
||||
mod clause;
|
||||
mod condition;
|
||||
mod game;
|
||||
mod location;
|
||||
mod mutation;
|
||||
mod save_file;
|
||||
|
||||
pub use action::Action;
|
||||
pub use action_parser::{Input, Pattern};
|
||||
pub use clause::Clause;
|
||||
pub use condition::Condition;
|
||||
pub use game::Game;
|
||||
pub use location::Location;
|
||||
pub use mutation::Mutation;
|
||||
pub use save_file::{Attributes, SaveFile};
|
|
@ -0,0 +1,95 @@
|
|||
use crate::{Action, Clause, Pattern};
|
||||
use log::info;
|
||||
use markdown_parser as md;
|
||||
use vagabond as vg;
|
||||
|
||||
pub struct Location {
|
||||
pub name: String,
|
||||
pub identifier: String,
|
||||
pub description: Vec<Clause>,
|
||||
pub actions: Vec<Action>,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
pub fn parse(entry: vg::Entry) -> Result<Location, LocationParseError> {
|
||||
info!("Parsing {}", entry.name);
|
||||
|
||||
let markdown = entry.read_as_utf8_string()?;
|
||||
let document =
|
||||
md::parse_heirarchical(&markdown).or(Err(LocationParseError::InvalidHeirarchy))?;
|
||||
if document.sections.len() != 1 {
|
||||
return Err(LocationParseError::InvalidHeirarchy);
|
||||
}
|
||||
let location = &document.sections[0];
|
||||
|
||||
let name = md::line_to_string(&location.title);
|
||||
let identifier = entry.split_name().0;
|
||||
|
||||
let description = {
|
||||
let mut clauses = Vec::new();
|
||||
for block in &location.content {
|
||||
if let md::Block::Quote(lines) = block {
|
||||
clauses.push(Clause::parse(&lines));
|
||||
}
|
||||
}
|
||||
clauses
|
||||
};
|
||||
let actions = {
|
||||
let mut actions = Vec::new();
|
||||
for section in &location.sections {
|
||||
let name = md::line_to_string(§ion.title);
|
||||
let mut clauses = Vec::new();
|
||||
let mut patterns = Vec::new();
|
||||
for block in §ion.content {
|
||||
match block {
|
||||
md::Block::Quote(lines) => clauses.push(Clause::parse(&lines)),
|
||||
md::Block::Paragraph(text) => {
|
||||
if let Some(md::Text::Code(content)) = text.get(0) {
|
||||
let mut chars = content.chars();
|
||||
let first_char = match chars.next() {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
let content: String = chars.collect();
|
||||
if first_char == '>' {
|
||||
patterns.push(Pattern::parse(&content).unwrap());
|
||||
} else {
|
||||
panic!("Pattern doesn't start with a '>': {}", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
actions.push(Action {
|
||||
name,
|
||||
clauses,
|
||||
patterns,
|
||||
})
|
||||
}
|
||||
actions
|
||||
};
|
||||
|
||||
Ok(Location {
|
||||
name,
|
||||
identifier,
|
||||
description,
|
||||
actions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(std::fmt::Debug)]
|
||||
pub enum LocationParseError {
|
||||
InvalidPermissions,
|
||||
NotFound,
|
||||
InvalidHeirarchy,
|
||||
}
|
||||
impl From<vg::EntryReadError> for LocationParseError {
|
||||
fn from(error: vg::EntryReadError) -> Self {
|
||||
match error {
|
||||
vg::EntryReadError::NotFound => Self::NotFound,
|
||||
vg::EntryReadError::PermissionDenied => Self::InvalidPermissions,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use midnight::Game;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::path::PathBuf;
|
||||
use vagabond as vg;
|
||||
use wetstring_http::*;
|
||||
|
||||
switches::generate! {
|
||||
Args {
|
||||
game_directory: PathBuf ("-g", "--game-directory"),
|
||||
saves_directory: PathBuf ("-s", "--saves-directory"),
|
||||
port: u16 ("-p", "--port"),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args = Args::parse_and_raise();
|
||||
let game_directory = vg::Entry::from_path(args.game_directory).unwrap();
|
||||
let saves_directory = vg::Entry::from_path(args.saves_directory).unwrap();
|
||||
let game = Game::new(game_directory, saves_directory);
|
||||
|
||||
let mut server = HttpServer::new(
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), args.port)),
|
||||
game,
|
||||
);
|
||||
server.run();
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use crate::Attributes;
|
||||
use log::debug;
|
||||
|
||||
pub struct Mutation {
|
||||
attribute: String,
|
||||
new_value: String,
|
||||
}
|
||||
|
||||
impl Mutation {
|
||||
pub fn parse(content: &str) -> Self {
|
||||
if let Some((key, value)) = content.split_once(':') {
|
||||
Self {
|
||||
attribute: key.trim().to_string(),
|
||||
new_value: value.trim().to_string(),
|
||||
}
|
||||
} else {
|
||||
panic!("Can't parse pattern: {}", content);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, attributes: &mut Attributes) {
|
||||
debug!(
|
||||
"Applying mutation: '{}', '{}'",
|
||||
self.attribute, self.new_value
|
||||
);
|
||||
attributes.set(&self.attribute, &self.new_value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use vagabond as vg;
|
||||
|
||||
pub struct SaveFile {
|
||||
path: PathBuf,
|
||||
pub attributes: Attributes,
|
||||
}
|
||||
|
||||
impl SaveFile {
|
||||
pub fn load<P: AsRef<Path>>(saves_directory: P, username: &str) -> Result<Self, ()> {
|
||||
let filename = format!("{}.save", username);
|
||||
let mut path = PathBuf::new();
|
||||
path.push(saves_directory);
|
||||
path.push(filename);
|
||||
let entry = match vg::Entry::from_path(&path) {
|
||||
Ok(v) => v,
|
||||
Err(vg::EntryReadError::NotFound) => return Err(()),
|
||||
Err(err) => panic!("{:?}", err),
|
||||
};
|
||||
let content = entry.read_as_utf8_string().unwrap();
|
||||
let attributes = Self::parse(&content)?;
|
||||
Ok(Self { path, attributes })
|
||||
}
|
||||
|
||||
pub fn parse(content: &str) -> Result<Attributes, ()> {
|
||||
let mut attributes = Attributes::new();
|
||||
for line in content.lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
attributes.set(key, value);
|
||||
}
|
||||
}
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
let mut content = String::new();
|
||||
for (key, value) in &self.attributes.map {
|
||||
content.push_str(key);
|
||||
content.push('=');
|
||||
content.push_str(value);
|
||||
content.push('\n');
|
||||
}
|
||||
std::fs::write(&self.path, &content).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Attributes {
|
||||
pub map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self, name: &str) -> &String {
|
||||
self.get_with_default(name, "")
|
||||
}
|
||||
pub fn get_with_default(&mut self, name: &str, default: &str) -> &String {
|
||||
self.map
|
||||
.entry(name.to_string())
|
||||
.or_insert(default.to_string())
|
||||
}
|
||||
|
||||
pub fn set(&mut self, name: &str, value: &str) {
|
||||
self.map.insert(name.to_string(), value.to_string());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue