Make maps carry real data that is both readable and writeable.
This commit is contained in:
parent
c5528d1ceb
commit
ac5d8e0c75
|
@ -45,6 +45,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
name = "coordinates"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"proptest",
|
||||
"thiserror",
|
||||
]
|
||||
|
@ -96,6 +97,28 @@ version = "0.2.139"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
|
|
|
@ -8,3 +8,4 @@ edition = "2021"
|
|||
[dependencies]
|
||||
proptest = "1.0"
|
||||
thiserror = "1.0"
|
||||
nom = { version = "7" }
|
||||
|
|
|
@ -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
|
|
@ -6,19 +6,19 @@ use std::collections::HashSet;
|
|||
|
||||
/// An address within the hex coordinate system
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HexAddr {
|
||||
pub struct AxialAddr {
|
||||
q: i32,
|
||||
r: i32,
|
||||
}
|
||||
|
||||
impl HexAddr {
|
||||
impl AxialAddr {
|
||||
/// Create a new axial coordinate address.
|
||||
pub fn new(q: i32, r: i32) -> Self {
|
||||
Self { q, r }
|
||||
}
|
||||
|
||||
pub fn origin() -> HexAddr {
|
||||
HexAddr { q: 0, r: 0 }
|
||||
pub fn origin() -> AxialAddr {
|
||||
AxialAddr { q: 0, r: 0 }
|
||||
}
|
||||
|
||||
pub fn q(&self) -> i32 {
|
||||
|
@ -34,49 +34,50 @@ impl HexAddr {
|
|||
}
|
||||
|
||||
/// 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![
|
||||
HexAddr::new(self.q + 1, self.r),
|
||||
HexAddr::new(self.q, self.r + 1),
|
||||
HexAddr::new(self.q - 1, self.r + 1),
|
||||
HexAddr::new(self.q - 1, self.r),
|
||||
HexAddr::new(self.q, self.r - 1),
|
||||
HexAddr::new(self.q + 1, self.r - 1),
|
||||
AxialAddr::new(self.q + 1, self.r),
|
||||
AxialAddr::new(self.q, self.r + 1),
|
||||
AxialAddr::new(self.q - 1, self.r + 1),
|
||||
AxialAddr::new(self.q - 1, self.r),
|
||||
AxialAddr::new(self.q, self.r - 1),
|
||||
AxialAddr::new(self.q + 1, self.r - 1),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
/// Test whether a coordinate is adjacent to this one
|
||||
pub fn is_adjacent(&self, dest: &HexAddr) -> bool {
|
||||
dest.adjacencies().collect::<Vec<HexAddr>>().contains(&self)
|
||||
pub fn is_adjacent(&self, dest: &AxialAddr) -> bool {
|
||||
dest.adjacencies()
|
||||
.collect::<Vec<AxialAddr>>()
|
||||
.contains(&self)
|
||||
}
|
||||
|
||||
/// 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())
|
||||
/ 2) as usize
|
||||
}
|
||||
|
||||
/// Get an iteration to all of the coordinates within the specified distance of this one.
|
||||
pub fn within(&self, distance: usize) -> impl Iterator<Item = HexAddr> {
|
||||
let eko = self.clone();
|
||||
let mut rezulto: HashSet<HexAddr> = HashSet::new();
|
||||
let mut vico: Vec<HexAddr> = Vec::new();
|
||||
/// Get an iteration of all of the coordinates within the specified distance of this one.
|
||||
pub fn addresses(&self, distance: usize) -> impl Iterator<Item = AxialAddr> {
|
||||
let item = self.clone();
|
||||
let mut results: HashSet<AxialAddr> = HashSet::new();
|
||||
let mut positions: Vec<AxialAddr> = Vec::new();
|
||||
|
||||
vico.push(eko);
|
||||
positions.push(item);
|
||||
|
||||
while vico.len() > 0 {
|
||||
let elem = vico.remove(0);
|
||||
while positions.len() > 0 {
|
||||
let elem = positions.remove(0);
|
||||
for adj in elem.adjacencies() {
|
||||
if self.distance(&adj) <= distance && !rezulto.contains(&adj) {
|
||||
vico.push(adj.clone());
|
||||
if self.distance(&adj) <= distance && !results.contains(&adj) {
|
||||
positions.push(adj.clone());
|
||||
}
|
||||
}
|
||||
rezulto.insert(elem);
|
||||
results.insert(elem);
|
||||
}
|
||||
|
||||
rezulto.remove(self);
|
||||
rezulto.into_iter()
|
||||
results.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,24 +88,25 @@ mod tests {
|
|||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn distance_0_is_empty() {
|
||||
let addr = HexAddr::origin();
|
||||
let lst: Vec<HexAddr> = addr.within(0).collect();
|
||||
assert_eq!(lst.len(), 0);
|
||||
fn distance_0_has_the_source() {
|
||||
let addr = AxialAddr::origin();
|
||||
let lst: Vec<AxialAddr> = addr.addresses(0).collect();
|
||||
assert_eq!(lst.len(), 1);
|
||||
assert!(lst.contains(&AxialAddr::origin()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_1_has_six_addresses() {
|
||||
let hexaddr = HexAddr::origin();
|
||||
let lst: Vec<HexAddr> = hexaddr.within(1).collect();
|
||||
assert_eq!(lst.len(), 6);
|
||||
fn distance_1_has_seven_addresses() {
|
||||
let hexaddr = AxialAddr::origin();
|
||||
let lst: Vec<AxialAddr> = hexaddr.addresses(1).collect();
|
||||
assert_eq!(lst.len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_2_has_18_addresses() {
|
||||
let hexaddr = HexAddr::origin();
|
||||
let lst: Vec<HexAddr> = hexaddr.within(2).collect();
|
||||
assert_eq!(lst.len(), 18);
|
||||
fn distance_2_has_19_addresses() {
|
||||
let hexaddr = AxialAddr::origin();
|
||||
let lst: Vec<AxialAddr> = hexaddr.addresses(2).collect();
|
||||
assert_eq!(lst.len(), 19);
|
||||
}
|
||||
|
||||
fn address() -> (std::ops::Range<i32>, std::ops::Range<i32>) {
|
||||
|
@ -114,20 +116,20 @@ mod tests {
|
|||
proptest! {
|
||||
#[test]
|
||||
fn produces_adjacent_coordinates((x, y) in address(), idx in 0_usize..6) {
|
||||
let coord1 = HexAddr::new(x, y);
|
||||
let lst1: HashSet<HexAddr> = coord1.adjacencies().collect();
|
||||
let coord1 = AxialAddr::new(x, y);
|
||||
let lst1: HashSet<AxialAddr> = coord1.adjacencies().collect();
|
||||
assert_eq!(lst1.len(), 6);
|
||||
|
||||
let lst1 = lst1.into_iter().collect::<Vec<_>>();
|
||||
let coord2 = &lst1[idx];
|
||||
let lst2: Vec<HexAddr> = coord2.adjacencies().collect();
|
||||
let lst2: Vec<AxialAddr> = coord2.adjacencies().collect();
|
||||
assert!(lst2.contains(&coord1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests_adjacencies((q, r) in address(), idx in 0_usize..6) {
|
||||
let coord1 = HexAddr::new(q, r);
|
||||
let lst1: Vec<HexAddr> = coord1.adjacencies().collect();
|
||||
let coord1 = AxialAddr::new(q, r);
|
||||
let lst1: Vec<AxialAddr> = coord1.adjacencies().collect();
|
||||
assert_eq!(lst1.len(), 6);
|
||||
|
||||
let coord2 = &lst1[idx];
|
||||
|
@ -138,8 +140,8 @@ mod tests {
|
|||
#[test]
|
||||
fn measures_distance((q1, r1) in address(),
|
||||
(q2, r2) in address()) {
|
||||
let hexaddr_1 = HexAddr::new(q1, r1);
|
||||
let hexaddr_2 = HexAddr::new(q2, r2);
|
||||
let hexaddr_1 = AxialAddr::new(q1, r1);
|
||||
let hexaddr_2 = AxialAddr::new(q2, r2);
|
||||
|
||||
let s1 = -q1 - r1;
|
||||
let s2 = -q2 - r2;
|
||||
|
@ -150,10 +152,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn calculates_distance((q, r) in address(), distance in 0_usize..6) {
|
||||
let hexaddr = HexAddr::new(q, r);
|
||||
let en_distancaj_hexaddr: Vec<HexAddr> = hexaddr.within(distance).collect();
|
||||
let hexaddr = AxialAddr::new(q, r);
|
||||
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);
|
||||
for c in en_distancaj_hexaddr {
|
||||
assert!(c.distance(&hexaddr) <= distance as usize);
|
||||
|
|
|
@ -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('.')));
|
||||
}
|
||||
}
|
|
@ -13,10 +13,11 @@ use thiserror;
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("La enhavita koordinato ne estas valida en ĉi-tiu sistemo")]
|
||||
InvalidCoordinate,
|
||||
#[error("IO error on reading or writing: {0}")]
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
mod hex;
|
||||
mod hex_map;
|
||||
|
||||
pub use hex::HexAddr;
|
||||
pub use hex::AxialAddr;
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"narHash": "sha256-VCSmIYJy/ZzTvEGjdfITmTYfybXBgZpMjyjDndbou+8=",
|
||||
"narHash": "sha256-/YOtiDKPUXKKpIhsAds11llfC42ScGW27bbHnNZebco=",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue