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.
This commit is contained in:
Griffin Smith 2019-07-27 22:16:23 -04:00
parent 68e8ad8a0e
commit f22bcad817
11 changed files with 488 additions and 54 deletions

7
Cargo.lock generated
View file

@ -310,6 +310,11 @@ name = "fuchsia-cprng"
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"
@ -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"

View file

@ -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"

View file

@ -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))),

View file

@ -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> {

View file

@ -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);

View file

@ -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(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(Combat) => {
self.attack_at(new_pos)?;
}
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(())

View file

@ -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,

View file

@ -26,6 +26,7 @@ extern crate include_dir;
extern crate nom;
#[macro_use]
extern crate matches;
extern crate futures;
#[macro_use]
mod util;

View file

@ -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}}"

View file

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

159
src/util/promise.rs Normal file
View file

@ -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));
}
}