diff --git a/Cargo.lock b/Cargo.lock index 8d501b3..5a597df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -2063,7 +2063,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hexlab" -version = "0.1.2" +version = "0.1.3" dependencies = [ "bevy", "hexx", diff --git a/Cargo.toml b/Cargo.toml index ba52da8..449320a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hexlab" authors = ["Kristofers Solo "] -version = "0.1.2" +version = "0.1.3" edition = "2021" description = "A hexagonal maze generation and manipulation library" repository = "https://github.com/kristoferssolo/hexlab" diff --git a/src/walls.rs b/src/walls.rs index 06b7473..25da5bf 100644 --- a/src/walls.rs +++ b/src/walls.rs @@ -1,13 +1,47 @@ -use std::{ - fmt::Debug, - ops::{Deref, DerefMut}, -}; - #[cfg(feature = "bevy")] -use bevy::prelude::*; +use bevy::prelude::{Component, Reflect, ReflectComponent}; use hexx::EdgeDirection; -/// Represents the walls of a hexagonal tile using bit flags +/// A bit-flag representation of walls in a hexagonal tile. +/// +/// `Walls` uses an efficient bit-flag system to track the presence or absence of walls +/// along each edge of a hexagonal tile. Each of the six possible walls is represented +/// by a single bit in an 8-bit integer, allowing for fast operations and minimal memory usage. +/// +/// # Examples +/// +/// Creating and manipulating walls: +/// ```rust +/// use hexlab::prelude::*; +/// +/// // Create a hexagon with all walls +/// let walls = Walls::new(); +/// assert!(walls.is_closed()); +/// +/// // Create a hexagon with no walls +/// let mut walls = Walls::empty(); +/// assert!(walls.is_empty()); +/// +/// // Add specific walls +/// walls.add(EdgeDirection::FLAT_NORTH); +/// walls.add(EdgeDirection::FLAT_SOUTH); +/// assert_eq!(walls.count(), 2); +/// ``` +/// +/// Using walls in game logic: +/// +/// ```rust +/// use hexlab::prelude::*; +/// let mut walls = Walls::empty(); +/// +/// // Add walls to create a corner +/// walls.add(EdgeDirection::FLAT_NORTH); +/// walls.add(EdgeDirection::FLAT_SOUTH_EAST); +/// +/// // Check if a specific direction has a wall +/// assert!(walls.contains(EdgeDirection::FLAT_NORTH)); +/// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH)); +/// ``` #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "bevy", derive(Reflect, Component))] @@ -15,38 +49,279 @@ use hexx::EdgeDirection; pub struct Walls(u8); impl Walls { + /// Creates a new set of walls with all edges closed. + /// + /// This is the default state where all six edges of the hexagon have walls. + /// + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let walls = Walls::new(); + /// assert!(walls.is_closed()); + /// assert_eq!(walls.count(), 6); + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Creates a new set of walls with no edges (completely open). + /// + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let walls = Walls::empty(); + /// assert!(walls.is_empty()); + /// assert_eq!(walls.count(), 0); + /// ``` + pub fn empty() -> Self { + Self(0) + } + + /// Checks if the walls are currently empty + /// + /// Returns `true` if all directions have no walls set. + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let walls = Walls::empty(); + /// assert!(walls.is_empty()); + /// + /// let walls = Walls::new(); + /// assert!(!walls.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.0 == 0 + } + /// Adds a wall in the specified direction + /// + /// This method uses bitwise operations to efficiently set the wall flag + /// for the given direction. Multiple walls can be added to the same hexagon. + /// + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// walls.add(EdgeDirection::FLAT_NORTH); + /// assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + /// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH)); + /// + /// walls.add(EdgeDirection::FLAT_SOUTH); + /// assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); + /// assert_eq!(walls.count(), 2); + /// ``` #[inline] pub fn add(&mut self, direction: T) where - T: Into, + T: Into + Copy, { self.0 |= direction.into().0; } /// Removes a wall in the specified direction + /// + /// Returns `true` if a wall was actually removed, `false` if there was no wall + /// in the specified direction. + /// + /// # Exmaples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::new(); + /// assert!(walls.remove(EdgeDirection::FLAT_NORTH)); + /// assert!(!walls.contains(EdgeDirection::FLAT_NORTH)); + /// + /// // Removing a non-existent wall returns false + /// assert!(!walls.remove(EdgeDirection::FLAT_NORTH)); + /// ``` #[inline] - pub fn remove(&mut self, direction: T) + pub fn remove(&mut self, direction: T) -> bool where - T: Into, + T: Into + Copy, { - self.0 &= !direction.into().0; + let was_removed = self.contains(direction); + if was_removed { + self.0 &= !direction.into().0; + } + was_removed } /// Returns true if there is a wall in the specified direction + /// + /// Uses efficient bitwise operations to check for the presence of a wall. + /// + /// # Exmaples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// + /// walls.add(EdgeDirection::FLAT_NORTH); + /// assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + /// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH)); + /// ``` #[inline] pub fn contains(&self, other: T) -> bool where - T: Into, + T: Into + Copy, { self.0 & other.into().0 != 0 } /// Returns the raw bit representation of the walls + /// + /// This method provides access to the underlying bit flags for advanced usage. + /// The bits are ordered according to the `EdgeDirection` indices. + /// + /// # Exmaples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::new(); + /// + /// assert_eq!(walls.as_bits(), 0b111111); + /// ``` #[inline] pub fn as_bits(&self) -> u8 { self.0 } + + /// Returns the total number of walls present + /// + /// Efficiently counts the number of set bits in the internal representation. + /// + /// # Exmaples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// + /// assert_eq!(walls.count(), 0); + /// + /// walls.add(EdgeDirection::FLAT_NORTH); + /// walls.add(EdgeDirection::FLAT_SOUTH); + /// assert_eq!(walls.count(), 2); + /// ``` + #[inline] + pub fn count(&self) -> u8 { + self.0.count_ones() as u8 + } + + /// Returns all possible directions as a `Walls` value + /// + /// This represents a hexagon with walls in all six directions. + /// + /// # Exmaples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let all_walls = Walls::all_directions(); + /// + /// assert_eq!(all_walls.count(), 6); + /// assert!(all_walls.is_closed()); + /// ``` + #[inline] + pub fn all_directions() -> Self { + Self(0b111111) + } + + /// Toggles a wall in the specified direction. + /// + /// If a wall exists in the given direction, it will be removed. + /// If no wall exists, one will be added. + /// Returns the previous state (`true` if a wall was present). + /// + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// + /// assert!(!walls.toggle(EdgeDirection::FLAT_NORTH)); // Returns false, wall was not present + /// assert!(walls.contains(EdgeDirection::FLAT_NORTH)); // Wall is now present + /// + /// let mut walls = Walls::new(); + /// + /// assert!(walls.toggle(EdgeDirection::FLAT_NORTH)); // Returns true, wall was present + /// assert!(!walls.contains(EdgeDirection::FLAT_NORTH)); // Wall is now removed + /// ``` + pub fn toggle(&mut self, direction: T) -> bool + where + T: Into + Copy, + { + let is_present = self.contains(direction); + if is_present { + self.remove(direction); + } else { + self.add(direction); + } + is_present + } + + /// Checks if walls are present in all six directions. + /// + /// Returns `true` if the hexagon has all possible walls, making it completely enclosed. + /// + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let walls = Walls::new(); + /// assert!(walls.is_closed()); + /// + /// let mut walls = Walls::empty(); + /// assert!(!walls.is_closed()); + /// // Add all walls manually + /// for direction in EdgeDirection::iter() { + /// walls.add(direction); + /// } + /// assert!(walls.is_closed()); + /// ``` + #[inline] + pub fn is_closed(&self) -> bool { + self.count() == 6 + } + + /// Sets walls for multiple directions at once. + /// + /// This method efficiently adds multiple walls in a single operation while + /// preserving any existing walls not specified in the input. + /// + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// walls.add(EdgeDirection::FLAT_NORTH); + /// + /// walls.fill([EdgeDirection::FLAT_SOUTH, EdgeDirection::FLAT_SOUTH_EAST]); + /// + /// assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); + /// assert_eq!(walls.count(), 3); + /// ``` + #[inline] + pub fn fill(&mut self, other: T) + where + T: Into, + { + self.0 |= other.into().0; + } } impl From for Walls { @@ -55,33 +330,25 @@ impl From for Walls { } } -impl From<[EdgeDirection; 6]> for Walls { - fn from(value: [EdgeDirection; 6]) -> Self { - let mut walls = 0u8; - for direction in value { - walls |= 1 << direction.index(); - } - Self(walls) - } -} - impl From for Walls { fn from(value: u8) -> Self { Self(1 << value) } } -impl Deref for Walls { - type Target = u8; - - fn deref(&self) -> &Self::Target { - &self.0 +impl FromIterator for Walls { + fn from_iter>(iter: T) -> Self { + let mut walls = 0u8; + for direction in iter { + walls |= 1 << direction.index(); + } + Self(walls) } } -impl DerefMut for Walls { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 +impl From<[EdgeDirection; N]> for Walls { + fn from(value: [EdgeDirection; N]) -> Self { + value.into_iter().collect() } } @@ -95,119 +362,308 @@ impl Default for Walls { mod tests { use super::*; + // all_directions #[test] - fn new_walls() { - let walls = Walls::default(); - // All walls should be present by default - for direction in EdgeDirection::iter() { - assert!( - walls.contains(direction), - "Wall should exist in direction {:?}", - direction - ); - } + fn all_directions_creates_closed_walls() { + let walls = Walls::all_directions(); + assert!(walls.is_closed()); + assert!(!walls.is_empty()); + assert_eq!(walls.as_bits(), 0b111111); } + // as_bits #[test] - fn add_remove_single_wall() { - let mut walls = Walls::default(); - - // Remove and verify each wall - walls.remove(EdgeDirection::FLAT_TOP); - assert!(!walls.contains(EdgeDirection::FLAT_TOP)); - - // Add back and verify - walls.add(EdgeDirection::FLAT_TOP); - assert!(walls.contains(EdgeDirection::FLAT_TOP)); - } - - #[test] - fn multiple_operations() { - let mut walls = Walls::default(); - - // Remove multiple walls - walls.remove(EdgeDirection::FLAT_TOP); - walls.remove(EdgeDirection::FLAT_BOTTOM); - - // Verify removed walls - assert!(!walls.contains(EdgeDirection::FLAT_TOP)); - assert!(!walls.contains(EdgeDirection::FLAT_BOTTOM)); - - // Verify other walls still exist - assert!(walls.contains(EdgeDirection::FLAT_TOP_RIGHT)); - assert!(walls.contains(EdgeDirection::FLAT_TOP_LEFT)); - - // Add back one wall - walls.add(EdgeDirection::FLAT_TOP); - assert!(walls.contains(EdgeDirection::FLAT_TOP)); - assert!(!walls.contains(EdgeDirection::FLAT_BOTTOM)); - } - - #[test] - fn bit_patterns() { - let mut walls = Walls::default(); - assert_eq!( - walls.as_bits(), - 0b111111, - "Initial state should have all walls" - ); - - walls.remove(EdgeDirection::FLAT_BOTTOM_RIGHT); - assert_eq!(walls.as_bits() & 0b000001, 0, "First bit should be cleared"); - - walls.add(EdgeDirection::FLAT_BOTTOM_RIGHT); - assert_eq!(walls.as_bits() & 0b000001, 1, "First bit should be set"); - } - - #[test] - fn remove_all_walls() { - let mut walls = Walls::default(); - - // Remove all walls - for direction in EdgeDirection::iter() { - walls.remove(direction); - } - - // Verify all walls are removed - assert_eq!(walls.as_bits(), 0, "All walls should be removed"); - - // Verify each direction - for direction in EdgeDirection::iter() { - assert!( - !walls.contains(direction), - "No wall should exist in direction {:?}", - direction - ); - } - } - - #[test] - fn deref_operations() { - let mut walls = Walls::default(); - - // Test Deref - let bits: &u8 = walls.deref(); - assert_eq!(*bits, 0b111111); - - // Test DerefMut - *walls.deref_mut() = 0; + fn as_bits_empty() { + let walls = Walls::empty(); assert_eq!(walls.as_bits(), 0); } #[test] - fn idempotent_operations() { - let mut walls = Walls::default(); + fn as_bits_single_wall() { + let mut walls = Walls::empty(); + walls.add(EdgeDirection::FLAT_NORTH); + assert_eq!(walls.as_bits(), 0b010000); + } - // Adding twice shouldn't change the result - walls.add(EdgeDirection::FLAT_TOP); - let first_add = walls.as_bits(); - walls.add(EdgeDirection::FLAT_TOP); - assert_eq!(walls.as_bits(), first_add); + #[test] + fn as_bits_multiple_walls() { + let mut walls = Walls::empty(); + walls.add(EdgeDirection::FLAT_NORTH); + walls.add(EdgeDirection::FLAT_SOUTH); + assert_eq!(walls.as_bits(), 0b010010); + } - // Removing twice shouldn't change the result - walls.remove(EdgeDirection::FLAT_TOP); - let first_remove = walls.as_bits(); - walls.remove(EdgeDirection::FLAT_TOP); - assert_eq!(walls.as_bits(), first_remove); + #[test] + fn as_bits_all_walls() { + let walls = Walls::new(); + assert_eq!(walls.as_bits(), 0b111111); + } + + // new + #[test] + fn new_created_closed_walls() { + let walls = Walls::new(); + assert!(walls.is_closed()); + assert_eq!(walls.as_bits(), 0b111111); + } + + // empty + #[test] + fn empty_creates_no_walls() { + let walls = Walls::empty(); + assert!(walls.is_empty()); + assert_eq!(walls.as_bits(), 0); + } + + // add + #[test] + fn add_single_wall() { + let mut walls = Walls::empty(); + walls.add(EdgeDirection::FLAT_NORTH); + assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + assert_eq!(walls.count(), 1); + } + + // remove + #[test] + fn remove_existing_wall() { + let mut walls = Walls::new(); + assert!(walls.remove(EdgeDirection::FLAT_NORTH)); + assert!(!walls.contains(EdgeDirection::FLAT_NORTH)); + } + + #[test] + fn remove_nonexistent_wall() { + let mut walls = Walls::empty(); + assert!(!walls.remove(EdgeDirection::FLAT_NORTH)); + walls.add(EdgeDirection::FLAT_NORTH); + assert!(walls.remove(EdgeDirection::FLAT_NORTH)); + } + + // toggle + #[test] + fn toggle_adds_wall() { + let mut walls = Walls::empty(); + assert!(!walls.toggle(EdgeDirection::FLAT_NORTH)); + assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + } + + #[test] + fn toggle_removes_wall() { + let mut walls = Walls::new(); + assert!(walls.toggle(EdgeDirection::FLAT_NORTH)); + assert!(!walls.contains(EdgeDirection::FLAT_NORTH)); + } + + // fill + #[test] + fn fill_adds_multiple_walls() { + let mut walls = Walls::empty(); + walls.fill([EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH]); + assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); + assert_eq!(walls.count(), 2); + } + + #[test] + fn fill_preserves_existing_walls() { + let mut walls = Walls::empty(); + walls.add(EdgeDirection::FLAT_NORTH); + walls.fill([EdgeDirection::FLAT_SOUTH, EdgeDirection::FLAT_SOUTH_EAST]); + assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); + assert!(walls.contains(EdgeDirection::FLAT_SOUTH_EAST)); + assert_eq!(walls.count(), 3); + } + + #[test] + fn from_edge_direction_conversion() { + let walls: Walls = EdgeDirection::FLAT_NORTH.into(); + assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + assert_eq!(walls.count(), 1); + } + + #[test] + fn from_u8_conversion() { + let walls: Walls = 0u8.into(); + assert!(walls.contains(EdgeDirection::FLAT_SOUTH_EAST)); + assert_eq!(walls.count(), 1); + } + + #[test] + fn from_array_conversion() { + let walls: Walls = [EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH].into(); + assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); + assert_eq!(walls.count(), 2); + } + + #[test] + fn from_iterator_handles_duplicates() { + let directions = vec![ + EdgeDirection::FLAT_NORTH, + EdgeDirection::FLAT_SOUTH, + EdgeDirection::FLAT_NORTH, // Duplicate + ]; + let walls: Walls = directions.into_iter().collect(); + assert_eq!(walls.count(), 2); + } + + #[test] + fn default_creates_closed_walls() { + let walls = Walls::default(); + assert!(walls.is_closed()); + assert_eq!(walls.as_bits(), 0b111111); + } + + #[test] + fn from_iterator() { + let directions = vec![ + EdgeDirection::FLAT_NORTH, + EdgeDirection::FLAT_SOUTH, + EdgeDirection::FLAT_NORTH, // Duplicate should not affect result + ]; + let walls: Walls = directions.into_iter().collect(); + assert_eq!(walls.count(), 2); + assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); + } + + #[test] + fn bit_manipulation() { + let mut walls = Walls::empty(); + + // Test single bit operations + walls.add(EdgeDirection::FLAT_NORTH); + assert_eq!(walls.as_bits(), 0b010000); + + walls.add(EdgeDirection::FLAT_SOUTH); + assert_eq!(walls.as_bits(), 0b010010); + + // Test removing middle bit + walls.add(EdgeDirection::FLAT_SOUTH_EAST); + assert_eq!(walls.as_bits(), 0b010011); + walls.remove(EdgeDirection::FLAT_SOUTH); + assert_eq!(walls.as_bits(), 0b010001); + } + + // From tests + #[test] + fn from_edge_direction_flat_south_east() { + let walls = Walls::from(EdgeDirection::FLAT_SOUTH_EAST); + assert_eq!(walls.as_bits(), 0b000001); + } + + #[test] + fn from_edge_direction_flat_south() { + let walls = Walls::from(EdgeDirection::FLAT_SOUTH); + assert_eq!(walls.as_bits(), 0b000010); + } + + #[test] + fn from_edge_direction_flat_south_west() { + let walls = Walls::from(EdgeDirection::FLAT_SOUTH_WEST); + assert_eq!(walls.as_bits(), 0b000100); + } + + #[test] + fn from_edge_direction_flat_north_west() { + let walls = Walls::from(EdgeDirection::FLAT_NORTH_WEST); + assert_eq!(walls.as_bits(), 0b001000); + } + + #[test] + fn from_edge_direction_flat_north() { + let walls = Walls::from(EdgeDirection::FLAT_NORTH); + assert_eq!(walls.as_bits(), 0b010000); + } + + #[test] + fn from_edge_direction_flat_east() { + let walls = Walls::from(EdgeDirection::FLAT_NORTH_EAST); + assert_eq!(walls.as_bits(), 0b100000); + } + + // FromIterator tests + #[test] + fn from_iterator_empty() { + let walls = Vec::new().into_iter().collect::(); + assert!(walls.is_empty()); + } + + #[test] + fn from_iterator_single() { + let walls = vec![EdgeDirection::FLAT_SOUTH] + .into_iter() + .collect::(); + assert_eq!(walls.as_bits(), 0b000010); + } + + #[test] + fn from_iterator_multiple() { + let walls = vec![EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH] + .into_iter() + .collect::(); + assert_eq!(walls.as_bits(), 0b010010); + } + + #[test] + fn from_iterator_duplicates() { + let walls = vec![ + EdgeDirection::FLAT_NORTH, + EdgeDirection::FLAT_NORTH, + EdgeDirection::FLAT_SOUTH, + ] + .into_iter() + .collect::(); + assert_eq!(walls.as_bits(), 0b010010); + } + + #[test] + fn from_iterator_all_directions() { + let walls = EdgeDirection::iter().collect::(); + assert_eq!(walls.as_bits(), 0b111111); + } + + // From<[EdgeDirection; N]> tests + #[test] + fn from_array_empty() { + let walls = Walls::from([]); + assert!(walls.is_empty()); + } + + #[test] + fn from_array_single() { + let walls = Walls::from([EdgeDirection::FLAT_NORTH]); + assert_eq!(walls.as_bits(), 0b010000); + } + + #[test] + fn from_array_multiple() { + let walls = Walls::from([EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH]); + assert_eq!(walls.as_bits(), 0b010010); + } + + #[test] + fn from_array_duplicates() { + let walls = Walls::from([ + EdgeDirection::FLAT_NORTH, + EdgeDirection::FLAT_NORTH, + EdgeDirection::FLAT_SOUTH, + ]); + assert_eq!(walls.as_bits(), 0b010010); + } + + #[test] + fn from_array_all_directions() { + let walls = Walls::from([ + EdgeDirection::FLAT_NORTH, + EdgeDirection::FLAT_NORTH_EAST, + EdgeDirection::FLAT_SOUTH_EAST, + EdgeDirection::FLAT_SOUTH, + EdgeDirection::FLAT_SOUTH_WEST, + EdgeDirection::FLAT_NORTH_WEST, + ]); + assert_eq!(walls.as_bits(), 0b111111); } }