From 84b95fac6a67cbe36e2242b9e83dfad4ebc32571 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Fri, 3 Jan 2025 00:24:47 +0200 Subject: [PATCH] feat: add more code examples --- assets/code/hexlab/builder.rs | 176 +++++++++++++++ assets/code/hexlab/generation.rs | 35 --- assets/code/hexlab/structs.rs | 32 --- assets/code/hexlab/walls.rs | 197 +++++++++++++++++ assets/code/maze-ascension/floor.rs | 69 ++++++ assets/code/maze-ascension/maze_generation.rs | 209 +++++++++++------- assets/krita/game-ui.kra | Bin 951390 -> 0 bytes assets/krita/game-ui.png | Bin 438614 -> 0 bytes bibliography.yml | 23 +- code.typ | 30 +-- doc.typ | 18 +- layout.typ | 18 +- main.typ | 73 +++--- utils.typ | 19 +- 14 files changed, 654 insertions(+), 245 deletions(-) create mode 100644 assets/code/hexlab/builder.rs delete mode 100644 assets/code/hexlab/generation.rs delete mode 100644 assets/code/hexlab/structs.rs create mode 100644 assets/code/hexlab/walls.rs create mode 100644 assets/code/maze-ascension/floor.rs delete mode 100644 assets/krita/game-ui.kra delete mode 100644 assets/krita/game-ui.png diff --git a/assets/code/hexlab/builder.rs b/assets/code/hexlab/builder.rs new file mode 100644 index 0000000..7c1da8b --- /dev/null +++ b/assets/code/hexlab/builder.rs @@ -0,0 +1,176 @@ +/// A builder pattern for creating hexagonal mazes. +/// +/// This struct provides a fluent interface for configuring and building hexagonal mazes. +/// It offers flexibility in specifying the maze size, random seed, generation algorithm, +/// and starting position. +/// +/// # Examples +/// +/// Basic usage: +/// ``` +/// use hexlab::prelude::*; +/// +/// let maze = MazeBuilder::new() +/// .with_radius(5) +/// .build() +/// .expect("Failed to create maze"); +/// +/// // A radius of 5 creates 61 hexagonal tiles +/// assert!(!maze.is_empty()); +/// assert_eq!(maze.len(), 91); +/// ``` +/// +/// Using a seed for reproducible results: +/// ``` +/// use hexlab::prelude::*; +/// +/// let maze1 = MazeBuilder::new() +/// .with_radius(3) +/// .with_seed(12345) +/// .build() +/// .expect("Failed to create maze"); +/// +/// let maze2 = MazeBuilder::new() +/// .with_radius(3) +/// .with_seed(12345) +/// .build() +/// .expect("Failed to create maze"); +/// +/// // Same seed should produce identical mazes +/// assert_eq!(maze1.len(), maze2.len()); +/// assert_eq!(maze1, maze2); +/// ``` +/// +/// Specifying a custom generator: +/// ``` +/// use hexlab::prelude::*; +/// +/// let maze = MazeBuilder::new() +/// .with_radius(7) +/// .with_generator(GeneratorType::RecursiveBacktracking) +/// .build() +/// .expect("Failed to create maze"); +/// ``` +#[derive(Default)] +pub struct MazeBuilder { + radius: Option, + seed: Option, + generator_type: GeneratorType, + start_position: Option, +} + +impl MazeBuilder { + /// Creates a new [`MazeBuilder`] instance with default settings. + #[inline] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the radius for the hexagonal maze. + /// + /// The radius determines the size of the maze, specifically the number of tiles + /// from the center (0,0) to the edge of the hexagon, not including the center tile. + /// For example, a radius of 3 would create a maze with 3 tiles from center to edge, + /// resulting in a total diameter of 7 tiles (3 + 1 + 3). + /// + /// # Arguments + /// + /// - `radius` - The number of tiles from the center to the edge of the hexagon. + #[inline] + #[must_use] + pub const fn with_radius(mut self, radius: u16) -> Self { + self.radius = Some(radius); + self + } + + /// Sets the random seed for maze generation. + /// + /// Using the same seed will produce identical mazes, allowing for reproducible results. + /// + /// # Arguments + /// + /// - `seed` - The random seed value. + #[inline] + #[must_use] + pub const fn with_seed(mut self, seed: u64) -> Self { + self.seed = Some(seed); + self + } + + /// Sets the generator algorithm for maze creation. + /// + /// Different generators may produce different maze patterns and characteristics. + /// + /// # Arguments + /// + /// - `generator_type` - The maze generation algorithm to use. + #[inline] + #[must_use] + pub const fn with_generator( + mut self, + generator_type: GeneratorType, + ) -> Self { + self.generator_type = generator_type; + self + } + + /// Sets the starting position for maze generation. + /// + /// # Arguments + /// + /// - `pos` - The hexagonal coordinates for the starting position. + #[inline] + #[must_use] + pub const fn with_start_position(mut self, pos: Hex) -> Self { + self.start_position = Some(pos); + self + } + + /// Builds the hexagonal maze based on the configured parameters. + /// + /// # Errors + /// + /// Returns [`MazeBuilderError::NoRadius`] if no radius is specified. + /// Returns [`MazeBuilderError::InvalidStartPosition`] if the start position is outside maze + /// bounds. + /// + /// # Examples + /// + /// ``` + /// use hexlab::prelude::*; + /// + /// // Should fail without radius + /// let result = MazeBuilder::new().build(); + /// assert!(result.is_err()); + /// + /// // Should succeed with radius + /// let result = MazeBuilder::new() + /// .with_radius(3) + /// .build(); + /// assert!(result.is_ok()); + /// + /// let maze = result.unwrap(); + /// assert!(!maze.is_empty()); + /// ``` + pub fn build(self) -> Result { + let radius = self.radius.ok_or(MazeBuilderError::NoRadius)?; + let mut maze = create_hex_maze(radius); + + if let Some(start_pos) = self.start_position { + if maze.get(&start_pos).is_none() { + return Err(MazeBuilderError::InvalidStartPosition(start_pos)); + } + } + + if !maze.is_empty() { + self.generator_type.generate( + &mut maze, + self.start_position, + self.seed, + ); + } + + Ok(maze) + } +} diff --git a/assets/code/hexlab/generation.rs b/assets/code/hexlab/generation.rs deleted file mode 100644 index c9c78eb..0000000 --- a/assets/code/hexlab/generation.rs +++ /dev/null @@ -1,35 +0,0 @@ -pub fn generate_backtracking( - maze: &mut HexMaze, - start_pos: Option, - seed: Option, -) { - if maze.is_empty() { - return; - } - let start = start_pos.unwrap_or(Hex::ZERO); - let mut visited = HashSet::new(); - let mut rng: Box = seed.map_or_else( - || Box::new(thread_rng()) as Box, - |seed| Box::new(ChaCha8Rng::seed_from_u64(seed)) as Box, - ); - recursive_backtrack(maze, start, &mut visited, &mut rng); -} - -fn recursive_backtrack( - maze: &mut HexMaze, - current: Hex, - visited: &mut HashSet, - rng: &mut R, -) { - visited.insert(current); - let mut directions = EdgeDirection::ALL_DIRECTIONS; - directions.shuffle(rng); - for direction in directions { - let neighbor = current + direction; - if maze.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) { - maze.remove_tile_wall(¤t, direction); - maze.remove_tile_wall(&neighbor, direction.const_neg()); - recursive_backtrack(maze, neighbor, visited, rng); - } - } -} diff --git a/assets/code/hexlab/structs.rs b/assets/code/hexlab/structs.rs deleted file mode 100644 index af579f1..0000000 --- a/assets/code/hexlab/structs.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[derive(Default)] -pub struct MazeBuilder { - radius: Option, - seed: Option, - generator_type: GeneratorType, - start_position: Option, -} - -#[derive(Debug, Clone, Copy, Default)] -pub enum GeneratorType { - #[default] - RecursiveBacktracking, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct HexMaze(HashMap); - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy", derive(Reflect, Component))] -#[cfg_attr(feature = "bevy", reflect(Component))] -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct HexTile { - pub(crate) pos: Hex, - pub(crate) walls: Walls, -} - -#[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); diff --git a/assets/code/hexlab/walls.rs b/assets/code/hexlab/walls.rs new file mode 100644 index 0000000..808328b --- /dev/null +++ b/assets/code/hexlab/walls.rs @@ -0,0 +1,197 @@ +/// 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: +/// ``` +/// use hexlab::prelude::*; +/// +/// // Create a hexagon with all walls +/// let walls = Walls::new(); +/// assert!(walls.is_enclosed()); +/// +/// // Create a hexagon with no walls +/// let mut walls = Walls::empty(); +/// assert!(walls.is_empty()); +/// +/// // Add specific walls +/// walls.insert(EdgeDirection::FLAT_NORTH); +/// walls.insert(EdgeDirection::FLAT_SOUTH); +/// assert_eq!(walls.count(), 2); +/// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy", derive(Component))] +#[cfg_attr(feature = "bevy", reflect(Component))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Walls(u8); + +impl Walls { + /// Insert a wall in the specified direction. + /// + /// # Arguments + /// + /// - `direction` - The direction in which to insert the wall. + /// + /// # Returns + /// + /// Returns `true` if a wall was present, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// assert_eq!(walls.count(), 0); + /// + /// assert!(!walls.insert(1)); + /// assert_eq!(walls.count(), 1); + /// + /// assert!(walls.insert(1)); + /// assert_eq!(walls.count(), 1); + /// + /// assert!(!walls.insert(EdgeDirection::FLAT_NORTH)); + /// assert_eq!(walls.count(), 2); + /// ``` + #[inline] + pub fn insert(&mut self, direction: T) -> bool + where + T: Into, + { + let mask = direction.into().0; + let was_present = self.0 & mask != 0; + self.0 |= mask; + was_present + } + + /// Removes a wall in the specified direction. + /// + /// # Arguments + /// + /// - `direction` - The direction from which to remove the wall. + /// + /// # Returns + /// + /// Returns `true` if a wall was present and removed, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::new(); + /// + /// assert!(walls.remove(1)); + /// assert_eq!(walls.count(), 5); + /// + /// assert!(!walls.remove(1)); + /// assert_eq!(walls.count(), 5); + /// + /// assert!(walls.remove(EdgeDirection::FLAT_NORTH)); + /// assert_eq!(walls.count(), 4); + /// ``` + #[inline] + pub fn remove(&mut self, direction: T) -> bool + where + T: Into, + { + let mask = direction.into().0; + let was_present = self.0 & mask != 0; + self.0 &= !mask; + was_present + } + + /// Checks if there is a wall in the specified direction. + /// + /// # Arguments + /// + /// - `other` - The direction to check for a wall. + /// + /// # Examples + /// + /// ``` + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// walls.insert(EdgeDirection::FLAT_NORTH); + /// + /// assert!(walls.contains(EdgeDirection::FLAT_NORTH)); + /// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH)); + /// ``` + #[inline] + pub fn contains(&self, direction: T) -> bool + where + T: Into, + { + self.0 & direction.into().0 != 0 + } + + /// 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. + /// + /// # Arguments + /// + /// - `direction` - The direction in which to toggle the wall. + /// + /// # Returns + /// + /// The previous state (`true` if a wall was present before toggling, `false` otherwise). + /// + /// # Examples + /// + /// ``` + /// use hexlab::prelude::*; + /// + /// let mut walls = Walls::empty(); + /// + /// assert!(!walls.toggle(0)); + /// assert_eq!(walls.count(), 1); + /// + /// assert!(walls.toggle(0)); + /// assert_eq!(walls.count(), 0); + /// ``` + pub fn toggle(&mut self, direction: T) -> bool + where + T: Into + Copy, + { + let mask = direction.into().0; + let was_present = self.0 & mask != 0; + self.0 ^= mask; + was_present + } +} + +impl From for Walls { + fn from(value: EdgeDirection) -> Self { + Self(1 << value.index()) + } +} + +impl From for Walls { + fn from(value: u8) -> Self { + Self(1 << value) + } +} + +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 From<[EdgeDirection; N]> for Walls { + fn from(value: [EdgeDirection; N]) -> Self { + value.into_iter().collect() + } +} diff --git a/assets/code/maze-ascension/floor.rs b/assets/code/maze-ascension/floor.rs new file mode 100644 index 0000000..299285c --- /dev/null +++ b/assets/code/maze-ascension/floor.rs @@ -0,0 +1,69 @@ +/// Move floor entities to their target Y positions based on movement speed +/// +/// # Behavior +/// - Calculates movement distance based on player speed and delta time +/// - Moves floors towards their target Y position +/// - Removes FloorYTarget component when floor reaches destination +pub fn move_floors( + mut commands: Commands, + mut maze_query: Query< + (Entity, &mut Transform, &FloorYTarget), + (With, With), + >, + player_query: Query<&MovementSpeed, With>, + time: Res