This does a 'fit_sort' whenever the state is changed. fit_sort effectively sorts the actions by distance+cost so that the cost is actually present unlike the original algorithm.
This commit is contained in:
parent
c014e65c13
commit
c1aba2d5c8
5 changed files with 30 additions and 27 deletions
31
ai.cpp
31
ai.cpp
|
@ -155,6 +155,7 @@ namespace ai {
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(State& state, std::string name, bool value) {
|
void set(State& state, std::string name, bool value) {
|
||||||
|
// resort by best fit
|
||||||
state.set(state_id(name), value);
|
state.set(state_id(name), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,39 +163,32 @@ namespace ai {
|
||||||
return state.test(state_id(name));
|
return state.test(state_id(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
ai::Action& EntityAI::best_fit() {
|
void EntityAI::fit_sort() {
|
||||||
dbc::check(plan.script.size() > 0, "empty action plan script");
|
if(active()) {
|
||||||
int lowest_cost = plan.script[0].cost;
|
std::sort(plan.script.begin(), plan.script.end(),
|
||||||
size_t best_action = 0;
|
[&](auto& l, auto& r) {
|
||||||
|
int l_cost = l.cost + (!l.can_effect(start) * ai::SCORE_MAX);
|
||||||
for(size_t i = 0; i < plan.script.size(); i++) {
|
int r_cost = r.cost + (!r.can_effect(start) * ai::SCORE_MAX);
|
||||||
auto& action = plan.script[i];
|
return l_cost < r_cost;
|
||||||
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");
|
||||||
dbc::check(plan.script.size() > 0, "empty action plan script");
|
return plan.script.size() > 0 && plan.script[0].name == name;
|
||||||
return best_fit().name == name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EntityAI::active() {
|
bool EntityAI::active() {
|
||||||
if(plan.script.size() == 1) {
|
if(plan.script.size() == 1) {
|
||||||
return plan.script[0] != FINAL_ACTION;
|
return plan.script[0] != FINAL_ACTION;
|
||||||
} else {
|
} else {
|
||||||
return plan.script.size() == 0;
|
return plan.script.size() != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityAI::set_state(std::string name, bool setting) {
|
void EntityAI::set_state(std::string name, bool setting) {
|
||||||
|
fit_sort();
|
||||||
ai::set(start, name, setting);
|
ai::set(start, name, setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +198,7 @@ namespace ai {
|
||||||
|
|
||||||
void EntityAI::update() {
|
void EntityAI::update() {
|
||||||
plan = ai::plan(script, start, goal);
|
plan = ai::plan(script, start, goal);
|
||||||
|
fit_sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
AIProfile* profile() {
|
AIProfile* profile() {
|
||||||
|
|
2
ai.hpp
2
ai.hpp
|
@ -23,7 +23,7 @@ namespace ai {
|
||||||
EntityAI() {};
|
EntityAI() {};
|
||||||
|
|
||||||
bool wants_to(std::string name);
|
bool wants_to(std::string name);
|
||||||
ai::Action& best_fit();
|
void fit_sort();
|
||||||
|
|
||||||
bool active();
|
bool active();
|
||||||
|
|
||||||
|
|
20
goap.cpp
20
goap.cpp
|
@ -4,6 +4,8 @@
|
||||||
#include "stats.hpp"
|
#include "stats.hpp"
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
|
||||||
|
// #define DEBUG_CYCLES 1
|
||||||
|
|
||||||
namespace ai {
|
namespace ai {
|
||||||
|
|
||||||
using namespace nlohmann;
|
using namespace nlohmann;
|
||||||
|
@ -63,11 +65,8 @@ namespace ai {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void path_invariant(std::unordered_map<Action, Action>& came_from, Action& current) {
|
inline void path_invariant(std::unordered_map<Action, Action>& came_from, Action current) {
|
||||||
#if defined(NDEBUG)
|
#if defined(DEBUG_CYCLES)
|
||||||
(void)came_from; // disable errors about unused
|
|
||||||
(void)current;
|
|
||||||
#else
|
|
||||||
bool final_found = current == FINAL_ACTION;
|
bool final_found = current == FINAL_ACTION;
|
||||||
|
|
||||||
for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
|
for(size_t i = 0; i <= came_from.size() && came_from.contains(current); i++) {
|
||||||
|
@ -79,6 +78,9 @@ namespace ai {
|
||||||
dump_came_from("CYCLE DETECTED!", came_from, current);
|
dump_came_from("CYCLE DETECTED!", came_from, current);
|
||||||
dbc::sentinel("AI CYCLE FOUND!");
|
dbc::sentinel("AI CYCLE FOUND!");
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
(void)came_from; // disable errors about unused
|
||||||
|
(void)current;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,15 +158,21 @@ namespace ai {
|
||||||
auto neighbor = neighbor_action.apply_effect(current.state);
|
auto neighbor = neighbor_action.apply_effect(current.state);
|
||||||
if(closed_set.contains(neighbor)) continue;
|
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 d_score = d(current.state, neighbor) + neighbor_action.cost;
|
||||||
|
|
||||||
int tentative_g_score = g_score[current.state] + d_score;
|
int tentative_g_score = g_score[current.state] + d_score;
|
||||||
int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
|
int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
|
||||||
|
|
||||||
if(tentative_g_score < neighbor_g_score) {
|
if(tentative_g_score + neighbor_action.cost < neighbor_g_score) {
|
||||||
came_from.insert_or_assign(neighbor_action, current.action);
|
came_from.insert_or_assign(neighbor_action, current.action);
|
||||||
|
|
||||||
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);
|
int score = tentative_g_score + h(neighbor, goal);
|
||||||
|
|
|
@ -205,7 +205,5 @@ 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();
|
||||||
auto& best = enemy.best_fit();
|
|
||||||
REQUIRE(best.name == "run_away");
|
|
||||||
REQUIRE(enemy.wants_to("run_away"));
|
REQUIRE(enemy.wants_to("run_away"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,10 @@ TEST_CASE("cause scared rat won't run away bug", "[combat-fail]") {
|
||||||
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", false);
|
rat.set_state("health_good", false);
|
||||||
|
REQUIRE(!rat.active());
|
||||||
battle.add_enemy(rat_id, rat);
|
battle.add_enemy(rat_id, rat);
|
||||||
battle.plan();
|
battle.plan();
|
||||||
|
REQUIRE(rat.active());
|
||||||
rat.dump();
|
rat.dump();
|
||||||
REQUIRE(rat.wants_to("run_away"));
|
REQUIRE(rat.wants_to("run_away"));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue