bestagon 0.6.0

An engine for discrete stuff in hexagonal grids
Documentation
use crate::hex::{Hex, HexAx, HexCb};
use std::isize;
use std::ops::BitAnd;
use std::{cmp::max, cmp::min, collections::HashSet};

pub struct HexRange {
  x_range: (isize, isize),
  y_range: (isize, isize),
  z_range: (isize, isize),
}

// https://www.redblobgames.com/grids/hexagons/#range-coordinate
impl HexRange {
  pub fn new(x_range: (isize, isize), y_range: (isize, isize), z_range: (isize, isize)) -> Self {
    HexRange {
      x_range,
      y_range,
      z_range,
    }
  }
  /// Returns a HexRange that represents the hexes that can be reached from
  /// `center` within at most `r` steps. (This includes `center` itself.)
  pub fn sphere<T>(center: &T, r: isize) -> HexRange
  where
    T: Hex,
  {
    let center_cb: HexCb = (*center).into();
    HexRange {
      x_range: (center_cb.q - r, center_cb.q + r),
      y_range: (center_cb.r - r, center_cb.r + r),
      z_range: (center_cb.s - r, center_cb.s + r),
    }
  }

  pub fn compute<T>(&self) -> HashSet<T>
  where
    T: Hex,
  {
    // TODO: Initial capacity is now the size of a Hex sphere with a radius
    // equal to the smallest of the 3 ranges, but can I compute this for a
    // general cuboid?
    let mut in_range: HashSet<T> = HashSet::with_capacity(hex_sphere_size(
      [
        self.x_range.1 - self.x_range.0,
        self.y_range.1 - self.y_range.0,
        self.z_range.1 - self.z_range.0,
      ]
      .into_iter()
      .min()
      .unwrap()
      .clamp(0, isize::MAX),
    ) as usize);
    for x in self.x_range.0..=self.x_range.1 {
      for y in max(self.y_range.0, -x - self.z_range.1)..=min(self.y_range.1, -x - self.z_range.0) {
        in_range.insert(HexAx::new(x, y).into());
      }
    }
    in_range
  }
}

impl BitAnd for HexRange {
  type Output = Self;

  /// Returns the intersection of the two ranges.
  fn bitand(self, rhs: Self) -> Self::Output {
    HexRange::new(
      (
        max(self.x_range.0, rhs.x_range.0),
        min(self.x_range.1, rhs.x_range.1),
      ),
      (
        max(self.y_range.0, rhs.y_range.0),
        min(self.y_range.1, rhs.y_range.1),
      ),
      (
        max(self.z_range.0, rhs.z_range.0),
        min(self.z_range.1, rhs.z_range.1),
      ),
    )
  }
}

/// Returns the number of hexes that are reachable in at most r steps from a
/// hex, including itself.
/// ```
/// use bestagon::hex_range::hex_sphere_size;
/// assert_eq!(1, hex_sphere_size(0));
/// assert_eq!(7, hex_sphere_size(1));
/// assert_eq!(19, hex_sphere_size(2));
/// assert_eq!(37, hex_sphere_size(3));
/// ```
pub fn hex_sphere_size(r: isize) -> isize {
  assert!(r >= 0, "hex sphere radius cannot be larger than 0");
  (1 + 6 * r as usize * (r as usize + 1) / 2) as isize
}

#[cfg(test)]
mod test {
  use super::*;
  use crate::hex::{HexAx, HexCb};

  #[test]
  fn sphere_case_0() {
    let center = HexCb::new(0, 0, 0);
    let radius = 3;
    let range = HexRange::sphere(&center, radius).compute();
    verify_sphere(range, &center, radius);
  }

  #[test]
  fn sphere_case_1() {
    let center = HexAx::new(2, 1);
    let radius = 5;
    let range = HexRange::sphere(&center, radius).compute();
    verify_sphere(range, &center, radius);
  }

  fn verify_sphere<T>(range: HashSet<T>, center: &T, radius: isize)
  where
    T: Hex,
  {
    assert_eq!(hex_sphere_size(radius), range.len() as isize);
    assert!(range.contains(center));
    for x in range {
      assert!(x.dist(center) <= radius);
    }
  }

  #[test]
  fn intersect_case_0() {
    let sphere0 = HexRange::sphere(&HexCb::new(-4, 0, 4), 3);
    let sphere1 = HexRange::sphere(&HexCb::new(1, 1, -2), 3);
    let range = (sphere0 & sphere1).compute::<HexCb>();
    assert_eq!(range.len(), 2);
    assert!(range.contains(&HexCb::new(-1, 0, 1)));
    assert!(range.contains(&HexCb::new(-2, 1, 1)));
  }

  #[test]
  fn intersect_case_1() {
    let sphere0 = HexRange::sphere(&HexCb::new(-4, 0, 4), 3);
    let sphere1 = HexRange::sphere(&HexCb::new(4, 0, -4), 3);
    let range = (sphere0 & sphere1).compute::<HexCb>();
    assert!(range.is_empty());
  }
}