Gave up on trying to get the GOAP algorithm to correctly apply the cost structure to competing choices, and instead I take the resulting action list and simply find the next best one based on cost.
This commit is contained in:
parent
52f45e1d45
commit
c014e65c13
11 changed files with 43 additions and 30 deletions
4
Makefile
4
Makefile
|
@ -22,7 +22,7 @@ tracy_build:
|
||||||
meson compile -j 10 -C builddir
|
meson compile -j 10 -C builddir
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
./builddir/runtests "[combat]"
|
./builddir/runtests
|
||||||
|
|
||||||
run: build test
|
run: build test
|
||||||
powershell "cp ./builddir/zedcaster.exe ."
|
powershell "cp ./builddir/zedcaster.exe ."
|
||||||
|
@ -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 "[combat]"
|
gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe -e
|
||||||
|
|
||||||
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'
|
||||||
|
|
26
ai.cpp
26
ai.cpp
|
@ -162,13 +162,28 @@ namespace ai {
|
||||||
return state.test(state_id(name));
|
return state.test(state_id(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
AIProfile* profile() {
|
ai::Action& EntityAI::best_fit() {
|
||||||
return &AIMGR.profile;
|
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) {
|
bool EntityAI::wants_to(std::string name) {
|
||||||
ai::check_valid_action(name, "EntityAI::wants_to");
|
ai::check_valid_action(name, "EntityAI::wants_to");
|
||||||
return plan.script.size() > 0 && plan.script[0].name == name;
|
dbc::check(plan.script.size() > 0, "empty action plan script");
|
||||||
|
return best_fit().name == name;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EntityAI::active() {
|
bool EntityAI::active() {
|
||||||
|
@ -190,4 +205,9 @@ namespace ai {
|
||||||
void EntityAI::update() {
|
void EntityAI::update() {
|
||||||
plan = ai::plan(script, start, goal);
|
plan = ai::plan(script, start, goal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AIProfile* profile() {
|
||||||
|
return &AIMGR.profile;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
2
ai.hpp
2
ai.hpp
|
@ -23,6 +23,7 @@ namespace ai {
|
||||||
EntityAI() {};
|
EntityAI() {};
|
||||||
|
|
||||||
bool wants_to(std::string name);
|
bool wants_to(std::string name);
|
||||||
|
ai::Action& best_fit();
|
||||||
|
|
||||||
bool active();
|
bool active();
|
||||||
|
|
||||||
|
@ -58,6 +59,5 @@ namespace ai {
|
||||||
ActionPlan plan(std::string script_name, State start, State goal);
|
ActionPlan plan(std::string script_name, State start, State goal);
|
||||||
|
|
||||||
/* Mostly used for debugging and validation. */
|
/* Mostly used for debugging and validation. */
|
||||||
AIProfile* profile();
|
|
||||||
void check_valid_action(std::string name, std::string msg);
|
void check_valid_action(std::string name, std::string msg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,4 +60,5 @@ namespace ai {
|
||||||
void EntityAI::dump() {
|
void EntityAI::dump() {
|
||||||
dump_script(script, start, plan.script);
|
dump_script(script, start, plan.script);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#include "goap.hpp"
|
#include "goap.hpp"
|
||||||
|
|
||||||
namespace ai {
|
namespace ai {
|
||||||
|
AIProfile* profile();
|
||||||
void dump_only(State state, bool matching, bool show_as);
|
void dump_only(State state, bool matching, bool show_as);
|
||||||
void dump_state(State state);
|
void dump_state(State state);
|
||||||
void dump_action(Action& action);
|
void dump_action(Action& action);
|
||||||
|
|
|
@ -120,6 +120,6 @@
|
||||||
"collect_items",
|
"collect_items",
|
||||||
"use_healing"],
|
"use_healing"],
|
||||||
"Enemy::actions":
|
"Enemy::actions":
|
||||||
["find_enemy", "kill_enemy", "run_away", "use_healing"]
|
["find_enemy", "run_away", "kill_enemy", "use_healing"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
"foreground": [205, 164, 246],
|
"foreground": [205, 164, 246],
|
||||||
"background": [30, 20, 75]
|
"background": [30, 20, 75]
|
||||||
},
|
},
|
||||||
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 20, "dead": false},
|
{"_type": "Combat", "hp": 200, "max_hp": 200, "damage": 20, "dead": false},
|
||||||
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
||||||
{"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
|
{"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
|
||||||
{"_type": "Personality", "hearing_distance": 5, "tough": false},
|
{"_type": "Personality", "hearing_distance": 5, "tough": false},
|
||||||
|
|
11
goap.cpp
11
goap.cpp
|
@ -40,8 +40,9 @@ namespace ai {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Action::can_effect(State& state) {
|
bool Action::can_effect(State& state) {
|
||||||
return ((state & $positive_preconds) == $positive_preconds) &&
|
bool posbit_match = (state & $positive_preconds) == $positive_preconds;
|
||||||
((state & $negative_preconds) == ALL_ZERO);
|
bool negbit_match = (state & $negative_preconds) == ALL_ZERO;
|
||||||
|
return posbit_match && negbit_match;
|
||||||
}
|
}
|
||||||
|
|
||||||
State Action::apply_effect(State& state) {
|
State Action::apply_effect(State& state) {
|
||||||
|
@ -113,11 +114,11 @@ namespace ai {
|
||||||
|
|
||||||
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
|
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
|
||||||
check(!open_set.empty(), "open set can't be empty in find_lowest");
|
check(!open_set.empty(), "open set can't be empty in find_lowest");
|
||||||
int found_score = SCORE_MAX;
|
int found_score = std::numeric_limits<int>::max();
|
||||||
ActionState found_as;
|
ActionState found_as;
|
||||||
|
|
||||||
for(auto& kv : open_set) {
|
for(auto& kv : open_set) {
|
||||||
if(kv.second < found_score) {
|
if(kv.second <= found_score) {
|
||||||
found_score = kv.second;
|
found_score = kv.second;
|
||||||
found_as = kv.first;
|
found_as = kv.first;
|
||||||
}
|
}
|
||||||
|
@ -166,7 +167,7 @@ namespace ai {
|
||||||
g_score.insert_or_assign(neighbor, tentative_g_score);
|
g_score.insert_or_assign(neighbor, tentative_g_score);
|
||||||
ActionState neighbor_as{neighbor_action, neighbor};
|
ActionState neighbor_as{neighbor_action, neighbor};
|
||||||
|
|
||||||
int score = tentative_g_score + h(neighbor, goal) + neighbor_action.cost;
|
int score = tentative_g_score + h(neighbor, goal);
|
||||||
|
|
||||||
// this maybe doesn't need score
|
// this maybe doesn't need score
|
||||||
open_set.insert_or_assign(neighbor_as, score);
|
open_set.insert_or_assign(neighbor_as, score);
|
||||||
|
|
|
@ -205,6 +205,7 @@ TEST_CASE("Confirm EntityAI behaves as expected", "[ai]") {
|
||||||
enemy.set_state("in_combat", true);
|
enemy.set_state("in_combat", true);
|
||||||
enemy.set_state("health_good", false);
|
enemy.set_state("health_good", false);
|
||||||
enemy.update();
|
enemy.update();
|
||||||
enemy.dump();
|
auto& best = enemy.best_fit();
|
||||||
|
REQUIRE(best.name == "run_away");
|
||||||
REQUIRE(enemy.wants_to("run_away"));
|
REQUIRE(enemy.wants_to("run_away"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
using namespace combat;
|
using namespace combat;
|
||||||
|
|
||||||
|
|
||||||
TEST_CASE("cause scared rat won't run away bug", "[combat]") {
|
TEST_CASE("cause scared rat won't run away bug", "[combat-fail]") {
|
||||||
ai::reset();
|
ai::reset();
|
||||||
ai::init("assets/ai.json");
|
ai::init("assets/ai.json");
|
||||||
|
|
||||||
|
@ -18,20 +18,9 @@ TEST_CASE("cause scared rat won't run away bug", "[combat]") {
|
||||||
DinkyECS::Entity rat_id = 1;
|
DinkyECS::Entity rat_id = 1;
|
||||||
ai::EntityAI rat("Enemy::actions", ai_start, ai_goal);
|
ai::EntityAI rat("Enemy::actions", ai_start, ai_goal);
|
||||||
rat.set_state("tough_personality", false);
|
rat.set_state("tough_personality", false);
|
||||||
rat.set_state("health_good", true);
|
|
||||||
battle.add_enemy(rat_id, rat);
|
|
||||||
|
|
||||||
// first confirm that everyone stops fightings
|
|
||||||
bool active = battle.plan();
|
|
||||||
rat.dump();
|
|
||||||
REQUIRE(active);
|
|
||||||
REQUIRE(rat.wants_to("kill_enemy"));
|
|
||||||
|
|
||||||
// this causes the plan to read END but if you set
|
|
||||||
// health_good to false it will run_away
|
|
||||||
|
|
||||||
rat.set_state("health_good", false);
|
rat.set_state("health_good", false);
|
||||||
active = battle.plan();
|
battle.add_enemy(rat_id, rat);
|
||||||
|
battle.plan();
|
||||||
rat.dump();
|
rat.dump();
|
||||||
REQUIRE(rat.wants_to("run_away"));
|
REQUIRE(rat.wants_to("run_away"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,10 @@ TEST_CASE("RitualEngine basic tests", "[rituals]") {
|
||||||
|
|
||||||
fmt::println("\n\n------------ TEST WILL DO MAGICK TOO");
|
fmt::println("\n\n------------ TEST WILL DO MAGICK TOO");
|
||||||
ritual.dump();
|
ritual.dump();
|
||||||
REQUIRE(ritual.will_do("magick_type"));
|
REQUIRE(ritual.will_do("pierce_type"));
|
||||||
|
|
||||||
ritual.pop();
|
ritual.pop();
|
||||||
REQUIRE(ritual.will_do("pierce_type"));
|
REQUIRE(ritual.will_do("magick_type"));
|
||||||
|
|
||||||
re.reset(ritual);
|
re.reset(ritual);
|
||||||
re.set_state(ritual, "has_magick", true);
|
re.set_state(ritual, "has_magick", true);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue