#include #include #include "algos/rand.hpp" #include "constants.hpp" #include "algos/maze.hpp" using std::string; using matrix::Matrix; namespace maze { inline size_t rand(size_t i, size_t j) { if(i < j) { return Random::uniform(i, j); } else if(j < i) { return Random::uniform(j, i); } else { return i; } } inline bool complete(Matrix& maze) { size_t width = matrix::width(maze); size_t height = matrix::height(maze); for(size_t row = 1; row < height; row += 2) { for(size_t col = 1; col < width; col += 2) { if(maze[row][col] != 0) return false; } } return true; } std::vector neighborsAB(Matrix& maze, Point on) { std::vector result; std::array points{{ {on.x, on.y - 2}, {on.x, on.y + 2}, {on.x - 2, on.y}, {on.x + 2, on.y} }}; for(auto point : points) { if(matrix::inbounds(maze, point.x, point.y)) { result.push_back(point); } } return result; } std::vector neighbors(Matrix& maze, Point on) { std::vector result; std::array points{{ {on.x, on.y - 2}, {on.x, on.y + 2}, {on.x - 2, on.y}, {on.x + 2, on.y} }}; for(auto point : points) { if(matrix::inbounds(maze, point.x, point.y)) { if(maze[point.y][point.x] == WALL_VALUE) { result.push_back(point); } } } return result; } inline std::pair find_coord(Matrix& maze) { size_t width = matrix::width(maze); size_t height = matrix::height(maze); for(size_t y = 1; y < height; y += 2) { for(size_t x = 1; x < width; x += 2) { if(maze[y][x] == WALL_VALUE) { auto found = neighborsAB(maze, {x, y}); for(auto point : found) { if(maze[point.y][point.x] == 0) { return {{x, y}, point}; } } } } } matrix::dump("BAD MAZE", maze); dbc::sentinel("failed to find coord?"); } bool Builder::room_should_exist(Room& room, bool allow_dupes) { if(!matrix::inbounds($walls, room.x, room.y) || !matrix::inbounds($walls, room.x + room.width, room.y + room.height)) { return false; } if(room.overlaps($no_rooms_region)) { return false; } for(auto& other : $rooms) { bool is_duped = allow_dupes ? room == other : room != other; if(is_duped && room.overlaps(other)) { return false; } } // it's in the map and doesn't collide with another room return true; } void Builder::randomize_rooms(size_t room_size) { // use those dead ends to randomly place rooms for(auto at : $dead_ends) { // skip 50% of them if(Random::uniform(0,1) == 0) continue; // get the room corners randomized std::array starts{{ {at.x, at.y}, // top left {at.x - room_size + 1, at.y}, // top right {at.x - room_size + 1, at.y - room_size + 1}, // bottom right {at.x, at.y - room_size + 1} // bottom left }}; size_t offset = Random::uniform(0, 3); // BUG: this still accidentally merges rooms for(size_t i = 0; i < starts.size(); i++) { size_t index = (i + offset) % starts.size(); Room cur{starts[index].x, starts[index].y, room_size, room_size}; // if it's out of bounds skip it if(room_should_exist(cur)) { $rooms.push_back(cur); break; } } } } void Builder::clear() { matrix::assign($walls, WALL_VALUE); } void Builder::divide(Point start, Point end) { for(matrix::line it{start, end}; it.next();) { $walls[it.y][it.x] = 0; $walls[it.y+1][it.x] = 0; } } void Builder::hunt_and_kill(Point on) { if($rooms.size() > 0) place_rooms(); while(!complete($walls)) { auto n = neighbors($walls, on); if(n.size() == 0) { // no neighbors, must be dead end add_dead_end(on); auto t = find_coord($walls); on = t.first; $walls[on.y][on.x] = 0; size_t row = (on.y + t.second.y) / 2; size_t col = (on.x + t.second.x) / 2; $walls[row][col] = 0; } else { // found neighbors, pick random one auto nb = n[rand(size_t(0), n.size() - 1)]; $walls[nb.y][nb.x] = 0; size_t row = (nb.y + on.y) / 2; size_t col = (nb.x + on.x) / 2; $walls[row][col] = 0; on = nb; } } } void Builder::place_rooms() { for(auto& room : $rooms) { for(matrix::rectangle it{$walls, room.x, room.y, room.width, room.height}; it.next();) { $walls[it.y][it.x] = 0; } } } void Builder::inner_donut(float outer_rad, float inner_rad) { size_t x = matrix::width($walls) / 2; size_t y = matrix::height($walls) / 2; for(matrix::circle it{$walls, {x, y}, outer_rad}; it.next();) { for(int x = it.left; x < it.right; x++) { $walls[it.y][x] = 0; } } for(matrix::circle it{$walls, {x, y}, inner_rad}; it.next();) { for(int x = it.left; x < it.right; x++) { $walls[it.y][x] = 1; } } } void Builder::remove_dead_ends() { dbc::check($dead_ends.size() > 0, "you have to run an algo first, no dead_ends to remove"); for(auto at : $dead_ends) { for(matrix::compass it{$walls, at.x, at.y}; it.next();) { if($walls[it.y][it.x] == SPACE_VALUE) { punch_dead_end(at.x, at.y, it.x, it.y); break; } } } } void Builder::dump(const std::string& msg) { auto wall_copy = $walls; // mark the rooms too for(auto& room : $rooms) { for(matrix::rectangle it{wall_copy, room.x, room.y, room.width, room.height}; it.next();) { if(wall_copy[it.y][it.x] == 0 && wall_copy[it.y][it.x] != 3) { wall_copy[it.y][it.x] = WALL_PATH_LIMIT; } } } // mark dead ends for(auto at : $dead_ends) { // don't mark dead ends if there's something else there wall_copy[at.y][at.x] = 32; } for(auto [at, _] : $doors) { wall_copy[at.y][at.x] = 0xd; } matrix::dump(msg, wall_copy); } void Builder::enclose() { size_t width = matrix::width($walls); size_t height = matrix::height($walls); for(matrix::perimeter it{0, 0, width, height}; it.next();) { $walls[it.y][it.x] = WALL_VALUE; Point at{it.x,it.y}; if($doors.contains(at)) $doors.erase(at); } } void Builder::open_box(size_t outer_size) { size_t center_x = matrix::width($walls) / 2; size_t center_y = matrix::height($walls) / 2; // compensate for the box's border now outer_size++; // this can't be right but for now it's working size_t x = center_x - outer_size; size_t y = center_y - outer_size; // BUG: is the + 1 here because the bug in perimeter size_t width = (outer_size * 2) + 1; size_t height = (outer_size * 2) + 1; for(matrix::perimeter p{x, y, width, height}; p.next();) { for(matrix::compass it{$walls, p.x, p.y}; it.next();) { if($ends_map.contains({it.x, it.y})) { $walls[y][x] = 0; break; } } } } void Builder::make_doors() { for(auto room : $rooms) { // walk the outer wall looking for an emergent door int possible_doors = 0; Point last_door{0,0}; matrix::perimeter it{room.x - 1, room.y - 1, room.width + 2, room.height + 2}; while(it.next()) { if($walls[it.y][it.x] == SPACE_VALUE) { last_door = {it.x, it.y}; possible_doors++; } } // if only found one then make that an actual door if(possible_doors == 1) { $doors.insert_or_assign(last_door, true); continue; } // no natural door found, need to make one it = {room.x - 1, room.y - 1, room.width + 2, room.height + 2}; last_door = {0,0}; bool found_door = false; while(it.next()) { for(matrix::compass near_door{$walls, it.x, it.y}; near_door.next();) { // skip the wall parts if($walls[near_door.y][near_door.x] == WALL_VALUE) continue; if($ends_map.contains({near_door.x, near_door.y})) { if(room.contains({near_door.x, near_door.y})) { // last ditch effort is use the internal dead end last_door = {it.x, it.y}; } else { $walls[it.y][it.x] = SPACE_VALUE; $doors.insert_or_assign({it.x, it.y}, true); found_door = true; break; } } } } if(!found_door && last_door.x != 0) { // didn't find an external door so punch one at the dead end $walls[last_door.y][last_door.x] = SPACE_VALUE; $doors.insert_or_assign({last_door.x, last_door.y}, true); } } } void Builder::inner_box(size_t outer_size, size_t inner_size) { size_t x = matrix::width($walls) / 2; size_t y = matrix::height($walls) / 2; for(matrix::box it{$walls, x, y, outer_size}; it.next();) { $walls[it.y][it.x] = 0; } for(matrix::box it{$walls, x, y, inner_size}; it.next();) { $walls[it.y][it.x] = 1; } // make a fake room that blocks others $no_rooms_region = {x - outer_size, y - outer_size, outer_size * 2 + 1, outer_size * 2 + 1}; } void Builder::add_dead_end(Point at) { // doing this ensures no dupes, if it's !inserted then it already existed auto [_, inserted] = $ends_map.insert_or_assign(at, true); // so skip it, it isn't new if(inserted) { $dead_ends.push_back(at); } } bool Builder::validate() { size_t width = matrix::width($walls); size_t height = matrix::height($walls); // no rooms can overlap for(auto& room : $rooms) { if(!room_should_exist(room)) return false; } for(matrix::perimeter it{0, 0, width, height}; it.next();) { if($ends_map.contains({it.x, it.y})) return false; if($doors.contains({it.x, it.y})) return false; } if($rooms.size() == 1) return true; // initial path test can just use one room then look for // any cells that are empty in the walls map but unpathed in the paths Room test_room = $rooms.at(0); Point test{test_room.x, test_room.y}; $paths.set_target(test); $paths.compute_paths($walls); // $paths.dump("AFTER COMPUTE"); $paths.clear_target(test); for(matrix::each_cell it{$walls}; it.next();) { if($walls[it.y][it.x] == SPACE_VALUE && $paths.$paths[it.y][it.x] == WALL_PATH_LIMIT) { return false; } } return true; } void Builder::punch_dead_end(size_t at_x, size_t at_y, size_t x, size_t y) { int diff_x = at_x - x; int diff_y = at_y - y; $walls[at_y + diff_y][at_x + diff_x] = SPACE_VALUE; } bool Builder::repair() { // possible fixes // go through each dead end and remove until it works // go through each room and add a door until it works // only find rooms that are unpathable and fix them // walk the path deleting dead ends at the end of the path std::vector removed_ends; bool now_valid = false; for(auto& at : $dead_ends) { // punch a hole for this dead end for(matrix::compass it{$walls, at.x, at.y}; it.next();) { if($walls[it.y][it.x] == SPACE_VALUE) { punch_dead_end(at.x, at.y, it.x, it.y); removed_ends.push_back({it.x, it.y}); break; } } // if that validates it then done if(validate()) { now_valid = true; fmt::println("MID dead_end removed={}, total={}, %={}", removed_ends.size(), $dead_ends.size(), float(removed_ends.size()) / float($dead_ends.size())); break; } } // now go back and see if we can add any back int added_back = 0; for(auto& at : removed_ends) { $walls[at.y][at.x] = WALL_VALUE; if(!validate()) { $walls[at.y][at.x] = SPACE_VALUE; } else { added_back++; } } enclose(); now_valid = validate(); fmt::println("FINAL now_valid={} added_back={} removed={}, total={}, %={}", now_valid, added_back, removed_ends.size() - added_back, $dead_ends.size(), float(removed_ends.size() - added_back) / float($dead_ends.size())); // didn't find a way to make it valid return now_valid; } }