Now have the ability to do partial solutions that will create potential paths to the goal, and a test that runs the scripts from plans in different scenarios. Also, this ai_debug thing needs some work.
This commit is contained in:
parent
3f83d3f0bb
commit
fc66d221d4
11 changed files with 252 additions and 107 deletions
2
Makefile
2
Makefile
|
@ -41,7 +41,7 @@ clean:
|
||||||
meson compile --clean -C builddir
|
meson compile --clean -C builddir
|
||||||
|
|
||||||
debug_test: build
|
debug_test: build
|
||||||
gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe -e "[goap]"
|
gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe -e "[ai]"
|
||||||
|
|
||||||
win_installer:
|
win_installer:
|
||||||
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" win_installer.ifp'
|
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" win_installer.ifp'
|
||||||
|
|
110
ai.cpp
110
ai.cpp
|
@ -15,12 +15,10 @@ namespace ai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Action config_action(nlohmann::json& profile, nlohmann::json& config) {
|
Action config_action(AIProfile& profile, nlohmann::json& config) {
|
||||||
check(config.contains("name"), "config_action: action config missing name");
|
check(config.contains("name"), "config_action: action config missing name");
|
||||||
check(config.contains("cost"), "config_action: action config missing cost");
|
check(config.contains("cost"), "config_action: action config missing cost");
|
||||||
|
|
||||||
validate_profile(profile);
|
|
||||||
|
|
||||||
Action result(config["name"], config["cost"]);
|
Action result(config["name"], config["cost"]);
|
||||||
|
|
||||||
check(config.contains("needs"),
|
check(config.contains("needs"),
|
||||||
|
@ -30,72 +28,84 @@ namespace ai {
|
||||||
|
|
||||||
for(auto& [name_key, value] : config["needs"].items()) {
|
for(auto& [name_key, value] : config["needs"].items()) {
|
||||||
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.$name, name_key));
|
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.$name, name_key));
|
||||||
int name = profile[name_key].template get<int>();
|
result.needs(profile.at(name_key), bool(value));
|
||||||
|
|
||||||
result.needs(name, bool(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(auto& [name_key, value] : config["effects"].items()) {
|
for(auto& [name_key, value] : config["effects"].items()) {
|
||||||
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.$name, name_key));
|
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.$name, name_key));
|
||||||
|
|
||||||
int name = profile[name_key].template get<int>();
|
result.effect(profile.at(name_key), bool(value));
|
||||||
|
|
||||||
result.effect(name, bool(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
State config_state(nlohmann::json& profile, nlohmann::json& config) {
|
State config_state(AIProfile& profile, nlohmann::json& config) {
|
||||||
State result;
|
State result;
|
||||||
validate_profile(profile);
|
|
||||||
|
|
||||||
for(auto& [name_key, value] : config.items()) {
|
for(auto& [name_key, value] : config.items()) {
|
||||||
check(profile.contains(name_key), fmt::format("config_state: profile does not have name {}", name_key));
|
check(profile.contains(name_key), fmt::format("config_state: profile does not have name {}", name_key));
|
||||||
|
|
||||||
int name = profile[name_key].template get<int>();
|
int name_id = profile.at(name_key);
|
||||||
result[name] = bool(value);
|
result[name_id] = bool(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
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 = R"({})"_json;
|
||||||
|
}
|
||||||
|
|
||||||
void init(std::string config_path) {
|
void init(std::string config_path) {
|
||||||
initialized = true;
|
if(!initialized) {
|
||||||
Config config(config_path);
|
Config config(config_path);
|
||||||
|
|
||||||
// profile specifies what keys (bitset indexes) are allowed
|
// profile specifies what keys (bitset indexes) are allowed
|
||||||
// and how they map to the bitset of State
|
// and how they map to the bitset of State
|
||||||
AIMGR.profile = config["profile"];
|
validate_profile(config["profile"]);
|
||||||
validate_profile(AIMGR.profile);
|
|
||||||
|
|
||||||
// load all actions
|
// relies on json conversion?
|
||||||
auto& actions = config["actions"];
|
AIMGR.profile = config["profile"];
|
||||||
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
|
// load all actions
|
||||||
auto& states = config["states"];
|
auto& actions = config["actions"];
|
||||||
for(auto& [name, state_vars] : states.items()) {
|
for(auto& action_vars : actions) {
|
||||||
auto the_state = config_state(AIMGR.profile, state_vars);
|
auto the_action = config_action(AIMGR.profile, action_vars);
|
||||||
AIMGR.states.insert_or_assign(name, the_state);
|
AIMGR.actions.insert_or_assign(the_action.$name, the_action);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
// 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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +132,7 @@ namespace ai {
|
||||||
return AIMGR.scripts.at(script_name);
|
return AIMGR.scripts.at(script_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Script> plan(std::string script_name, State start, State goal) {
|
ActionPlan plan(std::string script_name, State start, State goal) {
|
||||||
check(initialized, "you forgot to initialize the AI first.");
|
check(initialized, "you forgot to initialize the AI first.");
|
||||||
auto script = load_script(script_name);
|
auto script = load_script(script_name);
|
||||||
return plan_actions(script, start, goal);
|
return plan_actions(script, start, goal);
|
||||||
|
@ -134,4 +144,16 @@ namespace ai {
|
||||||
name));
|
name));
|
||||||
return AIMGR.profile.at(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));
|
||||||
|
}
|
||||||
|
|
||||||
|
AIProfile* profile() {
|
||||||
|
return &AIMGR.profile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
11
ai.hpp
11
ai.hpp
|
@ -10,13 +10,15 @@
|
||||||
|
|
||||||
namespace ai {
|
namespace ai {
|
||||||
struct AIManager {
|
struct AIManager {
|
||||||
nlohmann::json profile;
|
AIProfile profile;
|
||||||
|
|
||||||
std::unordered_map<std::string, Action> actions;
|
std::unordered_map<std::string, Action> actions;
|
||||||
std::unordered_map<std::string, State> states;
|
std::unordered_map<std::string, State> states;
|
||||||
std::unordered_map<std::string, std::vector<Action>> scripts;
|
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);
|
void init(std::string config_path);
|
||||||
|
|
||||||
Action config_action(nlohmann::json& profile, nlohmann::json& config);
|
Action config_action(nlohmann::json& profile, nlohmann::json& config);
|
||||||
|
@ -27,5 +29,10 @@ namespace ai {
|
||||||
Action load_action(std::string action_name);
|
Action load_action(std::string action_name);
|
||||||
std::vector<Action> load_script(std::string script_name);
|
std::vector<Action> load_script(std::string script_name);
|
||||||
|
|
||||||
std::optional<Script> plan(std::string script_name, State start, State goal);
|
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. */
|
||||||
|
AIProfile* profile();
|
||||||
}
|
}
|
||||||
|
|
56
ai_debug.cpp
Normal file
56
ai_debug.cpp
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
#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(AIProfile& profile, State state, bool matching, bool show_as) {
|
||||||
|
for(auto& [name, name_id] : profile) {
|
||||||
|
if(state.test(name_id) == matching) {
|
||||||
|
fmt::println("\t{}={}", name, show_as);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_state(AIProfile& profile, State state) {
|
||||||
|
for(auto& [name, name_id] : profile) {
|
||||||
|
fmt::println("\t{}={}", name,
|
||||||
|
state.test(name_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_action(AIProfile& profile, Action& action) {
|
||||||
|
fmt::println(" --ACTION: {}, cost={}", action.$name, action.$cost);
|
||||||
|
|
||||||
|
fmt::println(" PRECONDS:");
|
||||||
|
dump_only(profile, action.$positive_preconds, true, true);
|
||||||
|
dump_only(profile, action.$negative_preconds, true, false);
|
||||||
|
|
||||||
|
fmt::println(" EFFECTS:");
|
||||||
|
dump_only(profile, action.$positive_effects, true, true);
|
||||||
|
dump_only(profile, action.$negative_effects, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
State dump_script(AIProfile& profile, std::string msg, State start, Script& script) {
|
||||||
|
fmt::println("--SCRIPT DUMP: {}", msg);
|
||||||
|
fmt::println("# STATE BEFORE:");
|
||||||
|
dump_state(profile, start);
|
||||||
|
fmt::print("% ACTIONS PLANNED:");
|
||||||
|
for(auto& action : script) {
|
||||||
|
fmt::print("{} ", action.$name);
|
||||||
|
}
|
||||||
|
fmt::print("\n");
|
||||||
|
|
||||||
|
for(auto& action : script) {
|
||||||
|
dump_action(profile, action);
|
||||||
|
|
||||||
|
start = action.apply_effect(start);
|
||||||
|
fmt::println(" ## STATE AFTER:");
|
||||||
|
dump_state(profile, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
}
|
9
ai_debug.hpp
Normal file
9
ai_debug.hpp
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma once
|
||||||
|
#include "goap.hpp"
|
||||||
|
|
||||||
|
namespace ai {
|
||||||
|
void dump_only(AIProfile& profile, State state, bool matching, bool show_as);
|
||||||
|
void dump_state(AIProfile& profile, State state);
|
||||||
|
void dump_action(AIProfile& profile, Action& action);
|
||||||
|
State dump_script(AIProfile& profile, std::string msg, State start, Script& script);
|
||||||
|
}
|
|
@ -1,85 +1,81 @@
|
||||||
{
|
{
|
||||||
"profile": {
|
"profile": {
|
||||||
"target_acquired": 0,
|
"enemy_found": 0,
|
||||||
"target_lost": 1,
|
"enemy_dead": 1,
|
||||||
"target_in_warhead_range": 2,
|
"health_good": 2,
|
||||||
"target_dead": 3
|
"no_more_items": 3,
|
||||||
|
"no_more_enemies": 4
|
||||||
},
|
},
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"name": "searchSpiral",
|
"name": "find_enemy",
|
||||||
"cost": 10,
|
"cost": 5,
|
||||||
"needs": {
|
"needs": {
|
||||||
"target_acquired": false,
|
"no_more_enemies": false,
|
||||||
"target_lost": true
|
"health_good": true,
|
||||||
|
"enemy_found": false
|
||||||
},
|
},
|
||||||
"effects": {
|
"effects": {
|
||||||
"target_acquired": true
|
"enemy_found": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "searchSerpentine",
|
"name": "kill_enemy",
|
||||||
"cost": 5,
|
"cost": 5,
|
||||||
"needs": {
|
"needs": {
|
||||||
"target_acquired": false,
|
"no_more_enemies": false,
|
||||||
"target_lost": false
|
"enemy_found": true,
|
||||||
|
"health_good": true,
|
||||||
|
"enemy_dead": false
|
||||||
},
|
},
|
||||||
"effects": {
|
"effects": {
|
||||||
"target_acquired": true
|
"enemy_dead": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "searchSpiral",
|
"name": "collect_items",
|
||||||
"cost": 5,
|
"cost": 5,
|
||||||
"needs": {
|
"needs": {
|
||||||
"target_acquired": false,
|
"no_more_enemies": true,
|
||||||
"target_lost": true
|
"no_more_items": false
|
||||||
},
|
},
|
||||||
"effects": {
|
"effects": {
|
||||||
"target_acquired": true
|
"no_more_items": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "interceptTarget",
|
"name": "find_healing",
|
||||||
"cost": 5,
|
"cost": 5,
|
||||||
"needs": {
|
"needs": {
|
||||||
"target_acquired": true,
|
"health_good": false,
|
||||||
"target_dead": false
|
"no_more_items": false
|
||||||
},
|
},
|
||||||
"effects": {
|
"effects": {
|
||||||
"target_in_warhead_range": true
|
"health_good": true
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "detonateNearTarget",
|
|
||||||
"cost": 5,
|
|
||||||
"needs": {
|
|
||||||
"target_in_warhead_range": true,
|
|
||||||
"target_acquired": true,
|
|
||||||
"target_dead": false
|
|
||||||
},
|
|
||||||
"effects": {
|
|
||||||
"target_dead": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"states": {
|
"states": {
|
||||||
"test_start": {
|
"Walker::initial_state": {
|
||||||
"target_acquired": false,
|
"enemy_found": false,
|
||||||
"target_lost": true,
|
"enemy_dead": false,
|
||||||
"target_in_warhead_range": false,
|
"health_good": true,
|
||||||
"target_dead": false
|
"no_more_items": false,
|
||||||
|
"no_more_enemies": false
|
||||||
},
|
},
|
||||||
"test_goal": {
|
"Walker::final_state": {
|
||||||
"target_dead": true
|
"enemy_found": true,
|
||||||
|
"enemy_dead": true,
|
||||||
|
"health_good": true,
|
||||||
|
"no_more_items": true,
|
||||||
|
"no_more_enemies": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test1": [
|
"Walker::actions":
|
||||||
"searchSpiral",
|
["find_enemy",
|
||||||
"searchSerpentine",
|
"kill_enemy",
|
||||||
"searchSpiral",
|
"find_healing",
|
||||||
"interceptTarget",
|
"collect_items"]
|
||||||
"detonateNearTarget"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
goap.cpp
15
goap.cpp
|
@ -1,5 +1,6 @@
|
||||||
#include "dbc.hpp"
|
#include "dbc.hpp"
|
||||||
#include "goap.hpp"
|
#include "goap.hpp"
|
||||||
|
#include "ai_debug.hpp"
|
||||||
|
|
||||||
namespace ai {
|
namespace ai {
|
||||||
using namespace nlohmann;
|
using namespace nlohmann;
|
||||||
|
@ -82,21 +83,21 @@ namespace ai {
|
||||||
return *result;
|
return *result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Script> plan_actions(std::vector<Action>& actions, State& start, State& goal) {
|
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal) {
|
||||||
std::unordered_map<ActionState, int> open_set;
|
std::unordered_map<ActionState, int> open_set;
|
||||||
std::unordered_map<Action, Action> came_from;
|
std::unordered_map<Action, Action> came_from;
|
||||||
std::unordered_map<State, int> g_score;
|
std::unordered_map<State, int> g_score;
|
||||||
|
ActionState current{FINAL_ACTION, start};
|
||||||
ActionState start_state{FINAL_ACTION, start};
|
|
||||||
|
|
||||||
g_score[start] = 0;
|
g_score[start] = 0;
|
||||||
open_set[start_state] = g_score[start] + h(start, goal);
|
open_set[current] = g_score[start] + h(start, goal);
|
||||||
|
|
||||||
while(!open_set.empty()) {
|
while(!open_set.empty()) {
|
||||||
auto current = find_lowest(open_set);
|
current = find_lowest(open_set);
|
||||||
|
|
||||||
if(is_subset(current.state, goal)) {
|
if(is_subset(current.state, goal)) {
|
||||||
return std::make_optional<Script>(reconstruct_path(came_from, current.action));
|
return {true,
|
||||||
|
reconstruct_path(came_from, current.action)};
|
||||||
}
|
}
|
||||||
|
|
||||||
open_set.erase(current);
|
open_set.erase(current);
|
||||||
|
@ -122,6 +123,6 @@ namespace ai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::nullopt;
|
return {false, reconstruct_path(came_from, current.action)};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
goap.hpp
10
goap.hpp
|
@ -8,6 +8,9 @@
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
|
|
||||||
namespace ai {
|
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();
|
constexpr const int SCORE_MAX = std::numeric_limits<int>::max();
|
||||||
constexpr const size_t STATE_MAX = 32;
|
constexpr const size_t STATE_MAX = 32;
|
||||||
|
|
||||||
|
@ -56,11 +59,16 @@ namespace ai {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ActionPlan {
|
||||||
|
bool complete = false;
|
||||||
|
Script script;
|
||||||
|
};
|
||||||
|
|
||||||
bool is_subset(State& source, State& target);
|
bool is_subset(State& source, State& target);
|
||||||
|
|
||||||
int distance_to_goal(State& from, State& to);
|
int distance_to_goal(State& from, State& to);
|
||||||
|
|
||||||
std::optional<Script> plan_actions(std::vector<Action>& actions, State& start, State& goal);
|
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<> struct std::hash<ai::Action> {
|
template<> struct std::hash<ai::Action> {
|
||||||
|
|
2
main.cpp
2
main.cpp
|
@ -2,12 +2,14 @@
|
||||||
#include "textures.hpp"
|
#include "textures.hpp"
|
||||||
#include "sound.hpp"
|
#include "sound.hpp"
|
||||||
#include "autowalker.hpp"
|
#include "autowalker.hpp"
|
||||||
|
#include "ai.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
try {
|
try {
|
||||||
textures::init();
|
textures::init();
|
||||||
sound::init();
|
sound::init();
|
||||||
|
ai::init("assets/ai.json");
|
||||||
sound::mute(true);
|
sound::mute(true);
|
||||||
gui::FSM main;
|
gui::FSM main;
|
||||||
main.event(gui::Event::STARTED);
|
main.event(gui::Event::STARTED);
|
||||||
|
|
|
@ -82,6 +82,7 @@ dependencies += [
|
||||||
|
|
||||||
sources = [
|
sources = [
|
||||||
'ai.cpp',
|
'ai.cpp',
|
||||||
|
'ai_debug.cpp',
|
||||||
'ansi_parser.cpp',
|
'ansi_parser.cpp',
|
||||||
'autowalker.cpp',
|
'autowalker.cpp',
|
||||||
'boss_fight_ui.cpp',
|
'boss_fight_ui.cpp',
|
||||||
|
|
55
tests/ai.cpp
55
tests/ai.cpp
|
@ -2,6 +2,7 @@
|
||||||
#include "dbc.hpp"
|
#include "dbc.hpp"
|
||||||
#include "ai.hpp"
|
#include "ai.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include "ai_debug.hpp"
|
||||||
|
|
||||||
using namespace dbc;
|
using namespace dbc;
|
||||||
using namespace nlohmann;
|
using namespace nlohmann;
|
||||||
|
@ -92,11 +93,11 @@ TEST_CASE("basic feature tests", "[ai]") {
|
||||||
actions.push_back(move_closer);
|
actions.push_back(move_closer);
|
||||||
|
|
||||||
auto result = ai::plan_actions(actions, start, goal);
|
auto result = ai::plan_actions(actions, start, goal);
|
||||||
REQUIRE(result != std::nullopt);
|
REQUIRE(result.complete);
|
||||||
|
|
||||||
auto state = start;
|
auto state = start;
|
||||||
|
|
||||||
for(auto& action : *result) {
|
for(auto& action : result.script) {
|
||||||
state = action.apply_effect(state);
|
state = action.apply_effect(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,19 +106,61 @@ TEST_CASE("basic feature tests", "[ai]") {
|
||||||
|
|
||||||
|
|
||||||
TEST_CASE("ai as a module like sound/sprites", "[ai]") {
|
TEST_CASE("ai as a module like sound/sprites", "[ai]") {
|
||||||
|
ai::reset();
|
||||||
ai::init("tests/ai_fixture.json");
|
ai::init("tests/ai_fixture.json");
|
||||||
|
|
||||||
auto start = ai::load_state("test_start");
|
auto start = ai::load_state("test_start");
|
||||||
auto goal = ai::load_state("test_goal");
|
auto goal = ai::load_state("test_goal");
|
||||||
|
|
||||||
auto script = ai::plan("test1", start, goal);
|
auto a_plan = ai::plan("test1", start, goal);
|
||||||
REQUIRE(script != std::nullopt);
|
REQUIRE(a_plan.complete);
|
||||||
|
|
||||||
auto state = start;
|
auto state = start;
|
||||||
for(auto& action : *script) {
|
for(auto& action : a_plan.script) {
|
||||||
fmt::println("ACTION: {}", action.$name);
|
fmt::println("ACTION: {}", action.$name);
|
||||||
state = action.apply_effect(state);
|
state = action.apply_effect(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
REQUIRE(state[ai::state_id("target_dead")]);
|
REQUIRE(ai::test(state, "target_dead"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ai autowalker ai test", "[ai]") {
|
||||||
|
ai::reset();
|
||||||
|
ai::init("assets/ai.json");
|
||||||
|
ai::AIProfile* profile = ai::profile();
|
||||||
|
auto start = ai::load_state("Walker::initial_state");
|
||||||
|
auto goal = ai::load_state("Walker::final_state");
|
||||||
|
int enemy_count = 5;
|
||||||
|
|
||||||
|
ai::set(start, "no_more_enemies", enemy_count == 0);
|
||||||
|
|
||||||
|
// find an enemy and kill them
|
||||||
|
auto a_plan = ai::plan("Walker::actions", start, goal);
|
||||||
|
REQUIRE(!a_plan.complete);
|
||||||
|
|
||||||
|
auto result = ai::dump_script(*profile, "\n\nWALKER KILL STUFF", start, a_plan.script);
|
||||||
|
REQUIRE(ai::test(result, "enemy_found"));
|
||||||
|
REQUIRE(ai::test(result, "enemy_dead"));
|
||||||
|
REQUIRE(!ai::test(result, "no_more_enemies"));
|
||||||
|
|
||||||
|
// health is low, go heal
|
||||||
|
ai::set(result, "health_good", false);
|
||||||
|
REQUIRE(!ai::test(result, "health_good"));
|
||||||
|
|
||||||
|
auto health_plan = ai::plan("Walker::actions", result, goal);
|
||||||
|
result = ai::dump_script(*profile, "\n\nWALKER NEED HEALTH", result, health_plan.script);
|
||||||
|
REQUIRE(!health_plan.complete);
|
||||||
|
REQUIRE(ai::test(result, "health_good"));
|
||||||
|
|
||||||
|
// health is good, enemies dead, go get stuff
|
||||||
|
ai::set(result, "no_more_enemies", true);
|
||||||
|
REQUIRE(ai::test(result, "no_more_enemies"));
|
||||||
|
|
||||||
|
auto new_plan = ai::plan("Walker::actions", result, goal);
|
||||||
|
result = ai::dump_script(*profile, "\n\nWALKER COMPLETE", result, new_plan.script);
|
||||||
|
REQUIRE(new_plan.complete);
|
||||||
|
|
||||||
|
REQUIRE(ai::test(result, "enemy_found"));
|
||||||
|
REQUIRE(ai::test(result, "enemy_dead"));
|
||||||
|
REQUIRE(ai::test(result, "no_more_enemies"));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue