Autowalker is working way better and now I have a plan for using the AI in the System.

master
Zed A. Shaw 3 weeks ago
parent 0623170dbc
commit ee804581a8
  1. 29
      ai_debug.cpp
  2. 8
      ai_debug.hpp
  3. 50
      assets/ai.json
  4. 4
      assets/config.json
  5. 183
      autowalker.cpp
  6. 14
      autowalker.hpp
  7. 2
      inventory.cpp
  8. 7
      pathing.cpp
  9. 17
      systems.cpp
  10. 1
      systems.hpp
  11. 9
      tests/ai.cpp

@ -1,3 +1,4 @@
#include "ai.hpp"
#include "ai_debug.hpp"
namespace ai {
@ -6,37 +7,39 @@ 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) {
void dump_only(State state, bool matching, bool show_as) {
AIProfile* profile = ai::profile();
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) {
void dump_state(State state) {
AIProfile* profile = ai::profile();
for(auto& [name, name_id] : *profile) {
fmt::println("\t{}={}", name,
state.test(name_id));
}
}
void dump_action(AIProfile& profile, Action& action) {
void dump_action(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);
dump_only(action.$positive_preconds, true, true);
dump_only(action.$negative_preconds, true, false);
fmt::println(" EFFECTS:");
dump_only(profile, action.$positive_effects, true, true);
dump_only(profile, action.$negative_effects, true, false);
dump_only(action.$positive_effects, true, true);
dump_only(action.$negative_effects, true, false);
}
State dump_script(AIProfile& profile, std::string msg, State start, Script& script) {
State dump_script(std::string msg, State start, Script& script) {
fmt::println("--SCRIPT DUMP: {}", msg);
fmt::println("# STATE BEFORE:");
dump_state(profile, start);
dump_state(start);
fmt::print("% ACTIONS PLANNED:");
for(auto& action : script) {
fmt::print("{} ", action.name);
@ -44,11 +47,11 @@ namespace ai {
fmt::print("\n");
for(auto& action : script) {
dump_action(profile, action);
dump_action(action);
start = action.apply_effect(start);
fmt::println(" ## STATE AFTER:");
dump_state(profile, start);
dump_state(start);
}
return start;

@ -2,8 +2,8 @@
#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);
void dump_only(State state, bool matching, bool show_as);
void dump_state(State state);
void dump_action(Action& action);
State dump_script(std::string msg, State start, Script& script);
}

@ -16,7 +16,6 @@
"needs": {
"in_combat": false,
"no_more_enemies": false,
"health_good": true,
"enemy_found": false
},
"effects": {
@ -29,9 +28,9 @@
"needs": {
"no_more_enemies": false,
"enemy_found": true,
"health_good": true,
"enemy_dead": false
},
"effects": {
"enemy_dead": true
}
@ -47,30 +46,6 @@
"no_more_items": true
}
},
{
"name": "find_healing",
"cost": 0,
"needs": {
"enemy_found": false,
"in_combat": false,
"health_good": false,
"no_more_items": false
},
"effects": {
"health_good": true
}
},
{
"name": "use_item",
"cost": 0,
"needs": {
"have_item": true,
"health_good": true
},
"effects": {
"have_item": false
}
},
{
"name": "use_healing",
"cost": 0,
@ -90,23 +65,38 @@
"enemy_dead": false,
"health_good": true,
"no_more_items": false,
"no_more_enemies": false
"no_more_enemies": false,
"in_combat": false,
"have_item": false,
"have_healing": false
},
"Walker::final_state": {
"enemy_found": true,
"enemy_dead": true,
"health_good": true,
"no_more_items": true,
"in_combat": false,
"no_more_enemies": true
},
"Enemy::initial_state": {
"enemy_found": false,
"enemy_dead": false,
"health_good": true,
"in_combat": false
},
"Enemy::final_state": {
"enemy_found": true,
"enemy_dead": true,
"health_good": true
}
},
"scripts": {
"Walker::actions":
["find_enemy",
"kill_enemy",
"find_healing",
"collect_items",
"use_item",
"use_healing"]
"use_healing"],
"Enemy::actions":
["find_enemy", "kill_enemy"]
}
}

@ -54,8 +54,8 @@
"tunnel_with_rocks_stage": "assets/tunnel_with_rocks_stage.png"
},
"worldgen": {
"enemy_probability": 30,
"empty_room_probability": 10,
"enemy_probability": 50,
"empty_room_probability": 1,
"device_probability": 10
}
}

@ -1,6 +1,6 @@
#include "autowalker.hpp"
#include "inventory.hpp"
#include "ai.hpp"
#include "ai_debug.hpp"
template<typename Comp>
int number_left(gui::FSM& fsm) {
@ -66,17 +66,17 @@ Pathing Autowalker::path_to_devices() {
}
void Autowalker::window_events() {
void Autowalker::handle_window_events() {
fsm.$window.handleEvents(
[&](const sf::Event::KeyPressed &) {
fsm.autowalking = false;
close_status();
log("Aborting autowalk. You can move now.");
log("Aborting autowalk.");
},
[&](const sf::Event::MouseButtonPressed &) {
fsm.autowalking = false;
close_status();
log("Aborting autowalk. You can move now.");
log("Aborting autowalk.");
}
);
}
@ -98,24 +98,26 @@ Point Autowalker::get_current_position() {
return player_position.location;
}
void Autowalker::path_fail(Matrix& bad_paths, Point pos) {
status("PATH FAIL");
log("Autowalk failed to find a path.");
matrix::dump("MOVE FAIL PATHS", bad_paths, pos.x, pos.y);
send_event(gui::Event::STAIRS_DOWN);
}
bool Autowalker::path_player(Pathing& paths, Point& target_out) {
bool found = paths.random_walk(target_out, false, PATHING_TOWARD);
if(!found) {
// failed to find a linear path, try diagonal
if(!paths.random_walk(target_out, false, PATHING_TOWARD, MOVE_DIAGONAL)) {
status("PATH FAIL");
log("Autowalk failed to find a path.");
matrix::dump("MOVE FAIL PATHS", paths.$paths, target_out.x, target_out.y);
path_fail(paths.$paths, target_out);
return false;
}
}
if(!fsm.$level.map->can_move(target_out)) {
status("PATH FAIL");
log("Autowalk major pathing failure. You can move now.");
matrix::dump("BAD TARGET PATHS", paths.$paths, target_out.x, target_out.y);
matrix::dump("BAD TARGET MAP", fsm.$level.map->walls(), target_out.x, target_out.y);
path_fail(paths.$paths, target_out);
return false;
}
@ -180,8 +182,78 @@ void Autowalker::rotate_player(Point current, Point target) {
"player isn't facing the correct direction");
}
struct InventoryStats {
int healing = 0;
int other = 0;
};
ai::State Autowalker::update_state(ai::State start) {
int enemy_count = number_left<components::Combat>(fsm);
int item_count = number_left<components::InventoryItem>(fsm);
ai::set(start, "no_more_enemies", enemy_count == 0);
ai::set(start, "no_more_items", item_count == 0);
ai::set(start, "enemy_found",
fsm.in_state(gui::State::IN_COMBAT) ||
fsm.in_state(gui::State::ATTACKING));
ai::set(start, "health_good", player_health_good());
ai::set(start, "in_combat",
fsm.in_state(gui::State::IN_COMBAT) ||
fsm.in_state(gui::State::ATTACKING));
auto inv = player_item_count();
ai::set(start, "have_item", inv.other > 0 || inv.healing > 0);
ai::set(start, "have_healing", inv.healing > 0);
return start;
}
void Autowalker::handle_boss_fight() {
// skip the boss fight for now
if(fsm.in_state(gui::State::NEXT_LEVEL)) {
// eventually we'll have AI handle this too
send_event(gui::Event::STAIRS_DOWN);
}
}
void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) {
start = update_state(start);
auto a_plan = ai::plan("Walker::actions", start, goal);
dump_script("\n\n\n-----WALKER SCRIPT", start, a_plan.script);
auto action = a_plan.script.front();
if(action.name == "find_enemy") {
// this is where to test if enemy found and update state
status("FINDING ENEMY");
auto paths = path_to_enemies();
process_move(paths);
send_event(gui::Event::ATTACK);
} else if(action.name == "kill_enemy") {
status("KILLING ENEMY");
process_combat();
} else if(action.name == "use_healing") {
status("USING HEALING");
player_use_healing();
} else if(action.name == "collect_items") {
status("COLLECTING ITEMS");
auto paths = path_to_items();
process_move(paths);
// path to the items and get them all
} else if(action == ai::FINAL_ACTION) {
close_status();
log("Autowalk done, nothing left to do.");
send_event(gui::Event::STAIRS_DOWN);
} else {
close_status();
log("Autowalk has a bug. Unknown action.");
fmt::println("Unknown action: {}", action.name);
}
}
void Autowalker::autowalk() {
window_events();
handle_window_events();
if(!fsm.autowalking) {
close_status();
return;
@ -193,58 +265,11 @@ void Autowalker::autowalk() {
auto goal = ai::load_state("Walker::final_state");
do {
int enemy_count = number_left<components::Combat>(fsm);
int item_count = number_left<components::InventoryItem>(fsm);
window_events();
ai::set(start, "no_more_enemies", enemy_count == 0);
ai::set(start, "no_more_items", item_count == 0);
ai::set(start, "enemy_found",
fsm.in_state(gui::State::IN_COMBAT) ||
fsm.in_state(gui::State::ATTACKING));
ai::set(start, "health_good", player_health_good());
ai::set(start, "in_combat",
fsm.in_state(gui::State::IN_COMBAT) ||
fsm.in_state(gui::State::ATTACKING));
ai::set(start, "have_item", player_item_count() > 0);
auto a_plan = ai::plan("Walker::actions", start, goal);
// need a test for plan complete and only action is END
for(auto action : a_plan.script) {
if(action.name == "find_enemy") {
// this is where to test if enemy found and update state
status("FINDING ENEMY");
auto paths = path_to_enemies();
process_move(paths);
send_event(gui::Event::ATTACK);
} else if(action.name == "use_item") {
status("USE ITEMS");
} else if(action.name == "kill_enemy") {
status("KILLING ENEMY");
process_combat();
} else if(action.name == "find_healing") {
status("FINDING HEALING");
auto paths = path_to_items();
process_move(paths);
// do the path to healing thing
} else if(action.name == "collect_items") {
status("COLLECTING ITEMS");
auto paths = path_to_items();
process_move(paths);
// path to the items and get them all
} else if(action == ai::FINAL_ACTION) {
close_status();
log("Autowalk done, nothing left to do.");
fsm.autowalking = false;
} else {
close_status();
log("Autowalk has a bug. Unknown action.");
fmt::println("Unknown action: {}", action.name);
}
handle_window_events();
handle_boss_fight();
handle_player_walk(start, goal);
move_attempts++;
}
move_attempts++;
} while(move_attempts < 100 && fsm.autowalking);
}
@ -254,8 +279,7 @@ void Autowalker::process_move(Pathing& paths) {
if(!path_player(paths, target)) {
close_status();
log("No paths found, aborting autowalk. You can move now.");
fsm.autowalking = false;
log("No paths found, aborting autowalk.");
return;
}
@ -277,9 +301,32 @@ bool Autowalker::player_health_good() {
return float(combat.hp) / float(combat.max_hp) > 0.5f;
}
int Autowalker::player_item_count() {
auto inventory = fsm.$level.world->get<components::Inventory>(fsm.$level.player);
return inventory.count();
InventoryStats Autowalker::player_item_count() {
auto& inventory = fsm.$level.world->get<components::Inventory>(fsm.$level.player);
InventoryStats stats;
for(auto& item : inventory.items) {
if(item.data["id"] == "POTION_HEALING_SMALL") {
stats.healing += item.count;
} else {
stats.other += item.count;
}
}
return stats;
}
void Autowalker::player_use_healing() {
auto& inventory = fsm.$level.world->get<components::Inventory>(fsm.$level.player);
// find the healing slot
for(size_t slot = 0; slot < inventory.count(); slot++) {
auto& item = inventory.get(slot);
if(item.data["id"] == "POTION_HEALING_SMALL") {
inventory.use(fsm.$level, slot);
fsm.$status_ui.update();
return;
}
}
}
void Autowalker::start_autowalk() {

@ -1,7 +1,10 @@
#pragma once
#include "ai.hpp"
#include "gui_fsm.hpp"
struct InventoryStats;
struct Autowalker {
int enemy_count = 0;
int item_count = 0;
@ -13,10 +16,15 @@ struct Autowalker {
void autowalk();
void start_autowalk();
void handle_window_events();
void handle_boss_fight();
void handle_player_walk(ai::State& start, ai::State& goal);
void send_event(gui::Event ev);
void window_events();
void process_combat();
bool path_player(Pathing& paths, Point &target_out);
void path_fail(Matrix& bad_paths, Point pos);
Point get_current_position();
void rotate_player(Point current, Point target);
void process_move(Pathing& paths);
@ -24,7 +32,9 @@ struct Autowalker {
void status(std::string msg);
void close_status();
bool player_health_good();
int player_item_count();
void player_use_healing();
InventoryStats player_item_count();
ai::State update_state(ai::State start);
Pathing path_to_enemies();
Pathing path_to_items();

@ -52,6 +52,8 @@ namespace components {
if(item.count == 0) return {false, item.data["name"]};
dbc::log("INVENTORY IS HARDCODED YOU FUCKING MORON!!!!!");
if(item.data["id"] == "SWORD_RUSTY") {
auto weapon = components::get<components::Weapon>(item.data);
player_combat.damage = weapon.damage;

@ -78,13 +78,14 @@ bool Pathing::random_walk(Point &out, bool random, int direction, size_t dir_cou
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
// first 4 directions are n/s/e/w for most enemies
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
// the player and some enemies are more "agile"
{out.x+1,out.y-1}, // north east
{out.x+1,out.y+1}, // south east
{out.x-1,out.y+1}, // south west
@ -96,14 +97,14 @@ bool Pathing::random_walk(Point &out, bool random, int direction, size_t dir_cou
// pick a random start of directions
// BUG: is uniform inclusive of the dir.size()?
int rand_start = Random::uniform<int>(0, dirs.size());
int rand_start = Random::uniform<int>(0, dir_count);
// go through all possible directions
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
Point dir = dirs[(i + rand_start) % dirs.size()];
Point dir = dirs[(i + rand_start) % dir_count];
if(!shiterator::inbounds($paths, dir.x, dir.y)) continue; //skip unpathable stuff
int weight = cur - $paths[dir.y][dir.x];

@ -34,18 +34,28 @@ void System::lighting(GameLevel &level) {
});
}
void System::enemy_ai(GameLevel &level) {
(void)level;
// AI: look up Enemy::actions in ai.json
// AI: setup the state
// AI: process it and keep the next action in the world
}
void System::enemy_pathing(GameLevel &level) {
auto &world = *level.world;
auto &map = *level.map;
auto player = world.get_the<Player>();
// TODO: this will be on each enemy not a global thing
const auto &player_position = world.get<Position>(player.entity);
map.set_target(player_position.location);
map.make_paths();
world.query<Position, Motion>([&](auto ent, auto &position, auto &motion) {
if(ent != player.entity) {
// AI: EnemyConfig can be replaced with an AI thing
// AI: after the enemy_ai systems are run we can then look at what
// AI: their next actions is, and if it's pathing do that
dbc::check(world.has<EnemyConfig>(ent), "enemy is missing config");
const auto &config = world.get<EnemyConfig>(ent);
@ -159,8 +169,11 @@ void System::combat(GameLevel &level) {
// this is guaranteed to not return the given position
auto [found, nearby] = collider.neighbors(player_position.location);
if(found) {
for(auto entity : nearby) {
// AI: process AI combat actions here
if(world.has<Combat>(entity)) {
auto& enemy_combat = world.get<Combat>(entity);
@ -196,6 +209,8 @@ void System::collision(GameLevel &level) {
auto [found, nearby] = collider.neighbors(player_position.location);
int combat_count = 0;
// AI: I think also this would a possible place to run AI decisions
// BUG: this logic is garbage, needs a refactor
for(auto entity : nearby) {
if(world.has<Combat>(entity)) {

@ -12,6 +12,7 @@ namespace System {
void collision(GameLevel &level);
void death(GameLevel &level, components::ComponentMap& components);
void enemy_pathing(GameLevel &level);
void enemy_ai(GameLevel &level);
void init_positions(DinkyECS::World &world, SpatialMap &collider);
void device(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item);

@ -130,7 +130,6 @@ TEST_CASE("ai as a module like sound/sprites", "[ai]") {
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;
@ -141,7 +140,7 @@ TEST_CASE("ai autowalker ai test", "[ai]") {
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);
auto result = ai::dump_script("\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"));
@ -150,10 +149,12 @@ TEST_CASE("ai autowalker ai test", "[ai]") {
ai::set(result, "health_good", false);
ai::set(result, "in_combat", false);
ai::set(result, "enemy_found", false);
ai::set(result, "have_healing", true);
ai::set(result, "have_item", true);
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);
result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script);
REQUIRE(!health_plan.complete);
REQUIRE(ai::test(result, "health_good"));
@ -162,7 +163,7 @@ TEST_CASE("ai autowalker ai test", "[ai]") {
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);
result = ai::dump_script("\n\nWALKER COMPLETE", result, new_plan.script);
REQUIRE(new_plan.complete);
REQUIRE(ai::test(result, "enemy_found"));

Loading…
Cancel
Save