raycaster/src/algos/maze.cpp

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;
}
}