#![warn(missing_docs)]
use config::{Config, State};
use parser::interpreter::EventMessage;
use serde::{Deserialize, Serialize};
use std::{
error::Error,
fmt::{self, Display, Formatter},
};
extern crate console_error_panic_hook;
use util::parse_room_text;
pub mod config;
pub mod parser;
pub mod util;
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
pub type NRResult<T> = Result<T, Box<dyn Error>>;
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "messageType", content = "data")]
pub enum ParsingResult {
Help(String),
Look(String),
NewItem(String),
DropItem(String),
Inventory(String),
SubjectNoEvent(String),
EventSuccess(EventMessage),
Quit,
}
impl Display for ParsingResult {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
ParsingResult::Help(msg) => write!(f, "{}", msg),
ParsingResult::Look(msg) => write!(f, "{}", msg),
ParsingResult::NewItem(msg) => write!(f, "{}", msg),
ParsingResult::DropItem(msg) => write!(f, "{}", msg),
ParsingResult::Inventory(msg) => write!(f, "{}", msg),
ParsingResult::SubjectNoEvent(msg) => write!(f, "{}", msg),
ParsingResult::EventSuccess(event_msg) => {
let EventMessage {
message,
message_parts,
templated_words: _,
} = event_msg;
write!(f, "{}", message)?;
for part in message_parts.values() {
write!(f, "{}", part)?;
}
Ok(())
}
ParsingResult::Quit => write!(f, "Quitting game"),
}
}
}
#[wasm_bindgen]
#[derive(Debug, PartialEq)]
pub struct NightRunner {
state: State,
previous_states: Vec<State>,
future_states: Vec<State>,
}
#[derive(Debug, PartialEq, Eq)]
enum ConfigSource {
Default,
Path(String),
Json(String),
}
#[derive(Debug, PartialEq, Eq)]
pub struct NightRunnerBuilder {
source: ConfigSource,
}
impl NightRunnerBuilder {
pub fn new() -> NightRunnerBuilder {
NightRunnerBuilder {
source: ConfigSource::Default,
}
}
pub fn with_path_for_config(mut self, path: &str) -> NightRunnerBuilder {
self.source = ConfigSource::Path(path.to_string());
self
}
pub fn with_json_data(mut self, data: &str) -> NightRunnerBuilder {
self.source = ConfigSource::Json(data.to_string());
self
}
pub fn build(self) -> NRResult<NightRunner> {
let config = match self.source {
ConfigSource::Default => Config::default(),
ConfigSource::Path(path) => Config::from_path(&path)?,
ConfigSource::Json(data) => Config::from_json(&data)?,
};
let state = State::init(config);
Ok(NightRunner {
state,
previous_states: vec![],
future_states: vec![],
})
}
}
impl Default for NightRunnerBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl NightRunner {
pub fn parse_input(&mut self, input: &str) -> NRResult<ParsingResult> {
let (new_state, parsing_result) = parser::parse(&self.state, input)?;
self.previous_states.push(self.state.clone());
self.state = new_state;
Ok(parsing_result)
}
pub fn json_parse_input(&mut self, input: &str) -> String {
let result = parser::parse(&self.state, input);
match result {
Ok((new_state, ok)) => {
self.previous_states.push(self.state.clone());
self.state = new_state;
serde_json::to_string(&ok)
.unwrap_or_else(|_| r#"{"error":"failed to serialize result"}"#.to_string())
}
Err(err) => {
let message = serde_json::to_string(&err.to_string())
.unwrap_or_else(|_| "\"an unknown error occurred\"".to_string());
format!("{{\"error\":{}}}", message)
}
}
}
pub fn rewind_state(&mut self) -> NRResult<ParsingResult> {
if let Some(state) = self.previous_states.pop() {
self.future_states.push(self.state.clone());
self.state = state;
Ok(ParsingResult::Look("Rewound to previous state".to_string()))
} else {
Err("No previous state to rewind to".into())
}
}
pub fn fast_forward_state(&mut self) -> NRResult<ParsingResult> {
if let Some(state) = self.future_states.pop() {
self.previous_states.push(self.state.clone());
self.state = state;
Ok(ParsingResult::Look(
"Fast forwarded to next state".to_string(),
))
} else {
Err("No future state to fast forward to".into())
}
}
pub fn game_intro(&self) -> String {
self.state.config.intro.clone()
}
pub fn first_room_text(&self) -> NRResult<EventMessage> {
let narrative_id = self
.state
.rooms
.first()
.ok_or(parser::errors::NoRoom)?
.narrative;
let narrative_text = self
.state
.config
.narratives
.iter()
.find(|n| n.id == narrative_id)
.ok_or(parser::errors::InvalidNarrative)?
.text
.clone();
let event_message = parse_room_text(&self.state, narrative_text, "".to_string(), None)?;
Ok(event_message)
}
}
#[cfg(any(target_arch = "wasm32", doc))]
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "messageType", content = "data")]
#[serde(rename_all = "snake_case")]
pub enum JsMessage {
Help(String),
Look(String),
NewItem(String),
DropItem(String),
Inventory(String),
SubjectNoEvent(String),
EventSuccess(EventMessage),
NoOp,
}
#[cfg(any(target_arch = "wasm32", doc))]
#[wasm_bindgen]
impl NightRunner {
#[wasm_bindgen(constructor)]
pub fn new(config: &str) -> Result<NightRunner, JsError> {
console_error_panic_hook::set_once();
let config = Config::from_json(config).map_err(|e| JsError::new(&e.to_string()))?;
let state = State::init(config);
Ok(NightRunner {
state,
previous_states: vec![],
future_states: vec![],
})
}
pub fn parse(&mut self, input: &str) -> Result<JsValue, JsError> {
let result = parser::parse(&self.state, input);
match result {
Ok((new_state, ok)) => {
self.previous_states.push(self.state.clone());
self.state = new_state;
let message = match ok {
ParsingResult::Look(msg) => JsMessage::Look(msg),
ParsingResult::Help(msg) => JsMessage::Help(msg),
ParsingResult::NewItem(msg) => JsMessage::NewItem(msg),
ParsingResult::DropItem(msg) => JsMessage::DropItem(msg),
ParsingResult::Inventory(msg) => JsMessage::Inventory(msg),
ParsingResult::SubjectNoEvent(msg) => JsMessage::SubjectNoEvent(msg),
ParsingResult::EventSuccess(event_msg) => JsMessage::EventSuccess(event_msg),
ParsingResult::Quit => JsMessage::NoOp,
};
Ok(serde_wasm_bindgen::to_value(&message)?)
}
Err(err) => Err(JsError::new(&err.to_string())),
}
}
pub fn rewind_state(&mut self) -> Result<JsValue, JsError> {
match self.previous_states.pop() {
Some(state) => {
self.future_states.push(self.state.clone());
self.state = state;
Ok(serde_wasm_bindgen::to_value(&JsMessage::Look(
"Rewound to previous state".to_string(),
))?)
}
None => Err(JsError::new("No previous state to rewind to")),
}
}
pub fn fast_forward_state(&mut self) -> Result<JsValue, JsError> {
match self.future_states.pop() {
Some(state) => {
self.previous_states.push(self.state.clone());
self.state = state;
Ok(serde_wasm_bindgen::to_value(&JsMessage::Look(
"Fast forwarded to next state".to_string(),
))?)
}
None => Err(JsError::new("No future state to fast forward to")),
}
}
#[wasm_bindgen]
pub fn game_intro(&self) -> String {
self.state.config.intro.clone()
}
pub fn first_room_text(&self) -> Result<JsValue, JsError> {
let narrative_id = self
.state
.rooms
.first()
.ok_or_else(|| JsError::new("There are no rooms in the game."))?
.narrative;
let narrative_text = self
.state
.config
.narratives
.iter()
.find(|n| n.id == narrative_id)
.ok_or_else(|| JsError::new(&parser::errors::InvalidNarrative.to_string()))?
.text
.clone();
let event_message = parse_room_text(&self.state, narrative_text, "".to_string(), None)
.map_err(|e| JsError::new(&e.to_string()))?;
Ok(serde_wasm_bindgen::to_value(&event_message)?)
}
}