diff options
author | Griffin Smith <root@gws.fyi> | 2019-07-28T02·16-0400 |
---|---|---|
committer | Griffin Smith <root@gws.fyi> | 2019-07-28T02·16-0400 |
commit | f22bcad817ee354b355d29b6b289894e2d15cfaa (patch) | |
tree | 509aa3b88f834ffaccd6a90b61ae2c1e1567622d | |
parent | 68e8ad8a0e6a5ac38b34658f03807ade603a687c (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.
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/display/color.rs | 1 | ||||
-rw-r--r-- | src/display/viewport.rs | 97 | ||||
-rw-r--r-- | src/entities/character.rs | 13 | ||||
-rw-r--r-- | src/game.rs | 251 | ||||
-rw-r--r-- | src/level_gen/util.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 1 | ||||
-rw-r--r-- | src/messages.toml | 7 | ||||
-rw-r--r-- | src/util/mod.rs | 1 | ||||
-rw-r--r-- | src/util/promise.rs | 159 |
11 files changed, 488 insertions, 54 deletions
diff --git a/Cargo.lock b/Cargo.lock index cd08098985f2..4c8896cca160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,6 +311,11 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "futures" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "getrandom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1217,6 +1222,7 @@ dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "config 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "downcast-rs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", "include_dir 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1291,6 +1297,7 @@ dependencies = [ "checksum flate2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f87e68aa82b2de08a6e037f1385455759df6e445a8df5e005b4297191dbf18aa" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +"checksum futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "45dc39533a6cae6da2b56da48edae506bb767ec07370f86f70fc062e9d435869" "checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" diff --git a/Cargo.toml b/Cargo.toml index b290f6b4442d..f382bc23d7c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,14 @@ backtrace = "0.3" clap = {version = "^2.33.0", features = ["yaml"]} config = "*" downcast-rs = "^1.0.4" +futures = "0.1.28" include_dir = "0.2.1" itertools = "*" lazy_static = "*" log = "*" log4rs = "*" -matches = "0.1.8" maplit = "^1.0.1" +matches = "0.1.8" nom = "^5.0.0" prettytable-rs = "^0.8" proptest = "0.9.3" 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)); + } +} |