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

View File

@ -1,7 +1,8 @@
use std::collections::HashSet;
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;
@ -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) {
if self.is_empty() {
return;
}
let start = *self.tiles().keys().next().unwrap();
let start = *self.keys().next().unwrap();
let mut visited = HashSet::new();
let mut rng = thread_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,
current: Hex,
visited: &mut HashSet<Hex>,
rng: &mut ThreadRng,
rng: &mut R,
) {
visited.insert(current);
@ -42,13 +61,11 @@ impl HexMaze {
for direction in directions {
let neighbor = current + direction;
if let Some(_) = self.get_tile(&neighbor) {
if !visited.contains(&neighbor) {
self.remove_tile_wall(&current, direction);
self.remove_tile_wall(&neighbor, direction.const_neg());
if self.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) {
self.remove_tile_wall(&current, direction);
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)]
mod tests {
use hexx::HexLayout;
use super::*;
#[test]
fn backtracking_generation() {
let mut maze = HexMaze::default().with_radius(2);
let mut maze = HexMaze::with_radius(2);
// Before generation
for tile in maze.tiles.values() {
for tile in maze.values() {
assert_eq!(tile.walls.as_bits(), 0b111111);
}
@ -73,16 +88,13 @@ mod tests {
maze.generate(GeneratorType::BackTracking);
// After generation
let all_walls = maze
.tiles
.values()
.all(|tile| tile.walls.as_bits() == 0b111111);
let all_walls = maze.values().all(|tile| tile.walls.as_bits() == 0b111111);
assert!(!all_walls, "Some walls should be removed");
}
#[test]
fn empty_maze() {
let mut maze = HexMaze::with_layout(HexLayout::default());
let mut maze = HexMaze::default();
maze.generate(GeneratorType::BackTracking);
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};
/// Represents a hexagonal maze with tiles and walls
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct HexMaze {
pub tiles: HashMap<Hex, HexTile>,
layout: HexLayout,
}
pub struct HexMaze(HashMap<Hex, HexTile>);
impl HexMaze {
/// Creates a new empty maze with the specified layout
/// Creates a new empty maze
#[inline]
pub fn new(layout: HexLayout) -> Self {
Self {
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,
}
pub fn new() -> Self {
Self::default()
}
/// Creates a hexagonal maze with the given radius
/// Uses axial coordinates (q, r) to create a perfect hexagon
#[inline]
pub fn with_radius(mut self, radius: u32) -> Self {
pub fn with_radius(radius: u32) -> Self {
let mut maze = Self::default();
let radius = radius as i32;
for q in -radius..=radius {
let r1 = (-radius).max(-q - radius);
@ -41,24 +30,22 @@ impl HexMaze {
for r in r1..=r2 {
let pos = Hex::new(q, r);
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
#[inline]
pub fn add_tile(&mut self, coords: Hex) {
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
#[inline]
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)
}
}
@ -66,47 +53,46 @@ impl HexMaze {
/// Returns a reference to the tile at the specified coordinates
#[inline]
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
#[inline]
pub fn get_walls(&self, coord: &Hex) -> Option<&Walls> {
self.tiles.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
self.0.get(coord).map(|tile| tile.walls())
}
/// Returns the number of tiles in the maze
#[inline]
pub fn len(&self) -> usize {
self.tiles.len()
self.0.len()
}
/// Returns true if the maze is empty
#[inline]
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) {
if let Some(tile) = self.tiles.get_mut(coord) {
if let Some(tile) = self.0.get_mut(coord) {
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)]
mod tests {
use super::*;
@ -155,7 +141,7 @@ mod tests {
for &direction in &directions {
maze.add_wall(coord, direction);
assert!(
maze.get_walls(&coord).unwrap().has(direction),
maze.get_walls(&coord).unwrap().contains(direction),
"Wall should exist after adding"
);
}
@ -172,11 +158,7 @@ mod tests {
}
// Test iterator
let collected = maze
.tiles()
.iter()
.map(|(_, tile)| tile)
.collect::<Vec<_>>();
let collected = maze.iter().map(|(_, tile)| tile).collect::<Vec<_>>();
assert_eq!(
collected.len(),
coords.len(),
@ -202,7 +184,7 @@ mod tests {
cloned_maze
.get_walls(&coord)
.unwrap()
.has(EdgeDirection::FLAT_TOP),
.contains(EdgeDirection::FLAT_TOP),
"Cloned maze should preserve wall state"
);
}
@ -259,11 +241,7 @@ mod tests {
}
// Verify iterator
let iter_coords = maze
.tiles()
.iter()
.map(|(coord, _)| *coord)
.collect::<Vec<_>>();
let iter_coords = maze.iter().map(|(coord, _)| *coord).collect::<Vec<_>>();
assert_eq!(
iter_coords.len(),
coords.len(),
@ -281,8 +259,7 @@ mod tests {
#[test]
fn maze_builder() {
// Test builder pattern
let layout = HexLayout::default();
let maze = HexMaze::with_layout(layout).with_radius(2);
let maze = HexMaze::with_radius(2);
assert_eq!(maze.len(), 19, "Radius 2 should create 19 hexes");
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]
fn empty_maze() {
let layout = HexLayout::default();
let maze = HexMaze::with_layout(layout);
let maze = HexMaze::default();
assert!(maze.is_empty(), "New maze should be empty");
}
}

View File

@ -1,10 +1,19 @@
use std::fmt::Display;
use hexx::Hex;
#[cfg(feature = "bevy")]
use hexx::HexLayout;
use super::Walls;
#[cfg(feature = "bevy")]
use bevy::prelude::*;
/// Represents a single hexagonal tile in the maze
#[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 pos: Hex,
pub walls: Walls,
@ -12,7 +21,6 @@ pub struct HexTile {
impl HexTile {
/// Creates a new tile with pos and default walls
#[inline]
pub fn new(pos: Hex) -> Self {
Self {
pos,
@ -31,6 +39,17 @@ impl HexTile {
pub fn pos(&self) -> Hex {
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 {
@ -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)]
mod tests {
use hexx::EdgeDirection;
@ -82,37 +107,17 @@ mod tests {
// Modify walls
tile.walls.remove(EdgeDirection::FLAT_TOP);
assert!(
!tile.walls.has(EdgeDirection::FLAT_TOP),
!tile.walls.contains(EdgeDirection::FLAT_TOP),
"Wall should be removed"
);
tile.walls.add(EdgeDirection::FLAT_TOP);
assert!(
tile.walls.has(EdgeDirection::FLAT_TOP),
tile.walls.contains(EdgeDirection::FLAT_TOP),
"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]
fn tile_clone() {
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;
/// Represents the walls of a hexagonal tile using bit flags
#[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))]
pub struct Walls(u8);
impl Walls {
/// Creates a new walls configuration with all walls
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Adds a wall in the specified direction
#[inline]
pub fn add(&mut self, direction: EdgeDirection) {
@ -28,8 +29,11 @@ impl Walls {
/// Returns true if there is a wall in the specified direction
#[inline]
pub fn has(&self, direction: EdgeDirection) -> bool {
self.0 & Self::from(direction).0 != 0
pub fn contains<T>(&self, other: T) -> bool
where
T: Into<Self>,
{
self.0 & other.into().0 != 0
}
/// Returns the raw bit representation of the walls
@ -41,8 +45,13 @@ impl Walls {
impl From<EdgeDirection> for Walls {
fn from(value: EdgeDirection) -> Self {
let bits = 1 << value.index();
Self(bits)
Self(1 << value.index())
}
}
impl From<u8> for Walls {
fn from(value: u8) -> Self {
Self(1 << value)
}
}
@ -72,11 +81,11 @@ mod tests {
#[test]
fn new_walls() {
let walls = Walls::new();
let walls = Walls::default();
// All walls should be present by default
for direction in EdgeDirection::iter() {
assert!(
walls.has(direction),
walls.contains(direction),
"Wall should exist in direction {:?}",
direction
);
@ -85,42 +94,42 @@ mod tests {
#[test]
fn add_remove_single_wall() {
let mut walls = Walls::new();
let mut walls = Walls::default();
// Remove and verify each wall
walls.remove(EdgeDirection::FLAT_TOP);
assert!(!walls.has(EdgeDirection::FLAT_TOP));
assert!(!walls.contains(EdgeDirection::FLAT_TOP));
// Add back and verify
walls.add(EdgeDirection::FLAT_TOP);
assert!(walls.has(EdgeDirection::FLAT_TOP));
assert!(walls.contains(EdgeDirection::FLAT_TOP));
}
#[test]
fn multiple_operations() {
let mut walls = Walls::new();
let mut walls = Walls::default();
// Remove multiple walls
walls.remove(EdgeDirection::FLAT_TOP);
walls.remove(EdgeDirection::FLAT_BOTTOM);
// Verify removed walls
assert!(!walls.has(EdgeDirection::FLAT_TOP));
assert!(!walls.has(EdgeDirection::FLAT_BOTTOM));
assert!(!walls.contains(EdgeDirection::FLAT_TOP));
assert!(!walls.contains(EdgeDirection::FLAT_BOTTOM));
// Verify other walls still exist
assert!(walls.has(EdgeDirection::FLAT_TOP_RIGHT));
assert!(walls.has(EdgeDirection::FLAT_TOP_LEFT));
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.has(EdgeDirection::FLAT_TOP));
assert!(!walls.has(EdgeDirection::FLAT_BOTTOM));
assert!(walls.contains(EdgeDirection::FLAT_TOP));
assert!(!walls.contains(EdgeDirection::FLAT_BOTTOM));
}
#[test]
fn bit_patterns() {
let mut walls = Walls::new();
let mut walls = Walls::default();
assert_eq!(
walls.as_bits(),
0b111111,
@ -136,7 +145,7 @@ mod tests {
#[test]
fn remove_all_walls() {
let mut walls = Walls::new();
let mut walls = Walls::default();
// Remove all walls
for direction in EdgeDirection::iter() {
@ -149,7 +158,7 @@ mod tests {
// Verify each direction
for direction in EdgeDirection::iter() {
assert!(
!walls.has(direction),
!walls.contains(direction),
"No wall should exist in direction {:?}",
direction
);
@ -158,7 +167,7 @@ mod tests {
#[test]
fn deref_operations() {
let mut walls = Walls::new();
let mut walls = Walls::default();
// Test Deref
let bits: &u8 = walls.deref();
@ -171,7 +180,7 @@ mod tests {
#[test]
fn idempotent_operations() {
let mut walls = Walls::new();
let mut walls = Walls::default();
// Adding twice shouldn't change the result
walls.add(EdgeDirection::FLAT_TOP);