Skip to content

Commit

Permalink
Re-architectured map.hpp introducing a builder pattern to allow eas…
Browse files Browse the repository at this point in the history
…y construction of a dungeon using the `MapGenerator` class. This re-architect also introduced a bunch of other refactors massively simplifying various parts of the codebase and extracting out common functionality.

Removed `Neighbour` and replaced it with `Connection` (a renamed version of `Edge`) allowing `Connection` to work for `map.hpp` and `dijkstra.hpp`. This also simplified the backtracking as we didn't have to find the source.

Added `Grid::convert_position(int)` allowing the conversion of a 1D index into a 2D `Position`.

Changed `Leaf::create_room()` so that `rooms` is now the center `Rect` position by default instead of the whole `Rect` object.

Stopped `pathfind()` throwing an error as this caused a crash with `std::transform` if the grid is empty.

Removed `LevelConstants::level`.
  • Loading branch information
JackAshwell11 committed Oct 4, 2024
1 parent c51862f commit bec006a
Show file tree
Hide file tree
Showing 13 changed files with 646 additions and 507 deletions.
2 changes: 0 additions & 2 deletions hades_extensions/generation/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ class TileType(Enum):
Chest = ...

class LevelConstants:
@property
def level(self: LevelConstants) -> int: ...
@property
def width(self: LevelConstants) -> int: ...
@property
Expand Down
8 changes: 6 additions & 2 deletions src/hades_extensions/include/generation/bsp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
#pragma once

// Std headers
#include <memory>
#include <random>

// TODO: Build this in wsl

// Avoid having to include headers for these
struct Grid;
struct Position;
struct Rect;
struct Grid;

/// A binary spaced partition leaf used to generate the dungeon's rooms.
struct Leaf {
Expand Down Expand Up @@ -37,5 +41,5 @@ struct Leaf {
/// @param grid - The 2D grid which represents the dungeon.
/// @param random_generator - The random generator to use.
/// @param rooms - The vector of rooms to add the new room to.
void create_room(const Grid &grid, std::mt19937 &random_generator, std::vector<Rect> &rooms);
void create_room(const Grid &grid, std::mt19937 &random_generator, std::vector<Position> &rooms);
};
29 changes: 24 additions & 5 deletions src/hades_extensions/include/generation/dijkstra.hpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
// Ensure this file is only included once
#pragma once

// Std headers
#include <vector>
// Local headers
#include "generation/primitives.hpp"

// Avoid having to include headers for these
struct Grid;
struct Position;
/// Represents an undirected weighted connection in a graph.
struct Connection {
/// The less than operator.
auto operator<(const Connection &connection) const -> bool {
// std::priority_queue uses a max heap, but we want a min heap, so the operator needs to be reversed
return cost > connection.cost;
}

/// The equality operator.
auto operator==(const Connection &connection) const -> bool {
return cost == connection.cost && source == connection.source && destination == connection.destination;
}

/// The cost of the connection.
int cost;

/// The source position.
Position source;

/// The destination position.
Position destination;
};

/// Calculate the shortest path in a grid from one pair to another using the A*
/// algorithm.
Expand Down
131 changes: 70 additions & 61 deletions src/hades_extensions/include/generation/map.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,16 @@
// Std headers
#include <optional>
#include <random>
#include <unordered_set>

// Local headers
#include "primitives.hpp"
#include "generation/dijkstra.hpp"
#include "generation/primitives.hpp"

/// Represents an undirected weighted edge in a graph.
struct Edge {
// std::priority_queue uses a max heap, but we want a min heap, so the operator needs to be reversed
auto operator<(const Edge &edge) const -> bool { return cost > edge.cost; }

auto operator==(const Edge &edge) const -> bool {
return cost == edge.cost && source == edge.source && destination == edge.destination;
}

/// The cost of the edge.
int cost;

/// The source rect.
Rect source;

/// The destination rect.
Rect destination;
};
// Avoid having to include headers for this
struct Connection;

/// Holds the constants for a specific level.
struct LevelConstants {
/// The level of this game.
int level;

/// The width of the dungeon.
int width;

Expand All @@ -43,47 +24,75 @@ struct LevelConstants {
int enemy_limit;
};

template <>
struct std::hash<Edge> {
auto operator()(const Edge &edge) const noexcept -> size_t {
size_t res{0};
hash_combine(res, edge.cost);
hash_combine(res, edge.source);
hash_combine(res, edge.destination);
return res;
}
};
/// Manages the generation of the map.
class MapGenerator {
public:
/// Initialise the object.
///
/// @param level - The game level to generate a map for.
/// @param random_generator - The random generator to use for the map generation.
explicit MapGenerator(int level, std::mt19937 random_generator);

/// Place a given amount of tiles in the 2D grid.
///
/// @param grid - The 2D grid which represents the dungeon.
/// @param random_generator - The random generator used to pick the position.
/// @param target_tile - The tile to place in the 2D grid.
/// @param probability - The probability of placing the tile.
/// @param count - The number of tiles to place.
/// @return A vector containing the positions of the placed tiles.
[[maybe_unused]] auto place_tiles(const Grid &grid, std::mt19937 &random_generator, TileType target_tile,
double probability, int count = std::numeric_limits<int>::max())
-> std::vector<Position>;

/// Create a minimum spanning tree from a given complete graph.
///
/// @details https://en.wikipedia.org/wiki/Prim%27s_algorithm
/// @param rooms - The rooms to create connections between.
/// @throws std::length_error - If rooms is empty.
/// @return A set of edges which form the connections between rects.
auto create_connections(const std::vector<Rect> &rooms) -> std::unordered_set<Edge>;
/// Generate the rooms in the dungeon.
auto generate_rooms() -> MapGenerator &;

/// Create the hallways by using A* to pathfind between the rooms.
///
/// @param grid - The 2D grid which represents the dungeon.
/// @param connections - The connections to pathfind using the A* algorithm.
void create_hallways(const Grid &grid, const std::unordered_set<Edge> &connections);
/// Create connections between the rooms in the dungeon.
auto create_connections() -> MapGenerator &;

/// Perform a cellular automata simulation on the grid.
///
/// @param grid - The 2D grid which represents the dungeon.
void run_cellular_automata(Grid &grid);
/// Generate the hallways in the dungeon.
auto generate_hallways() -> MapGenerator &;

/// Perform the cellular automata simulation in the dungeon.
///
/// @param generations - The number of generations to simulate.
auto cellular_automata(int generations = 1) -> MapGenerator &;

/// Generate the walls in the dungeon.
auto generate_walls() -> MapGenerator &;

/// Place the obstacles in the dungeon.
auto place_obstacles() -> MapGenerator &;

/// Place the player in the dungeon.
auto place_player() -> MapGenerator &;

/// Place the items in the dungeon.
auto place_items() -> MapGenerator &;

/// Place the goal in the dungeon.
auto place_goal() -> MapGenerator &;

/// Get the grid.
///
/// @return The grid.
[[nodiscard]] auto get_grid() -> Grid & { return grid_; }

/// Get the rooms.
///
/// @return The rooms.
[[nodiscard]] auto get_rooms() -> std::vector<Position> & { return rooms_; }

/// Get the connections.
///
/// @return The connections.
[[nodiscard]] auto get_connections() -> std::vector<Connection> & { return connections_; }

private:
/// The level of the dungeon.
int level_;

/// The 2D grid which represents the dungeon.
Grid grid_;

/// The random generator used to generate the map.
std::mt19937 random_generator_;

/// The rooms that have been generated.
std::vector<Position> rooms_;

/// The connections between the rooms.
std::vector<Connection> connections_;
};

/// Generate the game map for a given game level.
///
Expand Down
25 changes: 13 additions & 12 deletions src/hades_extensions/include/generation/primitives.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#include <vector>

/// Stores the different types of tiles in the game map.
enum class TileType : std::uint8_t {
enum struct TileType : std::uint8_t {
Empty,
Floor,
Wall,
Expand Down Expand Up @@ -127,6 +127,18 @@ struct Grid {
return position.x >= 0 && position.x < width && position.y >= 0 && position.y < height;
}

/// Convert a 1D grid position to a 2D grid position.
///
/// @param position - The position to convert.
/// @throws std::out_of_range - If the position is not within the 2D grid.
/// @return The 2D grid position.
[[nodiscard]] auto convert_position(const int position) const -> Position {
if (position < 0 || position >= width * height) {
throw std::out_of_range("Position not within the grid.");
}
return {.x = position % width, .y = position / width};
}

/// Convert a 2D grid position to a 1D grid position.
///
/// @param position - The position to convert.
Expand Down Expand Up @@ -176,7 +188,6 @@ struct Grid {
/// @details It is the responsibility of the caller to ensure that the rect fits in the grid.
/// @param rect - The rect to place in the 2D grid.
void place_rect(const Rect &rect) const {
// Place only the floors as the walls will be placed after the cellular automata
for (int y{std::max(rect.top_left.y, 0)}; y < std::min(rect.bottom_right.y + 1, height); y++) {
for (int x{std::max(rect.top_left.x, 0)}; x < std::min(rect.bottom_right.x + 1, width); x++) {
set_value({.x = x, .y = y}, TileType::Floor);
Expand Down Expand Up @@ -205,13 +216,3 @@ struct std::hash<Position> {
return res;
}
};

template <>
struct std::hash<Rect> {
auto operator()(const Rect &rect) const noexcept -> std::size_t {
std::size_t res{0};
hash_combine(res, rect.top_left);
hash_combine(res, rect.bottom_right);
return res;
}
};
1 change: 0 additions & 1 deletion src/hades_extensions/src/binding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ PYBIND11_MODULE(hades_extensions, module) { // NOLINT
.value("HealthPotion", TileType::HealthPotion)
.value("Chest", TileType::Chest);
pybind11::class_<LevelConstants>(generation, "LevelConstants", "Holds the constants for a specific level.")
.def_readonly("level", &LevelConstants::level)
.def_readonly("width", &LevelConstants::width)
.def_readonly("height", &LevelConstants::height)
.def_readonly("enemy_limit", &LevelConstants::enemy_limit);
Expand Down
4 changes: 2 additions & 2 deletions src/hades_extensions/src/generation/bsp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ void Leaf::split(std::mt19937 &random_generator) { // NOLINT(misc-no-recursion)
}

void Leaf::create_room(const Grid &grid, std::mt19937 &random_generator, // NOLINT(misc-no-recursion)
std::vector<Rect> &rooms) {
std::vector<Position> &rooms) {
// Check if this leaf is already split or not, if so, create rooms for the left and right leafs
if (left && right) {
left->create_room(grid, random_generator, rooms);
Expand Down Expand Up @@ -103,5 +103,5 @@ void Leaf::create_room(const Grid &grid, std::mt19937 &random_generator, // NOL
// Place the rect in the 2D grid then save it in the leaf and the rooms vector
grid.place_rect(rect);
room = std::make_unique<Rect>(rect);
rooms.push_back(rect);
rooms.push_back(rect.centre);
}
34 changes: 8 additions & 26 deletions src/hades_extensions/src/generation/dijkstra.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,6 @@
// Std headers
#include <functional>
#include <queue>
#include <unordered_map>

// Local headers
#include "generation/primitives.hpp"

/// Represents a grid position and its distance from the start position.
struct Neighbour {
// std::priority_queue uses a max heap, but we want a min heap, so the operator needs to be reversed
auto operator<(const Neighbour &neighbour) const -> bool { return cost > neighbour.cost; }

/// The cost to traverse to this neighbour.
int cost;

/// The destination position in the grid.
Position destination;
};

namespace {
/// Perform pathfinding on a grid using a specific heuristic and cost calculation.
Expand All @@ -33,16 +17,16 @@ namespace {
auto pathfind(const Grid &grid, const Position &start, const Position &end,
const std::function<bool(const Position &)> &is_not_traversable,
const std::function<int(const Position &, int)> &heuristic_function)
-> std::unordered_map<Position, Neighbour> {
-> std::unordered_map<Position, Connection> {
// Check if the grid size is not zero
if (grid.width == 0 || grid.height == 0) {
throw std::length_error("Grid size must be bigger than 0.");
return {};
}

// Initialise the result vector, priority queue and neighbours map which will be used during the algorithm
std::priority_queue<Neighbour> queue;
std::unordered_map<Position, Neighbour> neighbours{{start, {.cost = 0, .destination = start}}};
queue.emplace(0, start);
std::priority_queue<Connection> queue;
std::unordered_map<Position, Connection> neighbours{{start, {.cost = 0, .source = start, .destination = start}}};
queue.emplace(0, start, start);

// Loop until we have explored every neighbour or until we've reached the end
while (!queue.empty()) {
Expand Down Expand Up @@ -72,8 +56,8 @@ auto pathfind(const Grid &grid, const Position &start, const Position &end,
}

// Add the neighbour to the queue and neighbours map
queue.emplace(heuristic_function(neighbour, distance), neighbour);
neighbours[neighbour] = {.cost = distance, .destination = current};
queue.emplace(heuristic_function(neighbour, distance), current, neighbour);
neighbours[neighbour] = {.cost = distance, .source = current, .destination = neighbour};
}
}
return neighbours;
Expand All @@ -95,10 +79,8 @@ auto calculate_astar_path(const Grid &grid, const Position &start, const Positio
// Backtrack through the neighbours to get the resultant path since we've
// reached the end
std::vector<Position> path;
Position current{end};
while (result.at(current).destination != current) {
for (Position current{end}; current != start; current = result.at(current).source) {
path.push_back(current);
current = result.at(current).destination;
}
path.push_back(start);
return path;
Expand Down
Loading

0 comments on commit bec006a

Please sign in to comment.