Initial commit

This commit is contained in:
Ben Bridle 2022-08-26 13:08:18 +12:00
commit 10b7a15c23
19 changed files with 1033 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

15
Cargo.toml Normal file
View File

@ -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"

18
src/action.rs Normal file
View File

@ -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;
}
}

View File

@ -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)
}
}

16
src/action_parser/mod.rs Normal file
View File

@ -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', " ")
}

View File

@ -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;

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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")));
}

32
src/action_parser/word.rs Normal file
View File

@ -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
}
}

63
src/clause.rs Normal file
View File

@ -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);
}
}
}

23
src/condition.rs Normal file
View File

@ -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
}
}

221
src/game.rs Normal file
View File

@ -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>
"#;

17
src/lib.rs Normal file
View File

@ -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};

95
src/location.rs Normal file
View File

@ -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(&section.title);
let mut clauses = Vec::new();
let mut patterns = Vec::new();
for block in &section.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,
}
}
}

28
src/main.rs Normal file
View File

@ -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();
}

28
src/mutation.rs Normal file
View File

@ -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);
}
}

71
src/save_file.rs Normal file
View File

@ -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());
}
}