Wipe Rust project

Sorry rust, but you're just not fun to write
This commit is contained in:
Griffin Smith 2019-08-25 13:25:13 -04:00
parent e2d2f011c6
commit fb0d1b3e66
47 changed files with 0 additions and 6087 deletions

1450
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,33 +0,0 @@
[package]
name = "xanthous"
version = "0.1.0"
authors = ["Griffin Smith <root@gws.fyi>"]
edition = "2018"
[dependencies]
alga = "0.9.1"
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 = "*"
maplit = "^1.0.1"
matches = "0.1.8"
nom = "^5.0.0"
prettytable-rs = "^0.8"
proptest = "0.9.3"
proptest-derive = "*"
rand = {version = "^0.7.0", features = ["small_rng"]}
serde = "^1.0.8"
serde_derive = "^1.0.8"
serde_json = "*"
serde_yaml = "0.8"
termion = "*"
toml = "^0.5.1"
[dev-dependencies]

View file

@ -1,2 +0,0 @@
[logging]
level = "debug"

View file

@ -1,7 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 92b51b5444b913aaa6cb89d7e7175ab6a6af5b5231ba047d123bb55d43d7d272 # shrinks to descriptions = []

View file

@ -1,12 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 7aff36a9f7b263e62434a3f61ada1d6aaf6ff4545a463548d96815a0e98cf5f1 # shrinks to dims = Dimensions { w: 0, h: 0 }, style = Thin
cc e4d96a13d6a8c7625e49d3545f6076d58152f3b5eb43fae65f0d407d1d34f96c # shrinks to dims = Dimensions { w: 1, h: 1 }, style = Thin
cc b5f0d7cb409896bd6692544c7c1f781174075c287d3b7a3b9dc73526ea489484 # shrinks to dims = Dimensions { w: 1, h: 1 }, style = Thin
cc 103b62b7c29c22adcbc23153638d3b37bad57aeec685d1eab38c49d0deed937f # shrinks to dims = Dimensions { w: 0, h: 1 }, style = Thin
cc 24c3858a543b0d8ff4966517040ec8c183ed311688d6863fd13facb5cdad7aa0 # shrinks to dims = Dimensions { w: 1, h: 1 }, style = Thin
cc 70a53a8b771937976a08a72d870b355a0995cc0251f45de4393c37a56a789b83 # shrinks to dims = Dimensions { w: 0, h: 0 }, style = Thin

View file

@ -1,7 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc b84a5a6dbba5cfc69329a119d9e20328c0372e0db2b72e5d71d971e3f13f8749 # shrinks to pos = Position { x: 0, y: 0 }, outer = BoundingBox { dimensions: Dimensions { w: 0, h: 0 }, position: Position { x: 0, y: 0 } }

View file

@ -1,9 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 16afe2473971397314ffa77acf7bad62f0c40bc3f591aff7aa9193c29e5a0921 # shrinks to items = [(Position { x: 92, y: 60 }, ""), (Position { x: 92, y: 60 }, "")]
cc 3a68a382c3bb8fdf60ea150a369abbdd45859e0c54cd6a4f7c75937a6c783b98 # shrinks to mut em = EntityMap { by_position: {Position { x: 25, y: 33 }: [1]}, by_id: {1: TestEntity { position: Position { x: 25, y: 33 }, name: "" }}, last_id: 1 }, ent = TestEntity { position: Position { x: 25, y: 33 }, name: "" }, new_position = Position { x: 0, y: 0 }
cc ffd7181e1c0343ab4c2ac92990f068d24c8663158c1c0a9526cd9edc470f950a # shrinks to mut em = EntityMap { by_position: {Position { x: 64, y: 58 }: [1]}, by_id: {1: TestEntity { position: Position { x: 64, y: 58 }, name: "" }}, last_id: 1 }, ent = TestEntity { position: Position { x: 0, y: 0 }, name: "" }, new_position = Position { x: 64, y: 58 }

View file

@ -1,8 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc a51cf37623f0e4024f4ba1450195be296d9b9e8ae954dbbf997ce5b57cd26792 # shrinks to a = Position { x: 44, y: 25 }, b = Position { x: 0, y: 25 }, c = Position { x: 0, y: 0 }
cc 0816b9348c53ef8c8328f0ea72d5ebef215f6764b1cbbd3c5db958e214c5fa3a # shrinks to pos = Position { x: 0, y: 0 }, dir = Down

View file

@ -1 +0,0 @@
max_width = 80

View file

@ -1,46 +0,0 @@
name: xanthous
version: "0.0"
author: Griffin Smith <root@gws.fyi>
about: hey, it's a terminal game
args:
- config:
short: c
long: config
value_name: FILE
help: Sets a custom config file
takes_value: true
subcommands:
- info:
about: Writes debug information to the terminal and exits
- generate-level:
about: Generate a level and print it to the screen
args:
- generator:
long: generator
value_name: GEN
help: Select which generator to use
takes_value: true
- width:
long: width
short: w
value_name: WIDTH
takes_value: true
- height:
long: height
short: h
value_name: HEIGHT
takes_value: true
- start-alive-chance:
long: start-alive-chance
takes_value: true
- birth_limit:
long: birth-limit
takes_value: true
- death_limit:
long: death-limit
takes_value: true
- steps:
long: steps
short: s
value_name: STEPS
takes_value: true

View file

@ -1,93 +0,0 @@
use crate::entities::Describe;
pub fn list_to_sentence(lst: &[String]) -> String {
let mut buf = String::with_capacity(
lst.iter()
.map(|e| e.len() + 2usize /* ", " */)
.sum::<usize>()
+ if lst.len() >= 3 {
3usize /* "and" */
} else {
0usize
},
);
match lst.len() {
0 => {}
1 => buf.push_str(&lst[0]),
2 => {
buf.push_str(&lst[0]);
buf.push_str(" and ");
buf.push_str(&lst[1]);
}
_ => {
for desc in &lst[..lst.len() - 1] {
buf.push_str(desc);
buf.push_str(", ");
}
buf.push_str("and ");
buf.push_str(&lst[lst.len() - 1]);
}
}
buf
}
pub fn describe_list<A: Describe>(lst: &[A]) -> String {
list_to_sentence(
&lst.iter().map(|e| e.description()).collect::<Vec<String>>(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use proptest_derive::Arbitrary;
#[derive(Debug, Arbitrary)]
struct Description(String);
impl Describe for Description {
fn description(&self) -> String {
self.0.clone()
}
}
proptest! {
#[test]
fn test_describe_list_includes_all_descriptions(
descriptions: Vec<Description>
) {
let res = describe_list(&descriptions);
for Description(desc) in descriptions {
assert!(res.contains(&desc));
}
}
}
#[test]
fn test_describe_list() {
assert_eq!(
describe_list(&[Description("one".to_string())]),
"one".to_string()
);
assert_eq!(
describe_list(&[
Description("one".to_string()),
Description("two".to_string())
]),
"one and two".to_string()
);
assert_eq!(
describe_list(&[
Description("one".to_string()),
Description("two".to_string()),
Description("three".to_string())
]),
"one, two, and three".to_string()
);
}
}

View file

@ -1,163 +0,0 @@
use serde::de::{self, Unexpected, Visitor};
use std::fmt;
use std::marker::PhantomData;
use termion::color;
#[derive(Debug)]
pub struct Color(Box<dyn color::Color>);
unsafe impl Sync for Color {}
unsafe impl Send for Color {}
impl Color {
pub fn new<C: color::Color + 'static>(c: C) -> Self {
Color(Box::new(c))
}
}
impl PartialEq for Color {
fn eq(&self, other: &Self) -> bool {
format!("{}{}", color::Fg(self), color::Bg(self))
== format!("{}{}", color::Fg(other), color::Bg(other))
}
}
impl Eq for Color {}
impl color::Color for Color {
fn write_fg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.write_fg(f)
}
fn write_bg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.write_bg(f)
}
}
impl<'a> color::Color for &'a Color {
fn write_fg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.write_fg(f)
}
fn write_bg(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.write_bg(f)
}
}
impl Default for Color {
fn default() -> Self {
Color::new(color::Reset)
}
}
pub struct ColorVisitor {
marker: PhantomData<fn() -> Color>,
}
impl ColorVisitor {
fn new() -> Self {
ColorVisitor {
marker: PhantomData,
}
}
}
impl<'de> Visitor<'de> for ColorVisitor {
type Value = Color;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("A color")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match v.to_lowercase().as_ref() {
"black" => Ok(Color(Box::new(color::Black))),
"blue" => Ok(Color(Box::new(color::Blue))),
"cyan" => Ok(Color(Box::new(color::Cyan))),
"green" => Ok(Color(Box::new(color::Green))),
"light black" | "light_black" => {
Ok(Color(Box::new(color::LightBlack)))
}
"light blue" | "light_blue" => {
Ok(Color(Box::new(color::LightBlue)))
}
"light cyan" | "light_cyan" => {
Ok(Color(Box::new(color::LightCyan)))
}
"light green" | "light_green" => {
Ok(Color(Box::new(color::LightGreen)))
}
"light magenta" | "light_magenta" => {
Ok(Color(Box::new(color::LightMagenta)))
}
"light red" | "light_red" => Ok(Color(Box::new(color::LightRed))),
"light white" | "light_white" => {
Ok(Color(Box::new(color::LightWhite)))
}
"light yellow" | "light_yellow" => {
Ok(Color(Box::new(color::LightYellow)))
}
"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))),
_ => Err(de::Error::invalid_value(
Unexpected::Str(v),
&"a valid color",
)),
}
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut red = None;
let mut green = None;
let mut blue = None;
while let Some((k, v)) = map.next_entry()? {
match k {
"red" => {
red = Some(v);
}
"green" => {
green = Some(v);
}
"blue" => {
blue = Some(v);
}
_ => {
return Err(de::Error::unknown_field(
k,
&["red", "green", "blue"],
));
}
}
}
match (red, green, blue) {
(Some(r), Some(g), Some(b)) => {
Ok(Color(Box::new(color::Rgb(r, g, b))))
}
(None, _, _) => Err(de::Error::missing_field("red")),
(_, None, _) => Err(de::Error::missing_field("green")),
(_, _, None) => Err(de::Error::missing_field("blue")),
}
}
fn visit_u8<E: de::Error>(self, v: u8) -> Result<Self::Value, E> {
Ok(Color(Box::new(color::AnsiValue(v))))
}
}
impl<'de> serde::Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(ColorVisitor::new())
}
}

View file

@ -1,274 +0,0 @@
use crate::display::utils::clone_times;
use crate::display::utils::times;
use crate::types::pos;
use crate::types::BoundingBox;
use crate::types::Dimensions;
use crate::types::Neighbors;
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
// U+250x ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
// U+251x ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
// U+252x ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
// U+253x ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
// U+254x ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
// U+255x ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
// U+256x ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
// U+257x ╰ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
static BOX: char = '☐';
static BOX_CHARS: [[char; 16]; 8] = [
// 0
[
// 0 1 2 3 4 5 6 7 8 9
'─', '━', '│', '┃', '┄', '┅', '┆', '┇', '┈', '┉',
// 10
'┊', '┋', '┌', '┍', '┎', '┏',
],
// 1
[
// 0 1 2 3 4 5 6 7 8 9
'┐', '┑', '┒', '┓', '└', '┕', '┖', '┗', '┘', '┙',
'┚', '┛', '├', '┝', '┞', '┟',
],
// 2
[
// 0 1 2 3 4 5 6 7 8 9
'┠', '┡', '┢', '┣', '┤', '┥', '┦', '┧', '┨', '┩',
'┪', '┫', '┬', '┭', '┮', '┯',
],
// 3
[
// 0 1 2 3 4 5 6 7 8 9
'┰', '┱', '┲', '┳', '┴', '┵', '┶', '┷', '┸', '┹',
'┺', '┻', '┼', '┽', '┾', '┿',
],
// 4
[
// 0 1 2 3 4 5 6 7 8 9
'╀', '╁', '╂', '╃', '╄', '╅', '╆', '╇', '╈', '╉',
'╊', '╋', '╌', '╍', '╎', '╏',
],
// 5
[
// 0 1 2 3 4 5 6 7 8 9
'═', '║', '╒', '╓', '╔', '╕', '╖', '╗', '╘', '╙',
'╚', '╛', '╜', '╝', '╞', '╟',
],
// 6
[
// 0 1 2 3 4 5 6 7 8 9
'╠', '╡', '╢', '╣', '╤', '╥', '╦', '╧', '╨', '╩',
'╪', '╫', '╬', '╭', '╮', '╯',
],
// 7
[
// 0 1 2 3 4 5 6 7 8 9
'╰', '', '╲', '', '╴', '╵', '╶', '╷', '╸', '╹',
'╺', '╻', '╼', '╽', '╾', '╿',
],
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BoxStyle {
Thin,
Thick,
Dotted,
ThickDotted,
Dashed,
ThickDashed,
Double,
}
impl Arbitrary for BoxStyle {
type Parameters = ();
type Strategy = strategy::Just<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
// TODO
strategy::Just(BoxStyle::Thin)
}
}
pub trait Stylable {
fn style(&self, style: BoxStyle) -> char;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
enum Corner {
TopRight,
TopLeft,
BottomRight,
BottomLeft,
}
impl Stylable for Corner {
fn style(&self, style: BoxStyle) -> char {
use BoxStyle::*;
use Corner::*;
match (self, style) {
(TopRight, Thin) => BOX_CHARS[1][0],
(TopLeft, Thin) => BOX_CHARS[0][12],
(BottomRight, Thin) => BOX_CHARS[1][8],
(BottomLeft, Thin) => BOX_CHARS[1][4],
_ => unimplemented!(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
enum Line {
H,
V,
}
impl Stylable for Line {
fn style(&self, style: BoxStyle) -> char {
use BoxStyle::*;
use Line::*;
match (self, style) {
(H, Thin) => BOX_CHARS[0][0],
(V, Thin) => BOX_CHARS[0][2],
_ => unimplemented!(),
}
}
}
impl Stylable for Neighbors<Option<BoxStyle>> {
fn style(&self, _style: BoxStyle) -> char {
use BoxStyle::*;
match (self.left, self.right, self.top, self.bottom) {
(None, None, None, None) => BOX,
(Some(Thin), None, None, None) => BOX_CHARS[7][4],
(None, Some(Thin), None, None) => BOX_CHARS[7][6],
(None, None, Some(Thin), None) => BOX_CHARS[7][5],
(None, None, None, Some(Thin)) => BOX_CHARS[7][7],
(Some(Thin), Some(Thin), None, None) => Line::H.style(Thin),
(Some(Thin), None, Some(Thin), None) => {
Corner::BottomRight.style(Thin)
}
(Some(Thin), None, None, Some(Thin)) => {
Corner::TopRight.style(Thin)
}
(None, Some(Thin), Some(Thin), None) => {
Corner::BottomLeft.style(Thin)
}
(None, Some(Thin), None, Some(Thin)) => Corner::TopLeft.style(Thin),
(None, None, Some(Thin), Some(Thin)) => Line::V.style(Thin),
(None, Some(Thin), Some(Thin), Some(Thin)) => BOX_CHARS[1][12],
(Some(Thin), None, Some(Thin), Some(Thin)) => BOX_CHARS[2][4],
(Some(Thin), Some(Thin), None, Some(Thin)) => BOX_CHARS[2][12],
(Some(Thin), Some(Thin), Some(Thin), None) => BOX_CHARS[3][4],
(Some(Thin), Some(Thin), Some(Thin), Some(Thin)) => {
BOX_CHARS[3][12]
}
neighs => panic!("unimplemented: {:?}", neighs),
}
}
}
#[must_use]
pub fn make_box(style: BoxStyle, dims: Dimensions) -> String {
if dims.h == 0 || dims.w == 0 {
"".to_string()
} else if dims.h == 1 && dims.w == 1 {
BOX.to_string()
} else if dims.h == 1 {
times(Line::H.style(style), dims.w)
} else if dims.w == 1 {
(0..dims.h).map(|_| Line::V.style(style)).join("\n\r")
} else {
let h_line: String = times(Line::H.style(style), dims.w - 2);
let v_line = Line::V.style(style);
let v_walls: String = clone_times(
format!(
"{}{}{}\n\r",
v_line,
times::<_, String>(' ', dims.w - 2),
v_line
),
dims.h - 2,
);
format!(
"{}{}{}\n\r{}{}{}{}",
Corner::TopLeft.style(style),
h_line,
Corner::TopRight.style(style),
v_walls,
Corner::BottomLeft.style(style),
h_line,
Corner::BottomRight.style(style),
)
}
}
/// 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<()> {
let box_str = make_box(style, bbox.dimensions);
if bbox.position.x == 0 {
write!(out, "{}{}", bbox.position.cursor_goto(), box_str)?;
} else {
for (i, line) in box_str.split("\n\r").enumerate() {
debug!("line: {:?}!", line);
write!(
out,
"{}{}",
(bbox.position + pos(0, i as i16)).cursor_goto(),
line
)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn make_thin_box() {
let res = make_box(BoxStyle::Thin, Dimensions { w: 10, h: 10 });
assert_eq!(
res,
"┌────────┐
\r
\r
\r
\r
\r
\r
\r
\r
\r"
);
}
proptest! {
#[test]
fn box_has_height_lines(dims: Dimensions, style: BoxStyle) {
let res = make_box(style, dims);
prop_assume!((dims.w > 0 && dims.h > 0));
assert_eq!(res.split("\n\r").count(), dims.h as usize);
}
#[test]
fn box_lines_have_width_length(dims: Dimensions, style: BoxStyle) {
let res = make_box(style, dims);
prop_assume!(dims.w == 0 && dims.h == 0 || (dims.w > 0 && dims.h > 0));
assert!(res.split("\n\r").all(|l| l.chars().count() == dims.w as usize));
}
}
}

View file

@ -1,52 +0,0 @@
pub mod color;
pub mod draw_box;
pub mod utils;
pub mod viewport;
use crate::entities::entity::Entity;
use crate::types::Neighbors;
use crate::types::Positioned;
pub use draw_box::{make_box, BoxStyle};
use std::io::{self, Write};
use termion::{clear, cursor, style};
pub use viewport::Viewport;
pub fn clear<T: Write>(out: &mut T) -> io::Result<()> {
write!(out, "{}{}{}", clear::All, style::Reset, cursor::Goto(1, 1))
}
pub trait Draw: Positioned {
/// Draw this entity, assuming the character is already at the correct
/// position
fn do_draw(&self, out: &mut dyn Write) -> io::Result<()>;
}
impl<T: Draw> Draw for &T {
fn do_draw(&self, out: &mut dyn Write) -> io::Result<()> {
(**self).do_draw(out)
}
}
impl<T: Draw> Draw for Box<T> {
fn do_draw(&self, out: &mut dyn Write) -> io::Result<()> {
(**self).do_draw(out)
}
}
pub trait DrawWithNeighbors: Positioned {
#[allow(clippy::borrowed_box)]
fn do_draw_with_neighbors<'a, 'b>(
&'a self,
out: &'b mut dyn Write,
neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
) -> io::Result<()>;
}
impl<T: Draw> DrawWithNeighbors for T {
fn do_draw_with_neighbors<'a, 'b>(
&'a self,
out: &'b mut dyn Write,
_neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
) -> io::Result<()> {
self.do_draw(out)
}
}

View file

@ -1,9 +0,0 @@
use std::iter::FromIterator;
pub fn times<A: Copy, B: FromIterator<A>>(elem: A, n: u16) -> B {
(0..n).map(|_| elem).collect()
}
pub fn clone_times<A: Clone, B: FromIterator<A>>(elem: A, n: u16) -> B {
(0..n).map(|_| elem.clone()).collect()
}

View file

@ -1,303 +0,0 @@
use super::BoxStyle;
use super::DrawWithNeighbors;
use crate::display::draw_box::draw_box;
use crate::display::utils::clone_times;
use crate::entities::entity::Entity;
use crate::types::menu::MenuInfo;
use crate::types::Neighbors;
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.
///
/// 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
pub inner: BoundingBox,
/// 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 game_cursor_position: Position,
}
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 }),
cursor_state: Default::default(),
game_cursor_position: pos(0, 0),
}
}
/// 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 {
write!(
f,
"Viewport {{ outer: {:?}, inner: {:?}, out: <OUT> }}",
self.outer, self.inner
)
}
}
impl<W: Write> Viewport<W> {
/// Draw the given entity to the viewport at its position, if visible
#[allow(clippy::borrowed_box)]
pub fn draw<T: DrawWithNeighbors>(
&mut self,
entity: &T,
neighbors: &Neighbors<Vec<&Box<dyn Entity>>>,
) -> io::Result<()> {
if !self.visible(entity) {
return Ok(());
}
self.cursor_goto(entity.position())?;
entity.do_draw_with_neighbors(self, neighbors)?;
self.reset_cursor()
}
fn reset_cursor(&mut self) -> io::Result<()> {
self.cursor_goto(self.game_cursor_position)
}
/// 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(),)?;
self.reset_cursor()
}
/// Initialize this viewport by drawing its outer box to the screen
pub fn init(&mut self) -> io::Result<()> {
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<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(),
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<()> {
if let CursorState::Prompt(pos) = self.cursor_state {
write!(self, "{}", chr)?;
self.cursor_state = CursorState::Prompt(pos + Direction::Right);
}
Ok(())
}
pub fn pop_prompt_chr(&mut self) -> io::Result<()> {
if let CursorState::Prompt(pos) = self.cursor_state {
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(())
}
pub fn write_menu(&mut self, menu: &MenuInfo) -> io::Result<()> {
let menu_dims = menu.dimensions();
// TODO: check if the menu is too big
let menu_position = self.game.position + pos(1, 1);
let menu_box = BoundingBox {
dimensions: menu_dims,
position: menu_position,
};
debug!("writing menu at: {:?}", menu_box);
draw_box(self, menu_box, BoxStyle::Thin)?;
write!(
self,
"{}{}",
(menu_position + pos(2, 2)).cursor_goto(),
menu.prompt
)?;
for (idx, option) in menu.options.iter().enumerate() {
write!(
self,
"{}{}",
(menu_position + pos(2, 4 + idx as i16)).cursor_goto(),
option
)?;
}
Ok(())
}
}
impl<W> Positioned for Viewport<W> {
fn position(&self) -> Position {
self.outer.position
}
}
impl<W: Write> Write for Viewport<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.out.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.out.flush()
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.out.write_all(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Dimensions;
#[test]
fn test_visible() {
assert!(Viewport::new(
BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
BoundingBox {
position: Position { x: -10, y: -10 },
dimensions: Dimensions { w: 15, h: 15 },
},
()
)
.visible(&Position { x: 13, y: 13 }));
assert!(!Viewport::new(
BoundingBox::at_origin(Dimensions { w: 10, h: 10 }),
BoundingBox {
position: Position { x: -10, y: -10 },
dimensions: Dimensions { w: 15, h: 15 },
},
(),
)
.visible(&Position { x: 1, y: 1 }));
}
#[test]
fn test_write_menu() {
let buf: Vec<u8> = Vec::new();
let mut viewport = Viewport::new(
BoundingBox::at_origin(Dimensions::default()),
BoundingBox::at_origin(Dimensions::default()),
buf,
);
let menu = MenuInfo::new(
"Test menu".to_string(),
vec!["option 1".to_string(), "option 2".to_string()],
);
viewport.write_menu(&menu).unwrap();
let res = std::str::from_utf8(&viewport.out).unwrap();
assert!(res.contains("Test menu"));
assert!(res.contains("option 1"));
assert!(res.contains("option 2"));
}
}

View file

@ -1,51 +0,0 @@
use crate::display;
use crate::entities::item::Item;
use crate::types::{Position, Speed};
use std::io::{self, Write};
const DEFAULT_SPEED: Speed = Speed(100);
entity! {
pub struct Character {
pub o_name: Option<String>,
pub inventory: Vec<Box<Item>>,
}
}
static_description!(Character, "yourself");
impl Character {
pub fn new() -> Character {
Character {
id: None,
position: Position { x: 0, y: 0 },
o_name: None,
inventory: Vec::new(),
}
}
pub fn speed(&self) -> Speed {
Speed(100)
}
pub fn damage(&self) -> u16 {
// TODO
1
}
pub fn name(&self) -> &str {
self.o_name
.as_ref()
.expect("Character name not initialized")
}
pub fn set_name(&mut self, name: String) {
self.o_name = Some(name);
}
}
impl display::Draw for Character {
fn do_draw(&self, out: &mut dyn Write) -> io::Result<()> {
write!(out, "@")
}
}

View file

@ -1,63 +0,0 @@
use crate::display;
use crate::entities::raws::CreatureType;
use crate::entities::raws::EntityRaw;
use crate::entities::{raw, Describe, EntityID};
use crate::types::Position;
use std::io::{self, Write};
#[derive(Debug, Clone)]
pub struct Creature {
pub id: Option<EntityID>,
pub typ: &'static CreatureType<'static>,
pub position: Position,
pub hitpoints: u16,
}
impl Creature {
pub fn new_from_raw(name: &'static str, position: Position) -> Self {
match raw(name) {
EntityRaw::Creature(typ) => Self::new_with_type(typ, position),
_ => panic!("Invalid raw type for {:?}, expected Creature", name),
}
}
pub fn new_with_type(
typ: &'static CreatureType<'static>,
position: Position,
) -> Self {
Creature {
id: None,
typ,
position,
hitpoints: typ.max_hitpoints,
}
}
/// Damage the given creature by the given amount
pub fn damage(&mut self, amount: u16) {
if self.hitpoints <= amount {
self.hitpoints = 0;
} else {
self.hitpoints -= amount;
}
}
/// Returns true if this creature has died
pub fn dead(&self) -> bool {
self.hitpoints == 0
}
}
entity!(Creature);
impl Describe for Creature {
fn description(&self) -> String {
self.typ.description.to_string()
}
}
impl display::Draw for Creature {
fn do_draw(&self, out: &mut dyn Write) -> io::Result<()> {
write!(out, "{}", self.typ.chr)
}
}

View file

@ -1,125 +0,0 @@
use crate::display::DrawWithNeighbors;
use crate::entities::EntityID;
use crate::types::Neighbors;
use crate::types::Position;
use crate::types::{Positioned, PositionedMut};
use downcast_rs::Downcast;
use std::fmt::Debug;
use std::io::{self, Write};
pub trait Identified<ID>: Debug {
fn opt_id(&self) -> Option<ID>;
fn set_id(&mut self, id: ID);
fn id(&self) -> ID {
self.opt_id()
.unwrap_or_else(|| panic!("Entity ({:?}) is not in the game", self))
}
}
impl<'a, A, ID> Identified<ID> for &'a mut A
where
A: Identified<ID>,
{
fn opt_id(&self) -> Option<ID> {
(**self).opt_id()
}
fn set_id(&mut self, id: ID) {
(**self).set_id(id);
}
}
impl<ID, A: Identified<ID>> Identified<ID> for Box<A> {
fn opt_id(&self) -> Option<ID> {
(**self).opt_id()
}
fn set_id(&mut self, id: ID) {
(**self).set_id(id);
}
}
pub trait Describe {
fn description(&self) -> String;
}
ref_impl! {
impl<T: Describe> Describe for &T {
fn description(&self) -> String {
(**self).description()
}
}
}
#[macro_export]
macro_rules! static_description {
($name: ident, $description: expr) => {
impl $crate::entities::entity::Describe for $name {
fn description(&self) -> String {
$description.to_string()
}
}
};
}
pub trait Entity:
Positioned
+ PositionedMut
+ Identified<EntityID>
+ DrawWithNeighbors
+ Downcast
+ Describe
{
}
impl Identified<EntityID> for Box<dyn Entity> {
fn opt_id(&self) -> Option<EntityID> {
(**self).opt_id()
}
fn set_id(&mut self, id: EntityID) {
(**self).set_id(id);
}
}
#[macro_export]
macro_rules! identified {
($name: ident, $typ: path) => {
identified!($name, $typ, id);
};
($name: ident, $typ: path, $attr: ident) => {
impl crate::entities::entity::Identified<$typ> for $name {
fn opt_id(&self) -> Option<$typ> {
self.$attr
}
fn set_id(&mut self, id: $typ) {
self.$attr = Some(id)
}
}
};
}
impl_downcast!(Entity);
impl DrawWithNeighbors for Box<dyn Entity> {
fn do_draw_with_neighbors<'a, 'b>(
&'a self,
out: &'b mut dyn Write,
neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
) -> io::Result<()> {
(**self).do_draw_with_neighbors(out, neighbors)
}
}
pub type AnEntity = Box<dyn Entity>;
impl Positioned for AnEntity {
fn position(&self) -> Position {
(**self).position()
}
}
impl PositionedMut for AnEntity {
fn set_position(&mut self, pos: Position) {
(**self).set_position(pos)
}
}

View file

@ -1,24 +0,0 @@
use crate::display::color::Color;
use std::fmt::{self, Display, Formatter};
use termion::color;
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct EntityChar {
#[serde(default)]
color: Color,
#[serde(rename = "char")]
chr: char,
}
impl Display for EntityChar {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}{}",
color::Fg(&self.color),
self.chr,
color::Fg(color::Reset)
)
}
}

View file

@ -1,36 +0,0 @@
use crate::display;
use crate::display::draw_box::{BoxStyle, Stylable};
use crate::entities::Entity;
use crate::types::{Neighbors, Position};
use std::io::{self, Write};
entity! {
pub struct Wall {
pub style: BoxStyle
}
}
static_description!(Wall, "a wall");
impl Wall {
pub fn new(position: Position, style: BoxStyle) -> Self {
new_entity!(Wall { position, style })
}
}
impl display::DrawWithNeighbors for Wall {
fn do_draw_with_neighbors<'a, 'b>(
&'a self,
out: &'b mut dyn Write,
neighbors: &'a Neighbors<Vec<&'a Box<dyn Entity>>>,
) -> io::Result<()> {
let neighbor_styles: Neighbors<Option<BoxStyle>> =
neighbors.map(|es| {
es.iter()
.filter_map(|e| e.downcast_ref::<Wall>())
.map(|wall| wall.style)
.next()
});
write!(out, "{}", neighbor_styles.style(self.style))
}
}

View file

@ -1,50 +0,0 @@
use crate::display;
use crate::entities::raws::{raw, EntityRaw, ItemType};
use crate::entities::{Describe, EntityID};
use crate::types::Position;
use std::io::{self, Write};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Item {
pub id: Option<EntityID>,
pub typ: &'static ItemType<'static>,
pub position: Position,
}
impl Item {
pub fn new_from_raw(name: &'static str, position: Position) -> Self {
match raw(name) {
EntityRaw::Item(typ) => Self::new_with_type(typ, position),
_ => panic!("Invalid raw type for {:?}, expected Item", name),
}
}
pub fn new_with_type(
typ: &'static ItemType<'static>,
position: Position,
) -> Self {
Item {
id: None,
typ,
position,
}
}
pub fn is_edible(&self) -> bool {
self.typ.is_edible()
}
}
entity!(Item);
impl Describe for Item {
fn description(&self) -> String {
self.typ.description.to_string()
}
}
impl display::Draw for Item {
fn do_draw(&self, out: &mut dyn Write) -> io::Result<()> {
write!(out, "{}", self.typ.chr)
}
}

View file

@ -1,20 +0,0 @@
#[macro_use]
pub mod entity;
#[macro_use]
pub mod util;
pub mod character;
pub mod creature;
pub mod entity_char;
pub mod environment;
pub mod item;
pub mod raw_types;
pub mod raws;
pub use character::Character;
pub use creature::Creature;
pub use entity::{AnEntity, Describe, Entity, Identified};
pub use entity_char::EntityChar;
pub use item::Item;
pub use raws::raw;
pub type EntityID = u32;

View file

@ -1,110 +0,0 @@
use crate::entities::entity_char::EntityChar;
use crate::messages::Message;
use crate::types::Speed;
#[derive(Debug, Deserialize)]
pub struct CreatureType<'a> {
/// The name of the creature. Used in raw lookups.
pub name: &'a str,
/// A description of the entity, used by the "look" command
pub description: &'a str,
#[serde(rename = "char")]
pub chr: EntityChar,
pub max_hitpoints: u16,
pub speed: Speed,
pub friendly: bool,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct EdibleItem<'a> {
#[serde(borrow)]
pub eat_message: Option<Message<'a>>,
/// The number of hitpoints that eating this item heals
pub hitpoints_healed: u16,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct ItemType<'a> {
pub name: &'a str,
/// A description of the item, used by the "look" command and when walking
/// over the item on the ground
pub description: &'a str,
/// A longer description of the item
pub long_description: &'a str,
pub edible_item: Option<EdibleItem<'a>>,
#[serde(rename = "char")]
pub chr: EntityChar,
}
#[cfg(test)]
mod item_type_tests {
use super::*;
#[test]
fn test_deserialize_item_type() {
let result = serde_json::from_str(
r#"{
"Item": {
"name": "noodles",
"description": "a big bowl o' noodles",
"long_description": "You know exactly what kind of noodles",
"char": { "char": "n" },
"edible_item": {
"eat_message": "You slurp up the noodles",
"hitpoints_healed": 2
}
}
}"#,
)
.unwrap();
assert_matches!(result, EntityRaw::Item(_));
if let EntityRaw::Item(item) = result {
assert_eq!(item.name, "noodles");
}
let toml_result = toml::from_str(
r#"[Item]
name = "noodles"
description = "a big bowl o' noodles"
long_description = "You know exactly what kind of noodles"
char = { char = "🍜" }
edible_item = { eat_message = "You slurp up the noodles", hitpoints_healed = 2 }
"#,
)
.unwrap();
assert_matches!(toml_result, EntityRaw::Item(_));
if let EntityRaw::Item(item) = toml_result {
assert_eq!(item.name, "noodles");
}
}
}
impl<'a> ItemType<'a> {
pub fn is_edible(&self) -> bool {
self.edible_item.is_some()
}
}
#[derive(Debug, Deserialize)]
pub enum EntityRaw<'a> {
Creature(#[serde(borrow)] CreatureType<'a>),
Item(#[serde(borrow)] ItemType<'a>),
}
impl<'a> EntityRaw<'a> {
pub fn name(&self) -> &'a str {
use EntityRaw::*;
match self {
Creature(typ) => typ.name,
Item(typ) => typ.name,
}
}
}

View file

@ -1,38 +0,0 @@
pub use crate::entities::raw_types::{CreatureType, EntityRaw, ItemType};
use std::collections::HashMap;
static_cfg! {
static ref RAWS: Vec<EntityRaw<'static>> = cfg_dir("src/entities/raws");
}
lazy_static! {
static ref RAWS_BY_NAME: HashMap<&'static str, &'static EntityRaw<'static>> = {
let mut hm = HashMap::new();
for er in RAWS.iter() {
if hm.contains_key(er.name()) {
panic!("Duplicate entity: {}", er.name())
}
hm.insert(er.name(), er);
}
hm
};
}
pub fn raw(name: &'static str) -> &'static EntityRaw<'static> {
RAWS_BY_NAME
.get(name)
.copied()
.unwrap_or_else(|| panic!("Raw not found: {}", name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raws() {
RAWS_BY_NAME.keys();
assert_eq!(raw("noodles").name(), "noodles");
}
}

View file

@ -1,10 +0,0 @@
[Creature]
name = "gormlak"
description = """
A chittering imp-like creature with bright yellow horns. It adores shiny objects
and gathers in swarms.
"""
char = { char = "g", color = "red" }
max_hitpoints = 5
speed = 120
friendly = false

View file

@ -1,15 +0,0 @@
{
"Item": {
"name": "noodles",
"char": {
"char": "n",
"color": "yellow"
},
"description": "a big bowl o' noodles",
"long_description": "You know exactly what kind of noodles",
"edible_item": {
"eat_message": "You slurp up the noodles",
"hitpoints_healed": 2
}
}
}

View file

@ -1,72 +0,0 @@
#[macro_export]
macro_rules! new_entity {
($name: ident) => {
new_entity!($name, {})
};
($name: ident { position: $position:expr $(, $fields:tt)* }) => {
$name {
id: None,
position: $position,
$($fields)*
}
};
($name: ident { $position:expr $(, $fields:tt)* }) => {
$name {
id: None,
position: $position,
$($fields)*
}
};
}
#[macro_export]
macro_rules! boring_entity {
($name:ident) => {
entity! {
pub struct $name {}
}
impl $name {
#[allow(dead_code)]
pub fn new(position: $crate::types::Position) -> Self {
$name { id: None, position }
}
}
};
($name:ident, char: $char: expr) => {
boring_entity!($name);
impl $crate::display::Draw for $name {
fn do_draw(&self, out: &mut Write) -> io::Result<()> {
write!(out, "{}", $char)
}
}
};
}
#[macro_export]
macro_rules! entity {
($name: ident) => {
positioned!($name);
positioned_mut!($name);
identified!($name, $crate::entities::EntityID);
impl $crate::entities::entity::Entity for $name {}
};
(pub struct $name:ident { $($struct_contents:tt)* } $($rest:tt)*) => {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct $name {
pub id: Option<$crate::entities::EntityID>,
pub position: $crate::types::Position,
$($struct_contents)*
}
entity!($name);
entity!($($rest)*);
};
() => {};
}

View file

@ -1,617 +0,0 @@
use crate::description::list_to_sentence;
use crate::display::{self, Viewport};
use crate::entities::entity::Describe;
use crate::entities::entity::Entity;
use crate::entities::{
AnEntity, Character, Creature, EntityID, Identified, Item,
};
use crate::messages::message;
use crate::settings::Settings;
use crate::types::command::Command;
use crate::types::entity_map::EntityMap;
use crate::types::{
pos, BoundingBox, Collision, Dimensions, Position, Positioned, 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;
use std::io::{self, StdinLock, StdoutLock, Write};
use termion::input::Keys;
use termion::input::TermRead;
use termion::raw::RawTerminal;
type Stdout<'a> = RawTerminal<StdoutLock<'a>>;
type Rng = SmallRng;
enum PromptResolution {
Uncancellable(Complete<String>),
Cancellable(Complete<Result<String, Cancelled>>),
}
/// The mode to use when describing entities on a tile to the user
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EntityDescriptionMode {
/// Describe the entities that the user is walking over.
///
/// This means:
/// - Skip the character themselves
/// - Describe nothing if there are no items other than the character
Walk,
/// Describe entities that the user is actively asking about.
///
/// This means:
/// - Describe the character themselves if they've asked to look at the tile
/// they're standing on
/// - Explicitly say there's nothing there if there's nothing there.
Look,
}
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,
viewport: Viewport<Stdout<'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>,
/// The entity ID of the player character
character_entity_id: EntityID,
/// The messages that have been said to the user, in forward time order
messages: Vec<String>,
/// The index of the currently-displayed message. Used to track the index of
/// the currently displayed message when handling PreviousMessage commands
message_idx: usize,
/// 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> {
pub fn new(
settings: Settings,
stdout: RawTerminal<StdoutLock<'a>>,
stdin: StdinLock<'a>,
w: u16,
h: u16,
) -> Game<'a> {
let rng = match settings.seed {
Some(seed) => SmallRng::seed_from_u64(seed),
None => SmallRng::from_entropy(),
};
let mut entities: EntityMap<AnEntity> = EntityMap::new();
// TODO make this dynamic
{
entities.insert(Box::new(Creature::new_from_raw(
"gormlak",
pos(10, 0),
)));
entities
.insert(Box::new(Item::new_from_raw("noodles", pos(0, 10))));
}
Game {
settings,
rng,
message_idx: 0,
viewport: Viewport::new(
BoundingBox::at_origin(Dimensions { w, h }),
BoundingBox::at_origin(Dimensions { w: w - 2, h: h - 2 }),
stdout,
),
keys: stdin.keys(),
input_state: Default::default(),
character_entity_id: entities.insert(Box::new(Character::new())),
messages: Vec::new(),
entities,
promises: Promises::new(),
}
}
fn downcast_entities_at<A: Entity>(&self, pos: Position) -> Vec<&A> {
self.entities
.at(pos)
.iter()
.filter_map(|e| e.downcast_ref())
.collect()
}
/// Returns a list of all creature entities at the given position
fn creatures_at(&self, pos: Position) -> Vec<&Creature> {
self.downcast_entities_at(pos)
}
/// Returns a list of all item entities at the given position
fn items_at(&self, pos: Position) -> Vec<&Item> {
self.downcast_entities_at(pos)
}
/// Returns a collision, if any, at the given Position in the game
fn collision_at(&self, pos: Position) -> Option<Collision> {
if !pos.within(self.viewport.inner) {
Some(Collision::Stop)
} else if self.creatures_at(pos).is_empty() {
None
} else {
Some(Collision::Combat)
}
}
fn character(&self) -> &Character {
(*self.entities.get(self.character_entity_id).unwrap())
.downcast_ref()
.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() {
self.viewport.draw(
entity,
&self.entities.neighbor_entities(entity.position()),
)?;
}
Ok(())
}
/// Draw all the game entities to the screen
fn draw_entities_at(&mut self, pos: Position) -> io::Result<()> {
for entity in self.entities.at(pos) {
self.viewport.draw(
entity,
&self.entities.neighbor_entities(entity.position()),
)?;
}
Ok(())
}
/// Draw the game entity with the given ID, if any, to the screen
fn draw_entity(&mut self, entity_id: EntityID) -> io::Result<bool> {
if let Some(entity) = self.entities.get(entity_id) {
self.viewport.draw(
entity,
&self.entities.neighbor_entities(entity.position()),
)?;
Ok(true)
} else {
Ok(false)
}
}
/// Describe all the entities at a given position to the user.
///
/// If `force` is not set to `true`, will not do anything if there are no
/// entities
fn describe_entities_at(
&mut self,
pos: Position,
mode: EntityDescriptionMode,
) -> io::Result<()> {
use EntityDescriptionMode::*;
let mut entities = self.entities.at(pos);
if mode == Walk {
entities.retain(|e| e.id() != self.character_entity_id);
}
if entities.is_empty() {
match mode {
Walk => return Ok(()),
Look => {
return self.say(
"global.describe_no_entities",
&template_params!(),
)
}
}
}
let descriptions = list_to_sentence(
&entities
.iter()
.map(|e| e.description())
.collect::<Vec<String>>(),
);
self.say(
"global.describe_entities",
&template_params!({ "descriptions" => &descriptions, }),
)
}
/// Remove the given entity from the game, drawing over it if it's visible
fn remove_entity(&mut self, entity_id: EntityID) -> io::Result<()> {
if let Some(entity) = self.entities.remove(entity_id) {
self.viewport.clear(entity.position())?;
}
Ok(())
}
/// Step the game forward the given number of ticks
fn tick(&mut self, _ticks: Ticks) {}
/// Get a message from the global map based on the rng in this game
fn message<'params>(
&mut self,
name: &'static str,
params: &TemplateParams<'params>,
) -> String {
message(name, &mut self.rng, params)
}
/// Say a message to the user
fn say<'params>(
&mut self,
message_name: &'static str,
params: &TemplateParams<'params>,
) -> io::Result<()> {
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)?;
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<()> {
if self.message_idx == 0 {
return Ok(());
}
self.message_idx -= 1;
let message = &self.messages[self.message_idx];
self.viewport.write_message(message)?;
Ok(())
}
fn clear_message(&mut self) -> io::Result<()> {
debug!("{:?} {:?}", self.message_idx, self.messages);
if self.message_idx == self.messages.len() {
return Ok(());
}
self.viewport.clear_message()?;
self.message_idx += 1;
Ok(())
}
fn creature(&self, creature_id: EntityID) -> Option<&Creature> {
self.entities
.get(creature_id)
.and_then(|e| e.downcast_ref::<Creature>())
}
fn expect_creature(&self, creature_id: EntityID) -> &Creature {
self.creature(creature_id).unwrap_or_else(|| {
panic!("Creature ID went away: {:?}", creature_id)
})
}
fn mut_creature(&mut self, creature_id: EntityID) -> Option<&mut Creature> {
self.entities
.get_mut(creature_id)
.and_then(|e| e.downcast_mut::<Creature>())
}
fn expect_mut_creature(&mut self, creature_id: EntityID) -> &mut Creature {
self.mut_creature(creature_id).unwrap_or_else(|| {
panic!("Creature ID went away: {:?}", creature_id)
})
}
fn attack(&mut self, creature_id: EntityID) -> io::Result<()> {
info!("Attacking creature {:?}", creature_id);
let damage = self.character().damage();
let creature_name = self.expect_creature(creature_id).typ.name;
let tps = template_params!({
"creature" => {
"name" => creature_name,
},
});
self.say("combat.attack", &tps)?;
let creature = self.expect_mut_creature(creature_id);
creature.damage(damage);
if creature.dead() {
self.say("combat.killed", &tps)?;
info!("Killed creature {:?}", creature_id);
self.remove_entity(creature_id)?;
}
Ok(())
}
fn attack_at(&mut self, pos: Position) -> io::Result<()> {
let creatures = self.creatures_at(pos);
match creatures.len() {
0 => Ok(()),
1 => {
let creature = creatures.get(0).unwrap();
let creature_id = creature.id();
self.attack(creature_id)
}
_ => {
// TODO prompt with a menu of creatures to combat
unimplemented!()
}
}
}
fn pick_up(&mut self) -> io::Result<()> {
let pos = self.character().position;
let items = self.items_at(pos);
match items.len() {
0 => Ok(()),
1 => {
let item_id = items.get(0).unwrap().id();
let item: Box<Item> =
self.entities.remove(item_id).unwrap().downcast().unwrap();
let desc = item.description();
self.mut_character().inventory.push(item);
self.say(
"global.pick_up",
&template_params!({
"item" => { "name" => &desc, },
}),
)
}
_ => {
// TODO prompt with a menu of items to pick up
unimplemented!()
}
}
}
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.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;
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(Combat) => {
self.attack_at(new_pos)?;
}
Some(Stop) => (),
}
}
Some(PreviousMessage) => self.previous_message()?,
Some(PickUp) => self.pick_up()?,
None => (),
}
if let Some(old_pos) = old_position {
let character = self.character();
let char_pos = character.position;
self.viewport.game_cursor_position = char_pos;
self.viewport.clear(old_pos)?;
self.draw_entities_at(old_pos)?;
self.draw_entity(self.character_entity_id)?;
self.clear_message()?;
self.describe_entities_at(
char_pos,
EntityDescriptionMode::Walk,
)?;
self.tick(
self.character().speed().tiles_to_ticks(
(old_pos - char_pos).as_tiles(),
),
);
}
}
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()?;
}
_ => {}
}
}
}
self.flush()?;
self.flush_promises();
debug!("{:?}", self.character());
}
Ok(())
}
}
impl<'a> Drop for Game<'a> {
fn drop(&mut self) {
display::clear(self).unwrap_or(());
}
}
impl<'a> Write for Game<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.viewport.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.viewport.flush()
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.viewport.write_all(buf)
}
}
impl<'a> Positioned for Game<'a> {
fn position(&self) -> Position {
Position { x: 0, y: 0 }
}
}

View file

@ -1,120 +0,0 @@
use crate::level_gen::util::fill_outer_edges;
use crate::level_gen::util::rand_initialize;
use crate::types::Dimensions;
use rand::Rng;
pub struct Params {
chance_to_start_alive: f64,
birth_limit: i32,
death_limit: i32,
steps: usize,
}
macro_rules! parse_optional {
($out: ident . $attr: ident, $matches: expr, $arg: expr) => {
if let Some(val_s) = $matches.value_of($arg) {
$out.$attr = val_s.parse().unwrap();
}
};
}
macro_rules! parse_optional_matches {
($matches: expr) => {};
($matches: expr , { $ret: ident . $attr: ident = $arg: expr }) => {
parse_optional!($ret.$attr, $matches, $arg);
};
($matches: expr, { $($ret: ident . $attr: ident = $arg: expr ,)* }) => {
$(parse_optional!($ret.$attr, $matches, $arg);)*
};
}
impl Params {
pub fn from_matches<'a>(matches: &clap::ArgMatches<'a>) -> Self {
let mut ret: Self = Default::default();
parse_optional_matches!(matches, {
ret.chance_to_start_alive = "start-alive-chance",
ret.birth_limit = "birth-limit",
ret.death_limit = "death-limit",
ret.steps = "steps",
});
ret
}
}
impl Default for Params {
fn default() -> Self {
Params {
chance_to_start_alive: 0.45,
birth_limit: 4,
death_limit: 3,
steps: 2,
}
}
}
pub fn generate<R: Rng + ?Sized>(
dimensions: Dimensions,
params: &Params,
rand: &mut R,
) -> Vec<Vec<bool>> {
let mut cells =
rand_initialize(dimensions, rand, params.chance_to_start_alive);
for _ in 0..params.steps {
step_automata(&mut cells, dimensions, params);
}
fill_outer_edges(&mut cells);
cells
}
fn step_automata(
cells: &mut Vec<Vec<bool>>,
dimensions: Dimensions,
params: &Params,
) {
let orig_cells = (*cells).clone();
for x in 0..(dimensions.h as usize) {
for y in 0..(dimensions.w as usize) {
let nbs = num_alive_neighbors(&orig_cells, x as i32, y as i32);
if orig_cells[x][y] {
if nbs < params.death_limit {
cells[x][y] = false;
} else {
cells[x][y] = true;
}
} else if nbs > params.birth_limit {
cells[x][y] = true;
} else {
cells[x][y] = false;
}
}
}
}
const COUNT_EDGES_AS_NEIGHBORS: bool = true;
fn num_alive_neighbors(cells: &[Vec<bool>], x: i32, y: i32) -> i32 {
let mut count = 0;
for i in -1..2 {
for j in -1..2 {
if i == 0 && j == 0 {
continue;
}
let neighbor_x = x + i;
let neighbor_y = y + j;
if (COUNT_EDGES_AS_NEIGHBORS
&& (neighbor_x < 0
|| neighbor_y < 0
|| neighbor_x >= (cells.len() as i32)
|| neighbor_y >= (cells[0].len()) as i32))
|| cells[neighbor_x as usize][neighbor_y as usize]
{
count += 1;
}
}
}
count
}

View file

@ -1,101 +0,0 @@
use crate::display::draw_box::BoxStyle;
use crate::display::utils::clone_times;
use crate::display::DrawWithNeighbors;
use crate::entities::entity::Entity;
use crate::entities::environment::Wall;
use crate::types::entity_map::EntityMap;
use crate::types::pos;
use itertools::Itertools;
use std::io;
pub mod cave_automata;
pub mod util;
pub fn level_to_entities(level: Vec<Vec<bool>>) -> EntityMap<Box<dyn Entity>> {
let mut res: EntityMap<Box<dyn Entity>> = EntityMap::new();
let xmax = level.len() as i16;
let ymax = if xmax == 0 {
0i16
} else {
level[0].len() as i16
};
let get = |mut x: i16, mut y: i16| {
if x < 0 {
x = 0;
}
if y < 0 {
y = 0;
}
if x >= xmax - 1 {
x = xmax - 1;
}
if y >= ymax - 1 {
y = ymax - 1;
}
level[x as usize][y as usize]
};
for x in 0..xmax {
for y in 0..ymax {
if get(x, y) {
// don't output walls that are surrounded on all 8 sides by
// walls
if (x == 0 || get(x - 1, y))
&& (y == 0 || get(x, y - 1))
&& (x == xmax - 1 || get(x + 1, y))
&& (y == ymax - 1 || get(x, y + 1))
&& ((x == 0 && y == 0) || get(x - 1, y - 1))
&& ((x == 0 && y == ymax - 1) || get(x - 1, y + 1))
&& ((x == xmax - 1 && y == 0) || get(x + 1, y - 1))
&& ((x == xmax - 1 && y == ymax - 1) || get(x + 1, y + 1))
{
continue;
}
res.insert(Box::new(Wall::new(
pos(y as i16, x as i16),
BoxStyle::Thin,
)));
}
}
}
res
}
pub fn draw_level<W: io::Write>(
level: Vec<Vec<bool>>,
out: &mut W,
) -> io::Result<()> {
if level.is_empty() {
return Ok(());
}
let mut lines = clone_times::<Vec<char>, Vec<Vec<char>>>(
clone_times(' ', level[0].len() as u16),
level.len() as u16,
);
let em = level_to_entities(level);
for entity in em.entities() {
let mut buf = Vec::new();
entity.do_draw_with_neighbors(
&mut buf,
&em.neighbor_entities(entity.position()),
)?;
let buf_s = std::str::from_utf8(&buf).unwrap();
if let Some(chr) = buf_s.chars().next() {
lines[entity.position().y as usize][entity.position().x as usize] =
chr;
}
}
let res = lines
.iter()
.map(|line| line.iter().collect::<String>())
.join("\n");
write!(out, "{}", res)
}

View file

@ -1,52 +0,0 @@
use crate::types::Dimensions;
use rand::{distributions, Rng};
pub fn falses(dims: Dimensions) -> Vec<Vec<bool>> {
let mut ret = Vec::with_capacity(dims.h as usize);
for _ in 0..dims.h {
let mut row = Vec::with_capacity(dims.w as usize);
for _ in 0..dims.w {
row.push(false);
}
ret.push(row);
}
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,
alive_chance: f64,
) -> Vec<Vec<bool>> {
let distrib = distributions::Bernoulli::new(alive_chance).unwrap();
let mut ret = Vec::with_capacity(dims.h as usize);
for _ in 0..dims.h {
let mut row = Vec::with_capacity(dims.w as usize);
for _ in 0..dims.w {
row.push(rng.sample(distrib));
}
ret.push(row);
}
ret
}
/// Fill the outer edges of a generated level with walls
pub fn fill_outer_edges(level: &mut Vec<Vec<bool>>) {
let xmax = level.len();
if xmax == 0 {
return;
}
let ymax = level[0].len();
for row in level.iter_mut() {
row[0] = true;
row[ymax - 1] = true;
}
for y in 0..level[0].len() {
level[0][y] = true;
level[xmax - 1][y] = true;
}
}

View file

@ -1,130 +0,0 @@
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate clap;
#[macro_use]
extern crate prettytable;
#[macro_use]
extern crate lazy_static;
#[cfg(test)]
#[macro_use]
extern crate maplit;
#[macro_use]
extern crate downcast_rs;
#[macro_use]
extern crate include_dir;
#[macro_use]
extern crate nom;
#[cfg(test)]
#[macro_use]
extern crate matches;
#[macro_use]
mod util;
#[macro_use]
mod types;
#[macro_use]
mod entities;
mod description;
mod display;
mod game;
mod level_gen;
mod messages;
mod settings;
use crate::types::Dimensions;
use clap::App;
use game::Game;
use prettytable::format::consts::FORMAT_BOX_CHARS;
use rand::rngs::SmallRng;
use rand::SeedableRng;
use settings::Settings;
use backtrace::Backtrace;
use std::io::{self, StdinLock, StdoutLock};
use std::panic;
use termion;
use termion::raw::IntoRawMode;
use termion::raw::RawTerminal;
fn init(
settings: Settings,
stdout: RawTerminal<StdoutLock<'_>>,
stdin: StdinLock<'_>,
w: u16,
h: u16,
) -> io::Result<()> {
panic::set_hook(if settings.logging.print_backtrace {
Box::new(|info| (error!("{}\n{:#?}", info, Backtrace::new())))
} else {
Box::new(|info| (error!("{}\n{:#?}", info, Backtrace::new())))
});
let game = Game::new(settings, stdout, stdin, w, h);
game.run()
}
fn generate_level<'a, W: io::Write>(
stdout: &mut W,
params: &clap::ArgMatches<'a>,
) -> io::Result<()> {
let mut rand = SmallRng::from_entropy();
let mut dimensions: Dimensions = Default::default();
if let Some(h_s) = params.value_of("height") {
dimensions.h = h_s.parse().unwrap();
}
if let Some(w_s) = params.value_of("width") {
dimensions.w = w_s.parse().unwrap();
}
let level = match params.value_of("generator") {
None => panic!("Must supply a generator with --generator"),
Some("cave_automata") => level_gen::cave_automata::generate(
dimensions,
&level_gen::cave_automata::Params::from_matches(params),
&mut rand,
),
Some(gen) => panic!("Unrecognized generator: {}", gen),
};
level_gen::draw_level(level, stdout)
}
fn main() -> io::Result<()> {
let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml).get_matches();
let settings = Settings::load().unwrap();
settings.logging.init_log();
let stdout = io::stdout();
let mut stdout = stdout.lock();
let stdin = io::stdin();
let stdin = stdin.lock();
let termsize = termion::terminal_size().ok();
let (termwidth, termheight) = termsize.unwrap_or((70, 40));
match matches.subcommand() {
("info", _) => {
let mut table = table!(
[br->"termwidth", termwidth],
[br->"termheight", termheight],
[br->"logfile", settings.logging.file],
[br->"loglevel", settings.logging.level]
);
table.set_format(*FORMAT_BOX_CHARS);
table.printstd();
Ok(())
}
("generate-level", params) => {
generate_level(&mut stdout, params.unwrap())
}
_ => {
let stdout = stdout.into_raw_mode().unwrap();
init(settings, stdout, stdin, termwidth, termheight)
}
}
}

View file

@ -1,166 +0,0 @@
use crate::util::template::Template;
use crate::util::template::TemplateParams;
use rand::seq::SliceRandom;
use rand::Rng;
use std::collections::HashMap;
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
pub enum Message<'a> {
#[serde(borrow)]
Single(Template<'a>),
Choice(Vec<Template<'a>>),
}
impl<'a> Message<'a> {
fn resolve<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&Template<'a>> {
use Message::*;
match self {
Single(msg) => Some(msg),
Choice(msgs) => msgs.choose(rng),
}
}
}
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
enum NestedMap<'a> {
#[serde(borrow)]
Direct(Message<'a>),
#[serde(borrow)]
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,
}
}
}
#[cfg(test)]
mod nested_map_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(Template::parse("Hello World!").unwrap())),
}),
"foo" => NestedMap::Nested(hashmap!{
"bar" => NestedMap::Nested(hashmap!{
"single" => NestedMap::Direct(Message::Single(
Template::parse("Single").unwrap()
)),
"choice" => NestedMap::Direct(Message::Choice(
vec![
Template::parse("Say this").unwrap(),
Template::parse("Or this").unwrap()
]
))
})
})
}))
)
}
#[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(Template::parse("Hello World!").unwrap()))
);
assert_eq!(
map.lookup("foo.bar.single"),
Some(&Message::Single(Template::parse("Single").unwrap()))
);
assert_eq!(
map.lookup("foo.bar.choice"),
Some(&Message::Choice(vec![
Template::parse("Say this").unwrap(),
Template::parse("Or this").unwrap()
]))
);
}
}
// static MESSAGES_RAW: &'static str = include_str!("messages.toml");
static_cfg! {
static ref MESSAGES: NestedMap<'static> = toml_file("messages.toml");
}
pub fn get<R: Rng + ?Sized>(
name: &'static str,
rng: &mut R,
) -> Option<&'static Template<'static>> {
MESSAGES.lookup(name).and_then(|msg| msg.resolve(rng))
}
/// Look up and format 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<'a, R: Rng + ?Sized>(
name: &'static str,
rng: &mut R,
params: &TemplateParams<'a>,
) -> String {
match get(name, rng) {
Some(msg) => msg.format(params).unwrap_or_else(|e| {
error!("Error formatting template: {}", e);
"Template Error".to_string()
}),
None => {
error!("Message not found: {}", name);
"Template Not Found".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::SmallRng;
use rand::SeedableRng;
#[test]
fn test_static_messages() {
message(
"global.welcome",
&mut SmallRng::from_entropy(),
&template_params!(),
);
}
}

View file

@ -1,27 +0,0 @@
[global]
welcome = "Welcome to Xanthous, {{character.name}}! It's dangerous out there, why not stay inside?"
describe_entities = "You see here {{descriptions}}"
describe_no_entities = "You see nothing here."
pick_up = "You pick up the {{item.name}}."
[combat]
attack = "You attack the {{creature.name}}."
killed = [
"You've killed the {{creature.name}}.",
"The {{creature.name}} dies.",
"The {{creature.name}} kicks it.",
"The {{creature.name}} beefs it."
]
[character]
name_prompt = [
"Hey there friend. What's your name?",
"Hey there friend. What should we call you?",
"Howdy. What's your name?",
"Name please!",
"What's your name?",
"Hey, what's your name?",
]
[defaults.item]
eat = "You eat the {{item.name}}. {{action.result}}"

View file

@ -1,70 +0,0 @@
use config::{Config, ConfigError};
use log::LevelFilter;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
#[derive(Debug, Deserialize, Clone)]
pub struct Logging {
#[serde(default = "Logging::default_level")]
pub level: LevelFilter,
#[serde(default = "Logging::default_file")]
pub file: String,
#[serde(default = "Logging::default_print_backtrace")]
pub print_backtrace: bool,
}
impl Default for Logging {
fn default() -> Self {
Logging {
level: LevelFilter::Off,
file: "debug.log".to_string(),
print_backtrace: true,
}
}
}
impl Logging {
pub fn init_log(&self) {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} {l} - {m}\n")))
.build(self.file.clone())
.unwrap();
let config = log4rs::config::Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.build(Root::builder().appender("logfile").build(self.level))
.unwrap();
log4rs::init_config(config).unwrap();
}
fn default_level() -> LevelFilter {
Logging::default().level
}
fn default_file() -> String {
Logging::default().file
}
fn default_print_backtrace() -> bool {
Logging::default().print_backtrace
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Settings {
pub seed: Option<u64>,
pub logging: Logging,
}
impl Settings {
pub fn load() -> Result<Self, ConfigError> {
let mut s = Config::new();
s.merge(config::File::with_name("Config").required(false))?;
s.merge(config::Environment::with_prefix("XAN"))?;
s.try_into()
}
}

View file

@ -1,9 +0,0 @@
/// Describes a kind of game collision
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Collision {
/// Stop moving - you can't move there!
Stop,
/// Moving into an entity at the given position indicates combat
Combat,
}

View file

@ -1,41 +0,0 @@
use super::Direction;
use super::Direction::*;
use termion::event::Key;
use termion::event::Key::{Char, Ctrl};
pub enum Command {
/// Quit the game
Quit,
/// Move the character in a direction
Move(Direction),
/// Pick up any item(s) at the current position
PickUp,
/// Display the previous message
PreviousMessage,
}
impl Command {
pub fn from_key(k: Key) -> Option<Command> {
use Command::*;
match k {
Char('q') => Some(Quit),
Char('h') | Char('a') | Key::Left => Some(Move(Left)),
Char('k') | Char('w') | Key::Up => Some(Move(Up)),
Char('j') | Char('s') | Key::Down => Some(Move(Down)),
Char('l') | Char('d') | Key::Right => Some(Move(Right)),
Char('y') => Some(Move(UpLeft)),
Char('u') => Some(Move(UpRight)),
Char('b') => Some(Move(DownLeft)),
Char('n') => Some(Move(DownRight)),
Ctrl('p') => Some(PreviousMessage),
Char(',') => Some(PickUp),
_ => None,
}
}
}

View file

@ -1,13 +0,0 @@
use proptest_derive::Arbitrary;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub enum Direction {
Left,
Up,
Down,
Right,
UpLeft,
UpRight,
DownRight,
DownLeft,
}

View file

@ -1,430 +0,0 @@
use crate::entities::entity::Identified;
use crate::entities::EntityID;
use crate::types::Neighbors;
use crate::types::Position;
use crate::types::Positioned;
use crate::types::PositionedMut;
use alga::general::{
AbstractMagma, AbstractMonoid, AbstractSemigroup, Additive, Identity,
};
use std::collections::{hash_map, BTreeMap, HashMap};
use std::iter::FromIterator;
#[derive(Debug, Clone, Default)]
pub struct EntityMap<A> {
by_position: BTreeMap<Position, Vec<EntityID>>,
by_id: HashMap<EntityID, A>,
last_id: EntityID,
}
impl<A: PartialEq> PartialEq for EntityMap<A> {
fn eq(&self, other: &Self) -> bool {
self.by_position == other.by_position && self.by_id == other.by_id
}
}
impl<A: Eq> Eq for EntityMap<A> {}
const BY_POS_INVARIANT: &str =
"Invariant: All references in EntityMap.by_position should point to existent references in by_id";
impl<A> EntityMap<A> {
pub fn new() -> EntityMap<A> {
EntityMap {
by_position: BTreeMap::new(),
by_id: HashMap::new(),
last_id: 0,
}
}
pub fn len(&self) -> usize {
self.by_id.len()
}
/// Returns a list of all entities at the given position
pub fn at<'a>(&'a self, pos: Position) -> Vec<&'a A> {
self.by_position
.get(&pos)
.iter()
.flat_map(|eids| {
eids.iter()
.map(|eid| self.by_id.get(eid).expect(BY_POS_INVARIANT))
})
.collect()
}
/// Remove all entities at the given position
pub fn remove_all_at(&mut self, pos: Position) {
if let Some(eids) = self.by_position.remove(&pos) {
for eid in eids {
self.by_id.remove(&eid).expect(BY_POS_INVARIANT);
}
}
}
pub fn get(&self, id: EntityID) -> Option<&A> {
self.by_id.get(&id)
}
pub fn get_mut(&mut self, id: EntityID) -> Option<&mut A> {
self.by_id.get_mut(&id)
}
pub fn entities(&self) -> impl Iterator<Item = &A> {
self.by_id.values()
}
pub fn entities_mut(&mut self) -> impl Iterator<Item = &mut A> {
self.by_id.values_mut()
}
pub fn ids(&self) -> hash_map::Keys<'_, EntityID, A> {
self.by_id.keys()
}
pub fn drain(&mut self) -> Drain<'_, A> {
let ids = self.ids().copied().collect::<Vec<_>>();
Drain {
map: self,
ids_iter: Box::new(ids.into_iter()),
}
}
fn next_id(&mut self) -> EntityID {
self.last_id += 1;
self.last_id
}
}
impl<A: Positioned + Identified<EntityID>> EntityMap<A> {
pub fn insert(&mut self, mut entity: A) -> EntityID {
let pos = entity.position();
let entity_id = self.next_id();
entity.set_id(entity_id);
self.by_id.entry(entity_id).or_insert(entity);
self.by_position
.entry(pos)
.or_insert_with(Vec::new)
.push(entity_id);
entity_id
}
/// Remove the entity with the given ID
pub fn remove(&mut self, id: EntityID) -> Option<A> {
self.by_id.remove(&id).map(|e| {
let mut empty = false;
let position = e.position();
if let Some(es) = self.by_position.get_mut(&position) {
es.retain(|e| *e != id);
if es.is_empty() {
empty = true;
}
}
if empty {
self.by_position.remove(&position);
}
e
})
}
/// Moves all elements from `other` into `Self`, leathing `other` empty.
pub fn append(&mut self, other: &mut Self) {
// TODO there's probably some perf opportunities here by calling
// reserve() on stuff
for (_, entity) in other.drain() {
self.insert(entity);
}
}
/// Gets all 8 neighbors of the given position.
pub fn neighbors<'a>(
&'a self,
position: Position,
) -> Neighbors<Vec<(EntityID, &'a A)>> {
Neighbors::of_position(position)
.map(|pos| self.at(*pos))
.mapmap(&|e| (e.id(), *e))
}
pub fn neighbor_entities<'a>(
&'a self,
position: Position,
) -> Neighbors<Vec<&'a A>> {
self.neighbors(position).mapmap(&|(_eid, ent)| *ent)
}
pub fn check_invariants(&self) {
for (id, ent) in &self.by_id {
assert_eq!(*id, ent.id());
}
for (pos, ents) in &self.by_position {
for eid in ents {
let ent = self.by_id.get(eid).unwrap();
assert_eq!(*pos, ent.position())
}
}
}
}
impl<'a, A: Positioned + Identified<EntityID>> IntoIterator
for &'a EntityMap<A>
{
type Item = (&'a EntityID, &'a A);
type IntoIter = std::collections::hash_map::Iter<'a, EntityID, A>;
fn into_iter(self) -> Self::IntoIter {
(&self.by_id).iter()
}
}
impl<A: Positioned + Identified<EntityID>> IntoIterator for EntityMap<A> {
type Item = (EntityID, A);
type IntoIter = std::collections::hash_map::IntoIter<EntityID, A>;
fn into_iter(self) -> Self::IntoIter {
self.by_id.into_iter()
}
}
impl<A: Positioned + Identified<EntityID>> FromIterator<A> for EntityMap<A> {
fn from_iter<I: IntoIterator<Item = A>>(iter: I) -> Self {
let mut em = EntityMap::new();
for ent in iter {
em.insert(ent);
}
em
}
}
impl<A: Positioned + Identified<EntityID> + Eq + Clone> AbstractMagma<Additive>
for EntityMap<A>
{
fn operate(&self, right: &Self) -> Self {
let mut by_position = self.by_position.clone();
by_position.append(&mut right.by_position.clone());
let mut by_id = self.by_id.clone();
for (k, v) in right.by_id.clone() {
by_id.insert(k, v);
}
EntityMap {
by_position,
by_id,
last_id: self.last_id.max(right.last_id),
}
}
}
impl<A: Positioned + Identified<EntityID> + Eq + Clone>
AbstractSemigroup<Additive> for EntityMap<A>
{
}
impl<A: Positioned + Identified<EntityID> + Eq> Identity<Additive>
for EntityMap<A>
{
fn identity() -> Self {
EntityMap::new()
}
}
impl<A: Positioned + Identified<EntityID> + Eq + Clone> AbstractMonoid<Additive>
for EntityMap<A>
{
}
impl<A: PositionedMut> EntityMap<A> {
pub fn update_position(
&mut self,
entity_id: EntityID,
new_position: Position,
) {
let mut old_pos = None;
if let Some(entity) = self.by_id.get_mut(&entity_id) {
if entity.position() == new_position {
return;
}
old_pos = Some(entity.position());
entity.set_position(new_position);
}
if let Some(p) = old_pos {
if let Some(es) = self.by_position.get_mut(&p) {
es.retain(|e| *e != entity_id);
}
self.by_position
.entry(new_position)
.or_insert_with(Vec::new)
.push(entity_id);
}
}
}
pub struct Drain<'a, A> {
map: &'a mut EntityMap<A>,
ids_iter: Box<dyn Iterator<Item = EntityID> + 'a>,
}
impl<A: Positioned + Identified<EntityID>> Iterator for Drain<'_, A> {
type Item = (EntityID, A);
fn next(&mut self) -> Option<Self::Item> {
self.ids_iter
.next()
.map(|eid| (eid, self.map.remove(eid).expect(BY_POS_INVARIANT)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::PositionedMut;
use proptest::prelude::*;
use proptest_derive::Arbitrary;
#[derive(Debug, Arbitrary, PartialEq, Eq, Clone)]
struct TestEntity {
_id: Option<EntityID>,
position: Position,
name: String,
}
impl Positioned for TestEntity {
fn position(&self) -> Position {
self.position
}
}
impl PositionedMut for TestEntity {
fn set_position(&mut self, pos: Position) {
self.position = pos
}
}
impl Identified<EntityID> for TestEntity {
fn opt_id(&self) -> Option<EntityID> {
self._id
}
fn set_id(&mut self, id: EntityID) {
self._id = Some(id);
}
}
fn gen_entity_map() -> BoxedStrategy<EntityMap<TestEntity>> {
any::<Vec<TestEntity>>()
.prop_map(|ents| {
ents.iter().cloned().collect::<EntityMap<TestEntity>>()
})
.boxed()
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10))]
#[test]
fn test_entity_map_len(items: Vec<TestEntity>) {
let mut map = EntityMap::new();
assert_eq!(map.len(), 0);
for ent in &items {
map.insert(ent.clone());
}
assert_eq!(map.len(), items.len());
}
#[test]
fn test_entity_map_getset(
mut em in gen_entity_map(),
ent: TestEntity
) {
em.insert(ent.clone());
assert!(em.at(ent.position).iter().any(|e| e.name == ent.name))
}
#[test]
fn test_entity_map_set_iter_contains(
mut em in gen_entity_map(),
ent: TestEntity
) {
em.insert(ent.clone());
assert!(em.entities().any(|e| e.name == ent.name))
}
#[test]
fn test_update_position(
mut em in gen_entity_map(),
ent: TestEntity,
new_position: Position,
) {
let original_position = ent.position();
let entity_id = em.insert(ent.clone());
em.update_position(entity_id, new_position);
if new_position != original_position {
assert!(em.at(original_position).iter().all(|e| e.name != ent.name));
}
assert_eq!(
em.get(entity_id).map(|e| e.position()),
Some(new_position)
);
assert!(
em.at(new_position).iter().map(
|e| e.name.clone()).any(|en| en == ent.name),
)
}
#[test]
fn test_remove_all_at(
mut em in gen_entity_map(),
pos: Position,
) {
em.remove_all_at(pos);
assert_eq!(em.at(pos).len(), 0);
}
#[test]
fn test_entity_map_semigroup_laws(
em1 in gen_entity_map(),
em2 in gen_entity_map(),
em3 in gen_entity_map(),
) {
assert!(AbstractSemigroup::prop_is_associative((em1, em2, em3)));
}
fn test_entity_map_monoid_laws(
em in gen_entity_map(),
) {
assert!(
AbstractMonoid::prop_operating_identity_element_is_noop((em,))
);
}
#[test]
fn test_entity_map_append(
mut target in gen_entity_map(),
mut source in gen_entity_map(),
) {
let orig_target = target.clone();
let orig_source = source.clone();
target.append(&mut source);
target.check_invariants();
assert_eq!(source, EntityMap::new());
for ent in orig_source.entities() {
assert!(
target.at(ent.position()).iter().any(|e| e.name == ent.name)
);
}
for ent in orig_target.entities() {
assert!(
target.at(ent.position()).iter().any(|e| e.name == ent.name)
);
}
}
}
}

View file

@ -1,31 +0,0 @@
use crate::types::Dimensions;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MenuInfo {
pub prompt: String,
pub options: Vec<String>,
}
impl MenuInfo {
pub fn new(prompt: String, options: Vec<String>) -> Self {
MenuInfo { prompt, options }
}
/// Returns the inner dimensions of a box necessary to draw this menu. Will
/// not trim either dimension to the size of the terminal
pub fn dimensions(&self) -> Dimensions {
Dimensions {
w: self
.options
.iter()
.map(|s| s.len())
.max()
.unwrap_or(0)
.max(self.prompt.len()) as u16
+ 4,
h: self.options.len() as u16
+ if self.prompt.is_empty() { 0 } else { 2 }
+ 4,
}
}
}

View file

@ -1,504 +0,0 @@
#![allow(clippy::unit_arg)]
#![allow(clippy::identity_conversion)]
use std::cmp::max;
use std::cmp::Ordering;
use std::ops;
use std::rc::Rc;
pub mod collision;
pub mod command;
pub mod direction;
pub mod entity_map;
pub mod menu;
pub use collision::Collision;
pub use direction::Direction;
pub use direction::Direction::*;
use proptest_derive::Arbitrary;
use termion::cursor;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub struct Dimensions {
#[proptest(strategy = "std::ops::Range::<u16>::from(0..100)")]
pub w: u16,
#[proptest(strategy = "std::ops::Range::<u16>::from(0..100)")]
pub h: u16,
}
pub const ZERO_DIMENSIONS: Dimensions = Dimensions { w: 0, h: 0 };
pub const UNIT_DIMENSIONS: Dimensions = Dimensions { w: 1, h: 1 };
impl Default for Dimensions {
fn default() -> Self {
Dimensions { w: 80, h: 20 }
}
}
impl ops::Sub<Dimensions> for Dimensions {
type Output = Dimensions;
fn sub(self, dims: Dimensions) -> Dimensions {
Dimensions {
w: self.w - dims.w,
h: self.h - dims.h,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub struct BoundingBox {
pub dimensions: Dimensions,
pub position: Position,
}
impl BoundingBox {
pub fn at_origin(dimensions: Dimensions) -> BoundingBox {
BoundingBox {
dimensions,
position: ORIGIN,
}
}
pub fn from_corners(
top_left: Position,
lower_right: Position,
) -> BoundingBox {
BoundingBox {
position: top_left,
dimensions: Dimensions {
w: (lower_right.x - top_left.x) as u16,
h: (lower_right.y - top_left.y) as u16,
},
}
}
pub fn lr_corner(self) -> Position {
self.position
+ (Position {
x: self.dimensions.w as i16,
y: self.dimensions.h as i16,
})
}
pub fn ll_corner(self) -> Position {
self.position
+ (Position {
x: 0,
y: self.dimensions.h as i16,
})
}
/// Returns a bounding box representing the *inside* of this box if it was
/// drawn on the screen.
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 {
type Output = BoundingBox;
fn add(self, pos: Position) -> BoundingBox {
BoundingBox {
position: self.position + pos,
..self
}
}
}
impl ops::Sub<Dimensions> for BoundingBox {
type Output = BoundingBox;
fn sub(self, dims: Dimensions) -> BoundingBox {
BoundingBox {
dimensions: self.dimensions - dims,
..self
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary, Hash, Ord)]
pub struct Position {
/// x (horizontal) position
#[proptest(strategy = "std::ops::Range::<i16>::from(0..100)")]
pub x: i16,
#[proptest(strategy = "std::ops::Range::<i16>::from(0..100)")]
/// y (vertical) position
pub y: i16,
}
pub fn pos(x: i16, y: i16) -> Position {
Position { x, y }
}
pub const ORIGIN: Position = Position { x: 0, y: 0 };
pub const UNIT_POSITION: Position = Position { x: 1, y: 1 };
impl Position {
/// Returns true if this position exists within the bounds of the given box,
/// inclusive
pub fn within(self, b: BoundingBox) -> bool {
(self > b.position - UNIT_POSITION) && self < (b.lr_corner())
}
/// Returns a sequence of ASCII escape characters for moving the cursor to
/// this Position
pub fn cursor_goto(self) -> cursor::Goto {
// + 1 because Goto is 1-based, but position is 0-based
cursor::Goto(self.x as u16 + 1, self.y as u16 + 1)
}
/// Converts this position to the number of `Tiles` away from the origin it
/// represents. Usually done after subtracting two positions. Gives distance
/// as the crow flies
pub fn as_tiles(self) -> Tiles {
Tiles(max(self.x.abs(), self.y.abs()).into())
}
}
impl PartialOrd for Position {
fn partial_cmp(&self, other: &Position) -> Option<Ordering> {
if self.x == other.x && self.y == other.y {
Some(Ordering::Equal)
} else if self.x > other.x && self.y > other.y {
Some(Ordering::Greater)
} else if self.x < other.x && self.y < other.y {
Some(Ordering::Less)
} else {
None
}
}
}
/// Implements (bounded) addition of a Dimension to a position.
///
/// # Examples
///
/// ```
/// let pos = Position { x: 1, y: 10 }
///
/// let left_pos = pos + Direction::Left
/// assert_eq!(left, Position { x: 0, y: 10 })
///
/// let right_pos = pos + Direction::Right
/// assert_eq!(right_pos, Position { x: 0, y: 10 })
/// ```
#[allow(clippy::suspicious_arithmetic_impl)]
impl ops::Add<Direction> for Position {
type Output = Position;
fn add(self, dir: Direction) -> Position {
match dir {
Left => {
if self.x > std::i16::MIN {
Position {
x: self.x - 1,
..self
}
} else {
self
}
}
Right => {
if self.x < std::i16::MAX {
Position {
x: self.x + 1,
..self
}
} else {
self
}
}
Up => {
if self.y > std::i16::MIN {
Position {
y: self.y - 1,
..self
}
} else {
self
}
}
Down => {
if self.y < std::i16::MAX {
Position {
y: self.y + 1,
..self
}
} else {
self
}
}
UpLeft => self + Up + Left,
UpRight => self + Up + Right,
DownLeft => self + Down + Left,
DownRight => self + Down + Right,
}
}
}
impl ops::Add<Position> for Position {
type Output = Position;
fn add(self, pos: Position) -> Position {
Position {
x: self.x + pos.x,
y: self.y + pos.y,
}
}
}
impl ops::Sub<Position> for Position {
type Output = Position;
fn sub(self, pos: Position) -> Position {
Position {
x: self.x - pos.x,
y: self.y - pos.y,
}
}
}
impl Positioned for Position {
fn position(&self) -> Position {
*self
}
}
pub trait Positioned {
fn x(&self) -> i16 {
self.position().x
}
fn y(&self) -> i16 {
self.position().y
}
fn position(&self) -> Position {
Position {
x: self.x(),
y: self.y(),
}
}
}
pub trait PositionedMut: Positioned {
fn set_position(&mut self, pos: Position);
}
// impl<A, I> Positioned for A where A : Deref<Target = I>, I: Positioned {
// fn position(&self) -> Position {
// self.position()
// }
// }
impl<T: Positioned> Positioned for Box<T> {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: Positioned> Positioned for &'a T {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: Positioned> Positioned for &'a mut T {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: Positioned> Positioned for Rc<T> {
fn position(&self) -> Position {
(**self).position()
}
}
impl<'a, T: PositionedMut> PositionedMut for &'a mut T {
fn set_position(&mut self, pos: Position) {
(**self).set_position(pos)
}
}
#[macro_export]
macro_rules! positioned {
($name:ident) => {
positioned!($name, position);
};
($name:ident, $attr:ident) => {
impl $crate::types::Positioned for $name {
fn position(&self) -> $crate::types::Position {
self.$attr
}
}
};
}
#[macro_export]
macro_rules! positioned_mut {
($name:ident) => {
positioned_mut!($name, position);
};
($name:ident, $attr:ident) => {
impl crate::types::PositionedMut for $name {
fn set_position(&mut self, pos: $crate::types::Position) {
self.$attr = pos;
}
}
};
}
/// A number of ticks
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub struct Ticks(pub u16);
/// A number of tiles
///
/// Expressed in terms of a float to allow moving partial tiles in a number of
/// ticks
#[derive(Clone, Copy, Debug, PartialEq, Arbitrary)]
pub struct Tiles(pub f32);
/// The speed of an entity, expressed in ticks per tile
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary, Deserialize)]
#[serde(transparent)]
pub struct Speed(pub u32);
impl Speed {
/// Returns the number of tiles that would be moved in the given number of
/// ticks at this speed
pub fn ticks_to_tiles(self, ticks: Ticks) -> Tiles {
Tiles(f32::from(ticks.0) / self.0 as f32)
}
/// Returns the number of ticks required to move the given number of tiles
/// at this speed
pub fn tiles_to_ticks(self, tiles: Tiles) -> Ticks {
Ticks(tiles.0 as u16 * self.0 as u16)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Arbitrary)]
pub struct Neighbors<A> {
pub top_left: A,
pub top: A,
pub top_right: A,
pub left: A,
pub right: A,
pub bottom_left: A,
pub bottom: A,
pub bottom_right: A,
}
impl Neighbors<Position> {
fn of_position(pos: Position) -> Self {
Neighbors {
top_left: pos + Direction::UpLeft,
top: pos + Direction::Up,
top_right: pos + Direction::UpRight,
left: pos + Direction::Left,
right: pos + Direction::Right,
bottom_left: pos + Direction::DownLeft,
bottom: pos + Direction::Down,
bottom_right: pos + Direction::DownRight,
}
}
}
impl<A> Neighbors<A> {
/// it's a functor, yo
pub fn map<B, F: Fn(&A) -> B>(&self, f: F) -> Neighbors<B> {
Neighbors {
top_left: f(&self.top_left),
top: f(&self.top),
top_right: f(&self.top_right),
left: f(&self.left),
right: f(&self.right),
bottom_left: f(&self.bottom_left),
bottom: f(&self.bottom),
bottom_right: f(&self.bottom_right),
}
}
}
impl<A> Neighbors<Vec<A>> {
pub fn mapmap<B, F: Fn(&A) -> B>(&self, f: &F) -> Neighbors<Vec<B>> {
self.map(|xs| xs.iter().map(f).collect())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unnecessary_operation)]
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn position_partialord_lt_transitive(
a: Position,
b: Position,
c: Position
) {
if a < b && b < c {
assert!(a < c)
}
}
#[test]
fn position_partialord_eq_transitive(
a: Position,
b: Position,
c: Position
) {
if a == b && b == c {
assert!(a == c)
}
}
#[test]
fn position_partialord_gt_transitive(
a: Position,
b: Position,
c: Position,
) {
if a > b && b > c {
assert!(a > c)
}
}
#[test]
fn position_partialord_antisymmetric(a: Position, b: Position) {
if a < b {
assert!(!(a > b))
} else if a > b {
assert!(!(a < b))
}
}
#[test]
fn test_position_plus_dimension_as_tiles_monoid_action(
pos: Position,
dir: Direction,
) {
prop_assume!(pos.y > 0 && pos.x > 0);
assert_eq!(((pos + dir) - pos).as_tiles(), Tiles(1.0));
}
}
#[test]
fn test_position_as_tiles() {
assert_eq!(pos(0, 0).as_tiles(), Tiles(0.0));
assert_eq!(pos(1, 1).as_tiles(), Tiles(1.0));
assert_eq!(pos(1, 2).as_tiles(), Tiles(2.0));
}
}

View file

@ -1,7 +0,0 @@
#[macro_use]
pub mod static_cfg;
#[macro_use]
pub mod template;
pub mod promise;
#[macro_use]
pub mod trait_impls;

View file

@ -1,160 +0,0 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use std::task::{Context, Poll, Waker};
type Waiter<Env, T> = Box<dyn Fn(&mut Env, &T)>;
pub struct Promise<Env, T> {
inner: Arc<RwLock<Inner<T>>>,
waiters: Arc<RwLock<Vec<Waiter<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 };
(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) {
self.ps.retain(|p| !p.give(env));
}
}

View file

@ -1,147 +0,0 @@
use include_dir::Dir;
use serde::de;
macro_rules! __static_cfg_include {
(toml_file, $filename:expr) => {
include_str!($filename)
};
(toml_dir, $filename:expr) => {
include_dir!($filename)
};
(json_file, $filename:expr) => {
include_str!($filename)
};
(json_dir, $filename:expr) => {
include_dir!($filename)
};
(cfg_dir, $filename:expr) => {
include_dir!($filename)
};
}
macro_rules! __static_cfg_type {
(toml_file) => (&'static str);
(json_file) => (&'static str);
(toml_dir) => (include_dir::Dir<'static>);
(json_dir) => (include_dir::Dir<'static>);
(cfg_dir) => (include_dir::Dir<'static>);
}
macro_rules! __static_cfg_parse {
(toml_file, $e:expr) => {
toml::from_str($e).unwrap()
};
(json_file, $e:expr) => {
serde_json::from_str($e).unwrap()
};
(toml_dir, $e:expr) => {
crate::util::static_cfg::parse_toml_dir($e)
};
(json_dir, $e:expr) => {
crate::util::static_cfg::parse_json_dir($e)
};
(cfg_dir, $e:expr) => {
crate::util::static_cfg::parse_cfg_dir($e);
};
}
macro_rules! __static_cfg_inner {
($(#[$attr:meta])* ($($vis:tt)*) static ref $N:ident : $T:ty = $kind:ident($filename:expr); $($t:tt)*) => {
// static RAW: &'static str = __static_cfg_include!($kind, $filename);
static RAW: __static_cfg_type!($kind) = __static_cfg_include!($kind, $filename);
lazy_static! {
$(#[$attr])* static ref $N: $T = __static_cfg_parse!($kind, RAW);
}
static_cfg!($($t)*);
}
}
#[macro_export]
macro_rules! static_cfg {
($(#[$attr:meta])* static ref $N:ident : $T:ty = $kind:ident($filename:expr); $($t:tt)*) => {
__static_cfg_inner!($(#[$attr])* () static ref $N : $T = $kind($filename); $($t)*);
};
($(#[$attr:meta])* pub static ref $N:ident : $T:ty = $kind:ident($filename:expr); $($t:tt)*) => {
__static_cfg_inner!($(#[$attr])* (pub) static ref $N : $T = $kind($filename); $($t)*);
};
($(#[$attr:meta])* pub ($($vis:tt)+) static ref $N:ident : $T:ty = $kind:ident($filename:expr); $($t:tt)*) => {
__static_cfg_inner!($(#[$attr])* (pub ($($vis)+)) static ref $N : $T = $kind($filename); $($t)*);
};
() => ()
}
pub fn parse_cfg_dir<'a, T>(d: Dir<'a>) -> Vec<T>
where
T: de::Deserialize<'a>,
{
d.files()
.iter()
.filter_map(|f| {
let path = f.path();
let contents = f.contents_utf8().unwrap();
match path.extension().and_then(|e| e.to_str()) {
Some("toml") => {
Some(toml::from_str(contents).unwrap_or_else(|e| {
panic!(
"Error parsing TOML file {}: {}",
path.display(),
e
)
}))
}
Some("json") => {
Some(serde_json::from_str(contents).unwrap_or_else(|e| {
panic!(
"Error parsing JSON file {}: {}",
path.display(),
e
)
}))
}
// > YAML currently does not support zero-copy deserialization
// Some("yaml") => {
// Some(serde_yaml::from_str(contents).unwrap_or_else(|e| {
// panic!(
// "Error parsing YAML file {}: {}",
// path.display(),
// e
// )
// }))
// }
_ => None,
}
})
.collect()
}
pub fn parse_toml_dir<'a, T>(d: Dir<'a>) -> Vec<T>
where
T: de::Deserialize<'a>,
{
d.files()
.iter()
.map(|f| {
toml::from_str(f.contents_utf8().unwrap()).unwrap_or_else(|e| {
panic!("Error parsing TOML file {}: {}", f.path, e)
})
})
.collect()
}
pub fn parse_json_dir<'a, T>(d: Dir<'a>) -> Vec<T>
where
T: de::Deserialize<'a>,
{
d.files()
.iter()
.map(|f| serde_json::from_str(f.contents_utf8().unwrap()).unwrap())
.collect()
}

View file

@ -1,362 +0,0 @@
use nom::combinator::rest;
use nom::error::ErrorKind;
use nom::{Err, IResult};
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::marker::PhantomData;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Path<'a> {
head: &'a str,
tail: Vec<&'a str>,
}
impl<'a> Path<'a> {
fn new(head: &'a str, tail: Vec<&'a str>) -> Self {
Path { head, tail }
}
}
impl<'a> Display for Path<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.head)?;
for part in &self.tail {
write!(f, ".{}", part)?;
}
Ok(())
}
}
// named!(path_ident, map_res!(is_not!(".}"), std::str::from_utf8));
fn path_ident<'a>(input: &'a str) -> IResult<&'a str, &'a str> {
take_till!(input, |c| c == '.' || c == '}')
}
fn path<'a>(input: &'a str) -> IResult<&'a str, Path<'a>> {
map!(
input,
tuple!(
path_ident,
many0!(complete!(preceded!(char!('.'), path_ident)))
),
|(h, t)| Path::new(h, t)
)
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TemplateToken<'a> {
Literal(&'a str),
Substitution(Path<'a>),
}
fn token_substitution<'a>(
input: &'a str,
) -> IResult<&'a str, TemplateToken<'a>> {
map!(
input,
delimited!(tag!("{{"), path, tag!("}}")),
TemplateToken::Substitution
)
}
fn template_token<'a>(input: &'a str) -> IResult<&'a str, TemplateToken<'a>> {
alt!(
input,
token_substitution
| map!(
alt!(complete!(take_until!("{{")) | complete!(rest)),
TemplateToken::Literal
)
)
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Template<'a> {
tokens: Vec<TemplateToken<'a>>,
}
impl<'a> Template<'a> {
pub fn new(tokens: Vec<TemplateToken<'a>>) -> Self {
Template { tokens }
}
}
pub struct TemplateVisitor<'a> {
marker: PhantomData<fn() -> Template<'a>>,
}
impl<'a> TemplateVisitor<'a> {
pub fn new() -> Self {
TemplateVisitor {
marker: PhantomData,
}
}
}
impl<'a> serde::de::Visitor<'a> for TemplateVisitor<'a> {
type Value = Template<'a>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a valid template string")
}
fn visit_borrowed_str<E: serde::de::Error>(
self,
v: &'a str,
) -> Result<Self::Value, E> {
Template::parse(v).map_err(|_| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(v),
&"a valid template string",
)
})
}
}
impl<'a> serde::Deserialize<'a> for Template<'a> {
fn deserialize<D: serde::Deserializer<'a>>(
deserializer: D,
) -> Result<Self, D::Error> {
deserializer.deserialize_str(TemplateVisitor::new())
}
}
impl<'a> Template<'a> {
pub fn parse(
input: &'a str,
) -> Result<Template<'a>, Err<(&'a str, ErrorKind)>> {
let (remaining, res) = template(input)?;
if !remaining.is_empty() {
unreachable!();
}
Ok(res)
}
pub fn format(
&self,
params: &TemplateParams<'a>,
) -> Result<String, TemplateError<'a>> {
use TemplateToken::*;
let mut res = String::new();
for token in &self.tokens {
match token {
Literal(s) => res.push_str(s),
Substitution(p) => match params.get(p.clone()) {
Some(s) => res.push_str(s),
None => return Err(TemplateError::MissingParam(p.clone())),
},
}
}
Ok(res)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateError<'a> {
MissingParam(Path<'a>),
}
impl<'a> Display for TemplateError<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use TemplateError::*;
match self {
MissingParam(path) => {
write!(f, "Missing template parameter: {}", path)
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateParams<'a> {
Direct(&'a str),
Nested(HashMap<&'a str, TemplateParams<'a>>),
}
impl<'a> TemplateParams<'a> {
fn get(&self, path: Path<'a>) -> Option<&'a str> {
use TemplateParams::*;
match self {
Direct(_) => None,
Nested(m) => m.get(path.head).and_then(|next| {
if path.tail.is_empty() {
match next {
Direct(s) => Some(*s),
_ => None,
}
} else {
next.get(Path {
head: path.tail[0],
tail: path.tail[1..].to_vec(),
})
}
}),
}
}
}
#[macro_export]
macro_rules! template_params {
(@count $head: expr => $hv: tt, $($rest:tt)+) => { 1 + template_params!(@count $($rest)+) };
(@count $one:expr => $($ov: tt)*) => { 1 };
(@inner $ret: ident, ($key: expr => {$($v:tt)*}, $($r:tt)*)) => {
$ret.insert($key, template_params!({ $($v)* }));
template_params!(@inner $ret, ($($r)*));
};
(@inner $ret: ident, ($key: expr => $value: expr, $($r:tt)*)) => {
$ret.insert($key, template_params!($value));
template_params!(@inner $ret, ($($r)*));
};
(@inner $ret: ident, ()) => {};
({ $($body: tt)* }) => {{
let _cap = template_params!(@count $($body)*);
let mut _m = ::std::collections::HashMap::with_capacity(_cap);
template_params!(@inner _m, ($($body)*));
TemplateParams::Nested(_m)
}};
($direct:expr) => { TemplateParams::Direct($direct) };
() => { TemplateParams::Nested(::std::collections::HashMap::new()) };
}
fn template<'a>(input: &'a str) -> IResult<&'a str, Template<'a>> {
complete!(
input,
map!(many1!(complete!(template_token)), Template::new)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_path_ident() {
assert_eq!(path_ident("foo}}"), Ok(("}}", "foo")));
assert_eq!(path_ident("foo.bar}}"), Ok((".bar}}", "foo")));
}
#[test]
fn test_parse_path() {
assert_eq!(path("foo}}"), Ok(("}}", Path::new("foo", vec![]))));
assert_eq!(
path("foo.bar}}"),
Ok(("}}", Path::new("foo", vec!["bar"])))
);
assert_eq!(
path("foo.bar.baz}}"),
Ok(("}}", Path::new("foo", vec!["bar", "baz"])))
);
}
#[test]
fn test_parse_template_token() {
assert_eq!(
template_token("foo bar"),
Ok(("", TemplateToken::Literal("foo bar")))
);
assert_eq!(
template_token("foo bar {{baz}}"),
Ok(("{{baz}}", TemplateToken::Literal("foo bar ")))
);
assert_eq!(
template_token("{{baz}}"),
Ok((
"",
TemplateToken::Substitution(Path::new("baz", Vec::new()))
))
);
assert_eq!(
template_token("{{baz}} foo bar"),
Ok((
" foo bar",
TemplateToken::Substitution(Path::new("baz", Vec::new()))
))
);
}
#[test]
fn test_parse_template() {
assert_eq!(
template("foo bar"),
Ok((
"",
Template {
tokens: vec![TemplateToken::Literal("foo bar")]
}
))
);
assert_eq!(
template("foo bar {{baz}} qux"),
Ok((
"",
Template {
tokens: vec![
TemplateToken::Literal("foo bar "),
TemplateToken::Substitution(Path::new(
"baz",
Vec::new()
)),
TemplateToken::Literal(" qux"),
]
}
))
);
}
#[test]
fn test_template_params_literal() {
// trace_macros!(true);
let expected = template_params!({
"direct" => "hi",
"other" => "here",
"nested" => {
"one" => "1",
"two" => "2",
"double" => {
"three" => "3",
},
},
});
// trace_macros!(false);
assert_eq!(
TemplateParams::Nested(hashmap! {
"direct" => TemplateParams::Direct("hi"),
"other" => TemplateParams::Direct("here"),
"nested" => TemplateParams::Nested(hashmap!{
"one" => TemplateParams::Direct("1"),
"two" => TemplateParams::Direct("2"),
"double" => TemplateParams::Nested(hashmap!{
"three" => TemplateParams::Direct("3"),
})
})
}),
expected,
)
}
#[test]
fn test_format_template() {
assert_eq!(
"foo bar baz qux",
Template::parse("foo {{x}} {{y.z}} {{y.w.z}}")
.unwrap()
.format(&template_params!({
"x" => "bar",
"y" => {
"z" => "baz",
"w" => {
"z" => "qux",
},
},
}))
.unwrap()
)
}
}

View file

@ -1,17 +0,0 @@
macro_rules! ref_impl {
(impl<T: $traitb: ident $(+ $bound:ident)*> $traiti:ident for &T {
$($body:tt)*
}) => {
impl<'a, T: $traitb $(+ $bound)*> $traiti for &'a T {
$($body)*
}
impl<'a, T: $traitb $(+ $bound)*> $traiti for &'a mut T {
$($body)*
}
impl<T: $traitb $(+ $bound)*> $traiti for ::std::boxed::Box<T> {
$($body)*
}
};
}