Add templates for messages

Implement a template syntax with a nom parser, and a formatter to render
templates to strings.
This commit is contained in:
Griffin Smith 2019-07-19 11:54:31 -04:00
parent bc93999cf3
commit e2d13bd76b
9 changed files with 549 additions and 102 deletions

View file

@ -5,7 +5,7 @@ use crate::entities::{raw, EntityID};
use crate::types::Position;
use std::io::{self, Write};
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Creature {
pub id: Option<EntityID>,
pub typ: &'static CreatureType<'static>,

View file

@ -8,6 +8,7 @@ use crate::types::{
pos, BoundingBox, Collision, Dimensions, Position, Positioned,
PositionedMut, Ticks,
};
use crate::util::template::TemplateParams;
use rand::rngs::SmallRng;
use rand::SeedableRng;
use std::io::{self, StdinLock, StdoutLock, Write};
@ -145,16 +146,24 @@ impl<'a> Game<'a> {
fn tick(&mut self, ticks: Ticks) {}
/// Get a message from the global map based on the rng in this game
fn message(&mut self, name: &str) -> &'static str {
message(name, &mut self.rng)
fn message<'params>(
&mut self,
name: &'static str,
params: &TemplateParams<'params>,
) -> String {
message(name, &mut self.rng, params)
}
/// Say a message to the user
fn say(&mut self, message_name: &str) -> io::Result<()> {
let message = self.message(message_name);
fn say<'params>(
&mut self,
message_name: &'static str,
params: &TemplateParams<'params>,
) -> io::Result<()> {
let message = self.message(message_name, params);
self.messages.push(message.to_string());
self.message_idx = self.messages.len() - 1;
self.viewport.write_message(message)
self.viewport.write_message(&message)
}
fn previous_message(&mut self) -> io::Result<()> {
@ -166,20 +175,45 @@ impl<'a> Game<'a> {
self.viewport.write_message(message)
}
fn attack(&mut self, creature_id: EntityID) -> io::Result<()> {
info!("Attacking creature {:?}", creature_id);
self.say("combat.attack")?;
let damage = self.character().damage();
let creature = self
.entities
fn creature(&self, creature_id: EntityID) -> Option<&Creature> {
self.entities
.get(creature_id)
.and_then(|e| e.downcast_ref::<Creature>())
}
fn expect_creature(&self, creature_id: EntityID) -> &Creature {
self.creature(creature_id).expect(
format!("Creature ID went away: {:?}", creature_id).as_str(),
)
}
fn mut_creature(&mut self, creature_id: EntityID) -> Option<&mut Creature> {
self.entities
.get_mut(creature_id)
.and_then(|e| e.downcast_mut::<Creature>())
.expect(
format!("Creature ID went away: {:?}", creature_id).as_str(),
);
}
fn expect_mut_creature(&mut self, creature_id: EntityID) -> &mut Creature {
self.mut_creature(creature_id).expect(
format!("Creature ID went away: {:?}", creature_id).as_str(),
)
}
fn attack(&mut self, creature_id: EntityID) -> io::Result<()> {
info!("Attacking creature {:?}", creature_id);
let damage = self.character().damage();
let creature_name = self.expect_creature(creature_id).typ.name;
let tps = template_params!({
"creature" => {
"name" => creature_name,
},
});
self.say("combat.attack", &tps)?;
let creature = self.expect_mut_creature(creature_id);
creature.damage(damage);
if creature.dead() {
self.say("combat.killed")?;
self.say("combat.killed", &tps)?;
info!("Killed creature {:?}", creature_id);
self.remove_entity(creature_id)?;
}
@ -202,7 +236,7 @@ impl<'a> Game<'a> {
info!("Running game");
self.viewport.init()?;
self.draw_entities()?;
self.say("global.welcome")?;
self.say("global.welcome", &template_params!())?;
self.flush()?;
loop {
let mut old_position = None;

View file

@ -21,6 +21,8 @@ extern crate downcast_rs;
extern crate backtrace;
#[macro_use]
extern crate include_dir;
#[macro_use]
extern crate nom;
#[macro_use]
mod util;

View file

@ -1,32 +1,33 @@
use crate::util::template::Template;
use crate::util::template::TemplateParams;
use rand::seq::SliceRandom;
use rand::Rng;
use serde::de::MapAccess;
use serde::de::SeqAccess;
use serde::de::Visitor;
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
enum Message<'a> {
Single(&'a str),
Choice(Vec<&'a str>),
#[serde(borrow)]
Single(Template<'a>),
Choice(Vec<Template<'a>>),
}
impl<'a> Message<'a> {
fn resolve<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&'a str> {
fn resolve<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&Template<'a>> {
use Message::*;
match self {
Single(msg) => Some(*msg),
Choice(msgs) => msgs.choose(rng).map(|msg| *msg),
Single(msg) => Some(msg),
Choice(msgs) => msgs.choose(rng),
}
}
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
enum NestedMap<'a> {
#[serde(borrow)]
Direct(Message<'a>),
#[serde(borrow)]
Nested(HashMap<&'a str, NestedMap<'a>>),
}
@ -46,63 +47,6 @@ impl<'a> NestedMap<'a> {
}
}
struct NestedMapVisitor<'a> {
marker: PhantomData<fn() -> NestedMap<'a>>,
}
impl<'a> NestedMapVisitor<'a> {
fn new() -> Self {
NestedMapVisitor {
marker: PhantomData,
}
}
}
impl<'de> Visitor<'de> for NestedMapVisitor<'de> {
type Value = NestedMap<'de>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"A message, a list of messages, or a nested map of messages",
)
}
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> {
Ok(NestedMap::Direct(Message::Single(v)))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut choices = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(choice) = seq.next_element()? {
choices.push(choice);
}
Ok(NestedMap::Direct(Message::Choice(choices)))
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut nested = HashMap::with_capacity(map.size_hint().unwrap_or(0));
while let Some((k, v)) = map.next_entry()? {
nested.insert(k, v);
}
Ok(NestedMap::Nested(nested))
}
}
impl<'de> serde::Deserialize<'de> for NestedMap<'de> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(NestedMapVisitor::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -122,13 +66,18 @@ choice = ["Say this", "Or this"]
result,
Ok(NestedMap::Nested(hashmap! {
"global" => NestedMap::Nested(hashmap!{
"hello" => NestedMap::Direct(Message::Single("Hello World!")),
"hello" => NestedMap::Direct(Message::Single(Template::parse("Hello World!").unwrap())),
}),
"foo" => NestedMap::Nested(hashmap!{
"bar" => NestedMap::Nested(hashmap!{
"single" => NestedMap::Direct(Message::Single("Single")),
"single" => NestedMap::Direct(Message::Single(
Template::parse("Single").unwrap()
)),
"choice" => NestedMap::Direct(Message::Choice(
vec!["Say this", "Or this"]
vec![
Template::parse("Say this").unwrap(),
Template::parse("Or this").unwrap()
]
))
})
})
@ -152,31 +101,43 @@ choice = ["Say this", "Or this"]
assert_eq!(
map.lookup("global.hello"),
Some(&Message::Single("Hello World!"))
Some(&Message::Single(Template::parse("Hello World!").unwrap()))
);
assert_eq!(
map.lookup("foo.bar.single"),
Some(&Message::Single("Single"))
Some(&Message::Single(Template::parse("Single").unwrap()))
);
assert_eq!(
map.lookup("foo.bar.choice"),
Some(&Message::Choice(vec!["Say this", "Or this"]))
Some(&Message::Choice(vec![
Template::parse("Say this").unwrap(),
Template::parse("Or this").unwrap()
]))
);
}
}
// static MESSAGES_RAW: &'static str = include_str!("messages.toml");
static_cfg! {
static ref MESSAGES: NestedMap<'static> = toml_file("messages.toml");
}
/// Look up a game message based on the given (dot-separated) name, with the
/// given random generator used to select from choice-based messages
pub fn message<R: Rng + ?Sized>(name: &str, rng: &mut R) -> &'static str {
MESSAGES
.lookup(name)
.and_then(|msg| msg.resolve(rng))
.unwrap_or_else(|| {
/// Look up and format a game message based on the given (dot-separated) name,
/// with the given random generator used to select from choice-based messages
pub fn message<'a, R: Rng + ?Sized>(
name: &'static str,
rng: &mut R,
params: &TemplateParams<'a>,
) -> String {
match MESSAGES.lookup(name).and_then(|msg| msg.resolve(rng)) {
Some(msg) => msg.format(params).unwrap_or_else(|e| {
error!("Error formatting template: {}", e);
"Template Error".to_string()
}),
None => {
error!("Message not found: {}", name);
"Message not found"
})
"Template Not Found".to_string()
}
}
}

View file

@ -2,5 +2,10 @@
welcome = "Welcome to Xanthous! It's dangerous out there, why not stay inside?"
[combat]
attack = "You attack the {{creature_name}}."
killed = "You killed the {{creature_name}}."
attack = "You attack the {{creature.name}}."
killed = [
"You've killed the {{creature.name}}.",
"The {{creature.name}} dies.",
"The {{creature.name}} kicks it.",
"The {{creature.name}} beefs it."
]

View file

@ -1,2 +1,4 @@
#[macro_use]
pub mod static_cfg;
#[macro_use]
pub mod template;

362
src/util/template.rs Normal file
View file

@ -0,0 +1,362 @@
use nom::combinator::rest;
use nom::error::ErrorKind;
use nom::{Err, IResult};
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::marker::PhantomData;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Path<'a> {
head: &'a str,
tail: Vec<&'a str>,
}
impl<'a> Path<'a> {
fn new(head: &'a str, tail: Vec<&'a str>) -> Self {
Path { head, tail }
}
}
impl<'a> Display for Path<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.head)?;
for part in &self.tail {
write!(f, ".{}", part)?;
}
Ok(())
}
}
// named!(path_ident, map_res!(is_not!(".}"), std::str::from_utf8));
fn path_ident<'a>(input: &'a str) -> IResult<&'a str, &'a str> {
take_till!(input, |c| c == '.' || c == '}')
}
fn path<'a>(input: &'a str) -> IResult<&'a str, Path<'a>> {
map!(
input,
tuple!(
path_ident,
many0!(complete!(preceded!(char!('.'), path_ident)))
),
|(h, t)| Path::new(h, t)
)
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TemplateToken<'a> {
Literal(&'a str),
Substitution(Path<'a>),
}
fn token_substitution<'a>(
input: &'a str,
) -> IResult<&'a str, TemplateToken<'a>> {
map!(
input,
delimited!(tag!("{{"), path, tag!("}}")),
TemplateToken::Substitution
)
}
fn template_token<'a>(input: &'a str) -> IResult<&'a str, TemplateToken<'a>> {
alt!(
input,
token_substitution
| map!(
alt!(complete!(take_until!("{{")) | complete!(rest)),
TemplateToken::Literal
)
)
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Template<'a> {
tokens: Vec<TemplateToken<'a>>,
}
impl<'a> Template<'a> {
pub fn new(tokens: Vec<TemplateToken<'a>>) -> Self {
Template { tokens }
}
}
pub struct TemplateVisitor<'a> {
marker: PhantomData<fn() -> Template<'a>>,
}
impl<'a> TemplateVisitor<'a> {
pub fn new() -> Self {
TemplateVisitor {
marker: PhantomData,
}
}
}
impl<'a> serde::de::Visitor<'a> for TemplateVisitor<'a> {
type Value = Template<'a>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid template string")
}
fn visit_borrowed_str<E: serde::de::Error>(
self,
v: &'a str,
) -> Result<Self::Value, E> {
Template::parse(v).map_err(|_| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(v),
&"a valid template string",
)
})
}
}
impl<'a> serde::Deserialize<'a> for Template<'a> {
fn deserialize<D: serde::Deserializer<'a>>(
deserializer: D,
) -> Result<Self, D::Error> {
deserializer.deserialize_str(TemplateVisitor::new())
}
}
impl<'a> Template<'a> {
pub fn parse(
input: &'a str,
) -> Result<Template<'a>, Err<(&'a str, ErrorKind)>> {
let (remaining, res) = template(input)?;
if remaining.len() > 0 {
unreachable!();
}
Ok(res)
}
pub fn format(
&self,
params: &TemplateParams<'a>,
) -> Result<String, TemplateError<'a>> {
use TemplateToken::*;
let mut res = String::new();
for token in &self.tokens {
match token {
Literal(s) => res.push_str(s),
Substitution(p) => match params.get(p.clone()) {
Some(s) => res.push_str(s),
None => return Err(TemplateError::MissingParam(p.clone())),
},
}
}
Ok(res)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateError<'a> {
MissingParam(Path<'a>),
}
impl<'a> Display for TemplateError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use TemplateError::*;
match self {
MissingParam(path) => {
write!(f, "Missing template parameter: {}", path)
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateParams<'a> {
Direct(&'a str),
Nested(HashMap<&'a str, TemplateParams<'a>>),
}
impl<'a> TemplateParams<'a> {
fn get(&self, path: Path<'a>) -> Option<&'a str> {
use TemplateParams::*;
match self {
Direct(_) => None,
Nested(m) => m.get(path.head).and_then(|next| {
if path.tail.len() == 0 {
match next {
Direct(s) => Some(*s),
_ => None,
}
} else {
next.get(Path {
head: path.tail[0],
tail: path.tail[1..].to_vec(),
})
}
}),
}
}
}
#[macro_export]
macro_rules! template_params {
(@count $head: expr => $hv: tt, $($rest:tt)+) => { 1 + template_params!(@count $($rest)+) };
(@count $one:expr => $($ov: tt)*) => { 1 };
(@inner $ret: ident, ($key: expr => {$($v:tt)*}, $($r:tt)*)) => {
$ret.insert($key, template_params!({ $($v)* }));
template_params!(@inner $ret, ($($r)*));
};
(@inner $ret: ident, ($key: expr => $value: expr, $($r:tt)*)) => {
$ret.insert($key, template_params!($value));
template_params!(@inner $ret, ($($r)*));
};
(@inner $ret: ident, ()) => {};
({ $($body: tt)* }) => {{
let _cap = template_params!(@count $($body)*);
let mut _m = ::std::collections::HashMap::with_capacity(_cap);
template_params!(@inner _m, ($($body)*));
TemplateParams::Nested(_m)
}};
($direct:expr) => { TemplateParams::Direct($direct) };
() => { TemplateParams::Nested(::std::collections::HashMap::new()) };
}
fn template<'a>(input: &'a str) -> IResult<&'a str, Template<'a>> {
complete!(
input,
map!(many1!(complete!(template_token)), Template::new)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_path_ident() {
assert_eq!(path_ident("foo}}"), Ok(("}}", "foo")));
assert_eq!(path_ident("foo.bar}}"), Ok((".bar}}", "foo")));
}
#[test]
fn test_parse_path() {
assert_eq!(path("foo}}"), Ok(("}}", Path::new("foo", vec![]))));
assert_eq!(
path("foo.bar}}"),
Ok(("}}", Path::new("foo", vec!["bar"])))
);
assert_eq!(
path("foo.bar.baz}}"),
Ok(("}}", Path::new("foo", vec!["bar", "baz"])))
);
}
#[test]
fn test_parse_template_token() {
assert_eq!(
template_token("foo bar"),
Ok(("", TemplateToken::Literal("foo bar")))
);
assert_eq!(
template_token("foo bar {{baz}}"),
Ok(("{{baz}}", TemplateToken::Literal("foo bar ")))
);
assert_eq!(
template_token("{{baz}}"),
Ok((
"",
TemplateToken::Substitution(Path::new("baz", Vec::new()))
))
);
assert_eq!(
template_token("{{baz}} foo bar"),
Ok((
" foo bar",
TemplateToken::Substitution(Path::new("baz", Vec::new()))
))
);
}
#[test]
fn test_parse_template() {
assert_eq!(
template("foo bar"),
Ok((
"",
Template {
tokens: vec![TemplateToken::Literal("foo bar")]
}
))
);
assert_eq!(
template("foo bar {{baz}} qux"),
Ok((
"",
Template {
tokens: vec![
TemplateToken::Literal("foo bar "),
TemplateToken::Substitution(Path::new(
"baz",
Vec::new()
)),
TemplateToken::Literal(" qux"),
]
}
))
);
}
#[test]
fn test_template_params_literal() {
// trace_macros!(true);
let expected = template_params!({
"direct" => "hi",
"other" => "here",
"nested" => {
"one" => "1",
"two" => "2",
"double" => {
"three" => "3",
},
},
});
// trace_macros!(false);
assert_eq!(
TemplateParams::Nested(hashmap! {
"direct" => TemplateParams::Direct("hi"),
"other" => TemplateParams::Direct("here"),
"nested" => TemplateParams::Nested(hashmap!{
"one" => TemplateParams::Direct("1"),
"two" => TemplateParams::Direct("2"),
"double" => TemplateParams::Nested(hashmap!{
"three" => TemplateParams::Direct("3"),
})
})
}),
expected,
)
}
#[test]
fn test_format_template() {
assert_eq!(
"foo bar baz qux",
Template::parse("foo {{x}} {{y.z}} {{y.w.z}}")
.unwrap()
.format(&template_params!({
"x" => "bar",
"y" => {
"z" => "baz",
"w" => {
"z" => "qux",
},
},
}))
.unwrap()
)
}
}