#include #include "dbc.hpp" #include #include #include "levelmanager.hpp" #include "matrix.hpp" #include "components.hpp" #include #include using namespace dbc; using namespace components; constexpr const int SCORE_MAX = std::numeric_limits::max(); enum StateNames { ENEMY_IN_RANGE, ENEMY_DEAD, STATE_MAX }; 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; } struct Action { std::string name; int cost = 0; std::unordered_map preconds; std::unordered_map effects; bool can_effect(GOAPState& state) { for(auto [name, setting] : preconds) { if(state[name] != setting) return false; } return true; } GOAPState apply_effect(GOAPState& state) { // RCR SUGGEST: state = (state & ~write_mask) | effect auto state_cp = state; for(auto [name, setting] : effects) { state_cp[name] = setting; } return state_cp; } bool operator==(const Action& other) const { return other.name == name; } }; template<> struct std::hash { size_t operator()(const Action& p) const { return std::hash{}(p.name); } }; struct ActionState { Action action; GOAPState state; bool operator==(const ActionState& other) const { return other.action == action && other.state == state; } }; template<> struct std::hash { size_t operator()(const ActionState& p) const { return std::hash{}(p.action) ^ std::hash{}(p.state); } }; using AStarPath = std::deque; int distance_to_goal(GOAPState& from, GOAPState& to) { auto result = from ^ to; return result.count(); } AStarPath reconstruct_path(std::unordered_map& came_from, Action& current) { AStarPath total_path{current}; while(came_from.contains(current)) { current = came_from[current]; total_path.push_front(current); } return total_path; } inline int h(GOAPState& start, GOAPState& goal) { return distance_to_goal(start, goal); } inline int d(GOAPState& start, GOAPState& goal) { 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; int lowest_score = SCORE_MAX; for(auto [as, score] : open_set) { if(score < lowest_score) { lowest_score = score; result = as; } } return result; } std::optional first_action(std::vector& actions, GOAPState& start) { Action start_action; for(auto& action : actions) { if(action.can_effect(start)) { return std::make_optional(action); } } return std::nullopt; } // map is the list of possible actions // start and goal are two world states std::optional plan_actions(std::vector& actions, GOAPState& start, GOAPState& goal) { std::unordered_map open_set; std::unordered_map came_from; std::unordered_map g_score; auto start_action = first_action(actions, start); dbc::check(start_action != std::nullopt, "no action can start"); ActionState start_state{*start_action, start}; g_score[start] = 0; open_set[start_state] = g_score[start] + h(start, goal); while(!open_set.empty()) { auto current = find_lowest(open_set); if(current.state == goal) { return std::make_optional(reconstruct_path(came_from, current.action)); } open_set.erase(current); for(auto& neighbor_action : actions) { // calculate the GOAPState being current/neighbor if(!neighbor_action.can_effect(current.state)) continue; auto neighbor = neighbor_action.apply_effect(current.state); int d_score = d(current.state, neighbor); 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) { // action attached? came_from[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); } } } return std::nullopt; } TEST_CASE("worldstate works", "[goap]") { GOAPState goal; GOAPState start; std::vector actions; // start off enemy not dead and not in range start[ENEMY_DEAD] = false; start[ENEMY_IN_RANGE] = false; // end goal is enemy is dead goal[ENEMY_DEAD] = true; Action move_closer; move_closer.name = "move_closer"; move_closer.cost = 10; move_closer.preconds[ENEMY_IN_RANGE] = false; move_closer.effects[ENEMY_IN_RANGE] = true; REQUIRE(move_closer.can_effect(start)); auto after_move_state = move_closer.apply_effect(start); REQUIRE(start[ENEMY_IN_RANGE] == false); REQUIRE(after_move_state[ENEMY_IN_RANGE] == true); REQUIRE(after_move_state[ENEMY_DEAD] == false); // start is clean but after move is dirty REQUIRE(move_closer.can_effect(start)); 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; kill_it.preconds[ENEMY_DEAD] = false; kill_it.effects[ENEMY_DEAD] = true; REQUIRE(kill_it.can_effect(start)); REQUIRE(kill_it.can_effect(after_move_state)); auto after_kill_state = kill_it.apply_effect(after_move_state); REQUIRE(!kill_it.can_effect(after_kill_state)); REQUIRE(distance_to_goal(after_move_state, after_kill_state) == 1); actions.push_back(kill_it); actions.push_back(move_closer); REQUIRE(start != goal); } TEST_CASE("basic feature tests", "[goap]") { GOAPState goal; GOAPState start; std::vector actions; // start off enemy not dead and not in range start[ENEMY_DEAD] = false; start[ENEMY_IN_RANGE] = false; // end goal is enemy is dead goal[ENEMY_DEAD] = true; Action move_closer; move_closer.name = "move_closer"; move_closer.cost = 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; kill_it.preconds[ENEMY_DEAD] = false; kill_it.effects[ENEMY_DEAD] = true; actions.push_back(kill_it); actions.push_back(move_closer); auto result = plan_actions(actions, start, goal); REQUIRE(result != std::nullopt); for(auto& action : *result) { fmt::println("ACTION: {}", action.name); } }