AI is now moved.
This commit is contained in:
parent
1d4ae911b9
commit
13ec422aae
25 changed files with 65 additions and 48 deletions
212
src/ai/ai.cpp
Normal file
212
src/ai/ai.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
#include "dbc.hpp"
|
||||
#include "ai.hpp"
|
||||
|
||||
namespace ai {
|
||||
using namespace nlohmann;
|
||||
using namespace dbc;
|
||||
|
||||
static AIManager AIMGR;
|
||||
static bool initialized = false;
|
||||
|
||||
inline void validate_profile(nlohmann::json& profile) {
|
||||
for(auto& [name_key, value] : profile.items()) {
|
||||
check(value < STATE_MAX,
|
||||
fmt::format("profile field {} has value {} greater than STATE_MAX {}", (std::string)name_key, (int)value, STATE_MAX));
|
||||
}
|
||||
}
|
||||
|
||||
Action config_action(AIProfile& profile, nlohmann::json& config) {
|
||||
check(config.contains("name"), "config_action: action config missing name");
|
||||
check(config.contains("cost"), "config_action: action config missing cost");
|
||||
|
||||
Action result(config["name"], config["cost"]);
|
||||
|
||||
check(config.contains("needs"),
|
||||
fmt::format("config_action: no 'needs' field", result.name));
|
||||
check(config.contains("effects"),
|
||||
fmt::format("config_action: no 'effects' field", result.name));
|
||||
|
||||
for(auto& [name_key, value] : config["needs"].items()) {
|
||||
check(profile.contains(name_key), fmt::format("config_action({}): profile does not have need named {}", result.name, name_key));
|
||||
result.needs(profile.at(name_key), bool(value));
|
||||
}
|
||||
|
||||
for(auto& [name_key, value] : config["effects"].items()) {
|
||||
check(profile.contains(name_key), fmt::format("config_action({}): profile does not have effect named {}", result.name, name_key));
|
||||
|
||||
result.effect(profile.at(name_key), bool(value));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
State config_state(AIProfile& profile, nlohmann::json& config) {
|
||||
State result;
|
||||
|
||||
for(auto& [name_key, value] : config.items()) {
|
||||
check(profile.contains(name_key), fmt::format("config_state: profile does not have name {}", name_key));
|
||||
|
||||
int name_id = profile.at(name_key);
|
||||
result[name_id] = bool(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is only used in tests so I can load different fixtures.
|
||||
*/
|
||||
void reset() {
|
||||
initialized = false;
|
||||
AIMGR.actions.clear();
|
||||
AIMGR.states.clear();
|
||||
AIMGR.scripts.clear();
|
||||
AIMGR.profile = json({});
|
||||
}
|
||||
|
||||
void init(std::string config_path) {
|
||||
if(!initialized) {
|
||||
auto config = settings::get(config_path);
|
||||
|
||||
// profile specifies what keys (bitset indexes) are allowed
|
||||
// and how they map to the bitset of State
|
||||
validate_profile(config["profile"]);
|
||||
|
||||
// relies on json conversion?
|
||||
AIMGR.profile = config["profile"];
|
||||
|
||||
// load all actions
|
||||
auto& actions = config["actions"];
|
||||
for(auto& action_vars : actions) {
|
||||
auto the_action = config_action(AIMGR.profile, action_vars);
|
||||
AIMGR.actions.insert_or_assign(the_action.name, the_action);
|
||||
}
|
||||
|
||||
// load all states
|
||||
auto& states = config["states"];
|
||||
for(auto& [name, state_vars] : states.items()) {
|
||||
auto the_state = config_state(AIMGR.profile, state_vars);
|
||||
AIMGR.states.insert_or_assign(name, the_state);
|
||||
}
|
||||
|
||||
auto& scripts = config["scripts"];
|
||||
for(auto& [script_name, action_names] : scripts.items()) {
|
||||
std::vector<Action> the_script;
|
||||
|
||||
for(auto name : action_names) {
|
||||
check(AIMGR.actions.contains(name),
|
||||
fmt::format("ai::init(): script {} uses action {} that doesn't exist",
|
||||
(std::string)script_name, (std::string)name));
|
||||
|
||||
the_script.push_back(AIMGR.actions.at(name));
|
||||
}
|
||||
|
||||
AIMGR.scripts.insert_or_assign(script_name, the_script);
|
||||
}
|
||||
initialized = true;
|
||||
} else {
|
||||
dbc::sentinel("DOUBLE INIT: AI manager should only be intialized once if not in tests.");
|
||||
}
|
||||
}
|
||||
|
||||
void check_valid_action(std::string name, std::string msg) {
|
||||
dbc::check(AIMGR.actions.contains(name),
|
||||
fmt::format("{} tried to access action that doesn't exist {}",
|
||||
msg, name));
|
||||
}
|
||||
|
||||
State load_state(std::string state_name) {
|
||||
check(initialized, "you forgot to initialize the AI first.");
|
||||
check(AIMGR.states.contains(state_name), fmt::format(
|
||||
"ai::load_state({}): state does not exist in config",
|
||||
state_name));
|
||||
|
||||
return AIMGR.states.at(state_name);
|
||||
}
|
||||
|
||||
Action load_action(std::string action_name) {
|
||||
check(initialized, "you forgot to initialize the AI first.");
|
||||
check(AIMGR.actions.contains(action_name), fmt::format(
|
||||
"ai::load_action({}): action does not exist in config",
|
||||
action_name));
|
||||
return AIMGR.actions.at(action_name);
|
||||
}
|
||||
|
||||
std::vector<Action> load_script(std::string script_name) {
|
||||
check(AIMGR.scripts.contains(script_name), fmt::format(
|
||||
"ai::load_script(): no script named {} configured", script_name));
|
||||
return AIMGR.scripts.at(script_name);
|
||||
}
|
||||
|
||||
ActionPlan plan(std::string script_name, State start, State goal) {
|
||||
// BUG: could probably memoize here, since:
|
||||
// same script+same start+same goal will/should produce the same results
|
||||
|
||||
check(initialized, "you forgot to initialize the AI first.");
|
||||
auto script = load_script(script_name);
|
||||
return plan_actions(script, start, goal);
|
||||
}
|
||||
|
||||
int state_id(std::string name) {
|
||||
check(AIMGR.profile.contains(name), fmt::format(
|
||||
"ai::state_id({}): id is not configured in profile",
|
||||
name));
|
||||
return AIMGR.profile.at(name);
|
||||
}
|
||||
|
||||
void set(State& state, std::string name, bool value) {
|
||||
// resort by best fit
|
||||
state.set(state_id(name), value);
|
||||
}
|
||||
|
||||
bool test(State state, std::string name) {
|
||||
return state.test(state_id(name));
|
||||
}
|
||||
|
||||
void EntityAI::fit_sort() {
|
||||
if(active()) {
|
||||
std::sort(plan.script.begin(), plan.script.end(),
|
||||
[&](auto& l, auto& r) {
|
||||
int l_cost = l.cost + ai::distance_to_goal(start, goal);
|
||||
int r_cost = r.cost + ai::distance_to_goal(start, goal);
|
||||
return l_cost < r_cost;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::string& EntityAI::wants_to() {
|
||||
return plan.script[0].name;
|
||||
}
|
||||
|
||||
bool EntityAI::wants_to(std::string name) {
|
||||
ai::check_valid_action(name, "EntityAI::wants_to");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void EntityAI::set_state(std::string name, bool setting) {
|
||||
fit_sort();
|
||||
ai::set(start, name, setting);
|
||||
}
|
||||
|
||||
bool EntityAI::get_state(std::string name) {
|
||||
return ai::test(start, name);
|
||||
}
|
||||
|
||||
void EntityAI::update() {
|
||||
plan = ai::plan(script, start, goal);
|
||||
fit_sort();
|
||||
}
|
||||
|
||||
AIProfile* profile() {
|
||||
return &AIMGR.profile;
|
||||
}
|
||||
|
||||
}
|
||||
65
src/ai/ai.hpp
Normal file
65
src/ai/ai.hpp
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include "matrix.hpp"
|
||||
#include <bitset>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "config.hpp"
|
||||
#include "goap.hpp"
|
||||
|
||||
namespace ai {
|
||||
struct EntityAI {
|
||||
std::string script;
|
||||
ai::State start;
|
||||
ai::State goal;
|
||||
ai::ActionPlan plan;
|
||||
|
||||
EntityAI(std::string script, ai::State start, ai::State goal) :
|
||||
script(script), start(start), goal(goal)
|
||||
{
|
||||
}
|
||||
|
||||
EntityAI() {};
|
||||
|
||||
bool wants_to(std::string name);
|
||||
std::string& wants_to();
|
||||
void fit_sort();
|
||||
|
||||
bool active();
|
||||
|
||||
void set_state(std::string name, bool setting);
|
||||
bool get_state(std::string name);
|
||||
|
||||
void update();
|
||||
|
||||
void dump();
|
||||
std::string to_string();
|
||||
};
|
||||
|
||||
struct AIManager {
|
||||
AIProfile profile;
|
||||
std::unordered_map<std::string, Action> actions;
|
||||
std::unordered_map<std::string, State> states;
|
||||
std::unordered_map<std::string, std::vector<Action>> scripts;
|
||||
};
|
||||
|
||||
/* This is really only used in test to load different fixtures. */
|
||||
void reset();
|
||||
void init(std::string config_path);
|
||||
|
||||
Action config_action(AIProfile& profile, nlohmann::json& config);
|
||||
State config_state(AIProfile& profile, nlohmann::json& config);
|
||||
|
||||
int state_id(std::string name);
|
||||
State load_state(std::string state_name);
|
||||
Action load_action(std::string action_name);
|
||||
std::vector<Action> load_script(std::string script_name);
|
||||
|
||||
void set(State& state, std::string name, bool value=true);
|
||||
bool test(State state, std::string name);
|
||||
ActionPlan plan(std::string script_name, State start, State goal);
|
||||
|
||||
/* Mostly used for debugging and validation. */
|
||||
void check_valid_action(std::string name, std::string msg);
|
||||
}
|
||||
74
src/ai/ai_debug.cpp
Normal file
74
src/ai/ai_debug.cpp
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#include "ai.hpp"
|
||||
#include "ai_debug.hpp"
|
||||
|
||||
namespace ai {
|
||||
|
||||
/*
|
||||
* Yeah this is weird but it's only to debug things like
|
||||
* the preconditions which are weirdly done.
|
||||
*/
|
||||
void dump_only(State state, bool matching, bool show_as) {
|
||||
AIProfile* profile = ai::profile();
|
||||
for(auto& [name, name_id] : *profile) {
|
||||
if(state.test(name_id) == matching) {
|
||||
fmt::println("\t{}={}", name, show_as);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dump_state(State state) {
|
||||
AIProfile* profile = ai::profile();
|
||||
for(auto& [name, name_id] : *profile) {
|
||||
fmt::println("\t{}={}", name,
|
||||
state.test(name_id));
|
||||
}
|
||||
}
|
||||
|
||||
void dump_action(Action& action) {
|
||||
fmt::println(" --ACTION: {}, cost={}", action.name, action.cost);
|
||||
|
||||
fmt::println(" PRECONDS:");
|
||||
dump_only(action.$positive_preconds, true, true);
|
||||
dump_only(action.$negative_preconds, true, false);
|
||||
|
||||
fmt::println(" EFFECTS:");
|
||||
dump_only(action.$positive_effects, true, true);
|
||||
dump_only(action.$negative_effects, true, false);
|
||||
}
|
||||
|
||||
State dump_script(std::string msg, State start, Script& script) {
|
||||
fmt::println("--SCRIPT DUMP: {}", msg);
|
||||
fmt::println("# STATE BEFORE:");
|
||||
dump_state(start);
|
||||
fmt::print("% ACTIONS PLANNED:");
|
||||
for(auto& action : script) {
|
||||
fmt::print("{} ", action.name);
|
||||
}
|
||||
fmt::print("\n");
|
||||
|
||||
for(auto& action : script) {
|
||||
dump_action(action);
|
||||
|
||||
start = action.apply_effect(start);
|
||||
fmt::println(" ## STATE AFTER:");
|
||||
dump_state(start);
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
void EntityAI::dump() {
|
||||
dump_script(script, start, plan.script);
|
||||
}
|
||||
|
||||
std::string EntityAI::to_string() {
|
||||
AIProfile* profile = ai::profile();
|
||||
std::string result = wants_to();
|
||||
|
||||
for(auto& [name, name_id] : *profile) {
|
||||
result += fmt::format("\n{}={}", name, start.test(name_id));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
10
src/ai/ai_debug.hpp
Normal file
10
src/ai/ai_debug.hpp
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
#include "goap.hpp"
|
||||
|
||||
namespace ai {
|
||||
AIProfile* profile();
|
||||
void dump_only(State state, bool matching, bool show_as);
|
||||
void dump_state(State state);
|
||||
void dump_action(Action& action);
|
||||
State dump_script(std::string msg, State start, Script& script);
|
||||
}
|
||||
188
src/ai/goap.cpp
Normal file
188
src/ai/goap.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#include "dbc.hpp"
|
||||
#include "goap.hpp"
|
||||
#include "ai_debug.hpp"
|
||||
#include "stats.hpp"
|
||||
#include <queue>
|
||||
|
||||
// #define DEBUG_CYCLES 1
|
||||
|
||||
namespace ai {
|
||||
|
||||
using namespace nlohmann;
|
||||
using namespace dbc;
|
||||
|
||||
bool is_subset(State& source, State& target) {
|
||||
State result = source & target;
|
||||
return result == target;
|
||||
}
|
||||
|
||||
void Action::needs(int name, bool val) {
|
||||
if(val) {
|
||||
$positive_preconds[name] = true;
|
||||
$negative_preconds[name] = false;
|
||||
} else {
|
||||
$negative_preconds[name] = true;
|
||||
$positive_preconds[name] = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Action::effect(int name, bool val) {
|
||||
if(val) {
|
||||
$positive_effects[name] = true;
|
||||
$negative_effects[name] = false;
|
||||
} else {
|
||||
$negative_effects[name] = true;
|
||||
$positive_effects[name] = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Action::ignore(int name) {
|
||||
$positive_preconds[name] = false;
|
||||
$negative_preconds[name] = false;
|
||||
}
|
||||
|
||||
bool Action::can_effect(State& state) {
|
||||
bool posbit_match = (state & $positive_preconds) == $positive_preconds;
|
||||
bool negbit_match = (state & $negative_preconds) == ALL_ZERO;
|
||||
return posbit_match && negbit_match;
|
||||
}
|
||||
|
||||
State Action::apply_effect(State& state) {
|
||||
return (state | $positive_effects) & ~$negative_effects;
|
||||
}
|
||||
|
||||
int distance_to_goal(State from, State to) {
|
||||
auto result = from ^ to;
|
||||
int count = result.count();
|
||||
return count;
|
||||
}
|
||||
|
||||
inline void dump_came_from(std::string msg, std::unordered_map<Action, Action>& came_from, Action& current) {
|
||||
fmt::println("{}: {}", msg, current.name);
|
||||
|
||||
for(auto& [from, to] : came_from) {
|
||||
fmt::println("from={}; to={}", from.name, to.name);
|
||||
}
|
||||
}
|
||||
|
||||
inline void path_invariant(std::unordered_map<Action, Action>& 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++) {
|
||||
current = came_from.at(current);
|
||||
final_found = current == FINAL_ACTION;
|
||||
}
|
||||
|
||||
if(!final_found) {
|
||||
dump_came_from("CYCLE DETECTED!", came_from, current);
|
||||
dbc::sentinel("AI CYCLE FOUND!");
|
||||
}
|
||||
#else
|
||||
(void)came_from; // disable errors about unused
|
||||
(void)current;
|
||||
#endif
|
||||
}
|
||||
|
||||
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
|
||||
Script total_path{current};
|
||||
|
||||
path_invariant(came_from, current);
|
||||
|
||||
for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
|
||||
auto next = came_from.at(current);
|
||||
|
||||
if(next != FINAL_ACTION) {
|
||||
// remove the previous node to avoid cycles and repeated actions
|
||||
total_path.push_front(next);
|
||||
came_from.erase(current);
|
||||
current = next;
|
||||
} else {
|
||||
// found the terminator, done
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return total_path;
|
||||
}
|
||||
|
||||
inline int h(State start, State goal) {
|
||||
return distance_to_goal(start, goal);
|
||||
}
|
||||
|
||||
inline int d(State start, State goal) {
|
||||
return distance_to_goal(start, goal);
|
||||
}
|
||||
|
||||
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
|
||||
check(!open_set.empty(), "open set can't be empty in find_lowest");
|
||||
int found_score = std::numeric_limits<int>::max();
|
||||
ActionState found_as;
|
||||
|
||||
for(auto& kv : open_set) {
|
||||
if(kv.second <= found_score) {
|
||||
found_score = kv.second;
|
||||
found_as = kv.first;
|
||||
}
|
||||
}
|
||||
|
||||
return found_as;
|
||||
}
|
||||
|
||||
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal) {
|
||||
std::unordered_map<ActionState, int> open_set;
|
||||
std::unordered_map<Action, Action> came_from;
|
||||
std::unordered_map<State, int> g_score;
|
||||
std::unordered_map<State, bool> closed_set;
|
||||
ActionState current{FINAL_ACTION, start};
|
||||
|
||||
g_score.insert_or_assign(start, 0);
|
||||
open_set.insert_or_assign(current, h(start, goal));
|
||||
|
||||
while(!open_set.empty()) {
|
||||
// current := the node in openSet having the lowest fScore[] value
|
||||
current = find_lowest(open_set);
|
||||
|
||||
if(is_subset(current.state, goal)) {
|
||||
return {true,
|
||||
reconstruct_path(came_from, current.action)};
|
||||
}
|
||||
|
||||
open_set.erase(current);
|
||||
closed_set.insert_or_assign(current.state, true);
|
||||
|
||||
for(auto& neighbor_action : actions) {
|
||||
// calculate the State being current/neighbor
|
||||
if(!neighbor_action.can_effect(current.state)) continue;
|
||||
|
||||
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_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);
|
||||
|
||||
// this maybe doesn't need score
|
||||
open_set.insert_or_assign(neighbor_as, score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {is_subset(current.state, goal), reconstruct_path(came_from, current.action)};
|
||||
}
|
||||
}
|
||||
90
src/ai/goap.hpp
Normal file
90
src/ai/goap.hpp
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include "matrix.hpp"
|
||||
#include <bitset>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "config.hpp"
|
||||
|
||||
namespace ai {
|
||||
// ZED: I don't know if this is the best place for this
|
||||
using AIProfile = std::unordered_map<std::string, int>;
|
||||
|
||||
constexpr const int SCORE_MAX = std::numeric_limits<int>::max() / 2;
|
||||
|
||||
constexpr const size_t STATE_MAX = 32;
|
||||
|
||||
using State = std::bitset<STATE_MAX>;
|
||||
|
||||
const State ALL_ZERO;
|
||||
const State ALL_ONES = ~ALL_ZERO;
|
||||
|
||||
struct Action {
|
||||
std::string name;
|
||||
int cost = 0;
|
||||
|
||||
State $positive_preconds;
|
||||
State $negative_preconds;
|
||||
|
||||
State $positive_effects;
|
||||
State $negative_effects;
|
||||
|
||||
Action() {}
|
||||
|
||||
Action(std::string name, int cost) :
|
||||
name(name), cost(cost) { }
|
||||
|
||||
void needs(int name, bool val);
|
||||
void effect(int name, bool val);
|
||||
void ignore(int name);
|
||||
|
||||
bool can_effect(State& state);
|
||||
State apply_effect(State& state);
|
||||
|
||||
bool operator==(const Action& other) const {
|
||||
return other.name == name;
|
||||
}
|
||||
};
|
||||
|
||||
using Script = std::deque<Action>;
|
||||
|
||||
const Action FINAL_ACTION("END", SCORE_MAX);
|
||||
|
||||
struct ActionState {
|
||||
Action action;
|
||||
State state;
|
||||
|
||||
ActionState(Action action, State state) :
|
||||
action(action), state(state) {}
|
||||
|
||||
ActionState() : action(FINAL_ACTION), state(0) {}
|
||||
|
||||
bool operator==(const ActionState& other) const {
|
||||
return other.action == action && other.state == state;
|
||||
}
|
||||
};
|
||||
|
||||
struct ActionPlan {
|
||||
bool complete = false;
|
||||
Script script;
|
||||
};
|
||||
|
||||
bool is_subset(State& source, State& target);
|
||||
|
||||
int distance_to_goal(State from, State to);
|
||||
|
||||
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
|
||||
}
|
||||
|
||||
template<> struct std::hash<ai::Action> {
|
||||
size_t operator()(const ai::Action& p) const {
|
||||
return std::hash<std::string>{}(p.name);
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct std::hash<ai::ActionState> {
|
||||
size_t operator()(const ai::ActionState& p) const {
|
||||
return std::hash<ai::Action>{}(p.action) ^ std::hash<ai::State>{}(p.state);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue