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
|
||||
|
||||
test: build
|
||||
./builddir/runtests "[combat]"
|
||||
./builddir/runtests
|
||||
|
||||
run: build test
|
||||
powershell "cp ./builddir/zedcaster.exe ."
|
||||
|
@ -41,7 +41,7 @@ clean:
|
|||
meson compile --clean -C builddir
|
||||
|
||||
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:
|
||||
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));
|
||||
}
|
||||
|
||||
AIProfile* profile() {
|
||||
return &AIMGR.profile;
|
||||
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");
|
||||
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() {
|
||||
|
@ -190,4 +205,9 @@ namespace ai {
|
|||
void EntityAI::update() {
|
||||
plan = ai::plan(script, start, goal);
|
||||
}
|
||||
|
||||
AIProfile* profile() {
|
||||
return &AIMGR.profile;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
2
ai.hpp
2
ai.hpp
|
@ -23,6 +23,7 @@ namespace ai {
|
|||
EntityAI() {};
|
||||
|
||||
bool wants_to(std::string name);
|
||||
ai::Action& best_fit();
|
||||
|
||||
bool active();
|
||||
|
||||
|
@ -58,6 +59,5 @@ namespace ai {
|
|||
ActionPlan plan(std::string script_name, State start, State goal);
|
||||
|
||||
/* Mostly used for debugging and validation. */
|
||||
AIProfile* profile();
|
||||
void check_valid_action(std::string name, std::string msg);
|
||||
}
|
||||
|
|
|
@ -60,4 +60,5 @@ namespace ai {
|
|||
void EntityAI::dump() {
|
||||
dump_script(script, start, plan.script);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#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);
|
||||
|
|
|
@ -120,6 +120,6 @@
|
|||
"collect_items",
|
||||
"use_healing"],
|
||||
"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],
|
||||
"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": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
|
||||
{"_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) {
|
||||
return ((state & $positive_preconds) == $positive_preconds) &&
|
||||
((state & $negative_preconds) == ALL_ZERO);
|
||||
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) {
|
||||
|
@ -113,11 +114,11 @@ namespace ai {
|
|||
|
||||
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 = SCORE_MAX;
|
||||
int found_score = std::numeric_limits<int>::max();
|
||||
ActionState found_as;
|
||||
|
||||
for(auto& kv : open_set) {
|
||||
if(kv.second < found_score) {
|
||||
if(kv.second <= found_score) {
|
||||
found_score = kv.second;
|
||||
found_as = kv.first;
|
||||
}
|
||||
|
@ -166,7 +167,7 @@ namespace ai {
|
|||
g_score.insert_or_assign(neighbor, tentative_g_score);
|
||||
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
|
||||
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("health_good", false);
|
||||
enemy.update();
|
||||
enemy.dump();
|
||||
auto& best = enemy.best_fit();
|
||||
REQUIRE(best.name == "run_away");
|
||||
REQUIRE(enemy.wants_to("run_away"));
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
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::init("assets/ai.json");
|
||||
|
||||
|
@ -18,20 +18,9 @@ TEST_CASE("cause scared rat won't run away bug", "[combat]") {
|
|||
DinkyECS::Entity rat_id = 1;
|
||||
ai::EntityAI rat("Enemy::actions", ai_start, ai_goal);
|
||||
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);
|
||||
active = battle.plan();
|
||||
battle.add_enemy(rat_id, rat);
|
||||
battle.plan();
|
||||
rat.dump();
|
||||
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");
|
||||
ritual.dump();
|
||||
REQUIRE(ritual.will_do("magick_type"));
|
||||
REQUIRE(ritual.will_do("pierce_type"));
|
||||
|
||||
ritual.pop();
|
||||
REQUIRE(ritual.will_do("pierce_type"));
|
||||
REQUIRE(ritual.will_do("magick_type"));
|
||||
|
||||
re.reset(ritual);
|
||||
re.set_state(ritual, "has_magick", true);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue