about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorGriffin Smith <root@gws.fyi>2019-07-07T16·41-0400
committerGriffin Smith <root@gws.fyi>2019-07-07T16·41-0400
commitc643ee1dfcb8d44b8cd198c768f31dd7659f2ff9 (patch)
tree8178c613e55c00944a6417a85ad304cf2af05587 /src
parent78a52142d191d25a74cb2124d5cca8a69d51ba7f (diff)
Add messages, with global lookup map
Add support for messages, along with a global lookup map and random
choice of messages.
Diffstat (limited to 'src')
-rw-r--r--src/display/draw_box.rs17
-rw-r--r--src/display/viewport.rs107
-rw-r--r--src/game.rs55
-rw-r--r--src/main.rs8
-rw-r--r--src/messages.rs186
-rw-r--r--src/messages.toml2
-rw-r--r--src/settings.rs1
-rw-r--r--src/types/mod.rs6
8 files changed, 332 insertions, 50 deletions
diff --git a/src/display/draw_box.rs b/src/display/draw_box.rs
index 986f09a49f7c..5dc1627a298d 100644
--- a/src/display/draw_box.rs
+++ b/src/display/draw_box.rs
@@ -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::*;
diff --git a/src/display/viewport.rs b/src/display/viewport.rs
index bd2fac07146f..24cc5272cb8e 100644
--- a/src/display/viewport.rs
+++ b/src/display/viewport.rs
@@ -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,36 +13,47 @@ 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> Debug for Viewport<W> {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(
-            f,
-            "Viewport {{ outer: {:?}, inner: {:?}, out: <OUT> }}",
-            self.outer, self.inner
-        )
+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 }),
+        }
     }
-}
 
-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())
+    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.outer.inner().position
+        pos + self.inner.position + self.game.inner().position
+    }
+}
+
+impl<W> Debug for Viewport<W> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "Viewport {{ outer: {:?}, inner: {:?}, out: <OUT> }}",
+            self.outer, self.inner
+        )
     }
 }
 
@@ -49,25 +63,46 @@ impl<W: Write> Viewport<W> {
         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 }));
     }
 
diff --git a/src/game.rs b/src/game.rs
index 6274ef573f58..b619f13423f5 100644
--- a/src/game.rs
+++ b/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);
diff --git a/src/main.rs b/src/main.rs
index f2c3d00f96d7..6b0ae18181ef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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;
diff --git a/src/messages.rs b/src/messages.rs
new file mode 100644
index 000000000000..2b9f098f98ca
--- /dev/null
+++ b/src/messages.rs
@@ -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"
+        })
+}
diff --git a/src/messages.toml b/src/messages.toml
new file mode 100644
index 000000000000..04746462d580
--- /dev/null
+++ b/src/messages.toml
@@ -0,0 +1,2 @@
+[global]
+welcome = "Welcome to Xanthous! It's dangerous out there, why not stay inside?"
diff --git a/src/settings.rs b/src/settings.rs
index 06f0d4e9d7de..8444bf80eec8 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -48,6 +48,7 @@ impl Logging {
 
 #[derive(Debug, Deserialize)]
 pub struct Settings {
+    pub seed: Option<u64>,
     pub logging: Logging,
 }
 
diff --git a/src/types/mod.rs b/src/types/mod.rs
index 146dfac9d99b..ab66a50cc218 100644
--- a/src/types/mod.rs
+++ b/src/types/mod.rs
@@ -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 {