#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) { Config config(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 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.states.contains(action_name), fmt::format( "ai::load_action({}): action does not exist in config", action_name)); return AIMGR.actions.at(action_name); } std::vector 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) { state.set(state_id(name), value); } bool test(State state, std::string name) { 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; } } 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; } 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) { 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); } AIProfile* profile() { return &AIMGR.profile; } }