feat(bevy): add bevy feature support

This commit is contained in:
Kristofers Solo 2024-11-12 16:38:31 +02:00
parent 971e1c760e
commit 37b63365f1
6 changed files with 4261 additions and 159 deletions

4121
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,23 @@
[package] [package]
name = "hexlab" name = "hexlab"
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "A hexagonal maze library" description = "A hexagonal maze library"
repository = "https://github.com/kristoferssolo/hexlab"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["maze", "hex", "hexagons"]
[dependencies] [dependencies]
bevy = { version = "0.14", optional = true }
hexx = { version = "0.18" } hexx = { version = "0.18" }
rand = "0.8" rand = "0.8"
rand_chacha = "0.3"
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }
[features] [features]
default = [] default = []
serde = ["dep:serde", "hexx/serde"] serde = ["dep:serde", "hexx/serde", "rand_chacha/serde"]
bevy = ["dep:bevy", "hexx/bevy_reflect"]
[dev-dependencies] [dev-dependencies]
serde_json = "1.0"

View File

@ -1,7 +1,8 @@
use std::collections::HashSet; use std::collections::HashSet;
use hexx::{EdgeDirection, Hex}; use hexx::{EdgeDirection, Hex};
use rand::{rngs::ThreadRng, seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::HexMaze; use crate::HexMaze;
@ -17,22 +18,40 @@ impl HexMaze {
} }
} }
pub fn generate_from_seed(&mut self, generator_type: GeneratorType, seed: u64) {
match generator_type {
GeneratorType::BackTracking => self.generate_backtracking_from_seed(seed),
}
}
pub fn generate_backtracking(&mut self) { pub fn generate_backtracking(&mut self) {
if self.is_empty() { if self.is_empty() {
return; return;
} }
let start = *self.tiles().keys().next().unwrap(); let start = *self.keys().next().unwrap();
let mut visited = HashSet::new(); let mut visited = HashSet::new();
let mut rng = thread_rng(); let mut rng = thread_rng();
self.recursive_backtrack(start, &mut visited, &mut rng); self.recursive_backtrack(start, &mut visited, &mut rng);
} }
fn recursive_backtrack( pub fn generate_backtracking_from_seed(&mut self, seed: u64) {
if self.is_empty() {
return;
}
// let start = *self.keys().next().unwrap();
let start = Hex::ZERO;
let mut visited = HashSet::new();
let mut rng = ChaCha8Rng::seed_from_u64(seed);
self.recursive_backtrack(start, &mut visited, &mut rng);
}
fn recursive_backtrack<R: Rng>(
&mut self, &mut self,
current: Hex, current: Hex,
visited: &mut HashSet<Hex>, visited: &mut HashSet<Hex>,
rng: &mut ThreadRng, rng: &mut R,
) { ) {
visited.insert(current); visited.insert(current);
@ -42,13 +61,11 @@ impl HexMaze {
for direction in directions { for direction in directions {
let neighbor = current + direction; let neighbor = current + direction;
if let Some(_) = self.get_tile(&neighbor) { if self.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) {
if !visited.contains(&neighbor) { self.remove_tile_wall(&current, direction);
self.remove_tile_wall(&current, direction); self.remove_tile_wall(&neighbor, direction.const_neg());
self.remove_tile_wall(&neighbor, direction.const_neg());
self.recursive_backtrack(neighbor, visited, rng); self.recursive_backtrack(neighbor, visited, rng);
}
} }
} }
} }
@ -56,16 +73,14 @@ impl HexMaze {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use hexx::HexLayout;
use super::*; use super::*;
#[test] #[test]
fn backtracking_generation() { fn backtracking_generation() {
let mut maze = HexMaze::default().with_radius(2); let mut maze = HexMaze::with_radius(2);
// Before generation // Before generation
for tile in maze.tiles.values() { for tile in maze.values() {
assert_eq!(tile.walls.as_bits(), 0b111111); assert_eq!(tile.walls.as_bits(), 0b111111);
} }
@ -73,16 +88,13 @@ mod tests {
maze.generate(GeneratorType::BackTracking); maze.generate(GeneratorType::BackTracking);
// After generation // After generation
let all_walls = maze let all_walls = maze.values().all(|tile| tile.walls.as_bits() == 0b111111);
.tiles
.values()
.all(|tile| tile.walls.as_bits() == 0b111111);
assert!(!all_walls, "Some walls should be removed"); assert!(!all_walls, "Some walls should be removed");
} }
#[test] #[test]
fn empty_maze() { fn empty_maze() {
let mut maze = HexMaze::with_layout(HexLayout::default()); let mut maze = HexMaze::default();
maze.generate(GeneratorType::BackTracking); maze.generate(GeneratorType::BackTracking);
assert!(maze.is_empty(), "Empty maze should remain empty"); assert!(maze.is_empty(), "Empty maze should remain empty");
} }

View File

@ -1,39 +1,28 @@
use std::collections::HashMap; use std::{
collections::HashMap,
ops::{Deref, DerefMut},
};
use hexx::{EdgeDirection, Hex, HexLayout}; use hexx::{EdgeDirection, Hex};
use super::{HexTile, Walls}; use super::{HexTile, Walls};
/// Represents a hexagonal maze with tiles and walls /// Represents a hexagonal maze with tiles and walls
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct HexMaze { pub struct HexMaze(HashMap<Hex, HexTile>);
pub tiles: HashMap<Hex, HexTile>,
layout: HexLayout,
}
impl HexMaze { impl HexMaze {
/// Creates a new empty maze with the specified layout /// Creates a new empty maze
#[inline] #[inline]
pub fn new(layout: HexLayout) -> Self { pub fn new() -> Self {
Self { Self::default()
tiles: HashMap::new(),
layout,
}
}
/// Creates a new empty maze with the specified layout
pub fn with_layout(layout: HexLayout) -> Self {
Self {
tiles: HashMap::new(),
layout,
}
} }
/// Creates a hexagonal maze with the given radius /// Creates a hexagonal maze with the given radius
/// Uses axial coordinates (q, r) to create a perfect hexagon /// Uses axial coordinates (q, r) to create a perfect hexagon
#[inline] pub fn with_radius(radius: u32) -> Self {
pub fn with_radius(mut self, radius: u32) -> Self { let mut maze = Self::default();
let radius = radius as i32; let radius = radius as i32;
for q in -radius..=radius { for q in -radius..=radius {
let r1 = (-radius).max(-q - radius); let r1 = (-radius).max(-q - radius);
@ -41,24 +30,22 @@ impl HexMaze {
for r in r1..=r2 { for r in r1..=r2 {
let pos = Hex::new(q, r); let pos = Hex::new(q, r);
let tile = HexTile::new(pos); let tile = HexTile::new(pos);
self.tiles.insert(pos, tile); maze.0.insert(pos, tile);
} }
} }
self maze
} }
/// Adds a new tile at the specified coordinates /// Adds a new tile at the specified coordinates
#[inline]
pub fn add_tile(&mut self, coords: Hex) { pub fn add_tile(&mut self, coords: Hex) {
let tile = HexTile::new(coords); let tile = HexTile::new(coords);
self.tiles.insert(coords, tile); self.0.insert(coords, tile);
} }
/// Adds a wall in the specified direction at the given coordinates /// Adds a wall in the specified direction at the given coordinates
#[inline]
pub fn add_wall(&mut self, coord: Hex, direction: EdgeDirection) { pub fn add_wall(&mut self, coord: Hex, direction: EdgeDirection) {
if let Some(tile) = self.tiles.get_mut(&coord) { if let Some(tile) = self.0.get_mut(&coord) {
tile.walls.add(direction) tile.walls.add(direction)
} }
} }
@ -66,47 +53,46 @@ impl HexMaze {
/// Returns a reference to the tile at the specified coordinates /// Returns a reference to the tile at the specified coordinates
#[inline] #[inline]
pub fn get_tile(&self, coord: &Hex) -> Option<&HexTile> { pub fn get_tile(&self, coord: &Hex) -> Option<&HexTile> {
self.tiles.get(coord) self.0.get(coord)
} }
/// Returns a reference to the walls at the specified coordinates /// Returns a reference to the walls at the specified coordinates
#[inline]
pub fn get_walls(&self, coord: &Hex) -> Option<&Walls> { pub fn get_walls(&self, coord: &Hex) -> Option<&Walls> {
self.tiles.get(coord).map(|tile| tile.walls()) self.0.get(coord).map(|tile| tile.walls())
}
/// Returns the layout of the maze
#[inline]
pub fn layout(&self) -> &HexLayout {
&self.layout
}
/// Returns an iterator over all tiles
#[inline]
pub fn tiles(&self) -> &HashMap<Hex, HexTile> {
&self.tiles
} }
/// Returns the number of tiles in the maze /// Returns the number of tiles in the maze
#[inline] #[inline]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.tiles.len() self.0.len()
} }
/// Returns true if the maze is empty /// Returns true if the maze is empty
#[inline] #[inline]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.tiles.is_empty() self.0.is_empty()
} }
#[inline]
pub fn remove_tile_wall(&mut self, coord: &Hex, direction: EdgeDirection) { pub fn remove_tile_wall(&mut self, coord: &Hex, direction: EdgeDirection) {
if let Some(tile) = self.tiles.get_mut(coord) { if let Some(tile) = self.0.get_mut(coord) {
tile.walls.remove(direction); tile.walls.remove(direction);
} }
} }
} }
impl Deref for HexMaze {
type Target = HashMap<Hex, HexTile>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for HexMaze {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -155,7 +141,7 @@ mod tests {
for &direction in &directions { for &direction in &directions {
maze.add_wall(coord, direction); maze.add_wall(coord, direction);
assert!( assert!(
maze.get_walls(&coord).unwrap().has(direction), maze.get_walls(&coord).unwrap().contains(direction),
"Wall should exist after adding" "Wall should exist after adding"
); );
} }
@ -172,11 +158,7 @@ mod tests {
} }
// Test iterator // Test iterator
let collected = maze let collected = maze.iter().map(|(_, tile)| tile).collect::<Vec<_>>();
.tiles()
.iter()
.map(|(_, tile)| tile)
.collect::<Vec<_>>();
assert_eq!( assert_eq!(
collected.len(), collected.len(),
coords.len(), coords.len(),
@ -202,7 +184,7 @@ mod tests {
cloned_maze cloned_maze
.get_walls(&coord) .get_walls(&coord)
.unwrap() .unwrap()
.has(EdgeDirection::FLAT_TOP), .contains(EdgeDirection::FLAT_TOP),
"Cloned maze should preserve wall state" "Cloned maze should preserve wall state"
); );
} }
@ -259,11 +241,7 @@ mod tests {
} }
// Verify iterator // Verify iterator
let iter_coords = maze let iter_coords = maze.iter().map(|(coord, _)| *coord).collect::<Vec<_>>();
.tiles()
.iter()
.map(|(coord, _)| *coord)
.collect::<Vec<_>>();
assert_eq!( assert_eq!(
iter_coords.len(), iter_coords.len(),
coords.len(), coords.len(),
@ -281,8 +259,7 @@ mod tests {
#[test] #[test]
fn maze_builder() { fn maze_builder() {
// Test builder pattern // Test builder pattern
let layout = HexLayout::default(); let maze = HexMaze::with_radius(2);
let maze = HexMaze::with_layout(layout).with_radius(2);
assert_eq!(maze.len(), 19, "Radius 2 should create 19 hexes"); assert_eq!(maze.len(), 19, "Radius 2 should create 19 hexes");
assert!( assert!(
@ -291,30 +268,9 @@ mod tests {
); );
} }
#[test]
fn different_layouts() {
// Test with different layouts
let layouts = [
HexLayout {
orientation: hexx::HexOrientation::Flat,
..Default::default()
},
HexLayout {
orientation: hexx::HexOrientation::Pointy,
..Default::default()
},
];
for layout in layouts {
let maze = HexMaze::with_layout(layout).with_radius(1);
assert_eq!(maze.len(), 7, "Should work with different layouts");
}
}
#[test] #[test]
fn empty_maze() { fn empty_maze() {
let layout = HexLayout::default(); let maze = HexMaze::default();
let maze = HexMaze::with_layout(layout);
assert!(maze.is_empty(), "New maze should be empty"); assert!(maze.is_empty(), "New maze should be empty");
} }
} }

View File

@ -1,10 +1,19 @@
use std::fmt::Display;
use hexx::Hex; use hexx::Hex;
#[cfg(feature = "bevy")]
use hexx::HexLayout;
use super::Walls; use super::Walls;
#[cfg(feature = "bevy")]
use bevy::prelude::*;
/// Represents a single hexagonal tile in the maze /// Represents a single hexagonal tile in the maze
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "bevy", derive(Reflect, Component))]
#[cfg_attr(feature = "bevy", reflect(Component))]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HexTile { pub struct HexTile {
pub pos: Hex, pub pos: Hex,
pub walls: Walls, pub walls: Walls,
@ -12,7 +21,6 @@ pub struct HexTile {
impl HexTile { impl HexTile {
/// Creates a new tile with pos and default walls /// Creates a new tile with pos and default walls
#[inline]
pub fn new(pos: Hex) -> Self { pub fn new(pos: Hex) -> Self {
Self { Self {
pos, pos,
@ -31,6 +39,17 @@ impl HexTile {
pub fn pos(&self) -> Hex { pub fn pos(&self) -> Hex {
self.pos self.pos
} }
#[cfg(feature = "bevy")]
pub fn to_vec2(&self, layout: &HexLayout) -> Vec2 {
layout.hex_to_world_pos(self.pos)
}
#[cfg(feature = "bevy")]
pub fn to_vec3(&self, layout: &HexLayout) -> Vec3 {
let pos = self.to_vec2(layout);
Vec3::new(pos.x, 0., pos.y)
}
} }
impl From<Hex> for HexTile { impl From<Hex> for HexTile {
@ -42,6 +61,12 @@ impl From<Hex> for HexTile {
} }
} }
impl Display for HexTile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({},{})", self.pos.x, self.pos.y)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use hexx::EdgeDirection; use hexx::EdgeDirection;
@ -82,37 +107,17 @@ mod tests {
// Modify walls // Modify walls
tile.walls.remove(EdgeDirection::FLAT_TOP); tile.walls.remove(EdgeDirection::FLAT_TOP);
assert!( assert!(
!tile.walls.has(EdgeDirection::FLAT_TOP), !tile.walls.contains(EdgeDirection::FLAT_TOP),
"Wall should be removed" "Wall should be removed"
); );
tile.walls.add(EdgeDirection::FLAT_TOP); tile.walls.add(EdgeDirection::FLAT_TOP);
assert!( assert!(
tile.walls.has(EdgeDirection::FLAT_TOP), tile.walls.contains(EdgeDirection::FLAT_TOP),
"Wall should be added back" "Wall should be added back"
); );
} }
#[test]
fn tile_copy() {
let pos = Hex::new(-1, 2);
let tile = HexTile::new(pos);
// Test Copy trait
let copied_tile = tile;
assert_eq!(tile, copied_tile, "Copied tile should equal original");
// Verify both tiles are still usable
assert_eq!(
tile.pos, copied_tile.pos,
"Positions should match after copy"
);
assert_eq!(
tile.walls, copied_tile.walls,
"Walls should match after copy"
);
}
#[test] #[test]
fn tile_clone() { fn tile_clone() {
let pos = Hex::new(0, -2); let pos = Hex::new(0, -2);

View File

@ -1,19 +1,20 @@
use std::ops::{Deref, DerefMut}; use std::{
fmt::Debug,
ops::{Deref, DerefMut},
};
#[cfg(feature = "bevy")]
use bevy::prelude::*;
use hexx::EdgeDirection; use hexx::EdgeDirection;
/// Represents the walls of a hexagonal tile using bit flags /// Represents the walls of a hexagonal tile using bit flags
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "bevy", derive(Reflect, Component))]
#[cfg_attr(feature = "bevy", reflect(Component))]
pub struct Walls(u8); pub struct Walls(u8);
impl Walls { impl Walls {
/// Creates a new walls configuration with all walls
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Adds a wall in the specified direction /// Adds a wall in the specified direction
#[inline] #[inline]
pub fn add(&mut self, direction: EdgeDirection) { pub fn add(&mut self, direction: EdgeDirection) {
@ -28,8 +29,11 @@ impl Walls {
/// Returns true if there is a wall in the specified direction /// Returns true if there is a wall in the specified direction
#[inline] #[inline]
pub fn has(&self, direction: EdgeDirection) -> bool { pub fn contains<T>(&self, other: T) -> bool
self.0 & Self::from(direction).0 != 0 where
T: Into<Self>,
{
self.0 & other.into().0 != 0
} }
/// Returns the raw bit representation of the walls /// Returns the raw bit representation of the walls
@ -41,8 +45,13 @@ impl Walls {
impl From<EdgeDirection> for Walls { impl From<EdgeDirection> for Walls {
fn from(value: EdgeDirection) -> Self { fn from(value: EdgeDirection) -> Self {
let bits = 1 << value.index(); Self(1 << value.index())
Self(bits) }
}
impl From<u8> for Walls {
fn from(value: u8) -> Self {
Self(1 << value)
} }
} }
@ -72,11 +81,11 @@ mod tests {
#[test] #[test]
fn new_walls() { fn new_walls() {
let walls = Walls::new(); let walls = Walls::default();
// All walls should be present by default // All walls should be present by default
for direction in EdgeDirection::iter() { for direction in EdgeDirection::iter() {
assert!( assert!(
walls.has(direction), walls.contains(direction),
"Wall should exist in direction {:?}", "Wall should exist in direction {:?}",
direction direction
); );
@ -85,42 +94,42 @@ mod tests {
#[test] #[test]
fn add_remove_single_wall() { fn add_remove_single_wall() {
let mut walls = Walls::new(); let mut walls = Walls::default();
// Remove and verify each wall // Remove and verify each wall
walls.remove(EdgeDirection::FLAT_TOP); walls.remove(EdgeDirection::FLAT_TOP);
assert!(!walls.has(EdgeDirection::FLAT_TOP)); assert!(!walls.contains(EdgeDirection::FLAT_TOP));
// Add back and verify // Add back and verify
walls.add(EdgeDirection::FLAT_TOP); walls.add(EdgeDirection::FLAT_TOP);
assert!(walls.has(EdgeDirection::FLAT_TOP)); assert!(walls.contains(EdgeDirection::FLAT_TOP));
} }
#[test] #[test]
fn multiple_operations() { fn multiple_operations() {
let mut walls = Walls::new(); let mut walls = Walls::default();
// Remove multiple walls // Remove multiple walls
walls.remove(EdgeDirection::FLAT_TOP); walls.remove(EdgeDirection::FLAT_TOP);
walls.remove(EdgeDirection::FLAT_BOTTOM); walls.remove(EdgeDirection::FLAT_BOTTOM);
// Verify removed walls // Verify removed walls
assert!(!walls.has(EdgeDirection::FLAT_TOP)); assert!(!walls.contains(EdgeDirection::FLAT_TOP));
assert!(!walls.has(EdgeDirection::FLAT_BOTTOM)); assert!(!walls.contains(EdgeDirection::FLAT_BOTTOM));
// Verify other walls still exist // Verify other walls still exist
assert!(walls.has(EdgeDirection::FLAT_TOP_RIGHT)); assert!(walls.contains(EdgeDirection::FLAT_TOP_RIGHT));
assert!(walls.has(EdgeDirection::FLAT_TOP_LEFT)); assert!(walls.contains(EdgeDirection::FLAT_TOP_LEFT));
// Add back one wall // Add back one wall
walls.add(EdgeDirection::FLAT_TOP); walls.add(EdgeDirection::FLAT_TOP);
assert!(walls.has(EdgeDirection::FLAT_TOP)); assert!(walls.contains(EdgeDirection::FLAT_TOP));
assert!(!walls.has(EdgeDirection::FLAT_BOTTOM)); assert!(!walls.contains(EdgeDirection::FLAT_BOTTOM));
} }
#[test] #[test]
fn bit_patterns() { fn bit_patterns() {
let mut walls = Walls::new(); let mut walls = Walls::default();
assert_eq!( assert_eq!(
walls.as_bits(), walls.as_bits(),
0b111111, 0b111111,
@ -136,7 +145,7 @@ mod tests {
#[test] #[test]
fn remove_all_walls() { fn remove_all_walls() {
let mut walls = Walls::new(); let mut walls = Walls::default();
// Remove all walls // Remove all walls
for direction in EdgeDirection::iter() { for direction in EdgeDirection::iter() {
@ -149,7 +158,7 @@ mod tests {
// Verify each direction // Verify each direction
for direction in EdgeDirection::iter() { for direction in EdgeDirection::iter() {
assert!( assert!(
!walls.has(direction), !walls.contains(direction),
"No wall should exist in direction {:?}", "No wall should exist in direction {:?}",
direction direction
); );
@ -158,7 +167,7 @@ mod tests {
#[test] #[test]
fn deref_operations() { fn deref_operations() {
let mut walls = Walls::new(); let mut walls = Walls::default();
// Test Deref // Test Deref
let bits: &u8 = walls.deref(); let bits: &u8 = walls.deref();
@ -171,7 +180,7 @@ mod tests {
#[test] #[test]
fn idempotent_operations() { fn idempotent_operations() {
let mut walls = Walls::new(); let mut walls = Walls::default();
// Adding twice shouldn't change the result // Adding twice shouldn't change the result
walls.add(EdgeDirection::FLAT_TOP); walls.add(EdgeDirection::FLAT_TOP);