diff --git a/ai_debug.cpp b/ai_debug.cpp index d808e45..16034a7 100644 --- a/ai_debug.cpp +++ b/ai_debug.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; diff --git a/ai_debug.hpp b/ai_debug.hpp index 7ff7357..8b19518 100644 --- a/ai_debug.hpp +++ b/ai_debug.hpp @@ -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); } diff --git a/assets/ai.json b/assets/ai.json index 7f53fa2..2facfd9 100644 --- a/assets/ai.json +++ b/assets/ai.json @@ -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"] } } diff --git a/assets/config.json b/assets/config.json index 60cb59c..94338f0 100644 --- a/assets/config.json +++ b/assets/config.json @@ -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 } } diff --git a/autowalker.cpp b/autowalker.cpp index 64ead22..19ebffe 100644 --- a/autowalker.cpp +++ b/autowalker.cpp @@ -1,6 +1,6 @@ #include "autowalker.hpp" #include "inventory.hpp" -#include "ai.hpp" +#include "ai_debug.hpp" template 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(fsm); + int item_count = number_left(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(fsm); - int item_count = number_left(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(fsm.$level.player); - return inventory.count(); +InventoryStats Autowalker::player_item_count() { + auto& inventory = fsm.$level.world->get(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(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() { diff --git a/autowalker.hpp b/autowalker.hpp index d645612..a6930a1 100644 --- a/autowalker.hpp +++ b/autowalker.hpp @@ -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(); diff --git a/inventory.cpp b/inventory.cpp index 60d4dad..5f0003f 100644 --- a/inventory.cpp +++ b/inventory.cpp @@ -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(item.data); player_combat.damage = weapon.damage; diff --git a/pathing.cpp b/pathing.cpp index 369ee4a..0995612 100644 --- a/pathing.cpp +++ b/pathing.cpp @@ -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 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(0, dirs.size()); + int rand_start = Random::uniform(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]; diff --git a/systems.cpp b/systems.cpp index 35acbba..a6ecd3b 100644 --- a/systems.cpp +++ b/systems.cpp @@ -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(); - // TODO: this will be on each enemy not a global thing const auto &player_position = world.get(player.entity); map.set_target(player_position.location); map.make_paths(); world.query([&](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(ent), "enemy is missing config"); const auto &config = world.get(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(entity)) { auto& enemy_combat = world.get(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(entity)) { diff --git a/systems.hpp b/systems.hpp index 8c821b5..06c6a9d 100644 --- a/systems.hpp +++ b/systems.hpp @@ -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); diff --git a/tests/ai.cpp b/tests/ai.cpp index 4c7a550..ae1a233 100644 --- a/tests/ai.cpp +++ b/tests/ai.cpp @@ -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"));