From c1aba2d5c8e072da71785a5be3945b42a60fada9 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Thu, 3 Apr 2025 10:14:50 -0400 Subject: [PATCH] This does a 'fit_sort' whenever the state is changed. fit_sort effectively sorts the actions by distance+cost so that the cost is actually present unlike the original algorithm. --- ai.cpp | 31 +++++++++++++------------------ ai.hpp | 2 +- goap.cpp | 20 ++++++++++++++------ tests/ai.cpp | 2 -- tests/combat.cpp | 2 ++ 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/ai.cpp b/ai.cpp index 061dabb..7bb6397 100644 --- a/ai.cpp +++ b/ai.cpp @@ -155,6 +155,7 @@ namespace ai { } void set(State& state, std::string name, bool value) { + // resort by best fit state.set(state_id(name), value); } @@ -162,39 +163,32 @@ namespace ai { return state.test(state_id(name)); } - ai::Action& EntityAI::best_fit() { - dbc::check(plan.script.size() > 0, "empty action plan script"); - int lowest_cost = plan.script[0].cost; - size_t best_action = 0; - - for(size_t i = 0; i < plan.script.size(); i++) { - auto& action = plan.script[i]; - if(!action.can_effect(start)) continue; - - if(action.cost < lowest_cost) { - lowest_cost = action.cost; - best_action = i; - } + void EntityAI::fit_sort() { + if(active()) { + std::sort(plan.script.begin(), plan.script.end(), + [&](auto& l, auto& r) { + int l_cost = l.cost + (!l.can_effect(start) * ai::SCORE_MAX); + int r_cost = r.cost + (!r.can_effect(start) * ai::SCORE_MAX); + return l_cost < r_cost; + }); } - - return plan.script[best_action]; } bool EntityAI::wants_to(std::string name) { ai::check_valid_action(name, "EntityAI::wants_to"); - dbc::check(plan.script.size() > 0, "empty action plan script"); - return best_fit().name == name; + return plan.script.size() > 0 && plan.script[0].name == name; } bool EntityAI::active() { if(plan.script.size() == 1) { return plan.script[0] != FINAL_ACTION; } else { - return plan.script.size() == 0; + return plan.script.size() != 0; } } void EntityAI::set_state(std::string name, bool setting) { + fit_sort(); ai::set(start, name, setting); } @@ -204,6 +198,7 @@ namespace ai { void EntityAI::update() { plan = ai::plan(script, start, goal); + fit_sort(); } AIProfile* profile() { diff --git a/ai.hpp b/ai.hpp index 0becbbe..87a6f32 100644 --- a/ai.hpp +++ b/ai.hpp @@ -23,7 +23,7 @@ namespace ai { EntityAI() {}; bool wants_to(std::string name); - ai::Action& best_fit(); + void fit_sort(); bool active(); diff --git a/goap.cpp b/goap.cpp index 7ad86cf..6048af2 100644 --- a/goap.cpp +++ b/goap.cpp @@ -4,6 +4,8 @@ #include "stats.hpp" #include +// #define DEBUG_CYCLES 1 + namespace ai { using namespace nlohmann; @@ -63,11 +65,8 @@ namespace ai { } } - inline void path_invariant(std::unordered_map& came_from, Action& current) { -#if defined(NDEBUG) - (void)came_from; // disable errors about unused - (void)current; -#else + inline void path_invariant(std::unordered_map& came_from, Action current) { +#if defined(DEBUG_CYCLES) bool final_found = current == FINAL_ACTION; for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) { @@ -79,6 +78,9 @@ namespace ai { dump_came_from("CYCLE DETECTED!", came_from, current); dbc::sentinel("AI CYCLE FOUND!"); } +#else + (void)came_from; // disable errors about unused + (void)current; #endif } @@ -156,15 +158,21 @@ namespace ai { auto neighbor = neighbor_action.apply_effect(current.state); if(closed_set.contains(neighbor)) continue; + // BUG: no matter what I do cost really doesn't impact the graph + // Additionally, every other GOAP implementation has the same problem, and + // it's probably because the selection of actions is based more on sets matching + // than actual weights of paths. This reduces the probability that an action will + // be chosen over another due to only cost. int d_score = d(current.state, neighbor) + neighbor_action.cost; int tentative_g_score = g_score[current.state] + d_score; int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX; - if(tentative_g_score < neighbor_g_score) { + if(tentative_g_score + neighbor_action.cost < neighbor_g_score) { came_from.insert_or_assign(neighbor_action, current.action); g_score.insert_or_assign(neighbor, tentative_g_score); + ActionState neighbor_as{neighbor_action, neighbor}; int score = tentative_g_score + h(neighbor, goal); diff --git a/tests/ai.cpp b/tests/ai.cpp index d545bd2..763aa44 100644 --- a/tests/ai.cpp +++ b/tests/ai.cpp @@ -205,7 +205,5 @@ TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") { enemy.set_state("in_combat", true); enemy.set_state("health_good", false); enemy.update(); - auto& best = enemy.best_fit(); - REQUIRE(best.name == "run_away"); REQUIRE(enemy.wants_to("run_away")); } diff --git a/tests/combat.cpp b/tests/combat.cpp index 9cf114d..2157b7e 100644 --- a/tests/combat.cpp +++ b/tests/combat.cpp @@ -19,8 +19,10 @@ TEST_CASE("cause scared rat won't run away bug", "[combat-fail]") { ai::EntityAI rat("Enemy::actions", ai_start, ai_goal); rat.set_state("tough_personality", false); rat.set_state("health_good", false); + REQUIRE(!rat.active()); battle.add_enemy(rat_id, rat); battle.plan(); + REQUIRE(rat.active()); rat.dump(); REQUIRE(rat.wants_to("run_away")); }