diff --git a/coordinates/Cargo.lock b/coordinates/Cargo.lock index 4aa5567..927843a 100644 --- a/coordinates/Cargo.lock +++ b/coordinates/Cargo.lock @@ -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" diff --git a/coordinates/Cargo.toml b/coordinates/Cargo.toml index a5f3e17..f93e5a2 100644 --- a/coordinates/Cargo.toml +++ b/coordinates/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] proptest = "1.0" thiserror = "1.0" +nom = { version = "7" } diff --git a/coordinates/proptest-regressions/hex.txt b/coordinates/proptest-regressions/hex.txt new file mode 100644 index 0000000..1b6729a --- /dev/null +++ b/coordinates/proptest-regressions/hex.txt @@ -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 diff --git a/coordinates/src/hex.rs b/coordinates/src/hex.rs index 3283c21..396a5ca 100644 --- a/coordinates/src/hex.rs +++ b/coordinates/src/hex.rs @@ -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 { + pub fn adjacencies(&self) -> impl Iterator { 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::>().contains(&self) + pub fn is_adjacent(&self, dest: &AxialAddr) -> bool { + dest.adjacencies() + .collect::>() + .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 { - let eko = self.clone(); - let mut rezulto: HashSet = HashSet::new(); - let mut vico: Vec = 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 { + let item = self.clone(); + let mut results: HashSet = HashSet::new(); + let mut positions: Vec = 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 = addr.within(0).collect(); - assert_eq!(lst.len(), 0); + fn distance_0_has_the_source() { + let addr = AxialAddr::origin(); + let lst: Vec = 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.within(1).collect(); - assert_eq!(lst.len(), 6); + fn distance_1_has_seven_addresses() { + let hexaddr = AxialAddr::origin(); + let lst: Vec = hexaddr.addresses(1).collect(); + assert_eq!(lst.len(), 7); } #[test] - fn distance_2_has_18_addresses() { - let hexaddr = HexAddr::origin(); - let lst: Vec = hexaddr.within(2).collect(); - assert_eq!(lst.len(), 18); + fn distance_2_has_19_addresses() { + let hexaddr = AxialAddr::origin(); + let lst: Vec = hexaddr.addresses(2).collect(); + assert_eq!(lst.len(), 19); } fn address() -> (std::ops::Range, std::ops::Range) { @@ -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 = coord1.adjacencies().collect(); + let coord1 = AxialAddr::new(x, y); + let lst1: HashSet = coord1.adjacencies().collect(); assert_eq!(lst1.len(), 6); let lst1 = lst1.into_iter().collect::>(); let coord2 = &lst1[idx]; - let lst2: Vec = coord2.adjacencies().collect(); + let lst2: Vec = 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 = coord1.adjacencies().collect(); + let coord1 = AxialAddr::new(q, r); + let lst1: Vec = 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.within(distance).collect(); + let hexaddr = AxialAddr::new(q, r); + let en_distancaj_hexaddr: Vec = 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); diff --git a/coordinates/src/hex_map.rs b/coordinates/src/hex_map.rs new file mode 100644 index 0000000..1b745ae --- /dev/null +++ b/coordinates/src/hex_map.rs @@ -0,0 +1,195 @@ +/* +Copyright 2022-2023, Savanni D'Gerinel + +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 . +*/ + +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 { + cells: HashMap, +} + +impl + Clone> Map +where + String: From, +{ + pub fn new_hexagonal(radius: usize) -> Map { + let cells = vec![(AxialAddr::origin(), Default::default())] + .into_iter() + .chain( + AxialAddr::origin() + .addresses(radius) + .map(|addr| (addr, Default::default())), + ) + .collect::>(); + Map { cells } + } + + pub fn contains(&self, coordinate: AxialAddr) -> bool { + self.cells.contains_key(&coordinate) + } + + pub fn cell(&mut self, addr: AxialAddr) -> Entry { + self.cells.entry(addr) + } + + pub fn get(&self, addr: &AxialAddr) -> Option<&A> { + self.cells.get(addr) + } + + // pub fn from_file(path: PathBuf) -> Map {} + + pub fn to_file(&self, path: PathBuf) -> () {} + + pub fn from_data(data: Vec) -> Map { + fn parse_line>( + input: &str, + ) -> Result<(AxialAddr, A), nom::error::Error<&str>> { + fn parse>(input: &str) -> IResult<&str, (AxialAddr, A)> { + let (input, addr) = parse_address(input)?; + let (input, value) = cell_value::(input)?; + Ok((input, (addr, value))) + } + + parse(input).finish().map(|(_, pair)| pair) + } + + let cells = data + .into_iter() + .map(|line| parse_line::(&line).unwrap()) + .collect::>(); + let cells = cells.into_iter().collect::>(); + Map { cells } + } + + pub fn to_data(&self) -> Vec { + 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>(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 for String { + fn from(v: MapVal) -> String { + format!("{}", v.0) + } + } + impl From for MapVal { + fn from(s: String) -> MapVal { + MapVal(s.chars().next().unwrap()) + } + } + + #[test] + fn it_can_modify_a_map() { + let mut map: Map = 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 = 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 = 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 = Map::from_data( + map_data + .lines() + .map(|l| l.to_owned()) + .collect::>(), + ); + 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('.'))); + } +} diff --git a/coordinates/src/lib.rs b/coordinates/src/lib.rs index 6e34e58..e90ef28 100644 --- a/coordinates/src/lib.rs +++ b/coordinates/src/lib.rs @@ -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; diff --git a/flake.lock b/flake.lock index fcc38c1..ebddba2 100644 --- a/flake.lock +++ b/flake.lock @@ -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" },