Add messages, with global lookup map
Add support for messages, along with a global lookup map and random choice of messages.
This commit is contained in:
parent
78a52142d1
commit
c643ee1dfc
10 changed files with 443 additions and 52 deletions
|
|
@ -1,10 +1,12 @@
|
|||
use crate::display::utils::clone_times;
|
||||
use crate::display::utils::times;
|
||||
use crate::types::BoundingBox;
|
||||
use crate::types::Dimensions;
|
||||
use itertools::Itertools;
|
||||
use proptest::prelude::Arbitrary;
|
||||
use proptest::strategy;
|
||||
use proptest_derive::Arbitrary;
|
||||
use std::io::{self, Write};
|
||||
|
||||
// Box Drawing
|
||||
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
|
|
@ -164,6 +166,21 @@ pub fn make_box(style: BoxStyle, dims: Dimensions) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw the box described by the given BoundingBox's position and dimensions to
|
||||
/// the given output, with the given style
|
||||
pub fn draw_box<W: Write>(
|
||||
out: &mut W,
|
||||
bbox: BoundingBox,
|
||||
style: BoxStyle,
|
||||
) -> io::Result<()> {
|
||||
write!(
|
||||
out,
|
||||
"{}{}",
|
||||
bbox.position.cursor_goto(),
|
||||
make_box(style, bbox.dimensions)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
use super::BoxStyle;
|
||||
use super::Draw;
|
||||
use super::{make_box, BoxStyle};
|
||||
use crate::display::draw_box::draw_box;
|
||||
use crate::display::utils::clone_times;
|
||||
use crate::display::utils::times;
|
||||
use crate::types::{BoundingBox, Position, Positioned};
|
||||
use std::fmt::{self, Debug};
|
||||
use std::io::{self, Write};
|
||||
|
|
@ -10,15 +13,39 @@ pub struct Viewport<W> {
|
|||
/// Generally the size of the terminal, and positioned at 0, 0
|
||||
pub outer: BoundingBox,
|
||||
|
||||
/// The box describing the game part of the viewport.
|
||||
pub game: BoundingBox,
|
||||
|
||||
/// The box describing the inner part of the viewport
|
||||
///
|
||||
/// Its position is relative to `outer.inner()`, and its size should generally not
|
||||
/// be smaller than outer
|
||||
/// Its position is relative to `outer.inner()`, and its size should
|
||||
/// generally not be smaller than outer
|
||||
pub inner: BoundingBox,
|
||||
|
||||
/// The actual screen that the viewport writes to
|
||||
pub out: W,
|
||||
}
|
||||
impl<W> Viewport<W> {
|
||||
pub fn new(outer: BoundingBox, inner: BoundingBox, out: W) -> Self {
|
||||
Viewport {
|
||||
outer,
|
||||
inner,
|
||||
out,
|
||||
game: outer.move_tr_corner(Position { x: 0, y: 1 }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the (inner-relative) position of the given entity is
|
||||
/// visible within this viewport
|
||||
pub fn visible<E: Positioned>(&self, ent: &E) -> bool {
|
||||
self.on_screen(ent.position()).within(self.game.inner())
|
||||
}
|
||||
|
||||
/// Convert the given inner-relative position to one on the actual screen
|
||||
fn on_screen(&self, pos: Position) -> Position {
|
||||
pos + self.inner.position + self.game.inner().position
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Debug for Viewport<W> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
|
@ -30,44 +57,52 @@ impl<W> Debug for Viewport<W> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<W> Viewport<W> {
|
||||
/// Returns true if the (inner-relative) position of the given entity is
|
||||
/// visible within this viewport
|
||||
fn visible<E: Positioned>(&self, ent: &E) -> bool {
|
||||
self.on_screen(ent.position()).within(self.outer.inner())
|
||||
}
|
||||
|
||||
/// Convert the given inner-relative position to one on the actual screen
|
||||
fn on_screen(&self, pos: Position) -> Position {
|
||||
pos + self.inner.position + self.outer.inner().position
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Viewport<W> {
|
||||
/// Draw the given entity to the viewport at its position, if visible
|
||||
pub fn draw<T: Draw>(&mut self, entity: &T) -> io::Result<()> {
|
||||
if !self.visible(entity) {
|
||||
return Ok(());
|
||||
}
|
||||
write!(
|
||||
self,
|
||||
"{}",
|
||||
(entity.position()
|
||||
+ self.inner.position
|
||||
+ self.outer.inner().position)
|
||||
.cursor_goto()
|
||||
)?;
|
||||
self.cursor_goto(entity.position())?;
|
||||
entity.do_draw(self)
|
||||
}
|
||||
|
||||
/// Clear whatever is drawn at the given inner-relative position, if visible
|
||||
/// Move the cursor to the given inner-relative position
|
||||
pub fn cursor_goto(&mut self, pos: Position) -> io::Result<()> {
|
||||
write!(self, "{}", self.on_screen(pos).cursor_goto())
|
||||
}
|
||||
|
||||
/// Clear whatever single character is drawn at the given inner-relative
|
||||
/// position, if visible
|
||||
pub fn clear(&mut self, pos: Position) -> io::Result<()> {
|
||||
write!(self, "{} ", self.on_screen(pos).cursor_goto(),)
|
||||
}
|
||||
|
||||
/// Initialize this viewport by drawing its outer box to the screen
|
||||
pub fn init(&mut self) -> io::Result<()> {
|
||||
write!(self, "{}", make_box(BoxStyle::Thin, self.outer.dimensions))
|
||||
draw_box(self, self.game, BoxStyle::Thin)
|
||||
}
|
||||
|
||||
/// Write a message to the message area on the screen
|
||||
///
|
||||
/// Will overwrite any message already present, and if the given message is
|
||||
/// longer than the screen will truncate. This means callers should handle
|
||||
/// message buffering and ellipsisization
|
||||
pub fn write_message(&mut self, msg: &str) -> io::Result<()> {
|
||||
write!(
|
||||
self,
|
||||
"{}{}{}",
|
||||
self.outer.position.cursor_goto(),
|
||||
if msg.len() <= self.outer.dimensions.w as usize {
|
||||
msg
|
||||
} else {
|
||||
&msg[0..self.outer.dimensions.w as usize]
|
||||
},
|
||||
clone_times::<_, String>(
|
||||
" ".to_string(),
|
||||
self.outer.dimensions.w - msg.len() as u16
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,24 +134,24 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_visible() {
|
||||
assert!(Viewport {
|
||||
outer: BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
|
||||
inner: BoundingBox {
|
||||
assert!(Viewport::new(
|
||||
BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
|
||||
BoundingBox {
|
||||
position: Position { x: -10, y: -10 },
|
||||
dimensions: Dimensions { w: 15, h: 15 },
|
||||
},
|
||||
out: (),
|
||||
}
|
||||
()
|
||||
)
|
||||
.visible(&Position { x: 13, y: 13 }));
|
||||
|
||||
assert!(!Viewport {
|
||||
outer: BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
|
||||
inner: BoundingBox {
|
||||
assert!(!Viewport::new(
|
||||
BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
|
||||
BoundingBox {
|
||||
position: Position { x: -10, y: -10 },
|
||||
dimensions: Dimensions { w: 15, h: 15 },
|
||||
},
|
||||
out: (),
|
||||
}
|
||||
(),
|
||||
)
|
||||
.visible(&Position { x: 1, y: 1 }));
|
||||
}
|
||||
|
||||
|
|
|
|||
55
src/game.rs
55
src/game.rs
|
|
@ -1,17 +1,21 @@
|
|||
use crate::display::{self, Viewport};
|
||||
use crate::entities::Character;
|
||||
use crate::messages::message;
|
||||
use crate::settings::Settings;
|
||||
use crate::types::command::Command;
|
||||
use crate::types::Positioned;
|
||||
use crate::types::{BoundingBox, Dimensions, Position};
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::SeedableRng;
|
||||
use std::io::{self, StdinLock, StdoutLock, Write};
|
||||
use termion::input::Keys;
|
||||
use termion::input::TermRead;
|
||||
use termion::raw::RawTerminal;
|
||||
|
||||
use crate::display::{self, Viewport};
|
||||
use crate::entities::Character;
|
||||
use crate::types::command::Command;
|
||||
|
||||
type Stdout<'a> = RawTerminal<StdoutLock<'a>>;
|
||||
|
||||
type Rng = SmallRng;
|
||||
|
||||
/// The full state of a running Game
|
||||
pub struct Game<'a> {
|
||||
settings: Settings,
|
||||
|
|
@ -23,6 +27,12 @@ pub struct Game<'a> {
|
|||
|
||||
/// The player character
|
||||
character: Character,
|
||||
|
||||
/// The messages that have been said to the user, in forward time order
|
||||
messages: Vec<String>,
|
||||
|
||||
/// A global random number generator for the game
|
||||
rng: Rng,
|
||||
}
|
||||
|
||||
impl<'a> Game<'a> {
|
||||
|
|
@ -33,18 +43,21 @@ impl<'a> Game<'a> {
|
|||
w: u16,
|
||||
h: u16,
|
||||
) -> Game<'a> {
|
||||
let rng = match settings.seed {
|
||||
Some(seed) => SmallRng::seed_from_u64(seed),
|
||||
None => SmallRng::from_entropy(),
|
||||
};
|
||||
Game {
|
||||
settings: settings,
|
||||
viewport: Viewport {
|
||||
outer: BoundingBox::at_origin(Dimensions { w, h }),
|
||||
inner: BoundingBox::at_origin(Dimensions {
|
||||
w: w - 2,
|
||||
h: h - 2,
|
||||
}),
|
||||
out: stdout,
|
||||
},
|
||||
settings,
|
||||
rng,
|
||||
viewport: Viewport::new(
|
||||
BoundingBox::at_origin(Dimensions { w, h }),
|
||||
BoundingBox::at_origin(Dimensions { w: w - 2, h: h - 2 }),
|
||||
stdout,
|
||||
),
|
||||
keys: stdin.keys(),
|
||||
character: Character::new(),
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,15 +66,29 @@ impl<'a> Game<'a> {
|
|||
!pos.within(self.viewport.inner)
|
||||
}
|
||||
|
||||
/// Draw all the game entities to the screen
|
||||
fn draw_entities(&mut self) -> io::Result<()> {
|
||||
self.viewport.draw(&self.character)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Say a message to the user
|
||||
fn say(&mut self, message_name: &str) -> io::Result<()> {
|
||||
let message = self.message(message_name);
|
||||
self.messages.push(message.to_string());
|
||||
self.viewport.write_message(message)
|
||||
}
|
||||
|
||||
/// Run the game
|
||||
pub fn run(mut self) -> io::Result<()> {
|
||||
info!("Running game");
|
||||
self.viewport.init()?;
|
||||
self.draw_entities()?;
|
||||
self.say("global.welcome")?;
|
||||
self.flush()?;
|
||||
loop {
|
||||
let mut old_position = None;
|
||||
|
|
@ -86,7 +113,7 @@ impl<'a> Game<'a> {
|
|||
self.viewport.clear(old_pos)?;
|
||||
self.viewport.draw(&self.character)?;
|
||||
}
|
||||
None => ()
|
||||
None => (),
|
||||
}
|
||||
self.flush()?;
|
||||
debug!("{:?}", self.character);
|
||||
|
|
|
|||
|
|
@ -3,18 +3,26 @@ extern crate termion;
|
|||
extern crate log;
|
||||
extern crate config;
|
||||
extern crate log4rs;
|
||||
extern crate serde;
|
||||
extern crate toml;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate maplit;
|
||||
|
||||
|
||||
mod display;
|
||||
mod game;
|
||||
#[macro_use]
|
||||
mod types;
|
||||
mod entities;
|
||||
mod messages;
|
||||
mod settings;
|
||||
|
||||
use clap::App;
|
||||
|
|
|
|||
186
src/messages.rs
Normal file
186
src/messages.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
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>),
|
||||
}
|
||||
|
||||
impl<'a> Message<'a> {
|
||||
fn resolve<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&'a str> {
|
||||
use Message::*;
|
||||
match self {
|
||||
Single(msg) => Some(*msg),
|
||||
Choice(msgs) => msgs.choose(rng).map(|msg| *msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum NestedMap<'a> {
|
||||
Direct(Message<'a>),
|
||||
Nested(HashMap<&'a str, NestedMap<'a>>),
|
||||
}
|
||||
|
||||
impl<'a> NestedMap<'a> {
|
||||
fn lookup(&'a self, path: &str) -> Option<&'a Message<'a>> {
|
||||
use NestedMap::*;
|
||||
let leaf =
|
||||
path.split(".")
|
||||
.fold(Some(self), |current, key| match current {
|
||||
Some(Nested(m)) => m.get(key),
|
||||
_ => None,
|
||||
});
|
||||
match leaf {
|
||||
Some(Direct(msg)) => Some(msg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_nested_map() {
|
||||
let src = r#"
|
||||
[global]
|
||||
hello = "Hello World!"
|
||||
|
||||
[foo.bar]
|
||||
single = "Single"
|
||||
choice = ["Say this", "Or this"]
|
||||
"#;
|
||||
let result = toml::from_str(src);
|
||||
assert_eq!(
|
||||
result,
|
||||
Ok(NestedMap::Nested(hashmap! {
|
||||
"global" => NestedMap::Nested(hashmap!{
|
||||
"hello" => NestedMap::Direct(Message::Single("Hello World!")),
|
||||
}),
|
||||
"foo" => NestedMap::Nested(hashmap!{
|
||||
"bar" => NestedMap::Nested(hashmap!{
|
||||
"single" => NestedMap::Direct(Message::Single("Single")),
|
||||
"choice" => NestedMap::Direct(Message::Choice(
|
||||
vec!["Say this", "Or this"]
|
||||
))
|
||||
})
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup() {
|
||||
let map: NestedMap<'static> = toml::from_str(
|
||||
r#"
|
||||
[global]
|
||||
hello = "Hello World!"
|
||||
|
||||
[foo.bar]
|
||||
single = "Single"
|
||||
choice = ["Say this", "Or this"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
map.lookup("global.hello"),
|
||||
Some(&Message::Single("Hello World!"))
|
||||
);
|
||||
assert_eq!(
|
||||
map.lookup("foo.bar.single"),
|
||||
Some(&Message::Single("Single"))
|
||||
);
|
||||
assert_eq!(
|
||||
map.lookup("foo.bar.choice"),
|
||||
Some(&Message::Choice(vec!["Say this", "Or this"]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static MESSAGES_RAW: &'static str = include_str!("messages.toml");
|
||||
|
||||
lazy_static! {
|
||||
static ref MESSAGES: NestedMap<'static> =
|
||||
toml::from_str(MESSAGES_RAW).unwrap();
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
use Message::*;
|
||||
MESSAGES
|
||||
.lookup(name)
|
||||
.and_then(|msg| msg.resolve(rng))
|
||||
.unwrap_or_else(|| {
|
||||
error!("Message not found: {}", name);
|
||||
"Message not found"
|
||||
})
|
||||
}
|
||||
2
src/messages.toml
Normal file
2
src/messages.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[global]
|
||||
welcome = "Welcome to Xanthous! It's dangerous out there, why not stay inside?"
|
||||
|
|
@ -48,6 +48,7 @@ impl Logging {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub seed: Option<u64>,
|
||||
pub logging: Logging,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@ impl BoundingBox {
|
|||
pub fn inner(self) -> BoundingBox {
|
||||
self + UNIT_POSITION - UNIT_DIMENSIONS - UNIT_DIMENSIONS
|
||||
}
|
||||
|
||||
/// Moves the top right corner of the bounding box by the offset specified
|
||||
/// by the given position, keeping the lower right corner in place
|
||||
pub fn move_tr_corner(self, offset: Position) -> BoundingBox {
|
||||
self + offset - Dimensions { w: offset.x as u16, h: offset.y as u16 }
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<Position> for BoundingBox {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue