monorepo/coordinates/src/hex.rs

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);
}
}
}
}