177 lines
5.8 KiB
Rust
177 lines
5.8 KiB
Rust
/*
|
|
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 Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/// This module contains the elements of cube coordinates.
|
|
///
|
|
/// This code is based on https://www.redblobgames.com/grids/hexagons/
|
|
use std::collections::HashSet;
|
|
|
|
/// An address within the hex coordinate system
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct AxialAddr {
|
|
q: i32,
|
|
r: i32,
|
|
}
|
|
|
|
impl AxialAddr {
|
|
/// Create a new axial coordinate address.
|
|
pub fn new(q: i32, r: i32) -> Self {
|
|
Self { q, r }
|
|
}
|
|
|
|
pub fn origin() -> AxialAddr {
|
|
AxialAddr { q: 0, r: 0 }
|
|
}
|
|
|
|
pub fn q(&self) -> i32 {
|
|
self.q
|
|
}
|
|
|
|
pub fn r(&self) -> i32 {
|
|
self.r
|
|
}
|
|
|
|
pub fn s(&self) -> i32 {
|
|
-self.q - self.r
|
|
}
|
|
|
|
/// Get a list of coordinates adjacent to this one.
|
|
pub fn adjacencies(&self) -> impl Iterator<Item = AxialAddr> {
|
|
vec![
|
|
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: &AxialAddr) -> bool {
|
|
dest.adjacencies()
|
|
.collect::<Vec<AxialAddr>>()
|
|
.contains(self)
|
|
}
|
|
|
|
/// Measure the distance to a destination
|
|
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 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();
|
|
|
|
positions.push(item);
|
|
|
|
while !positions.is_empty() {
|
|
let elem = positions.remove(0);
|
|
for adj in elem.adjacencies() {
|
|
if self.distance(&adj) <= distance && !results.contains(&adj) {
|
|
positions.push(adj.clone());
|
|
}
|
|
}
|
|
results.insert(elem);
|
|
}
|
|
|
|
results.into_iter()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use proptest::prelude::*;
|
|
use std::collections::HashSet;
|
|
|
|
#[test]
|
|
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_seven_addresses() {
|
|
let hexaddr = AxialAddr::origin();
|
|
let lst: Vec<AxialAddr> = hexaddr.addresses(1).collect();
|
|
assert_eq!(lst.len(), 7);
|
|
}
|
|
|
|
#[test]
|
|
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>) {
|
|
(-65536_i32..65535, -65536_i32..65535)
|
|
}
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn produces_adjacent_coordinates((x, y) in address(), idx in 0_usize..6) {
|
|
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<AxialAddr> = coord2.adjacencies().collect();
|
|
assert!(lst2.contains(&coord1));
|
|
}
|
|
|
|
#[test]
|
|
fn tests_adjacencies((q, r) in address(), idx in 0_usize..6) {
|
|
let coord1 = AxialAddr::new(q, r);
|
|
let lst1: Vec<AxialAddr> = coord1.adjacencies().collect();
|
|
assert_eq!(lst1.len(), 6);
|
|
|
|
let coord2 = &lst1[idx];
|
|
assert!(coord2.is_adjacent(&coord1));
|
|
assert!(coord1.is_adjacent(&coord2));
|
|
}
|
|
|
|
#[test]
|
|
fn measures_distance((q1, r1) in address(),
|
|
(q2, r2) in address()) {
|
|
let hexaddr_1 = AxialAddr::new(q1, r1);
|
|
let hexaddr_2 = AxialAddr::new(q2, r2);
|
|
|
|
let s1 = -q1 - r1;
|
|
let s2 = -q2 - r2;
|
|
let expected = (((q1 - q2).abs() + (r1 - r2).abs() + (s1 - s2).abs()) / 2) as usize;
|
|
assert_eq!(hexaddr_1.distance(&hexaddr_2), expected);
|
|
assert_eq!(hexaddr_2.distance(&hexaddr_1), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn calculates_distance((q, r) in address(), distance in 0_usize..6) {
|
|
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(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);
|
|
}
|
|
}
|
|
}
|
|
}
|