From 2992193447433a8e3b4cb98d5bd3940014184a80 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Mon, 10 Mar 2025 00:42:59 -0400 Subject: [PATCH] GOAP is now matching cppGOAP but needs a serious cleanup. --- tests/goap.cpp | 157 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 55 deletions(-) diff --git a/tests/goap.cpp b/tests/goap.cpp index f1e9fa3..9af7d99 100644 --- a/tests/goap.cpp +++ b/tests/goap.cpp @@ -12,20 +12,12 @@ using namespace dbc; using namespace components; constexpr const int SCORE_MAX = std::numeric_limits::max(); - -enum StateNames { - ENEMY_IN_RANGE, - ENEMY_DEAD, - STATE_MAX -}; +constexpr const size_t STATE_MAX = 16; using GOAPState = std::bitset; bool is_subset(GOAPState& source, GOAPState& target) { - GOAPState result = source & target; - - std::cout << "IS_SUBSET: source: " << source << " target: " << target << " result: " << result << " is it? " << (result == target) << std::endl; return result == target; } @@ -33,8 +25,11 @@ struct Action { std::string name; int cost = 0; - std::unordered_map preconds; - std::unordered_map effects; + std::unordered_map preconds; + std::unordered_map effects; + + Action(std::string name, int cost) : + name(name), cost(cost) {} bool can_effect(GOAPState& state) { for(auto [name, setting] : preconds) { @@ -66,12 +61,15 @@ template<> struct std::hash { } }; -const Action FINAL_ACTION{"END", SCORE_MAX, {}, {}}; +const Action FINAL_ACTION("END", SCORE_MAX); struct ActionState { Action action; GOAPState state; + ActionState(Action action, GOAPState state) : + action(action), state(state) {} + bool operator==(const ActionState& other) const { return other.action == action && other.state == state; } @@ -92,50 +90,40 @@ int distance_to_goal(GOAPState& from, GOAPState& to) { AStarPath reconstruct_path(std::unordered_map& came_from, Action& current) { - fmt::println(">> reconstruct path: {}", current.name); AStarPath total_path{current}; int count = 0; while(came_from.contains(current) && count++ < 10) { - current = came_from[current]; + current = came_from.at(current); if(current != FINAL_ACTION) { - fmt::println("adding next action: {}", current.name); total_path.push_front(current); } } - fmt::println("Exited reconstruct path."); return total_path; } inline int h(GOAPState& start, GOAPState& goal) { - int result = distance_to_goal(start, goal); - std::cout << "h on " << start << " and " << goal << " gives distance " << result << "\n"; - return result; + return distance_to_goal(start, goal); } inline int d(GOAPState& start, GOAPState& goal) { - int result = distance_to_goal(start, goal); - std::cout << "d on " << start << " and " << goal << " gives distance " << result << "\n"; - return result; + return distance_to_goal(start, goal); } inline ActionState find_lowest(std::unordered_map& open_set) { dbc::check(!open_set.empty(), "open set can't be empty in find_lowest"); - ActionState result; + const ActionState *result = nullptr; int lowest_score = SCORE_MAX; - for(auto [as, score] : open_set) { - fmt::println("### find_lowest: action={}, score={}", as.action.name, score); - - if(score < lowest_score) { - lowest_score = score; - result = as; + for(auto& kv : open_set) { + if(kv.second < lowest_score) { + lowest_score = kv.second; + result = &kv.first; } } - fmt::println("<<< found lowest: action={}, score={}", result.action.name, lowest_score); - return result; + return *result; } @@ -152,7 +140,6 @@ std::optional plan_actions(std::vector& actions, GOAPState& s open_set[start_state] = g_score[start] + h(start, goal); while(!open_set.empty()) { - fmt::println(">>>>>>>>>>>>>>>>>>>>>> TOP OF WHILE"); auto current = find_lowest(open_set); if(is_subset(current.state, goal)) { @@ -162,11 +149,8 @@ std::optional plan_actions(std::vector& actions, GOAPState& s open_set.erase(current); for(auto& neighbor_action : actions) { - fmt::println("^^^ NEXT ACTION {}", neighbor_action.name); - // calculate the GOAPState being current/neighbor if(!neighbor_action.can_effect(current.state)) { - fmt::println("^^^ SKIP action {}", neighbor_action.name); continue; } @@ -175,21 +159,14 @@ std::optional plan_actions(std::vector& actions, GOAPState& s 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) { - fmt::println("!!! NEW LOW SCORE::: SETTING {} with PARENT {}, tg_score={}, ng_score={}", - neighbor_action.name, current.action.name, - tentative_g_score, neighbor_g_score); - - came_from[neighbor_action] = current.action; + came_from.insert_or_assign(neighbor_action, current.action); 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); } - fmt::println("^^^ END ACTION LOOP"); } - - fmt::println("<<<<<<<<<<<<<<<<< END OF WHILE"); } return std::nullopt; @@ -197,6 +174,11 @@ std::optional plan_actions(std::vector& actions, GOAPState& s TEST_CASE("worldstate works", "[goap]") { + enum StateNames { + ENEMY_IN_RANGE, + ENEMY_DEAD + }; + GOAPState goal; GOAPState start; std::vector actions; @@ -208,9 +190,7 @@ TEST_CASE("worldstate works", "[goap]") { // end goal is enemy is dead goal[ENEMY_DEAD] = true; - Action move_closer; - move_closer.name = "move_closer"; - move_closer.cost = 10; + Action move_closer("move_closer", 10); move_closer.preconds[ENEMY_IN_RANGE] = false; move_closer.effects[ENEMY_IN_RANGE] = true; @@ -224,9 +204,7 @@ TEST_CASE("worldstate works", "[goap]") { REQUIRE(!move_closer.can_effect(after_move_state)); REQUIRE(distance_to_goal(start, after_move_state) == 1); - Action kill_it; - kill_it.name = "kill_it"; - kill_it.cost = 10; + Action kill_it("kill_it", 10); kill_it.preconds[ENEMY_IN_RANGE] = true; kill_it.preconds[ENEMY_DEAD] = false; kill_it.effects[ENEMY_DEAD] = true; @@ -245,6 +223,11 @@ TEST_CASE("worldstate works", "[goap]") { } TEST_CASE("basic feature tests", "[goap]") { + enum StateNames { + ENEMY_IN_RANGE, + ENEMY_DEAD + }; + GOAPState goal; GOAPState start; std::vector actions; @@ -256,15 +239,11 @@ TEST_CASE("basic feature tests", "[goap]") { // end goal is enemy is dead goal[ENEMY_DEAD] = true; - Action move_closer; - move_closer.name = "move_closer"; - move_closer.cost = 10; + Action move_closer("move_closer", 10); move_closer.preconds[ENEMY_IN_RANGE] = false; move_closer.effects[ENEMY_IN_RANGE] = true; - Action kill_it; - kill_it.name = "kill_it"; - kill_it.cost = 10; + Action kill_it("kill_it", 10); kill_it.preconds[ENEMY_IN_RANGE] = true; kill_it.preconds[ENEMY_DEAD] = false; kill_it.effects[ENEMY_DEAD] = true; @@ -276,7 +255,75 @@ TEST_CASE("basic feature tests", "[goap]") { auto result = plan_actions(actions, start, goal); REQUIRE(result != std::nullopt); + auto state = start; + for(auto& action : *result) { fmt::println("ACTION: {}", action.name); + state = action.apply_effect(state); } + + REQUIRE(state[ENEMY_DEAD]); +} + +TEST_CASE("wargame test from cppGOAP", "[goap]") { + std::vector actions; + + // Constants for the various states are helpful to keep us from + // accidentally mistyping a state name. + enum WarGameStates { + target_acquired, + target_lost, + target_in_warhead_range, + target_dead + }; + + // Now establish all the possible actions for the action pool + // In this example we're providing the AI some different FPS actions + Action spiral("searchSpiral", 5); + spiral.preconds[target_acquired] = false; + spiral.preconds[target_lost] = true; + spiral.effects[target_acquired] = true; + actions.push_back(spiral); + + Action serpentine("searchSerpentine", 5); + serpentine.preconds[target_acquired] = false; + serpentine.preconds[target_lost] = false; + serpentine.effects[target_acquired] = true; + actions.push_back(serpentine); + + Action intercept("interceptTarget", 5); + intercept.preconds[target_acquired] = true; + intercept.preconds[target_dead] = false; + intercept.effects[target_in_warhead_range] = true; + actions.push_back(intercept); + + Action detonateNearTarget("detonateNearTarget", 5); + detonateNearTarget.preconds[target_in_warhead_range] = true; + detonateNearTarget.preconds[target_acquired] = true; + detonateNearTarget.preconds[target_dead] = false; + detonateNearTarget.effects[target_dead] = true; + actions.push_back(detonateNearTarget); + + // Here's the initial state... + GOAPState initial_state; + initial_state[target_acquired] = false; + initial_state[target_lost] = true; + initial_state[target_in_warhead_range] = false; + initial_state[target_dead] = false; + + // ...and the goal state + GOAPState goal_target_dead; + goal_target_dead[target_dead] = true; + + auto result = plan_actions(actions, initial_state, goal_target_dead); + REQUIRE(result != std::nullopt); + + auto state = initial_state; + + for(auto& action : *result) { + fmt::println("ACTION: {}", action.name); + state = action.apply_effect(state); + } + + REQUIRE(state[target_dead]); }