464 lines
12 KiB
C++
464 lines
12 KiB
C++
#include <fmt/core.h>
|
|
#include <string>
|
|
#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<Point> neighborsAB(Matrix& maze, Point on) {
|
|
std::vector<Point> result;
|
|
|
|
std::array<Point, 4> 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<Point> neighbors(Matrix& maze, Point on) {
|
|
std::vector<Point> result;
|
|
|
|
std::array<Point, 4> 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<Point, Point> 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<Point, 4> 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<Point> 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;
|
|
}
|
|
}
|