vendredi 13 janvier 2023

Segmentation fault in C++ program which generates a 2D grid [duplicate]

So I've been trying to convert this Python module to a C++ module and I've finished the logic, however, I've encountered some segmentation faults. The module is meant to create a binary space partition to generate the rooms, then it will use a minimum spanning tree and the A* algorithm to draw the hallways.

The map.cpp file:

// Custom includes
#include "astar.h"
#include "bsp.h"

// External includes
#include <unordered_set>


// ----- STRUCTURES ------------------------------
struct LevelConstants {
    int level, width, height;
};


struct Edge {
    int cost;
    Rect source, destination;

    inline bool operator<(const Edge edg) const {
        // The priority_queue data structure gets the maximum priority, so we need to
        // override that functionality to get the minimum priority
        return cost > edg.cost;
    }

    inline bool operator==(const Edge edg) const {
        return cost == edg.cost && source == edg.source && destination == edg.destination;
    }
};

template<>
struct std::hash<Edge> {
    size_t operator()(const Edge &edg) const {
        size_t res = 0;
        hash_combine(res, edg.cost);
        hash_combine(res, edg.source);
        hash_combine(res, edg.destination);
        return res;
    }
};

// ----- FUNCTIONS ------------------------------
inline std::vector<Point> collect_positions(std::vector<std::vector<int>> *grid, TileType target) {
    // Iterate over grid and check each position
    std::vector<Point> result;
    for (int y = 0; y < grid->size(); y++) {
        for (int x = 0; x < grid[y].size(); x++) {
            if ((*grid)[y][x] == target) {
                result.push_back(Point{
                    x, y
                });
            }
        }
    }

    // Return result
    return result;
}

void
split_bsp(Leaf *bsp, std::vector<std::vector<int>> *grid, std::mt19937 *random_generator, int split_iteration) {
    // Start the splitting using a queue
    std::queue<Leaf> queue;
    queue.push(*bsp);
    while (split_iteration > 0 && !queue.empty()) {
        // Get the current leaf from the deque object
        Leaf current = queue.front();
        queue.pop();

        // Split the bsp if possible
        if (current.split(grid, random_generator, true) && current.left && current.right) {
            // Add the child leafs so they can be split
            queue.push(*current.left);
            queue.push(*current.right);

            // Decrement the split iteration
            split_iteration -= 1;
        }
    }
}

std::vector<Rect> generate_rooms(Leaf *bsp, std::vector<std::vector<int>> *grid, std::mt19937 *random_generator) {
    // Create the rooms
    std::vector<Rect> rooms;
    std::queue<Leaf> queue;
    queue.push(*bsp);
    while (!queue.empty()) {
        // Get the current leaf from the stack
        Leaf current = queue.front();
        queue.pop();

        // Check if a room already exists in this leaf
        if (current.room) {
            continue;
        }

        // Test if we can create a room in the current leaf
        if (current.left && current.right) {
            // Room creation not successful meaning there are child leafs so try again on the child
            // leafs
            queue.push(*current.left);
            queue.push(*current.right);
        } else {
            // Create a room in the current leaf and save the rect
            while (!current.create_room(grid, random_generator)) {
                // Width to height ratio is outside of range so try again
                continue;
            }

            // Add the created room to the rooms list
            rooms.push_back(*current.room);
        }
    }

    // Return all the created rooms
    return rooms;
}

std::unordered_set<Edge> create_connections(std::unordered_map<Rect, std::vector<Rect>> *complete_graph) {
    // Use Prim's algorithm to construct a minimum spanning tree from complete_graph
    Rect start = complete_graph->begin()->first;
    std::priority_queue<Edge> unexplored;
    std::unordered_set<Rect> visited;
    std::unordered_set<Edge> mst;
    unexplored.push(Edge{0, start, start});
    while (mst.size() < complete_graph->size() - 1) {
        // Get the neighbour with the lowest cost
        Edge lowest = unexplored.top();
        unexplored.pop();

        // Check if the neighbour is already visited or not
        if (visited.count(lowest.destination)) {
            continue;
        }

        // Neighbour isn't visited so mark it as visited and add its neighbours to the heap
        visited.insert(lowest.destination);
        for (Rect neighbour: complete_graph->at(lowest.destination)) {
            if (!visited.count(neighbour)) {
                unexplored.push(
                    Edge{
                        lowest.destination.get_distance_to(&neighbour),
                        lowest.destination,
                        neighbour,
                    }
                );
            }
        }

        // Add a new edge towards the lowest cost neighbour onto the mst
        if (lowest.source != lowest.destination) {
            // Save the connection
            mst.insert(lowest);
        }
    }

    // Return the constructed minimum spanning tree
    return mst;
}

void create_hallways(std::vector<std::vector<int>> *grid, std::mt19937 *random_generator,
                     std::unordered_set<Edge> *connections, int obstacle_count) {
    // Place random obstacles in the grid
    std::vector<Point> obstacle_positions = collect_positions(grid, TileType::Empty);
    for (int i = 0; i < obstacle_count; i++) {
        std::uniform_int_distribution<> obstacle_point_distribution(0, (int) obstacle_positions.size());
        int obstacle_point_index = obstacle_point_distribution(*random_generator);
        Point obstacle_point = obstacle_positions[obstacle_point_index];
        obstacle_positions.erase(obstacle_positions.begin() + obstacle_point_index);
        (*grid)[obstacle_point.y][obstacle_point.x] = TileType::Obstacle;
    }

    // Use the A* algorithm with to connect each pair of rooms making sure to avoid the obstacles
    // giving us natural looking hallways. Note that the width of the hallways will always be odd in
    // this implementation due to numpy indexing
    const int HALF_HALLWAY_SIZE = HALLWAY_SIZE / 2;
    for (Edge connection: *connections) {
        for (Point path_point: calculate_astar_path(grid, connection.source.center, connection.destination.center)) {
            // Place a rect box around the path_point using HALLWAY_SIZE to determine the width and
            // height
            Rect{
                Point{
                    path_point.x - HALF_HALLWAY_SIZE,
                    path_point.y - HALF_HALLWAY_SIZE,
                },
                Point{
                    path_point.x + HALF_HALLWAY_SIZE,
                    path_point.y + HALF_HALLWAY_SIZE,
                }
            }.place_rect(grid);
        }
    }
}

void place_tile(std::vector<std::vector<int>> *grid, std::mt19937 *random_generator, TileType target_tile,
                std::vector<Point> *possible_tiles) {
    // Get a random floor position and place the target tile
    std::uniform_int_distribution<> possible_tiles_distribution(0, (int) (*possible_tiles).size());
    int possible_tile_index = possible_tiles_distribution(*random_generator);
    Point possible_tile = possible_tiles->at(possible_tile_index);
    (*grid)[possible_tile.y][possible_tile.x] = target_tile;
}


std::pair<std::vector<std::vector<int>>, LevelConstants> create_map(int level, unsigned int seed) {
    // Initialise a few variables needed for the map generation
    int grid_width = MAP_GENERATION_CONSTANTS.width.generate_value(level);
    int grid_height = MAP_GENERATION_CONSTANTS.height.generate_value(level);
    std::mt19937 random_generator(seed);
    std::vector<std::vector<int>> grid(grid_height, std::vector<int>(grid_width, TileType::Empty));
    Leaf bsp = Leaf{
        Rect{
            Point{0, 0},
            Point{grid_width - 1, grid_height - 1}
        }
    };

    // Split the bsp and create the rooms
    split_bsp(&bsp, &grid, &random_generator, MAP_GENERATION_CONSTANTS.split_iteration.generate_value(level));
    std::vector<Rect> rooms = generate_rooms(&bsp, &grid, &random_generator);

    // Create the hallways between the rooms
    std::unordered_map<Rect, std::vector<Rect>> complete_graph;
    for (Rect room: rooms) {
        std::vector<Rect> temp;
        for (Rect x: rooms) {
            if (x != room) {
                temp.push_back(x);
            }
        }
        complete_graph.insert({room, temp});
    }
    create_hallways(&grid, &random_generator, &create_connections(&complete_graph),
                    MAP_GENERATION_CONSTANTS.obstacle_count.generate_value(level));

    // Get all the tiles which can support items being placed on them and then place the player and
    // all the items
    int item_limit = MAP_GENERATION_CONSTANTS.item_count.generate_value(level);
    std::vector<Point> possible_tiles = collect_positions(&grid, TileType::Floor);
    place_tile(&grid, &random_generator, TileType::Player, &possible_tiles);
    for (std::pair<TileType, double> item: ITEM_PROBABILITIES) {
        int tile_limit = (int) round((double) (item.second * item_limit));
        int tiles_placed = 0;
        while (tiles_placed < tile_limit) {
            place_tile(&grid, &random_generator, item.first, &possible_tiles);
            tiles_placed += 1;
        }
    }

    // Return the grid and a LevelConstants object
    return std::make_pair(grid, LevelConstants{level, grid_width, grid_height});
}

The bsp.h file:

#ifndef PRIMITIVES_H
#define PRIMITIVES_H
// Custom includes
#include "primitives.h"

#endif

// External includes
#include <random>

// ----- STRUCTURES ------------------------------
struct Leaf {
    // Parameters
    Rect container;

    // Attributes
    Leaf *left, *right;
    Rect *room;
    bool split_vertical;

    Leaf() {}

    Leaf(Rect container_val) {
        container = container_val;
    }

    bool split(std::vector<std::vector<int>> *grid, std::mt19937 *random_generator, bool debug_game) {
        // Check if this leaf is already split or not
        if (left && right) {
            return false;
        }

        // To determine the direction of split, we test if the width is 25% larger than the height,
        // if so we split vertically. However, if the height is 25% larger than the width, we split
        // horizontally. Otherwise, we split randomly
        std::uniform_int_distribution<> split_vertical_distribution(0, 1);
        bool split_vertical_val = split_vertical_distribution(*random_generator);
        if ((container.width > container.height) && ((double) (container.width / container.height) >= 1.25)) {
            split_vertical_val = true;
        } else if ((container.height > container.width) && ((double) (container.height / container.width) >= 1.25)) {
            split_vertical_val = false;
        }

        // To determine the range of values that we could split on, we need to find out if the
        // container is too small. Once we've done that, we can use the x1, y1, x2 and y2
        // coordinates to specify the range of values
        int max_size = (split_vertical_val) ? container.width - MIN_CONTAINER_SIZE : container.height -
                                                                                     MIN_CONTAINER_SIZE;
        if (max_size <= MIN_CONTAINER_SIZE) {
            // Container too small to split
            return false;
        }

        // Create the split position. This ensures that there will be MIN_CONTAINER_SIZE on each
        // side
        std::uniform_int_distribution<> pos_distribution(MIN_CONTAINER_SIZE, max_size);
        int pos = pos_distribution(*random_generator);

        // Split the container
        if (split_vertical_val) {
            // Split vertically making sure to adjust pos, so it can be within range of the actual
            // container
            pos += container.top_left.x;
            if (debug_game) {
                for (int y = container.top_left.y; y <= container.bottom_right.y + 1; y++) {
                    (*grid)[y][pos] = TileType::DebugWall;
                }
            }

            // Create the child leafs
            left = &Leaf{
                Rect{
                    Point{
                        container.top_left.x, container.top_left.y
                    },
                    Point{
                        pos - 1, container.bottom_right.y,
                    }
                }
            };
            right = &Leaf{
                Rect{
                    Point{
                        pos + 1, container.top_left.y,
                    },
                    Point{
                        container.bottom_right.x, container.bottom_right.y
                    }
                }
            };
        } else {
            // Split horizontally making sure to adjust pos, so it can be within range of the actual
            // container
            pos += container.top_left.y;
            if (debug_game) {
                for (int x = container.top_left.x; x <= container.bottom_right.x + 1; x++) {
                    (*grid)[pos][x] = TileType::DebugWall;
                }
            }

            // Create the child leafs
            left = &Leaf{
                Rect{
                    Point{
                        container.top_left.x, container.top_left.y
                    },
                    Point{
                        container.bottom_right.x, pos - 1,
                    }
                }
            };
            right = &Leaf{
                Rect{
                    Point{
                        container.top_left.x, pos + 1,
                    },
                    Point{
                        container.bottom_right.x, container.bottom_right.y
                    }
                }
            };
        }

        // Set the leaf's split direction
        split_vertical = split_vertical_val;

        // Successful split
        return true;
    }

    bool create_room(std::vector<std::vector<int>> *grid, std::mt19937 *random_generator) {
        // Test if this container is already split or not. If it is, we do not want to create a room
        // inside it otherwise it will overwrite other rooms
        if (left && right) {
            return false;
        }

        // Pick a random width and height making sure it is at least min_room_size but doesn't
        // exceed the container
        std::uniform_int_distribution<> width_distribution(MIN_ROOM_SIZE, container.width);
        int width = width_distribution(*random_generator);
        std::uniform_int_distribution<> height_distribution(MIN_ROOM_SIZE, container.height);
        int height = height_distribution(*random_generator);

        // Use the width and height to find a suitable x and y position which can create the room
        std::uniform_int_distribution<> x_pos_distribution(container.top_left.x, container.bottom_right.x - width);
        int x_pos = x_pos_distribution(*random_generator);
        std::uniform_int_distribution<> y_pos_distribution(container.top_left.y, container.bottom_right.y - height);
        int y_pos = y_pos_distribution(*random_generator);

        // Create the room rect and test if its width to height ratio will make an oddly-shaped room
        Rect rect = Rect{
            Point{
                x_pos, y_pos
            },
            Point{
                x_pos + width - 1, y_pos + height - 1
            }
        };
        if ((((double) std::min(rect.width, rect.height)) / ((double) std::min(rect.width, rect.height))) <
            ROOM_RATIO) {
            return false;
        }

        // Width to height ratio is fine so place the rect in the 2D grid and store it
        rect.place_rect(grid);
        room = &rect;

        // Successful room creation
        return true;
    }
};

The astar.h file:

#ifndef PRIMITIVES_H
#define PRIMITIVES_H
// Custom includes
#include "primitives.h"

#endif

// External includes
#include <queue>

// ----- CONSTANTS ------------------------------
/* The ID of a TileType obstacle */
const std::vector<Point> INTERCARDINAL_OFFSETS = {
    {-1, -1},
    {0,  -1},
    {1,  -1},
    {-1, 0},
    {1,  0},
    {-1, 1},
    {0,  1},
    {1,  1},
};


// ----- STRUCTURES ------------------------------
struct Neighbour {
    int cost;
    Point pair;

    inline bool operator<(const Neighbour nghbr) const {
        // The priority_queue data structure gets the maximum priority, so we need to
        // override that functionality to get the minimum priority
        return cost > nghbr.cost;
    }
};


// ----- FUNCTIONS ------------------------------
std::vector<Point> grid_bfs(Point target, int height, int width) {
    // Create a vector to store the neighbours
    std::vector<Point> result;

    // Iterate over each offset and check if it's a valid neighbour
    for (Point offset: INTERCARDINAL_OFFSETS) {
        int x = target.x + offset.x;
        int y = target.y + offset.y;
        if ((x >= 0 && x < width) && (y >= 0 && y < height)) {
            result.push_back({x, y});
        }
    }

    // Return the result
    return result;
}


std::vector<Point> calculate_astar_path(std::vector<std::vector<int>> *grid, Point start, Point end) {
    // Set up a few variables needed for the pathfinding
    std::vector<Point> result;
    std::priority_queue<Neighbour> queue;
    std::unordered_map<Point, Point> came_from = ;
    std::unordered_map<Point, int> distances = ;
    int height = (int) grid->capacity();
    int width = (int) grid[0].capacity();
    queue.push({0, start});

    // Loop until the priority queue is empty
    while (!queue.empty()) {
        // Get the lowest cost pair from the priority queue
        Point current = queue.top().pair;
        queue.pop();

        // Check if we've reached our target
        if (current == end) {
            // Backtrack through came_from to get the path
            while (!(came_from[current] == current)) {
                // Add the current pair to the result list
                result.push_back({current.x, current.y});

                // Get the next pair in the path
                current = came_from[current];
            }

            // Add the start pair and exit out of the loop
            result.push_back({start.x, start.y});
            break;
        }

        // Add all the neighbours to the heap with their cost being f = g + h:
        //   f - The total cost of traversing the neighbour.
        //   g - The distance between the start pair and the neighbour pair.
        //   h - The estimated distance from the neighbour pair to the end pair. We're using the Chebyshev distance for
        //       this.
        for (Point neighbour: grid_bfs(current, height, width)) {
            if (!came_from.count(neighbour)) {
                // Store the neighbour's parent and calculate its distance from the start pair
                came_from[neighbour] = current;
                distances[neighbour] = distances[current] + 1;

                // Check if the neighbour is an obstacle. If so, set the total cost to infinity, otherwise, set it to f = g + h
                int f_cost = ((*grid)[neighbour.y][neighbour.x] == TileType::Obstacle) ? std::numeric_limits<int>::max()
                                                                                       :
                             distances[neighbour] +
                             std::max(abs(neighbour.x - current.x), abs(neighbour.y - current.y));

                // Add the neighbour to the priority queue
                queue.push({f_cost, neighbour});
            }
        }
    }

    // Return result
    return result;
}

The primitives.h file:

// Custom includes
#include "constants.h"

// External includes
#include <unordered_map>

// ----- STRUCTURES ------------------------------
template<class T>
inline void hash_combine(size_t &seed, const T &v) {
    std::hash <T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

struct Point {
    int x, y;

    inline bool operator==(const Point pnt) const {
        return x == pnt.x && y == pnt.y;
    }

    inline bool operator!=(const Point pnt) const {
        return x != pnt.x && y != pnt.y;
    }

    Point() {}

    Point(int x_val, int y_val) {
        x = x_val;
        y = y_val;
    }

    inline std::pair<int, int> sum(Point *other) {
        return std::make_pair(x + other->x, y + other->y);
    }

    inline std::pair<int, int> abs_diff(Point *other) {
        return std::make_pair(abs(x + other->x), abs(y + other->y));
    }
};


template<>
struct std::hash<Point> {
    size_t operator()(const Point &pnt) const {
        size_t res = 0;
        hash_combine(res, pnt.x);
        hash_combine(res, pnt.y);
        return res;
    }
};

struct Rect {
    // Parameters
    Point top_left, bottom_right;

    // Attributes
    Point center;
    int width, height;

    inline bool operator==(const Rect rct) const {
        return top_left == rct.top_left && bottom_right == rct.bottom_right;
    }

    inline bool operator!=(const Rect rct) const {
        return top_left != rct.top_left && bottom_right != rct.bottom_right;
    }

    Rect() {}

    Rect(Point top_left_val, Point bottom_right_val) {
        std::pair<int, int> sum = top_left_val.sum(&bottom_right_val);
        std::pair<int, int> diff = bottom_right_val.abs_diff(&top_left_val);
        top_left = top_left_val;
        bottom_right = bottom_right_val;
        center = Point((int) round(((double) sum.first) / 2.0), (int) round(((double) sum.second) / 2.0));
        width = diff.first;
        height = diff.second;
    }

    inline int get_distance_to(Rect *other) {
        return std::max(abs(center.x - other->center.x), abs(center.y - other->center.y));
    }

    void place_rect(std::vector<std::vector<int>> *grid) {
        // Get the width and height of the grid
        int grid_height = (int) grid->size();
        int grid_width = (int) grid[0].size();

        // Place the walls
        for (int y = std::max(top_left.y, 0); y < std::max(bottom_right.y + 1, grid_height); y++) {
            for (int x = std::max(top_left.x, 0); x < std::max(bottom_right.x + 1, grid_width); x++) {
                if (std::count(std::begin(REPLACEABLE_TILES), std::end(REPLACEABLE_TILES), (*grid)[y][x]) > 0) {
                    (*grid)[y][x] = TileType::Wall;
                }
            }
        }

        // Place the floors. The ranges must be -1 in all directions since we don't want to
        // overwrite the walls keeping the player in, but we still want to overwrite walls that
        // block the path for hallways
        for (int y = std::max(top_left.y + 1, 1); y < std::max(bottom_right.y, grid_height - 1); y++) {
            for (int x = std::max(top_left.x + 1, 1); x < std::max(bottom_right.x, grid_width - 1); x++) {
                (*grid)[y][x] = TileType::Floor;
            }
        }
    }
};

template<>
struct std::hash<Rect> {
    size_t operator()(const Rect &rct) const {
        size_t res = 0;
        hash_combine(res, rct.top_left);
        hash_combine(res, rct.bottom_right);
        return res;
    }
};

The constants.h file:

// External includes
#include <algorithm>
#include <math.h>

// ----- ENUMS ------------------------------
enum TileType {
    DebugWall,
    Empty,
    Floor,
    Wall,
    Obstacle,
    Player,
    HealthPotion,
    ArmourPotion,
    HealthBoostPotion,
    ArmourBoostPotion,
    SpeedBoostPotion,
    FireRateBoostPotion
};

// ----- STRUCTURES ------------------------------
struct MapGenerationConstant {
    double base_value, increase, max_value;

    inline int generate_value(int level) const {
        return (int) std::min(round(base_value * pow(increase, level)), max_value);
    }
};

struct MapGenerationConstants {
    MapGenerationConstant width, height, split_iteration, obstacle_count, item_count;
};

// ----- CONSTANTS ------------------------------
// Defines the constants for the map generation
const MapGenerationConstants MAP_GENERATION_CONSTANTS = {
    {30, 1.2, 150},
    {20, 1.2, 100},
    {5, 1.5, 25},
    {20,1.3, 200},
    {5, 1.1, 30},
};

// Defines the probabilities for each item
const std::pair<TileType, double> ITEM_PROBABILITIES[6] = {
    {HealthPotion,        0.3},
    {ArmourPotion,        0.3},
    {HealthBoostPotion,   0.2},
    {ArmourBoostPotion,   0.1},
    {SpeedBoostPotion,    0.05},
    {FireRateBoostPotion, 0.05},
};

// Defines constants for the binary space partition
const int MIN_CONTAINER_SIZE = 5;
const int MIN_ROOM_SIZE = 4;
const double ROOM_RATIO = 0.625;

// Defines constants for hallway and entity generation
const TileType REPLACEABLE_TILES[3] = {
    Empty, Obstacle, DebugWall
};
const int HALLWAY_SIZE = 5;

Sometimes the program fails on the create_hallways function, but other times it fails on the split_bsp function and is not consistent. I think it may be because of the recursive Leaf struct and its left and right attriutes, however, I'm not sure. Any ideas why this might be occuring?

Thanks.

Aucun commentaire:

Enregistrer un commentaire