about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorGriffin Smith <root@gws.fyi>2019-07-28T02·16-0400
committerGriffin Smith <root@gws.fyi>2019-07-28T02·16-0400
commitf22bcad817ee354b355d29b6b289894e2d15cfaa (patch)
tree509aa3b88f834ffaccd6a90b61ae2c1e1567622d /src
parent68e8ad8a0e6a5ac38b34658f03807ade603a687c (diff)
Add a generic text-prompt system
Add a generic text-prompt system to the Game, and use it to prompt the
character for their name on startup. There's also a Promise type in
util, which is used for the result of the prompt.
Diffstat (limited to 'src')
-rw-r--r--src/display/color.rs1
-rw-r--r--src/display/viewport.rs97
-rw-r--r--src/entities/character.rs13
-rw-r--r--src/game.rs251
-rw-r--r--src/level_gen/util.rs2
-rw-r--r--src/main.rs1
-rw-r--r--src/messages.toml7
-rw-r--r--src/util/mod.rs1
-rw-r--r--src/util/promise.rs159
9 files changed, 479 insertions, 53 deletions
diff --git a/src/display/color.rs b/src/display/color.rs
index 7d024a960d97..2a023f1d9564 100644
--- a/src/display/color.rs
+++ b/src/display/color.rs
@@ -92,7 +92,6 @@ impl<'de> Visitor<'de> for ColorVisitor {
                 Ok(Color(Box::new(color::LightYellow)))
             }
             "magenta" => Ok(Color(Box::new(color::Magenta))),
-            "magenta" => Ok(Color(Box::new(color::Magenta))),
             "red" => Ok(Color(Box::new(color::Red))),
             "white" => Ok(Color(Box::new(color::White))),
             "yellow" => Ok(Color(Box::new(color::Yellow))),
diff --git a/src/display/viewport.rs b/src/display/viewport.rs
index b510b0504c58..372c0a2969d5 100644
--- a/src/display/viewport.rs
+++ b/src/display/viewport.rs
@@ -2,10 +2,21 @@ use super::BoxStyle;
 use super::Draw;
 use crate::display::draw_box::draw_box;
 use crate::display::utils::clone_times;
-use crate::types::{pos, BoundingBox, Position, Positioned};
+use crate::types::{pos, BoundingBox, Direction, Position, Positioned};
 use std::fmt::{self, Debug};
 use std::io::{self, Write};
 
+pub enum CursorState {
+    Game,
+    Prompt(Position),
+}
+
+impl Default for CursorState {
+    fn default() -> Self {
+        CursorState::Game
+    }
+}
+
 pub struct Viewport<W> {
     /// The box describing the visible part of the viewport.
     ///
@@ -24,9 +35,12 @@ pub struct Viewport<W> {
     /// The actual screen that the viewport writes to
     pub out: W,
 
+    cursor_state: CursorState,
+
     /// Reset the cursor back to this position after every draw
-    pub cursor_position: Position,
+    pub game_cursor_position: Position,
 }
+
 impl<W> Viewport<W> {
     pub fn new(outer: BoundingBox, inner: BoundingBox, out: W) -> Self {
         Viewport {
@@ -34,7 +48,8 @@ impl<W> Viewport<W> {
             inner,
             out,
             game: outer.move_tr_corner(Position { x: 0, y: 1 }),
-            cursor_position: pos(0, 0),
+            cursor_state: Default::default(),
+            game_cursor_position: pos(0, 0),
         }
     }
 
@@ -72,7 +87,7 @@ impl<W: Write> Viewport<W> {
     }
 
     fn reset_cursor(&mut self) -> io::Result<()> {
-        self.cursor_goto(self.cursor_position)
+        self.cursor_goto(self.game_cursor_position)
     }
 
     /// Move the cursor to the given inner-relative position
@@ -97,23 +112,85 @@ impl<W: Write> Viewport<W> {
     /// 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<()> {
+    pub fn write_message(&mut self, msg: &str) -> io::Result<usize> {
+        let msg_to_write = if msg.len() <= self.outer.dimensions.w as usize {
+            msg
+        } else {
+            &msg[0..self.outer.dimensions.w as usize]
+        };
         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]
-            },
+            msg_to_write,
             clone_times::<_, String>(
                 " ".to_string(),
                 self.outer.dimensions.w - msg.len() as u16
             ),
         )?;
+        self.reset_cursor()?;
+        Ok(msg_to_write.len())
+    }
+
+    pub fn clear_message(&mut self) -> io::Result<()> {
+        write!(
+            self,
+            "{}{}",
+            self.outer.position.cursor_goto(),
+            clone_times::<_, String>(
+                " ".to_string(),
+                self.outer.dimensions.w as u16
+            )
+        )?;
         self.reset_cursor()
     }
+
+    /// Write a prompt requesting text input 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_prompt<'a, 'b>(&'a mut self, msg: &'b str) -> io::Result<()> {
+        let len = self.write_message(msg)? + 1;
+        let pos = self.outer.position + pos(len as i16, 0);
+        self.cursor_state = CursorState::Prompt(pos);
+        write!(self, "{}", pos.cursor_goto())?;
+        self.flush()
+    }
+
+    pub fn push_prompt_chr(&mut self, chr: char) -> io::Result<()> {
+        match self.cursor_state {
+            CursorState::Prompt(pos) => {
+                write!(self, "{}", chr)?;
+                self.cursor_state = CursorState::Prompt(pos + Direction::Right);
+            }
+            _ => {}
+        }
+        Ok(())
+    }
+
+    pub fn pop_prompt_chr(&mut self) -> io::Result<()> {
+        match self.cursor_state {
+            CursorState::Prompt(pos) => {
+                let new_pos = pos + Direction::Left;
+                write!(
+                    self,
+                    "{} {}",
+                    new_pos.cursor_goto(),
+                    new_pos.cursor_goto()
+                )?;
+                self.cursor_state = CursorState::Prompt(new_pos);
+            }
+            _ => {}
+        }
+        Ok(())
+    }
+
+    pub fn clear_prompt(&mut self) -> io::Result<()> {
+        self.clear_message()?;
+        self.cursor_state = CursorState::Game;
+        Ok(())
+    }
 }
 
 impl<W> Positioned for Viewport<W> {
diff --git a/src/entities/character.rs b/src/entities/character.rs
index 7bcb8b5c87e4..b917f140e635 100644
--- a/src/entities/character.rs
+++ b/src/entities/character.rs
@@ -13,6 +13,8 @@ pub struct Character {
 
     /// The position of the character, relative to the game
     pub position: Position,
+
+    pub o_name: Option<String>,
 }
 
 impl Character {
@@ -20,6 +22,7 @@ impl Character {
         Character {
             id: None,
             position: Position { x: 0, y: 0 },
+            o_name: None,
         }
     }
 
@@ -31,6 +34,16 @@ impl Character {
         // TODO
         1
     }
+
+    pub fn name<'a>(&'a self) -> &'a str {
+        self.o_name
+            .as_ref()
+            .expect("Character name not initialized")
+    }
+
+    pub fn set_name(&mut self, name: String) {
+        self.o_name = Some(name);
+    }
 }
 
 entity!(Character);
diff --git a/src/game.rs b/src/game.rs
index c4fc6d2be10a..49068361b5d1 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -10,6 +10,8 @@ use crate::types::{
     pos, BoundingBox, Collision, Dimensions, Position, Positioned,
     PositionedMut, Ticks,
 };
+use crate::util::promise::Cancelled;
+use crate::util::promise::{promise, Complete, Promise, Promises};
 use crate::util::template::TemplateParams;
 use rand::rngs::SmallRng;
 use rand::SeedableRng;
@@ -36,6 +38,75 @@ impl<'a> PositionedMut for AnEntity<'a> {
     }
 }
 
+enum PromptResolution {
+    Uncancellable(Complete<String>),
+    Cancellable(Complete<Result<String, Cancelled>>),
+}
+
+impl PromptResolution {
+    fn is_cancellable(&self) -> bool {
+        use PromptResolution::*;
+        match self {
+            Uncancellable(_) => false,
+            Cancellable(_) => true,
+        }
+    }
+
+    fn fulfill(&mut self, val: String) {
+        use PromptResolution::*;
+        match self {
+            Cancellable(complete) => complete.ok(val),
+            Uncancellable(complete) => complete.fulfill(val),
+        }
+    }
+
+    fn cancel(&mut self) {
+        use PromptResolution::*;
+        match self {
+            Cancellable(complete) => complete.cancel(),
+            Uncancellable(complete) => {}
+        }
+    }
+}
+
+/// The kind of input the game is waiting to receive
+enum InputState {
+    /// The initial input state of the game - we're currently waiting for direct
+    /// commands.
+    Initial,
+
+    /// A free text prompt has been shown to the user, and every character
+    /// besides "escape" is interpreted as a response to that prompt
+    Prompt {
+        complete: PromptResolution,
+        buffer: String,
+    },
+}
+
+impl InputState {
+    fn uncancellable_prompt(complete: Complete<String>) -> Self {
+        InputState::Prompt {
+            complete: PromptResolution::Uncancellable(complete),
+            buffer: String::new(),
+        }
+    }
+
+    fn cancellable_prompt(
+        complete: Complete<Result<String, Cancelled>>,
+    ) -> Self {
+        InputState::Prompt {
+            complete: PromptResolution::Cancellable(complete),
+            buffer: String::new(),
+        }
+    }
+}
+
+impl Default for InputState {
+    fn default() -> Self {
+        InputState::Initial
+    }
+}
+
 /// The full state of a running Game
 pub struct Game<'a> {
     settings: Settings,
@@ -45,6 +116,9 @@ pub struct Game<'a> {
     /// An iterator on keypresses from the user
     keys: Keys<StdinLock<'a>>,
 
+    /// The kind of input the game is waiting to receive
+    input_state: InputState,
+
     /// The map of all the entities in the game
     entities: EntityMap<AnEntity<'a>>,
 
@@ -60,6 +134,9 @@ pub struct Game<'a> {
 
     /// A global random number generator for the game
     rng: Rng,
+
+    /// A list of promises that are waiting on the game and a result
+    promises: Promises<'a, Self>,
 }
 
 impl<'a> Game<'a> {
@@ -97,9 +174,11 @@ impl<'a> Game<'a> {
                 stdout,
             ),
             keys: stdin.keys(),
+            input_state: Default::default(),
             character_entity_id: entities.insert(Box::new(Character::new())),
             messages: Vec::new(),
             entities,
+            promises: Promises::new(),
         }
     }
 
@@ -131,6 +210,12 @@ impl<'a> Game<'a> {
             .unwrap()
     }
 
+    fn mut_character(&mut self) -> &mut Character {
+        (*self.entities.get_mut(self.character_entity_id).unwrap())
+            .downcast_mut()
+            .unwrap()
+    }
+
     /// Draw all the game entities to the screen
     fn draw_entities(&mut self) -> io::Result<()> {
         for entity in self.entities.entities() {
@@ -168,7 +253,36 @@ impl<'a> Game<'a> {
         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)?;
+        Ok(())
+    }
+
+    /// Prompt the user for input, returning a Future for the result of the
+    /// prompt
+    fn prompt(
+        &mut self,
+        name: &'static str,
+        params: &TemplateParams<'_>,
+    ) -> io::Result<Promise<Self, String>> {
+        let (complete, promise) = promise();
+        self.input_state = InputState::uncancellable_prompt(complete);
+        let message = self.message(name, params);
+        self.viewport.write_prompt(&message)?;
+        self.promises.push(Box::new(promise.clone()));
+        Ok(promise)
+    }
+
+    fn prompt_cancellable(
+        &mut self,
+        name: &'static str,
+        params: &TemplateParams<'_>,
+    ) -> io::Result<Promise<Self, Result<String, Cancelled>>> {
+        let (complete, promise) = promise();
+        self.input_state = InputState::cancellable_prompt(complete);
+        let message = self.message(name, params);
+        self.viewport.write_prompt(&message)?;
+        self.promises.push(Box::new(promise.clone()));
+        Ok(promise)
     }
 
     fn previous_message(&mut self) -> io::Result<()> {
@@ -177,7 +291,8 @@ impl<'a> Game<'a> {
         }
         self.message_idx -= 1;
         let message = &self.messages[self.message_idx];
-        self.viewport.write_message(message)
+        self.viewport.write_message(message)?;
+        Ok(())
     }
 
     fn creature(&self, creature_id: EntityID) -> Option<&Creature> {
@@ -236,60 +351,116 @@ impl<'a> Game<'a> {
         }
     }
 
+    fn flush_promises(&mut self) {
+        unsafe {
+            let game = self as *mut Self;
+            (*game).promises.give_all(&mut *game);
+        }
+    }
+
     /// Run the game
     pub fn run(mut self) -> io::Result<()> {
         info!("Running game");
         self.viewport.init()?;
         self.draw_entities()?;
-        self.say("global.welcome", &template_params!())?;
-        self.flush()?;
+        self.flush().unwrap();
+
+        self.prompt("character.name_prompt", &template_params!())?
+            .on_fulfill(|game, char_name| {
+                game.say(
+                    "global.welcome",
+                    &template_params!({
+                        "character" => {
+                            "name" => char_name,
+                        },
+                    }),
+                )
+                .unwrap();
+                game.flush().unwrap();
+                game.mut_character().set_name(char_name.to_string());
+            });
+
         loop {
             let mut old_position = None;
-            use Command::*;
-            match Command::from_key(self.keys.next().unwrap().unwrap()) {
-                Some(Quit) => {
-                    info!("Quitting game due to user request");
-                    break;
-                }
+            let next_key = self.keys.next().unwrap().unwrap();
+            match &mut self.input_state {
+                InputState::Initial => {
+                    use Command::*;
+                    match Command::from_key(next_key) {
+                        Some(Quit) => {
+                            info!("Quitting game due to user request");
+                            break;
+                        }
 
-                Some(Move(direction)) => {
-                    use Collision::*;
-                    let new_pos = self.character().position + direction;
-                    match self.collision_at(new_pos) {
-                        None => {
-                            old_position = Some(self.character().position);
-                            self.entities.update_position(
-                                self.character_entity_id,
-                                new_pos,
-                            );
+                        Some(Move(direction)) => {
+                            use Collision::*;
+                            let new_pos = self.character().position + direction;
+                            match self.collision_at(new_pos) {
+                                None => {
+                                    old_position =
+                                        Some(self.character().position);
+                                    self.entities.update_position(
+                                        self.character_entity_id,
+                                        new_pos,
+                                    );
+                                }
+                                Some(Combat) => {
+                                    self.attack_at(new_pos)?;
+                                }
+                                Some(Stop) => (),
+                            }
                         }
-                        Some(Combat) => {
-                            self.attack_at(new_pos)?;
+
+                        Some(PreviousMessage) => self.previous_message()?,
+
+                        None => (),
+                    }
+
+                    match old_position {
+                        Some(old_pos) => {
+                            self.tick(
+                                self.character().speed().tiles_to_ticks(
+                                    (old_pos - self.character().position)
+                                        .as_tiles(),
+                                ),
+                            );
+                            self.viewport.clear(old_pos)?;
+                            self.viewport.game_cursor_position =
+                                self.character().position;
+                            self.viewport.draw(
+                                // TODO this clone feels unnecessary.
+                                &self.character().clone(),
+                            )?;
                         }
-                        Some(Stop) => (),
+                        None => (),
                     }
                 }
 
-                Some(PreviousMessage) => self.previous_message()?,
-
-                None => (),
-            }
-
-            match old_position {
-                Some(old_pos) => {
-                    self.tick(self.character().speed().tiles_to_ticks(
-                        (old_pos - self.character().position).as_tiles(),
-                    ));
-                    self.viewport.clear(old_pos)?;
-                    self.viewport.cursor_position = self.character().position;
-                    self.viewport.draw(
-                        // TODO this clone feels unnecessary.
-                        &self.character().clone(),
-                    )?;
+                InputState::Prompt { complete, buffer } => {
+                    use termion::event::Key::*;
+                    match next_key {
+                        Char('\n') => {
+                            info!("Prompt complete: \"{}\"", buffer);
+                            self.viewport.clear_prompt()?;
+                            complete.fulfill(buffer.clone());
+                            self.input_state = InputState::Initial;
+                        }
+                        Char(chr) => {
+                            buffer.push(chr);
+                            self.viewport.push_prompt_chr(chr)?;
+                        }
+                        Esc => complete.cancel(),
+                        Backspace => {
+                            buffer.pop();
+                            self.viewport.pop_prompt_chr()?;
+                        }
+                        _ => {}
+                    }
                 }
-                None => (),
             }
+
             self.flush()?;
+            self.flush_promises();
             debug!("{:?}", self.character());
         }
         Ok(())
diff --git a/src/level_gen/util.rs b/src/level_gen/util.rs
index 89a4a6a882da..629292c430fa 100644
--- a/src/level_gen/util.rs
+++ b/src/level_gen/util.rs
@@ -13,6 +13,8 @@ pub fn falses(dims: &Dimensions) -> Vec<Vec<bool>> {
     ret
 }
 
+/// Randomly initialize a 2-dimensional boolean vector of the given
+/// `Dimensions`, using the given random number generator and alive chance
 pub fn rand_initialize<R: Rng + ?Sized>(
     dims: &Dimensions,
     rng: &mut R,
diff --git a/src/main.rs b/src/main.rs
index dc958ca1a16d..2f0d1c3ffb05 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -26,6 +26,7 @@ extern crate include_dir;
 extern crate nom;
 #[macro_use]
 extern crate matches;
+extern crate futures;
 
 #[macro_use]
 mod util;
diff --git a/src/messages.toml b/src/messages.toml
index e7d097a76f61..c4a86beffff4 100644
--- a/src/messages.toml
+++ b/src/messages.toml
@@ -1,5 +1,5 @@
 [global]
-welcome = "Welcome to Xanthous! It's dangerous out there, why not stay inside?"
+welcome = "Welcome to Xanthous, {{character.name}}! It's dangerous out there, why not stay inside?"
 
 [combat]
 attack = "You attack the {{creature.name}}."
@@ -10,5 +10,8 @@ killed = [
     "The {{creature.name}} beefs it."
     ]
 
+[character]
+name_prompt = "What's your name?"
+
 [defaults.item]
-eat = "You eat the {{item.name}}"
+eat = "You eat the {{item.name}}. {{action.result}}"
diff --git a/src/util/mod.rs b/src/util/mod.rs
index c2b4eecaf5f4..c55fdfeae2b4 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -2,3 +2,4 @@
 pub mod static_cfg;
 #[macro_use]
 pub mod template;
+pub mod promise;
diff --git a/src/util/promise.rs b/src/util/promise.rs
new file mode 100644
index 000000000000..41f3d76e7737
--- /dev/null
+++ b/src/util/promise.rs
@@ -0,0 +1,159 @@
+use std::future::Future;
+use std::pin::Pin;
+use std::sync::{Arc, RwLock};
+use std::task::{Context, Poll, Waker};
+
+pub struct Promise<Env, T> {
+    inner: Arc<RwLock<Inner<T>>>,
+    waiters: Arc<RwLock<Vec<Box<dyn Fn(&mut Env, &T)>>>>,
+}
+
+pub struct Complete<T> {
+    inner: Arc<RwLock<Inner<T>>>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Cancelled;
+
+struct Inner<T> {
+    value: Option<Arc<T>>,
+    waker: Option<Waker>,
+}
+
+pub fn promise<Env, T>() -> (Complete<T>, Promise<Env, T>) {
+    let inner = Arc::new(RwLock::new(Inner {
+        value: None,
+        waker: None,
+    }));
+    let promise = Promise {
+        inner: inner.clone(),
+        waiters: Arc::new(RwLock::new(Vec::new())),
+    };
+    let complete = Complete { inner: inner };
+    (complete, promise)
+}
+
+impl<T> Complete<T> {
+    pub fn fulfill(&self, val: T) {
+        let mut inner = self.inner.write().unwrap();
+        inner.value = Some(Arc::new(val));
+        if let Some(waker) = inner.waker.take() {
+            waker.wake()
+        }
+    }
+}
+
+impl<T> Complete<Result<T, Cancelled>> {
+    pub fn cancel(&mut self) {
+        self.fulfill(Err(Cancelled))
+    }
+}
+
+impl<E, T> Complete<Result<T, E>> {
+    pub fn ok(&mut self, val: T) {
+        self.fulfill(Ok(val))
+    }
+
+    pub fn err(&mut self, e: E) {
+        self.fulfill(Err(e))
+    }
+}
+
+impl<Env, T> Promise<Env, T> {
+    pub fn on_fulfill<F: Fn(&mut Env, &T) + 'static>(&mut self, f: F) {
+        let mut waiters = self.waiters.write().unwrap();
+        waiters.push(Box::new(f));
+    }
+}
+
+impl<Env, T> Promise<Env, Result<T, Cancelled>> {
+    pub fn on_cancel<F: Fn(&mut Env) + 'static>(&mut self, f: F) {
+        self.on_err(move |env, _| f(env))
+    }
+}
+
+impl<Env, E, T> Promise<Env, Result<T, E>> {
+    pub fn on_ok<F: Fn(&mut Env, &T) + 'static>(&mut self, f: F) {
+        self.on_fulfill(move |env, r| {
+            if let Ok(val) = r {
+                f(env, val)
+            }
+        })
+    }
+
+    pub fn on_err<F: Fn(&mut Env, &E) + 'static>(&mut self, f: F) {
+        self.on_fulfill(move |env, r| {
+            if let Err(e) = r {
+                f(env, e)
+            }
+        })
+    }
+}
+
+pub trait Give<Env> {
+    fn give(&self, env: &mut Env) -> bool;
+}
+
+impl<Env, T> Give<Env> for Promise<Env, T> {
+    fn give(&self, env: &mut Env) -> bool {
+        let inner = self.inner.read().unwrap();
+        if let Some(value) = &inner.value {
+            let mut waiters = self.waiters.write().unwrap();
+            for waiter in waiters.iter() {
+                waiter(env, value);
+            }
+            waiters.clear();
+            true
+        } else {
+            false
+        }
+    }
+}
+
+impl<Env, T> Clone for Promise<Env, T> {
+    fn clone(&self) -> Self {
+        Promise {
+            inner: self.inner.clone(),
+            waiters: self.waiters.clone(),
+        }
+    }
+}
+
+impl<Env, P: Give<Env>> Give<Env> for &P {
+    fn give(&self, env: &mut Env) -> bool {
+        (*self).give(env)
+    }
+}
+
+impl<Env, T> Future for Promise<Env, T> {
+    type Output = Arc<T>;
+    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+        let mut inner = self.inner.write().unwrap();
+        match inner.value {
+            Some(ref v) => Poll::Ready(v.clone()),
+            None => {
+                inner.waker = Some(cx.waker().clone());
+                Poll::Pending
+            }
+        }
+    }
+}
+
+pub struct Promises<'a, Env> {
+    ps: Vec<Box<dyn Give<Env> + 'a>>,
+}
+
+impl<'a, Env> Promises<'a, Env> {
+    pub fn new() -> Self {
+        Promises { ps: Vec::new() }
+    }
+
+    pub fn push(&mut self, p: Box<dyn Give<Env> + 'a>) {
+        self.ps.push(p);
+    }
+
+    pub fn give_all(&mut self, env: &mut Env) {
+        debug!("promises: {}", self.ps.len());
+        self.ps.retain(|p| !p.give(env));
+    }
+}