From a34becdaeb75e7e2a06928b24416e4326ae816c3 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Sat, 8 Mar 2025 23:23:29 -0500 Subject: [PATCH] A simple A* pathing function that works on maps, but I'll be changing it to do the GOAP pathing. --- Makefile | 2 +- README.md | 5 +++ lel.cpp | 13 +++--- main.cpp | 2 +- matrix.cpp | 2 +- meson.build | 9 ++++ tests/goap.cpp | 110 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 tests/goap.cpp diff --git a/Makefile b/Makefile index 6144bd3..cdba2a3 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ tracy_build: meson compile -j 10 -C builddir test: build - ./builddir/runtests + ./builddir/runtests "[goap]" run: build test powershell "cp ./builddir/zedcaster.exe ." diff --git a/README.md b/README.md index c57f896..25d4c3e 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,11 @@ Libraries Needed: * libxi-dev * libfreetype-dev +It uses c++ so you may need to install a libg++ or libc++ for your system. Usually this is all you +need: + +apt install build-essential + ## OSX Build Notes * Quite a bad experience. Need to install Python, cmake, meson, and ninja all which are in homebrew but if you don't use homebrew then this is a problem. diff --git a/lel.cpp b/lel.cpp index d95061b..e2579fd 100644 --- a/lel.cpp +++ b/lel.cpp @@ -43,18 +43,14 @@ namespace lel { for(auto& row : grid) { size_t columns = row.size(); + int cell_width = grid_w / columns; + dbc::check(cell_width > 0, "invalid cell width calc"); + dbc::check(cell_height > 0, "invalid cell height calc"); for(auto& name : row) { if(name == "_") continue; auto& cell = cells.at(name); - int cell_width = grid_w / columns; - dbc::check(cell_width > 0, "invalid cell width calc"); - dbc::check(cell_height > 0, "invalid cell height calc"); - - cell.x = grid_x + (cell.col * cell_width); - cell.y = grid_y + (cell.row * cell_height); - // ZED: getting a bit hairy but this should work if(cell.percent) { // when percent mode we have to take unset to 100% @@ -73,6 +69,9 @@ namespace lel { dbc::check(cell.h > 0, fmt::format("invalid height cell {}", name)); dbc::check(cell.w > 0, fmt::format("invalid width cell {}", name)); + cell.x = grid_x + (cell.col * cell_width); + cell.y = grid_y + (cell.row * cell_height); + // keep the midpoint since it is used a lot cell.mid_x = std::midpoint(cell.x, cell.x + cell.w); cell.mid_y = std::midpoint(cell.y, cell.y + cell.h); diff --git a/main.cpp b/main.cpp index 06a679b..5067ac8 100644 --- a/main.cpp +++ b/main.cpp @@ -8,7 +8,7 @@ int main(int argc, char* argv[]) { try { textures::init(); sound::init(); - sound::mute(false); + sound::mute(true); gui::FSM main; main.event(gui::Event::STARTED); Autowalker walker(main); diff --git a/matrix.cpp b/matrix.cpp index e3f9ed9..73f1c23 100644 --- a/matrix.cpp +++ b/matrix.cpp @@ -20,7 +20,7 @@ namespace matrix { } else if(cell == WALL_PATH_LIMIT) { print("# "); } else if(cell > 15) { - print("* "); + print("[{:x}]", cell); } else { print("{:x} ", cell); } diff --git a/meson.build b/meson.build index 26ac596..5a56267 100644 --- a/meson.build +++ b/meson.build @@ -120,6 +120,14 @@ sources = [ 'textures.cpp', 'tilemap.cpp', 'worldbuilder.cpp', + 'goap2/Action.cpp', + 'goap2/Action.h', + 'goap2/Node.cpp', + 'goap2/Node.h', + 'goap2/Planner.cpp', + 'goap2/Planner.h', + 'goap2/WorldState.cpp', + 'goap2/WorldState.h', ] executable('runtests', sources + [ @@ -130,6 +138,7 @@ executable('runtests', sources + [ 'tests/dbc.cpp', 'tests/dinkyecs.cpp', 'tests/fsm.cpp', + 'tests/goap.cpp', 'tests/guecs.cpp', 'tests/inventory.cpp', 'tests/lel.cpp', diff --git a/tests/goap.cpp b/tests/goap.cpp new file mode 100644 index 0000000..25931f1 --- /dev/null +++ b/tests/goap.cpp @@ -0,0 +1,110 @@ +#include +#include "dbc.hpp" +#include +#include +#include "levelmanager.hpp" +#include "matrix.hpp" +#include "components.hpp" +#include + +using namespace dbc; +using namespace components; + +using AStarPath = std::deque; + +void update_map(Matrix& map, std::deque& total_path) { + for(auto &point : total_path) { + map[point.y][point.x] = 10; + } +} + +AStarPath reconstruct_path(std::unordered_map& came_from, Point current) { + std::deque total_path{current}; + + while(came_from.contains(current)) { + current = came_from[current]; + total_path.push_front(current); + } + + return total_path; +} + +inline h(Point from, Point to) { + return std::hypot(float(from.x) - float(to.x), + float(from.y) - float(to.y)); +} + +inline d(Point current, Point neighbor) { + return std::hypot(float(current.x) - float(neighbor.x), + float(current.y) - float(neighbor.y)); +} + + +inline Point find_lowest(std::unordered_map& open_set) { + dbc::check(!open_set.empty(), "open set can't be empty in find_lowest"); + Point result; + float lowest_score = 10000; + + for(auto [point, score] : open_set) { + if(score < lowest_score) { + lowest_score = score; + result = point; + } + } + + return result; +} + + +std::optional path_to_player(Matrix& map, Point start, Point goal) { + std::unordered_map open_set; + std::unordered_map came_from; + std::unordered_map g_score; + g_score[start] = 0; + + open_set[start] = g_score[start] + h(start, goal); + + while(!open_set.empty()) { + auto current = find_lowest(open_set); + + if(current == goal) { + return std::make_optional(reconstruct_path(came_from, current)); + } + + open_set.erase(current); + + for(matrix::compass it{map, current.x, current.y}; it.next();) { + Point neighbor{it.x, it.y}; + + float d_score = d(current, neighbor) + map[it.y][it.x] * 1000; + float tentative_g_score = g_score[current] + d_score; + float neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : 10000.0f; + if(tentative_g_score < neighbor_g_score) { + came_from[neighbor] = current; + g_score[neighbor] = tentative_g_score; + // open_set gets the fScore + open_set[neighbor] = tentative_g_score + h(neighbor, goal); + } + } + } + + return std::nullopt; +} + +TEST_CASE("basic feature tests", "[goap]") { + for(int i = 0; i < 10; i++) { + LevelManager levels; + GameLevel level = levels.current(); + auto &map = *level.map; + + auto& player_at = level.world->get(level.player); + // matrix::dump("A* PLAYER", map.walls(), player_at.location.x, player_at.location.y); + + level.world->query([&](const auto ent, auto& enemy_at, auto&) { + if(ent != level.player) { + auto result = path_to_player(map.walls(), enemy_at.location, player_at.location); + REQUIRE(result != std::nullopt); + } + }); + } +}