Autowalker is now using the GOAP AI system and works way better. Still quite a lot of jank in the code but that'll get removed over time. Next thing is being able to detect when its near an item/enemy and properly react.

master
Zed A. Shaw 3 weeks ago
parent ff81c78d13
commit d15c9b12fd
  1. 10
      ai.cpp
  2. 4
      ai_debug.cpp
  3. 43
      autowalker.cpp
  4. 26
      goap.cpp
  5. 14
      goap.hpp
  6. 6
      matrix.cpp
  7. 14
      pathing.cpp
  8. 3
      pathing.hpp
  9. 11
      tests/ai.cpp

@ -22,17 +22,17 @@ namespace ai {
Action result(config["name"], config["cost"]);
check(config.contains("needs"),
fmt::format("config_action: no 'needs' field", result.$name));
fmt::format("config_action: no 'needs' field", result.name));
check(config.contains("effects"),
fmt::format("config_action: no 'effects' field", result.$name));
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 name {}", result.$name, name_key));
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", 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 name {}", result.$name, name_key));
check(profile.contains(name_key), fmt::format("config_action: profile does not have name {}", result.name, name_key));
result.effect(profile.at(name_key), bool(value));
}
@ -79,7 +79,7 @@ namespace ai {
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);
AIMGR.actions.insert_or_assign(the_action.name, the_action);
}
// load all states

@ -22,7 +22,7 @@ namespace ai {
}
void dump_action(AIProfile& profile, Action& action) {
fmt::println(" --ACTION: {}, cost={}", action.$name, action.$cost);
fmt::println(" --ACTION: {}, cost={}", action.name, action.cost);
fmt::println(" PRECONDS:");
dump_only(profile, action.$positive_preconds, true, true);
@ -39,7 +39,7 @@ namespace ai {
dump_state(profile, start);
fmt::print("% ACTIONS PLANNED:");
for(auto& action : script) {
fmt::print("{} ", action.$name);
fmt::print("{} ", action.name);
}
fmt::print("\n");

@ -89,9 +89,12 @@ bool Autowalker::path_player(Pathing& paths, Point& target_out) {
bool found = paths.random_walk(target_out, false, PATHING_TOWARD);
if(!found) {
dbc::log("no neighbor found in any direction, aborting autowalk");
matrix::dump("NO TOWARD", paths.$paths, target_out.x, target_out.y);
return false;
// failed to find a linear path, try diagonal
if(!paths.random_walk(target_out, false, PATHING_TOWARD, MOVE_DIAGONAL)) {
dbc::log("couldn't find a diagonal direction");
matrix::dump("MOVE FAIL PATHS", paths.$paths, target_out.x, target_out.y);
return false;
}
}
if(!fsm.$level.map->can_move(target_out)) {
@ -111,6 +114,11 @@ void Autowalker::rotate_player(Point current, Point target) {
int facing = fsm.$main_ui.$compass_dir;
int target_facing = 0;
/* This is a massive pile of garbage. Need a way
* to determine player facing direction without
* hacking into the compass, and also do accurate
* turns.
*/
if(delta_x == -1 && delta_y == 0) {
// west
target_facing = 4;
@ -123,9 +131,21 @@ void Autowalker::rotate_player(Point current, Point target) {
} else if(delta_x == 0 && delta_y == -1) {
// north
target_facing = 6;
} else if(delta_x == 1 && delta_y == -1) {
// north east
target_facing = 5;
} else if(delta_x == 1 && delta_y == 1) {
// south east
target_facing = 1;
} else if(delta_x == -1 && delta_y == 1) {
// south west
target_facing = 3;
} else if(delta_x == -1 && delta_y == -1) {
// north west
target_facing = 5;
} else {
dbc::sentinel(
fmt::format("got more than 4 direction result: "
fmt::format("got more than 8 direction result: "
"current={},{} "
"target={},{} "
"delta={},{} ",
@ -197,31 +217,30 @@ void Autowalker::autowalk() {
// need a test for plan complete and only action is END
for(auto action : a_plan.script) {
if(action.$name == "find_enemy") {
if(action.name == "find_enemy") {
// this is where to test if enemy found and update state
fmt::println("FINDING AN ENEMY");
auto paths = path_to_enemies();
auto pos = get_current_position();
matrix::dump("FINDING", paths.$paths, pos.x, pos.y);
process_move(paths);
} else if(action.$name == "kill_enemy") {
send_event(gui::Event::ATTACK);
} else if(action.name == "kill_enemy") {
fmt::println("KILLING ENEMY");
process_combat();
} else if(action.$name == "find_healing") {
} else if(action.name == "find_healing") {
fmt::println("FINDING HEALING");
auto paths = path_to_items();
process_move(paths);
// do the path to healing thing
} else if(action.$name == "collect_items") {
} else if(action.name == "collect_items") {
fmt::println("COLLECTING ITEMS");
auto paths = path_to_items();
process_move(paths);
// path to the items and get them all
} else if(action.$name == "END") {
} else if(action == ai::FINAL_ACTION) {
fmt::println("END STATE, complete? {}", a_plan.complete);
fsm.autowalking = false;
} else {
fmt::println("Unknown action: {}", action.$name);
fmt::println("Unknown action: {}", action.name);
}
move_attempts++;

@ -31,6 +31,11 @@ namespace ai {
}
}
void Action::ignore(int name) {
$positive_preconds[name] = false;
$negative_preconds[name] = false;
}
bool Action::can_effect(State& state) {
return ((state & $positive_preconds) == $positive_preconds) &&
@ -41,16 +46,15 @@ namespace ai {
return (state | $positive_effects) & ~$negative_effects;
}
int distance_to_goal(State& from, State& to) {
int distance_to_goal(State from, State to, Action& action) {
auto result = from ^ to;
return result.count();
return result.count() + action.cost;
}
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
Script total_path{current};
int count = 0;
while(came_from.contains(current) && count++ < 10) {
while(came_from.contains(current)) {
current = came_from.at(current);
if(current != FINAL_ACTION) {
total_path.push_front(current);
@ -60,12 +64,12 @@ namespace ai {
return total_path;
}
inline int h(State& start, State& goal) {
return distance_to_goal(start, goal);
inline int h(State start, State goal, Action& action) {
return distance_to_goal(start, goal, action);
}
inline int d(State& start, State& goal) {
return distance_to_goal(start, goal);
inline int d(State start, State goal, Action& action) {
return distance_to_goal(start, goal, action);
}
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
@ -90,7 +94,7 @@ namespace ai {
ActionState current{FINAL_ACTION, start};
g_score[start] = 0;
open_set[current] = g_score[start] + h(start, goal);
open_set[current] = g_score[start] + h(start, goal, current.action);
while(!open_set.empty()) {
current = find_lowest(open_set);
@ -109,7 +113,7 @@ namespace ai {
}
auto neighbor = neighbor_action.apply_effect(current.state);
int d_score = d(current.state, neighbor);
int d_score = d(current.state, neighbor, current.action);
int tentative_g_score = g_score[current.state] + d_score;
int neighbor_g_score = g_score.contains(neighbor) ? g_score[neighbor] : SCORE_MAX;
if(tentative_g_score < neighbor_g_score) {
@ -118,7 +122,7 @@ namespace ai {
g_score[neighbor] = tentative_g_score;
// open_set gets the fScore
ActionState neighbor_as{neighbor_action, neighbor};
open_set[neighbor_as] = tentative_g_score + h(neighbor, goal);
open_set[neighbor_as] = tentative_g_score + h(neighbor, goal, neighbor_as.action);
}
}
}

@ -12,6 +12,7 @@ namespace ai {
using AIProfile = std::unordered_map<std::string, int>;
constexpr const int SCORE_MAX = std::numeric_limits<int>::max();
constexpr const size_t STATE_MAX = 32;
using State = std::bitset<STATE_MAX>;
@ -20,8 +21,8 @@ namespace ai {
const State ALL_ONES = ~ALL_ZERO;
struct Action {
std::string $name;
int $cost = 0;
std::string name;
int cost = 0;
State $positive_preconds;
State $negative_preconds;
@ -30,16 +31,17 @@ namespace ai {
State $negative_effects;
Action(std::string name, int cost) :
$name(name), $cost(cost) { }
name(name), cost(cost) { }
void needs(int name, bool val);
void effect(int name, bool val);
void ignore(int name);
bool can_effect(State& state);
State apply_effect(State& state);
bool operator==(const Action& other) const {
return other.$name == $name;
return other.name == name;
}
};
@ -66,14 +68,14 @@ namespace ai {
bool is_subset(State& source, State& target);
int distance_to_goal(State& from, State& to);
int distance_to_goal(State from, State to, Action& action);
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
}
template<> struct std::hash<ai::Action> {
size_t operator()(const ai::Action& p) const {
return std::hash<std::string>{}(p.$name);
return std::hash<std::string>{}(p.name);
}
};

@ -19,8 +19,10 @@ namespace matrix {
print("{:x}<", cell);
} else if(cell == WALL_PATH_LIMIT) {
print("# ");
} else if(cell > 15) {
print("[{:x}]", cell);
} else if(cell > 15 && cell < 32) {
print("{:x}+", cell - 16);
} else if(cell > 31) {
print("* ");
} else {
print("{:x} ", cell);
}

@ -74,15 +74,21 @@ void Pathing::clear_target(const Point &at) {
$input[at.y][at.x] = 1;
}
bool Pathing::random_walk(Point &out, bool random, int direction) {
bool Pathing::random_walk(Point &out, bool random, int direction, size_t dir_count) {
bool zero_found = false;
dbc::check(dir_count == 4 || dir_count == 8, "Only 8 or 4 directions allowed.");
// just make a list of the four directions
std::array<Point, 4> dirs{{
std::array<Point, 8> dirs{{
{out.x,out.y-1}, // north
{out.x+1,out.y}, // east
{out.x,out.y+1}, // south
{out.x-1,out.y} // west
{out.x-1,out.y}, // west
{out.x+1,out.y-1}, // north east
{out.x+1,out.y+1}, // south east
{out.x-1,out.y+1}, // south west
{out.x-1,out.y-1} // north west
}};
// get the current dijkstra number
@ -93,7 +99,7 @@ bool Pathing::random_walk(Point &out, bool random, int direction) {
int rand_start = Random::uniform<int>(0, dirs.size());
// go through all possible directions
for(size_t i = 0; i < dirs.size(); i++) {
for(size_t i = 0; i < dir_count; i++) {
// but start at the random start, effectively randomizing
// which valid direction to go
// BUG: this might be wrong given the above ranom from 0-size

@ -7,6 +7,7 @@ using matrix::Matrix;
constexpr const int PATHING_TOWARD=1;
constexpr const int PATHING_AWAY=-1;
constexpr const int MOVE_DIAGONAL=8;
class Pathing {
public:
@ -28,7 +29,7 @@ public:
Matrix &paths() { return $paths; }
Matrix &input() { return $input; }
int distance(Point to) { return $paths[to.y][to.x];}
bool random_walk(Point &out, bool random, int direction);
bool random_walk(Point &out, bool random, int direction, size_t dir_count = 4);
bool INVARIANT();
};

@ -7,7 +7,7 @@
using namespace dbc;
using namespace nlohmann;
TEST_CASE("worldstate works", "[ai]") {
TEST_CASE("state and actions work", "[ai]") {
enum StateNames {
ENEMY_IN_RANGE,
ENEMY_DEAD
@ -36,7 +36,7 @@ TEST_CASE("worldstate works", "[ai]") {
// start is clean but after move is dirty
REQUIRE(move_closer.can_effect(start));
REQUIRE(!move_closer.can_effect(after_move_state));
REQUIRE(ai::distance_to_goal(start, after_move_state) == 1);
REQUIRE(ai::distance_to_goal(start, after_move_state, move_closer) == 11);
ai::Action kill_it("kill_it", 10);
kill_it.needs(ENEMY_IN_RANGE, true);
@ -48,7 +48,10 @@ TEST_CASE("worldstate works", "[ai]") {
auto after_kill_state = kill_it.apply_effect(after_move_state);
REQUIRE(!kill_it.can_effect(after_kill_state));
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state) == 1);
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state, kill_it) == 11);
kill_it.ignore(ENEMY_IN_RANGE);
REQUIRE(kill_it.can_effect(after_move_state));
actions.push_back(kill_it);
actions.push_back(move_closer);
@ -117,7 +120,7 @@ TEST_CASE("ai as a module like sound/sprites", "[ai]") {
auto state = start;
for(auto& action : a_plan.script) {
fmt::println("ACTION: {}", action.$name);
fmt::println("ACTION: {}", action.name);
state = action.apply_effect(state);
}

Loading…
Cancel
Save