From 8090251a71cad8fd6079f96c7b8bca27c08868c8 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Tue, 10 Mar 2026 00:42:58 -0400 Subject: [PATCH] I can validate that a map is pathable, but can't fix it. --- Makefile | 2 +- src/algos/maze.cpp | 87 ++++++++++++++++++++++++++++++++++++------- src/algos/maze.hpp | 12 +++++- src/algos/pathing.cpp | 4 ++ src/algos/pathing.hpp | 1 + src/game/map.hpp | 3 +- tests/mazes.cpp | 28 ++++++++++++-- 7 files changed, 116 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 1323a10..52d5aed 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ clean: meson compile --clean -C builddir debug_test: build - gdb --nx -x .gdbinit --ex run --ex bt --ex q --args builddir/runtests + gdb --nx -x .gdbinit --ex run --ex bt --ex q --args builddir/runtests "[mazes]" win_installer: powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp' diff --git a/src/algos/maze.cpp b/src/algos/maze.cpp index 2c0da3a..d945dd1 100644 --- a/src/algos/maze.cpp +++ b/src/algos/maze.cpp @@ -7,7 +7,6 @@ using std::string; using matrix::Matrix; - namespace maze { inline size_t rand(size_t i, size_t j) { if(i < j) { @@ -94,7 +93,7 @@ namespace maze { dbc::sentinel("failed to find coord?"); } - bool Builder::room_should_exist(Room& room) { + 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)) { @@ -106,7 +105,11 @@ namespace maze { } for(auto& other : $rooms) { - if(room.overlaps(other)) return false; + 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 @@ -181,8 +184,6 @@ namespace maze { on = nb; } } - - enclose(); } void Builder::place_rooms() { @@ -191,8 +192,6 @@ namespace maze { $walls[it.y][it.x] = 0; } } - - enclose(); } void Builder::inner_donut(float outer_rad, float inner_rad) { @@ -245,7 +244,11 @@ namespace maze { // 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; + wall_copy[at.y][at.x] = 32; + } + + for(auto [at, _] : $doors) { + wall_copy[at.y][at.x] = 0xd; } matrix::dump(msg, wall_copy); @@ -273,7 +276,9 @@ namespace maze { size_t height = matrix::height($walls); perimeter(0, 0, width, height, [&](size_t x, size_t y) { - $walls[y][x] = 1; + $walls[y][x] = WALL_VALUE; + Point at{x,y}; + if($doors.contains(at)) $doors.erase(at); }); } @@ -303,20 +308,35 @@ namespace maze { void Builder::make_doors() { for(auto room : $rooms) { - // BUG: still makes rooms without doors, so detect if no door added and brute force one in - perimeter(room.x, room.y, room.width, room.height, [&](auto x, auto y) { + // walk the outer wall looking for an emergent door + int has_door = 0; + Point last_door{0,0}; + + perimeter(room.x - 1, room.y - 1, room.width + 2, room.height + 2, [&](size_t x, size_t y) { + if($walls[y][x] == SPACE_VALUE) { + last_door = {x, y}; + has_door++; + } + }); + + // if only found one then make that an actual door + if(has_door == 1) { + $doors.insert_or_assign(last_door, true); + } + + // no emergent door found, punch out one + perimeter(room.x, room.y, room.width, room.height, [&](size_t x, size_t y) { if($ends_map.contains({x, y})) { for(matrix::compass door_at{$walls, x, y}; door_at.next();) { if($walls[door_at.y][door_at.x] == WALL_VALUE) { $walls[door_at.y][door_at.x] = SPACE_VALUE; + $doors.insert_or_assign({size_t(door_at.x), size_t(door_at.y)}, true); break; } } } }); } - - enclose(); } void Builder::inner_box(size_t outer_size, size_t inner_size) { @@ -338,10 +358,51 @@ namespace maze { } 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; + } + + bool bad_perimeter = false; + // BUG: this is dogshit, get rid of perimeter, I can't even spell it right it's so bad + perimeter(0, 0, width, height, [&](size_t x, size_t y) { + if($ends_map.contains({x, y})) bad_perimeter = true; + if($doors.contains({x, y})) bad_perimeter = true; + }); + + if(bad_perimeter) 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; + } } diff --git a/src/algos/maze.hpp b/src/algos/maze.hpp index afad953..ff8efac 100644 --- a/src/algos/maze.hpp +++ b/src/algos/maze.hpp @@ -11,10 +11,17 @@ namespace maze { std::vector& $dead_ends; std::unordered_map $ends_map; Room $no_rooms_region{0,0,0,0}; + // BUG: instead of bool map it to the room? + std::unordered_map $doors; + Pathing $paths; Builder(Map& map) : - $walls(map.$walls), $rooms(map.$rooms), $dead_ends(map.$dead_ends) + $walls(map.$walls), $rooms(map.$rooms), $dead_ends(map.$dead_ends), + $paths{matrix::width(map.$walls), matrix::height(map.$walls)} { + dbc::check(map.$width % 2 == 1, "map width not an ODD number (perimter dead ends bug)"); + dbc::check(map.$height % 2 == 1, "map height not an ODD number (perimter dead ends bug)"); + clear(); } @@ -31,7 +38,8 @@ namespace maze { void perimeter(size_t x, size_t y, size_t width, size_t height, std::function cb); void open_box(size_t outer_size); void add_dead_end(Point at); - bool room_should_exist(Room& room); + bool room_should_exist(Room& room, bool allow_dupes=false); void make_doors(); + bool validate(); }; } diff --git a/src/algos/pathing.cpp b/src/algos/pathing.cpp index 5d7d10e..57d3f69 100644 --- a/src/algos/pathing.cpp +++ b/src/algos/pathing.cpp @@ -132,3 +132,7 @@ bool Pathing::INVARIANT() { return true; } + +void Pathing::dump(const std::string& msg) { + matrix::dump(msg, $paths); +} diff --git a/src/algos/pathing.hpp b/src/algos/pathing.hpp index 2e07285..d069b28 100644 --- a/src/algos/pathing.hpp +++ b/src/algos/pathing.hpp @@ -37,4 +37,5 @@ public: PathingResult find_path(Point &out, int direction, bool diag); bool INVARIANT(); + void dump(const std::string& msg); }; diff --git a/src/game/map.hpp b/src/game/map.hpp index 79fbd70..9428785 100644 --- a/src/game/map.hpp +++ b/src/game/map.hpp @@ -19,7 +19,6 @@ struct Room { size_t width = 0; size_t height = 0; - bool contains(Point at) { return at.x >= x && at.x <= x + width -1 @@ -41,6 +40,8 @@ struct Room { // other bottom < this top == too far above || other.y + other.height < y); } + + bool operator==(const Room&) const = default; }; using EntityGrid = std::unordered_map; diff --git a/tests/mazes.cpp b/tests/mazes.cpp index 653a89e..7487e49 100644 --- a/tests/mazes.cpp +++ b/tests/mazes.cpp @@ -28,8 +28,8 @@ TEST_CASE("hunt-and-kill", "[mazes]") { } TEST_CASE("hunt-and-kill box", "[mazes]") { - for(int i = 5; i < 100; i++) { - Map map((i / 2) + 20, (i / 2) + 20); + for(int i = 25; i < 65; i += 2) { + Map map(i, i); maze::Builder maze(map); maze.hunt_and_kill(); @@ -83,8 +83,8 @@ TEST_CASE("hunt-and-kill no-dead-ends", "[mazes]") { } TEST_CASE("hunt-and-kill too much", "[mazes]") { - for(int i = 5; i < 100; i++) { - Map map((i / 2) + 20, (i / 2) + 20); + for(int i = 25; i < 65; i += 2) { + Map map(i, i); maze::Builder maze(map); maze.hunt_and_kill(); @@ -100,3 +100,23 @@ TEST_CASE("hunt-and-kill too much", "[mazes]") { } } } + +TEST_CASE("hunt-and-kill validator", "[mazes]") { + Map map(33, 33); + maze::Builder maze(map); + + maze.hunt_and_kill(); + maze.clear(); + maze.inner_box(6, 4); + maze.randomize_rooms(ROOM_SIZE); + maze.hunt_and_kill(); + maze.open_box(6); + maze.make_doors(); + maze.enclose(); + + bool valid = maze.validate(); + + maze.dump("VALIDATED"); + + REQUIRE(valid == true); +}