From 47c6bfd531b09ddf7785c8ee108202a7364f6ee6 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Wed, 26 Mar 2025 13:34:52 -0400 Subject: [PATCH] AI engine is working and I have a little BattleEngine going but the AI is working better than it should in systems.cpp. Need to find out why then make the BattleEngine avoid running entities that have END in action lists. --- ai.cpp | 12 +++++++++++ ai.hpp | 3 +++ assets/ai.json | 11 +++++----- autowalker.cpp | 6 +++--- combat.cpp | 2 ++ dbc.cpp | 2 +- inventory.cpp | 3 --- rituals.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++----- rituals.hpp | 12 +++++++++++ systems.cpp | 2 +- tests/ai.cpp | 10 ++++----- tests/combat.cpp | 34 +++++++++++++++++++++++++++-- tests/rituals.cpp | 6 ++++-- 13 files changed, 131 insertions(+), 27 deletions(-) diff --git a/ai.cpp b/ai.cpp index 913c278..54b7283 100644 --- a/ai.cpp +++ b/ai.cpp @@ -171,10 +171,22 @@ namespace ai { return plan.script[0].name == name; } + bool EntityAI::active() { + if(plan.script.size() == 1) { + return plan.script[0] != FINAL_ACTION; + } else { + return plan.script.size() == 0; + } + } + void EntityAI::set_state(std::string name, bool setting) { ai::set(start, name, setting); } + bool EntityAI::get_state(std::string name) { + return ai::test(start, name); + } + void EntityAI::update() { plan = ai::plan(script, start, goal); } diff --git a/ai.hpp b/ai.hpp index 23a5e1e..6113b52 100644 --- a/ai.hpp +++ b/ai.hpp @@ -24,7 +24,10 @@ namespace ai { bool wants_to(std::string name); + bool active(); + void set_state(std::string name, bool setting); + bool get_state(std::string name); void update(); diff --git a/assets/ai.json b/assets/ai.json index 6ff51d4..fb86176 100644 --- a/assets/ai.json +++ b/assets/ai.json @@ -27,10 +27,11 @@ }, { "name": "kill_enemy", - "cost": 5, + "cost": 10, "needs": { - "tough_personality": true, + "health_good": true, "no_more_enemies": false, + "in_combat": true, "enemy_found": true, "enemy_dead": false }, @@ -78,7 +79,7 @@ } ], "states": { - "Walker::initial_state": { + "Host::initial_state": { "tough_personality": true, "enemy_found": false, "enemy_dead": false, @@ -90,7 +91,7 @@ "have_healing": false, "detect_enemy": true }, - "Walker::final_state": { + "Host::final_state": { "enemy_found": true, "enemy_dead": true, "health_good": true, @@ -114,7 +115,7 @@ } }, "scripts": { - "Walker::actions": + "Host::actions": ["find_enemy", "kill_enemy", "collect_items", diff --git a/autowalker.cpp b/autowalker.cpp index ed3e4aa..4f7d9e7 100644 --- a/autowalker.cpp +++ b/autowalker.cpp @@ -220,7 +220,7 @@ void Autowalker::handle_boss_fight() { void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) { start = update_state(start); - auto a_plan = ai::plan("Walker::actions", start, goal); + auto a_plan = ai::plan("Host::actions", start, goal); auto action = a_plan.script.front(); if(action.name == "find_enemy") { @@ -269,8 +269,8 @@ void Autowalker::autowalk() { int move_attempts = 0; - auto start = ai::load_state("Walker::initial_state"); - auto goal = ai::load_state("Walker::final_state"); + auto start = ai::load_state("Host::initial_state"); + auto goal = ai::load_state("Host::final_state"); do { handle_window_events(); diff --git a/combat.cpp b/combat.cpp index 62b6371..419150b 100644 --- a/combat.cpp +++ b/combat.cpp @@ -13,4 +13,6 @@ namespace components { return my_dmg; } + + } diff --git a/dbc.cpp b/dbc.cpp index 4653617..6b17faf 100644 --- a/dbc.cpp +++ b/dbc.cpp @@ -2,7 +2,7 @@ #include void dbc::log(const string &message, const std::source_location location) { - std::clog << '[' << location.file_name() << ':' + std::cout << '[' << location.file_name() << ':' << location.line() << "|" << location.function_name() << "] " << message << std::endl; diff --git a/inventory.cpp b/inventory.cpp index e9e5620..0aec16c 100644 --- a/inventory.cpp +++ b/inventory.cpp @@ -15,9 +15,6 @@ namespace components { } bool Inventory::has_item(size_t at) { - dbc::log( - fmt::format("requesting item at {}, have {} items in stock", - at, items.size())); return at < items.size(); } diff --git a/rituals.cpp b/rituals.cpp index 48f7596..d727e33 100644 --- a/rituals.cpp +++ b/rituals.cpp @@ -4,6 +4,51 @@ namespace combat { + void BattleEngine::add_enemy(DinkyECS::Entity enemy_id, ai::EntityAI& enemy) { + combatants.insert_or_assign(enemy_id, enemy); + } + + bool BattleEngine::plan() { + int active = 0; + + for(auto& [entity, enemy_ai] : combatants) { + fmt::println("\n\n==== ENTITY {} has AI:", entity); + enemy_ai.dump(); + enemy_ai.set_state("enemy_found", true); + enemy_ai.set_state("in_combat", true); + enemy_ai.update(); + + fmt::println("\n\n---- AFTER UPDATE:"); + enemy_ai.dump(); + + active += enemy_ai.active(); + } + + return active > 0; + } + + void BattleEngine::fight(std::function cb) { + for(auto& [entity, enemy_ai] : combatants) { + if(enemy_ai.wants_to("kill_enemy")) { + cb(entity, enemy_ai); + } else if(!enemy_ai.active()) { + enemy_ai.dump(); + dbc::sentinel("enemy AI ended early, fix your ai.json"); + } else { + dbc::log("enemy doesn't want to fight"); + enemy_ai.dump(); + } + } + } + + void BattleEngine::dump() { + for(auto& [entity, enemy_ai] : combatants) { + fmt::println("\n\n###### ENTITY #{}", entity); + enemy_ai.dump(); + } + } + + RitualEngine::RitualEngine(std::string config_path) : $config(config_path) { @@ -23,12 +68,12 @@ namespace combat { auto& scripts = $config["scripts"]; for(auto& [script_name, action_names] : scripts.items()) { - std::vector the_script; - for(auto name : action_names) { - the_script.push_back($actions.at(name)); - } + std::vector the_script; + for(auto name : action_names) { + the_script.push_back($actions.at(name)); + } - $scripts.insert_or_assign(script_name, the_script); + $scripts.insert_or_assign(script_name, the_script); } } diff --git a/rituals.hpp b/rituals.hpp index a6f4b73..54270e8 100644 --- a/rituals.hpp +++ b/rituals.hpp @@ -2,8 +2,20 @@ #include "goap.hpp" #include "ai.hpp" #include "config.hpp" +#include +#include "dinkyecs.hpp" namespace combat { + + struct BattleEngine { + std::unordered_map combatants; + + void add_enemy(DinkyECS::Entity enemy_id, ai::EntityAI& enemy); + bool plan(); + void fight(std::function cb); + void dump(); + }; + struct RitualAI { std::string script; ai::State start; diff --git a/systems.cpp b/systems.cpp index e0f1b97..1a2cccc 100644 --- a/systems.cpp +++ b/systems.cpp @@ -61,7 +61,7 @@ void System::enemy_ai_initialize(GameLevel &level) { auto ai_goal = ai::load_state(config.ai_goal_name); ai::EntityAI enemy(config.ai_script, ai_start, ai_goal); - auto&personality = world.get(ent); + auto& personality = world.get(ent); enemy.set_state("tough_personality", personality.tough); enemy.set_state("detect_enemy", map.distance(pos.location) < personality.hearing_distance); diff --git a/tests/ai.cpp b/tests/ai.cpp index 5ad2367..4e3c8be 100644 --- a/tests/ai.cpp +++ b/tests/ai.cpp @@ -131,14 +131,14 @@ TEST_CASE("ai as a module like sound/sprites", "[ai]") { TEST_CASE("ai autowalker ai test", "[ai]") { ai::reset(); ai::init("assets/ai.json"); - auto start = ai::load_state("Walker::initial_state"); - auto goal = ai::load_state("Walker::final_state"); + auto start = ai::load_state("Host::initial_state"); + auto goal = ai::load_state("Host::final_state"); int enemy_count = 5; ai::set(start, "no_more_enemies", enemy_count == 0); // find an enemy and kill them - auto a_plan = ai::plan("Walker::actions", start, goal); + auto a_plan = ai::plan("Host::actions", start, goal); REQUIRE(!a_plan.complete); auto result = ai::dump_script("\n\nWALKER KILL STUFF", start, a_plan.script); @@ -154,7 +154,7 @@ TEST_CASE("ai autowalker ai test", "[ai]") { ai::set(result, "have_item", true); REQUIRE(!ai::test(result, "health_good")); - auto health_plan = ai::plan("Walker::actions", result, goal); + auto health_plan = ai::plan("Host::actions", result, goal); result = ai::dump_script("\n\nWALKER NEED HEALTH", result, health_plan.script); REQUIRE(!health_plan.complete); REQUIRE(ai::test(result, "health_good")); @@ -163,7 +163,7 @@ TEST_CASE("ai autowalker ai test", "[ai]") { ai::set(result, "no_more_enemies", true); REQUIRE(ai::test(result, "no_more_enemies")); - auto new_plan = ai::plan("Walker::actions", result, goal); + auto new_plan = ai::plan("Host::actions", result, goal); result = ai::dump_script("\n\nWALKER COMPLETE", result, new_plan.script); REQUIRE(new_plan.complete); diff --git a/tests/combat.cpp b/tests/combat.cpp index 4fcb887..b7a64aa 100644 --- a/tests/combat.cpp +++ b/tests/combat.cpp @@ -1,9 +1,39 @@ #include #include #include "rituals.hpp" +#include "fsm.hpp" +#include "dinkyecs.hpp" using namespace combat; -TEST_CASE("turn based combat engine sorted", "[combat]") { - dbc::log("does nothing."); + +TEST_CASE("cause scared rat won't run away bug", "[combat]") { + ai::reset(); + ai::init("assets/ai.json"); + auto ai_start = ai::load_state("Enemy::initial_state"); + auto ai_goal = ai::load_state("Enemy::final_state"); + BattleEngine battle; + + 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(); + REQUIRE(active); + + // 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(); + REQUIRE(rat.wants_to("run_away")); + + battle.fight([&](const auto entity, auto& ai) { + fmt::println("\n\n======= FIGHT! {}", entity); + ai.dump(); + }); } diff --git a/tests/rituals.cpp b/tests/rituals.cpp index a2172e2..e3c78f2 100644 --- a/tests/rituals.cpp +++ b/tests/rituals.cpp @@ -1,10 +1,12 @@ #include #include #include "rituals.hpp" +#include "fsm.hpp" +#include "dinkyecs.hpp" using namespace combat; -TEST_CASE("prototype combat system ideas", "[combat]") { +TEST_CASE("RitualEngine basic tests", "[rituals]") { RitualEngine re("assets/rituals.json"); auto ritual = re.start(); @@ -47,7 +49,7 @@ TEST_CASE("prototype combat system ideas", "[combat]") { ritual.dump(); } -TEST_CASE("confirm that cycles are avoided/detected", "[combat]") { +TEST_CASE("confirm that cycles are avoided/detected", "[rituals]") { RitualEngine re("assets/rituals.json"); auto ritual = re.start();