From 49531ba148308373b4bd742e22c2af3511ee69de Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Sun, 16 Mar 2025 13:34:38 -0400 Subject: [PATCH] Rituals are more or less sorted out in theory, and they helped find a cycle in the GOAP algorithm that I'm detecting/preventing. --- Makefile | 2 +- goap.cpp | 22 ++++++++++++++---- meson.build | 3 ++- rituals.cpp | 23 +++++++++++++++++++ rituals.hpp | 23 +++++++++++++++++++ stats.cpp | 6 ++--- stats.hpp | 2 +- tests/ai.cpp | 21 +++++++++++++++++ tests/{combat.cpp => rituals.cpp} | 38 +------------------------------ 9 files changed, 93 insertions(+), 47 deletions(-) create mode 100644 rituals.cpp create mode 100644 rituals.hpp rename tests/{combat.cpp => rituals.cpp} (63%) diff --git a/Makefile b/Makefile index 39d1b1b..2f4e663 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ release_build: meson compile -j 10 -C builddir debug_build: - meson setup --wipe builddir --buildtype debug + meson setup --wipe builddir -Db_ndebug=true --buildtype debugoptimized meson compile -j 10 -C builddir tracy_build: diff --git a/goap.cpp b/goap.cpp index 49fee47..a7f9097 100644 --- a/goap.cpp +++ b/goap.cpp @@ -1,8 +1,10 @@ #include "dbc.hpp" #include "goap.hpp" #include "ai_debug.hpp" +#include "stats.hpp" namespace ai { + using namespace nlohmann; using namespace dbc; @@ -36,7 +38,6 @@ namespace ai { $negative_preconds[name] = false; } - bool Action::can_effect(State& state) { return ((state & $positive_preconds) == $positive_preconds) && ((state & $negative_preconds) == ALL_ZERO); @@ -53,14 +54,24 @@ namespace ai { Script reconstruct_path(std::unordered_map& came_from, Action& current) { Script total_path{current}; + bool final_found = false; - while(came_from.contains(current)) { + for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) { current = came_from.at(current); if(current != FINAL_ACTION) { total_path.push_front(current); + } else { + final_found = true; } } + // this here temporarily while I figure out cycle detects/prevention + if(!final_found && total_path[0] != FINAL_ACTION) { + auto error = fmt::format("!!!!! You may have a cycle in your json. No FINAL found. Here's the path: "); + for(auto& action : total_path) error += fmt::format("{} ", action.name); + dbc::sentinel(error); + } + return total_path; } @@ -96,7 +107,7 @@ namespace ai { ActionState current{FINAL_ACTION, start}; g_score[start] = 0; - open_set[current] = g_score[start] + h(start, goal, current.action); + open_set.insert_or_assign(current, g_score[start] + h(start, goal, current.action)); while(!open_set.empty()) { current = find_lowest(open_set); @@ -127,7 +138,10 @@ namespace ai { g_score[neighbor] = tentative_g_score; // open_set gets the fScore ActionState neighbor_as{neighbor_action, neighbor}; - open_set[neighbor_as] = tentative_g_score + h(neighbor, goal, neighbor_as.action); + + int score = tentative_g_score + h(neighbor, goal, neighbor_as.action); + // could maintain lowest here and avoid searching all things + open_set.insert_or_assign(neighbor_as, score); } } } diff --git a/meson.build b/meson.build index 6a7943f..39814da 100644 --- a/meson.build +++ b/meson.build @@ -111,6 +111,7 @@ sources = [ 'rand.cpp', 'raycaster.cpp', 'render.cpp', + 'rituals.cpp', 'save.cpp', 'shiterator.hpp', 'sound.cpp', @@ -126,7 +127,7 @@ sources = [ executable('runtests', sources + [ 'tests/ansi_parser.cpp', 'tests/base.cpp', - 'tests/combat.cpp', + 'tests/rituals.cpp', 'tests/components.cpp', 'tests/config.cpp', 'tests/dbc.cpp', diff --git a/rituals.cpp b/rituals.cpp new file mode 100644 index 0000000..dcc1321 --- /dev/null +++ b/rituals.cpp @@ -0,0 +1,23 @@ +#include "rituals.hpp" +#include "ai_debug.hpp" + +void RitualAI::reset() { + start = original; +} + +bool RitualAI::will_do(std::string name) { + ai::check_valid_action(name, "RitualAI::is_able_to"); + return plan.script[0].name == name; +} + +void RitualAI::set_state(std::string name, bool setting) { + ai::set(start, name, setting); +} + +void RitualAI::update() { + plan = ai::plan(script, start, goal); +} + +void RitualAI::dump() { + ai::dump_script(script, start, plan.script); +} diff --git a/rituals.hpp b/rituals.hpp new file mode 100644 index 0000000..90eeaa7 --- /dev/null +++ b/rituals.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "ai.hpp" + +struct RitualAI { + std::string script; + ai::State start; + ai::State original; + ai::State goal; + ai::ActionPlan plan; + + RitualAI(std::string script, ai::State start, ai::State goal) : + script(script), start(start), original(start), goal(goal) + { + } + + RitualAI() {}; + + void reset(); + bool will_do(std::string name); + void set_state(std::string name, bool setting); + void update(); + void dump(); +}; diff --git a/stats.cpp b/stats.cpp index 0508b85..5d26c83 100644 --- a/stats.cpp +++ b/stats.cpp @@ -1,10 +1,10 @@ #include "stats.hpp" #include -void Stats::dump() +void Stats::dump(std::string msg) { - fmt::println("sum: {}, sumsq: {}, n: {}, " + fmt::println("{}: sum: {}, sumsq: {}, n: {}, " "min: {}, max: {}, mean: {}, stddev: {}", - sum, sumsq, n, min, max, mean(), + msg, sum, sumsq, n, min, max, mean(), stddev()); } diff --git a/stats.hpp b/stats.hpp index 8ff9555..b600ce0 100644 --- a/stats.hpp +++ b/stats.hpp @@ -53,5 +53,5 @@ struct Stats { sample(1/elapsed.count()); } - void dump(); + void dump(std::string msg=""); }; diff --git a/tests/ai.cpp b/tests/ai.cpp index 2d86b62..87db0c8 100644 --- a/tests/ai.cpp +++ b/tests/ai.cpp @@ -3,6 +3,7 @@ #include "ai.hpp" #include #include "ai_debug.hpp" +#include "rituals.hpp" using namespace dbc; using namespace nlohmann; @@ -207,5 +208,25 @@ TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") { enemy.set_state("health_good", false); enemy.update(); REQUIRE(enemy.wants_to("run_away")); +} + +TEST_CASE("confirm that cycles are avoided/detected", "[ai]") { + ai::reset(); + ai::init("tests/cyclic_rituals.json"); + + auto start = ai::load_state("initial"); + auto goal = ai::load_state("final"); + + RitualAI ritual("actions", start, goal); + ritual.reset(); + ritual.set_state("has_magick", true); + ritual.set_state("cursed_item", true); + ritual.set_state("shiny_bauble", true); + + bool it_throws = false; + try { ritual.update(); } catch(...) { it_throws = true; } + REQUIRE(it_throws); + fmt::println("\n\n------------ CYCLES AVOIDED"); + ritual.dump(); } diff --git a/tests/combat.cpp b/tests/rituals.cpp similarity index 63% rename from tests/combat.cpp rename to tests/rituals.cpp index b1365ec..a6cdba1 100644 --- a/tests/combat.cpp +++ b/tests/rituals.cpp @@ -1,43 +1,7 @@ #include #include -#include "ai.hpp" -#include "ai_debug.hpp" +#include "rituals.hpp" -struct RitualAI { - std::string script; - ai::State start; - ai::State original; - ai::State goal; - ai::ActionPlan plan; - - RitualAI(std::string script, ai::State start, ai::State goal) : - script(script), start(start), original(start), goal(goal) - { - } - - RitualAI() {}; - - void reset() { - start = original; - } - - bool will_do(std::string name) { - ai::check_valid_action(name, "RitualAI::is_able_to"); - return plan.script[0].name == name; - } - - void set_state(std::string name, bool setting) { - ai::set(start, name, setting); - } - - void update() { - plan = ai::plan(script, start, goal); - } - - void dump() { - dump_script(script, start, plan.script); - } -}; TEST_CASE("prototype combat system ideas", "[combat]") { ai::reset();