Make maps carry real data that is both readable and writeable.

This commit is contained in:
Savanni D'Gerinel 2023-01-21 10:53:21 -05:00
parent c5528d1ceb
commit ac5d8e0c75
7 changed files with 283 additions and 53 deletions

23
coordinates/Cargo.lock generated
View File

@ -45,6 +45,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
name = "coordinates" name = "coordinates"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"nom",
"proptest", "proptest",
"thiserror", "thiserror",
] ]
@ -96,6 +97,28 @@ version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.15" version = "0.2.15"

View File

@ -8,3 +8,4 @@ edition = "2021"
[dependencies] [dependencies]
proptest = "1.0" proptest = "1.0"
thiserror = "1.0" thiserror = "1.0"
nom = { version = "7" }

View File

@ -0,0 +1,8 @@
# 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 a2c4f3291703eca342388b116ee0849a96e5f977c5af22a6b5bc805edecfeb5d # shrinks to (q, r) = (0, 0), idx = 0
cc f80f4dd28944bad3a39512535e38ca3db1870c6daefd5317856f70bb58a505df # shrinks to (x, y) = (0, 0), idx = 0

View File

@ -6,19 +6,19 @@ use std::collections::HashSet;
/// An address within the hex coordinate system /// An address within the hex coordinate system
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct HexAddr { pub struct AxialAddr {
q: i32, q: i32,
r: i32, r: i32,
} }
impl HexAddr { impl AxialAddr {
/// Create a new axial coordinate address. /// Create a new axial coordinate address.
pub fn new(q: i32, r: i32) -> Self { pub fn new(q: i32, r: i32) -> Self {
Self { q, r } Self { q, r }
} }
pub fn origin() -> HexAddr { pub fn origin() -> AxialAddr {
HexAddr { q: 0, r: 0 } AxialAddr { q: 0, r: 0 }
} }
pub fn q(&self) -> i32 { pub fn q(&self) -> i32 {
@ -34,49 +34,50 @@ impl HexAddr {
} }
/// Get a list of coordinates adjacent to this one. /// Get a list of coordinates adjacent to this one.
pub fn adjacencies(&self) -> impl Iterator<Item = HexAddr> { pub fn adjacencies(&self) -> impl Iterator<Item = AxialAddr> {
vec![ vec![
HexAddr::new(self.q + 1, self.r), AxialAddr::new(self.q + 1, self.r),
HexAddr::new(self.q, self.r + 1), AxialAddr::new(self.q, self.r + 1),
HexAddr::new(self.q - 1, self.r + 1), AxialAddr::new(self.q - 1, self.r + 1),
HexAddr::new(self.q - 1, self.r), AxialAddr::new(self.q - 1, self.r),
HexAddr::new(self.q, self.r - 1), AxialAddr::new(self.q, self.r - 1),
HexAddr::new(self.q + 1, self.r - 1), AxialAddr::new(self.q + 1, self.r - 1),
] ]
.into_iter() .into_iter()
} }
/// Test whether a coordinate is adjacent to this one /// Test whether a coordinate is adjacent to this one
pub fn is_adjacent(&self, dest: &HexAddr) -> bool { pub fn is_adjacent(&self, dest: &AxialAddr) -> bool {
dest.adjacencies().collect::<Vec<HexAddr>>().contains(&self) dest.adjacencies()
.collect::<Vec<AxialAddr>>()
.contains(&self)
} }
/// Measure the distance to a destination /// Measure the distance to a destination
pub fn distance(&self, dest: &HexAddr) -> usize { pub fn distance(&self, dest: &AxialAddr) -> usize {
(((self.q() - dest.q()).abs() + (self.r() - dest.r()).abs() + (self.s() - dest.s()).abs()) (((self.q() - dest.q()).abs() + (self.r() - dest.r()).abs() + (self.s() - dest.s()).abs())
/ 2) as usize / 2) as usize
} }
/// Get an iteration to all of the coordinates within the specified distance of this one. /// Get an iteration of all of the coordinates within the specified distance of this one.
pub fn within(&self, distance: usize) -> impl Iterator<Item = HexAddr> { pub fn addresses(&self, distance: usize) -> impl Iterator<Item = AxialAddr> {
let eko = self.clone(); let item = self.clone();
let mut rezulto: HashSet<HexAddr> = HashSet::new(); let mut results: HashSet<AxialAddr> = HashSet::new();
let mut vico: Vec<HexAddr> = Vec::new(); let mut positions: Vec<AxialAddr> = Vec::new();
vico.push(eko); positions.push(item);
while vico.len() > 0 { while positions.len() > 0 {
let elem = vico.remove(0); let elem = positions.remove(0);
for adj in elem.adjacencies() { for adj in elem.adjacencies() {
if self.distance(&adj) <= distance && !rezulto.contains(&adj) { if self.distance(&adj) <= distance && !results.contains(&adj) {
vico.push(adj.clone()); positions.push(adj.clone());
} }
} }
rezulto.insert(elem); results.insert(elem);
} }
rezulto.remove(self); results.into_iter()
rezulto.into_iter()
} }
} }
@ -87,24 +88,25 @@ mod tests {
use std::collections::HashSet; use std::collections::HashSet;
#[test] #[test]
fn distance_0_is_empty() { fn distance_0_has_the_source() {
let addr = HexAddr::origin(); let addr = AxialAddr::origin();
let lst: Vec<HexAddr> = addr.within(0).collect(); let lst: Vec<AxialAddr> = addr.addresses(0).collect();
assert_eq!(lst.len(), 0); assert_eq!(lst.len(), 1);
assert!(lst.contains(&AxialAddr::origin()));
} }
#[test] #[test]
fn distance_1_has_six_addresses() { fn distance_1_has_seven_addresses() {
let hexaddr = HexAddr::origin(); let hexaddr = AxialAddr::origin();
let lst: Vec<HexAddr> = hexaddr.within(1).collect(); let lst: Vec<AxialAddr> = hexaddr.addresses(1).collect();
assert_eq!(lst.len(), 6); assert_eq!(lst.len(), 7);
} }
#[test] #[test]
fn distance_2_has_18_addresses() { fn distance_2_has_19_addresses() {
let hexaddr = HexAddr::origin(); let hexaddr = AxialAddr::origin();
let lst: Vec<HexAddr> = hexaddr.within(2).collect(); let lst: Vec<AxialAddr> = hexaddr.addresses(2).collect();
assert_eq!(lst.len(), 18); assert_eq!(lst.len(), 19);
} }
fn address() -> (std::ops::Range<i32>, std::ops::Range<i32>) { fn address() -> (std::ops::Range<i32>, std::ops::Range<i32>) {
@ -114,20 +116,20 @@ mod tests {
proptest! { proptest! {
#[test] #[test]
fn produces_adjacent_coordinates((x, y) in address(), idx in 0_usize..6) { fn produces_adjacent_coordinates((x, y) in address(), idx in 0_usize..6) {
let coord1 = HexAddr::new(x, y); let coord1 = AxialAddr::new(x, y);
let lst1: HashSet<HexAddr> = coord1.adjacencies().collect(); let lst1: HashSet<AxialAddr> = coord1.adjacencies().collect();
assert_eq!(lst1.len(), 6); assert_eq!(lst1.len(), 6);
let lst1 = lst1.into_iter().collect::<Vec<_>>(); let lst1 = lst1.into_iter().collect::<Vec<_>>();
let coord2 = &lst1[idx]; let coord2 = &lst1[idx];
let lst2: Vec<HexAddr> = coord2.adjacencies().collect(); let lst2: Vec<AxialAddr> = coord2.adjacencies().collect();
assert!(lst2.contains(&coord1)); assert!(lst2.contains(&coord1));
} }
#[test] #[test]
fn tests_adjacencies((q, r) in address(), idx in 0_usize..6) { fn tests_adjacencies((q, r) in address(), idx in 0_usize..6) {
let coord1 = HexAddr::new(q, r); let coord1 = AxialAddr::new(q, r);
let lst1: Vec<HexAddr> = coord1.adjacencies().collect(); let lst1: Vec<AxialAddr> = coord1.adjacencies().collect();
assert_eq!(lst1.len(), 6); assert_eq!(lst1.len(), 6);
let coord2 = &lst1[idx]; let coord2 = &lst1[idx];
@ -138,8 +140,8 @@ mod tests {
#[test] #[test]
fn measures_distance((q1, r1) in address(), fn measures_distance((q1, r1) in address(),
(q2, r2) in address()) { (q2, r2) in address()) {
let hexaddr_1 = HexAddr::new(q1, r1); let hexaddr_1 = AxialAddr::new(q1, r1);
let hexaddr_2 = HexAddr::new(q2, r2); let hexaddr_2 = AxialAddr::new(q2, r2);
let s1 = -q1 - r1; let s1 = -q1 - r1;
let s2 = -q2 - r2; let s2 = -q2 - r2;
@ -150,10 +152,10 @@ mod tests {
#[test] #[test]
fn calculates_distance((q, r) in address(), distance in 0_usize..6) { fn calculates_distance((q, r) in address(), distance in 0_usize..6) {
let hexaddr = HexAddr::new(q, r); let hexaddr = AxialAddr::new(q, r);
let en_distancaj_hexaddr: Vec<HexAddr> = hexaddr.within(distance).collect(); let en_distancaj_hexaddr: Vec<AxialAddr> = hexaddr.addresses(distance).collect();
let expected_cnt = ((0..distance+1).map(|v| v * 6).fold(0, |acc, val| acc + val)) as usize; let expected_cnt = ((0..distance+1).map(|v| v * 6).fold(1, |acc, val| acc + val)) as usize;
assert_eq!(en_distancaj_hexaddr.len(), expected_cnt); assert_eq!(en_distancaj_hexaddr.len(), expected_cnt);
for c in en_distancaj_hexaddr { for c in en_distancaj_hexaddr {
assert!(c.distance(&hexaddr) <= distance as usize); assert!(c.distance(&hexaddr) <= distance as usize);

195
coordinates/src/hex_map.rs Normal file
View File

@ -0,0 +1,195 @@
/*
Copyright 2022-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of the Luminescent Dreams Tools.
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Luminescent Dreams Tools. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::hex::AxialAddr;
use nom::{
bytes::complete::tag,
character::complete::alphanumeric1,
error::ParseError,
multi::many1,
sequence::{delimited, separated_pair},
Finish, IResult, Parser,
};
use std::{
collections::{hash_map::Entry, HashMap},
path::PathBuf,
};
#[derive(Clone, Debug, PartialEq)]
pub struct Map<A> {
cells: HashMap<AxialAddr, A>,
}
impl<A: Default + From<String> + Clone> Map<A>
where
String: From<A>,
{
pub fn new_hexagonal(radius: usize) -> Map<A> {
let cells = vec![(AxialAddr::origin(), Default::default())]
.into_iter()
.chain(
AxialAddr::origin()
.addresses(radius)
.map(|addr| (addr, Default::default())),
)
.collect::<HashMap<AxialAddr, A>>();
Map { cells }
}
pub fn contains(&self, coordinate: AxialAddr) -> bool {
self.cells.contains_key(&coordinate)
}
pub fn cell(&mut self, addr: AxialAddr) -> Entry<AxialAddr, A> {
self.cells.entry(addr)
}
pub fn get(&self, addr: &AxialAddr) -> Option<&A> {
self.cells.get(addr)
}
// pub fn from_file(path: PathBuf) -> Map<A> {}
pub fn to_file(&self, path: PathBuf) -> () {}
pub fn from_data(data: Vec<String>) -> Map<A> {
fn parse_line<A: From<String>>(
input: &str,
) -> Result<(AxialAddr, A), nom::error::Error<&str>> {
fn parse<A: From<String>>(input: &str) -> IResult<&str, (AxialAddr, A)> {
let (input, addr) = parse_address(input)?;
let (input, value) = cell_value::<A>(input)?;
Ok((input, (addr, value)))
}
parse(input).finish().map(|(_, pair)| pair)
}
let cells = data
.into_iter()
.map(|line| parse_line::<A>(&line).unwrap())
.collect::<Vec<(AxialAddr, A)>>();
let cells = cells.into_iter().collect::<HashMap<AxialAddr, A>>();
Map { cells }
}
pub fn to_data(&self) -> Vec<String> {
self.cells
.iter()
.map(|(addr, val)| {
format!("[{}, {}] {}", addr.q(), addr.r(), String::from(val.clone()))
})
.collect()
}
}
fn parse_address(input: &str) -> IResult<&str, AxialAddr> {
let (input, (q, r)) = delimited(
tag("["),
separated_pair(
nom::character::complete::i32,
tag(", "),
nom::character::complete::i32,
),
tag("] "),
)(input)?;
Ok((input, (AxialAddr::new(q, r))))
}
fn cell_value<A: From<String>>(input: &str) -> IResult<&str, A> {
many1(alphanumeric1)
.or(tag(".").map(|v| vec![v]))
.map(|v| v.join(""))
.map(|v| A::from(v))
.parse(input)
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::*;
#[derive(Clone, Debug, PartialEq)]
struct MapVal(char);
impl Default for MapVal {
fn default() -> MapVal {
MapVal('.')
}
}
impl From<MapVal> for String {
fn from(v: MapVal) -> String {
format!("{}", v.0)
}
}
impl From<String> for MapVal {
fn from(s: String) -> MapVal {
MapVal(s.chars().next().unwrap())
}
}
#[test]
fn it_can_modify_a_map() {
let mut map: Map<MapVal> = Map::new_hexagonal(1);
map.cell(AxialAddr::new(0, 0))
.and_modify(|cell| *cell = MapVal('L'));
assert_eq!(map.get(&AxialAddr::new(0, 0)), Some(&MapVal('L')));
assert_eq!(map.get(&AxialAddr::new(0, 1)), Some(&MapVal('.')));
}
#[test]
fn it_can_serialize_a_map() {
let mut map: Map<MapVal> = Map::new_hexagonal(1);
map.cell(AxialAddr::new(0, 0))
.and_modify(|cell| *cell = MapVal('L'));
map.cell(AxialAddr::new(1, 0))
.and_modify(|cell| *cell = MapVal('A'));
let mut expected: HashSet<String> = HashSet::new();
expected.insert("[0, 0] L".to_owned());
expected.insert("[1, 0] A".to_owned());
expected.insert("[0, 1] .".to_owned());
expected.insert("[-1, 1] .".to_owned());
expected.insert("[-1, 0] .".to_owned());
expected.insert("[0, -1] .".to_owned());
expected.insert("[1, -1] .".to_owned());
let map_rows = map.to_data();
assert_eq!(map_rows.len(), expected.len());
map_rows
.iter()
.for_each(|row| assert!(expected.contains(row)));
expected
.iter()
.for_each(|expected| assert!(map_rows.contains(expected)));
}
#[test]
fn it_can_deserialize_a_map() {
let map_data = "[0, 0] L
[1, 0] A
[0, 1] .
[-1, 1] .
[-1, 0] .
[0, -1] .
[1, -1] .";
let map: Map<MapVal> = Map::from_data(
map_data
.lines()
.map(|l| l.to_owned())
.collect::<Vec<String>>(),
);
assert_eq!(map.get(&AxialAddr::new(0, 0)), Some(&MapVal('L')));
assert_eq!(map.get(&AxialAddr::new(1, 0)), Some(&MapVal('A')));
assert_eq!(map.get(&AxialAddr::new(0, 1)), Some(&MapVal('.')));
}
}

View File

@ -13,10 +13,11 @@ use thiserror;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("La enhavita koordinato ne estas valida en ĉi-tiu sistemo")] #[error("IO error on reading or writing: {0}")]
InvalidCoordinate, IO(std::io::Error),
} }
mod hex; mod hex;
mod hex_map;
pub use hex::HexAddr; pub use hex::AxialAddr;

View File

@ -52,7 +52,7 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"narHash": "sha256-VCSmIYJy/ZzTvEGjdfITmTYfybXBgZpMjyjDndbou+8=", "narHash": "sha256-/YOtiDKPUXKKpIhsAds11llfC42ScGW27bbHnNZebco=",
"type": "tarball", "type": "tarball",
"url": "https://github.com/oxalica/rust-overlay/archive/master.tar.gz" "url": "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"
}, },