Wipe Rust project
Sorry rust, but you're just not fun to write
This commit is contained in:
parent
e2d2f011c6
commit
fb0d1b3e66
47 changed files with 0 additions and 6087 deletions
1450
Cargo.lock
generated
1450
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
33
Cargo.toml
33
Cargo.toml
|
@ -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]
|
|
@ -1,2 +0,0 @@
|
|||
[logging]
|
||||
level = "debug"
|
|
@ -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 = []
|
|
@ -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
|
|
@ -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 } }
|
|
@ -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 }
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
max_width = 80
|
46
src/cli.yml
46
src/cli.yml
|
@ -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
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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, "@")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)*);
|
||||
};
|
||||
|
||||
() => {};
|
||||
}
|
617
src/game.rs
617
src/game.rs
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
130
src/main.rs
130
src/main.rs
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
166
src/messages.rs
166
src/messages.rs
|
@ -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!(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}}"
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
504
src/types/mod.rs
504
src/types/mod.rs
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#[macro_use]
|
||||
pub mod static_cfg;
|
||||
#[macro_use]
|
||||
pub mod template;
|
||||
pub mod promise;
|
||||
#[macro_use]
|
||||
pub mod trait_impls;
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)*
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue