From c78b2ae75e1124ae42859f683b778fd6e3775e04 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Mon, 1 Dec 2025 00:14:08 -0500 Subject: [PATCH] Now have the basics of the turn based battle engine with AI rebellion working. --- battle.cpp | 47 +++++++++++++++++++++++----------------- battle.hpp | 23 +++++++++++++------- boss/fight.cpp | 10 +++++++-- boss/system.cpp | 35 ++++++++++++++++++++++-------- boss/ui.cpp | 10 ++++----- boss/ui.hpp | 2 +- systems.cpp | 4 ++-- tests/battle.cpp | 56 ++++++++++++++++++++++++++---------------------- 8 files changed, 114 insertions(+), 73 deletions(-) diff --git a/battle.cpp b/battle.cpp index cd774db..8b36d90 100644 --- a/battle.cpp +++ b/battle.cpp @@ -3,34 +3,41 @@ namespace combat { void BattleEngine::add_enemy(Combatant enemy) { - combatants.try_emplace(enemy.entity, enemy); + $combatants.try_emplace(enemy.entity, enemy); + } + + void BattleEngine::player_request(const std::string& request) { + $player_requests.emplace(request); } bool BattleEngine::plan() { + dbc::check($player_requests.size() > 0, "Calling plan without any player reqeusts queued."); + using enum BattleHostState; + int active = 0; - for(auto& [entity, enemy] : combatants) { + for(auto& [entity, enemy] : $combatants) { enemy.ai->update(); active += enemy.ai->active(); if(enemy.ai->active()) { for(auto& action : enemy.ai->plan.script) { - if(enemy.ai->wants_to("kill_enemy")) { - pending_actions.emplace_back(enemy, action, BattleAction::ATTACK); - } else if(enemy.ai->wants_to("run_away")) { - pending_actions.emplace_back(enemy, action, BattleAction::ESCAPE); - } else { - pending_actions.emplace_back(enemy, action, BattleAction::OTHER); + BattleHostState host_state = not_host; + + if(enemy.is_host) { + host_state = $player_requests.contains(action.name) ? agree : disagree; } + + $pending_actions.emplace_back(enemy, action.name, action.cost, host_state); } } } - if(pending_actions.size() > 0) { - std::sort(pending_actions.begin(), pending_actions.end(), + if($pending_actions.size() > 0) { + std::sort($pending_actions.begin(), $pending_actions.end(), [](const auto& a, const auto& b) -> bool { - return a.wants_to.cost > b.wants_to.cost; + return a.cost > b.cost; }); } @@ -38,35 +45,35 @@ namespace combat { } std::optional BattleEngine::next() { - if(pending_actions.size() == 0) return std::nullopt; + if($pending_actions.size() == 0) return std::nullopt; - auto ba = pending_actions.back(); - pending_actions.pop_back(); + auto ba = $pending_actions.back(); + $pending_actions.pop_back(); return std::make_optional(ba); } void BattleEngine::dump() { - for(auto& [entity, enemy] : combatants) { + for(auto& [entity, enemy] : $combatants) { fmt::println("\n\n###### ENTITY #{}", entity); enemy.ai->dump(); } } void BattleEngine::set(DinkyECS::Entity entity, const std::string& state, bool setting) { - dbc::check(combatants.contains(entity), "invalid combatant given to BattleEngine"); - auto& action = combatants.at(entity); + dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine"); + auto& action = $combatants.at(entity); action.ai->set_state(state, setting); } void BattleEngine::set_all(const std::string& state, bool setting) { - for(auto& [ent, action] : combatants) { + for(auto& [ent, action] : $combatants) { action.ai->set_state(state, setting); } } Combatant& BattleEngine::get_enemy(DinkyECS::Entity entity) { - dbc::check(combatants.contains(entity), "invalid combatant given to BattleEngine"); + dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine"); - return combatants.at(entity); + return $combatants.at(entity); } } diff --git a/battle.hpp b/battle.hpp index 133c162..eaa6937 100644 --- a/battle.hpp +++ b/battle.hpp @@ -4,28 +4,34 @@ #include "dinkyecs.hpp" #include #include "components.hpp" +#include namespace combat { + enum class BattleHostState { + not_host = 0, + agree = 1, + disagree = 2 + }; + struct Combatant { DinkyECS::Entity entity = DinkyECS::NONE; ai::EntityAI* ai = nullptr; components::Combat* combat = nullptr; - }; - - enum class BattleAction { - ATTACK, BLOCK, ESCAPE, OTHER + bool is_host=false; }; struct BattleResult { Combatant state; - ai::Action wants_to; - BattleAction action; + std::string wants_to; + int cost; + BattleHostState host_state; }; struct BattleEngine { - std::unordered_map combatants; - std::vector pending_actions; + std::unordered_map $combatants; + std::vector $pending_actions; + std::set $player_requests; void add_enemy(Combatant ba); Combatant& get_enemy(DinkyECS::Entity entity); @@ -34,5 +40,6 @@ namespace combat { void dump(); void set(DinkyECS::Entity entity, const std::string& state, bool setting); void set_all(const std::string& state, bool setting); + void player_request(const std::string& request); }; } diff --git a/boss/fight.cpp b/boss/fight.cpp index 1697f11..7779a2a 100644 --- a/boss/fight.cpp +++ b/boss/fight.cpp @@ -70,6 +70,8 @@ namespace boss { $ui.status(L"PLAYER TURN"); state(State::PLAYER_TURN); break; + case TICK: + break; // ignore tick default: fmt::println("BOSS_FIGHT:START unknown event {}", (int)ev); break; @@ -98,9 +100,11 @@ namespace boss { $ui.update_stats(); state(State::PLAYER_TURN); } break; + case TICK: + break; // ignore tick default: + fmt::println("BOSS_FIGHT:BOSS_TURN unknown event {}", (int)ev); break; - // skip it } } @@ -124,8 +128,10 @@ namespace boss { boss::System::combat($world, $boss_id, attack_id); state(State::BOSS_TURN); } break; + case TICK: + break; // ignore tick default: - // skip it + fmt::println("BOSS_FIGHT:PLAYER_TURN unknown event {}", (int)ev); break; } } diff --git a/boss/system.cpp b/boss/system.cpp index 7776608..0f82a9f 100644 --- a/boss/system.cpp +++ b/boss/system.cpp @@ -51,34 +51,51 @@ namespace boss { auto& level = GameDB::current_level(); dbc::check(world->has(boss_id), "boss doesn't have an AI"); + auto host_start = ai::load_state("Host::initial_state"); + auto host_goal = ai::load_state("Host::final_state"); + ai::EntityAI host_ai("Host::actions", host_start, host_goal); + auto& player_combat = world->get(level.player); auto& boss_combat = world->get(boss_id); auto& boss_ai = world->get(boss_id); combat::BattleEngine battle; battle.add_enemy({boss_id, &boss_ai, &boss_combat}); + battle.add_enemy({level.player, &host_ai, &player_combat}); battle.set_all("enemy_found", true); battle.set_all("in_combat", true); + battle.set(boss_id, "tough_personality", true); + battle.set(level.player, "tough_personality", false); + battle.set(level.player, "have_healing", false); + battle.set(level.player, "health_good", player_combat.hp > 20); + + battle.player_request("kill_enemy"); + battle.plan(); while(auto act = battle.next()) { - auto [enemy, ai_action, enemy_action] = *act; + auto [enemy, wants_to, cost, host_state] = *act; + Events::Combat result{}; - Events::Combat result { - player_combat.attack(*enemy.combat), 0 - }; + switch(host_state) { + case combat::BattleHostState::agree: + result.player_did = player_combat.attack(*enemy.combat); + break; + case combat::BattleHostState::disagree: + fmt::println("HOST DISAGREES! {}", wants_to); + break; + case combat::BattleHostState::not_host: + if(wants_to == "kill_enemy") { + result.enemy_did = enemy.combat->attack(player_combat); + } + } if(result.player_did > 0) { auto& the_belt = world->get_the(); - dbc::check(the_belt.has(attack_id), "STOP passing invalid attack IDs to the system."); } - if(enemy_action == combat::BattleAction::ATTACK) { - result.enemy_did = enemy.combat->attack(player_combat); - } - // need to replicate this in the boss UI world->send(Events::GUI::COMBAT, enemy.entity, result); } diff --git a/boss/ui.cpp b/boss/ui.cpp index 1f3c843..1bed3bb 100644 --- a/boss/ui.cpp +++ b/boss/ui.cpp @@ -72,11 +72,11 @@ namespace boss { } if(result.player_did > 0) { - zoom(boss_is.cell); + zoom("boss14", 1.8); } else if(result.enemy_did > 0) { - zoom(player_is.cell); + zoom(player_is.cell, 2.0); } else { - zoom(""); + zoom("", 0.0); } } @@ -115,14 +115,14 @@ namespace boss { $arena.play_animations(); } - void UI::zoom(const std::string &cell_name) { + void UI::zoom(const std::string &cell_name, double ratio) { if(cell_name == "") { dbc::log("!!!!!!!!! you should add this to guecs"); $camera.reset($view_texture, BOSS_VIEW_WIDTH, BOSS_VIEW_HEIGHT); } else { auto& cell = $arena.$ui.cell_for(cell_name); - $camera.resize(BOSS_VIEW_WIDTH/2, BOSS_VIEW_HEIGHT/2); + $camera.resize(double(BOSS_VIEW_WIDTH)/ratio, double(BOSS_VIEW_HEIGHT)/ratio); $camera.move(float(cell.mid_x), float(cell.mid_y)); $camera.play(); } diff --git a/boss/ui.hpp b/boss/ui.hpp index 66c343e..365f1e0 100644 --- a/boss/ui.hpp +++ b/boss/ui.hpp @@ -38,6 +38,6 @@ namespace boss { void animate_actor(const std::string& actor); void update_stats(); void play_animations(); - void zoom(const std::string &cell); + void zoom(const std::string &cell, double ratio); }; } diff --git a/systems.cpp b/systems.cpp index 202b5a8..170b7a3 100644 --- a/systems.cpp +++ b/systems.cpp @@ -256,7 +256,7 @@ void System::combat(int attack_id) { battle.dump(); while(auto act = battle.next()) { - auto [enemy, ai_action, enemy_action] = *act; + auto [enemy, enemy_action, cost, host_state] = *act; Events::Combat result { player_combat.attack(*enemy.combat), 0 @@ -266,7 +266,7 @@ void System::combat(int attack_id) { spawn_attack(world, attack_id, enemy.entity); } - if(enemy_action == combat::BattleAction::ATTACK) { + if(enemy_action == "kill_enemy") { result.enemy_did = enemy.combat->attack(player_combat); animation::animate_entity(world, enemy.entity); } diff --git a/tests/battle.cpp b/tests/battle.cpp index 8bb980f..d8f51b7 100644 --- a/tests/battle.cpp +++ b/tests/battle.cpp @@ -18,11 +18,10 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { auto host_goal = ai::load_state("Host::final_state"); BattleEngine battle; - DinkyECS::Entity host = 0; ai::EntityAI host_ai("Host::actions", host_start, host_goal); components::Combat host_combat{100, 100, 20}; - battle.add_enemy({host, &host_ai, &host_combat}); + battle.add_enemy({host, &host_ai, &host_combat, true}); DinkyECS::Entity axe_ranger = 1; ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal); @@ -40,39 +39,44 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { battle.set_all("health_good", true); battle.set(rat, "tough_personality", false); - battle.set(host, "have_healing", false); - battle.set(host, "health_good", false); battle.set(host, "tough_personality", false); - battle.plan(); - std::set requests{ - "use_healing", - "kill_enemy" - }; + while(host_combat.hp > 0) { + battle.set(host, "health_good", host_combat.hp > 20); - while(auto act = battle.next()) { - auto& [enemy, wants_to, action] = *act; + battle.player_request("use_healing"); + battle.player_request("kill_enemy"); - fmt::println(">>>>> entity: {} wants to {} cost={} and has {} HP and {} damage", - enemy.entity, wants_to.name, - wants_to.cost, enemy.combat->hp, - enemy.combat->damage); + battle.plan(); - if(enemy.entity == host) { - // negotiate between the player requested actions and the AI action - if(requests.contains(wants_to.name)) { - fmt::println("HOST and PLAYER requests match {}, doing it.", - wants_to.name); - requests.erase(wants_to.name); - } else { - fmt::println("REBELIOUS ACT: {}", wants_to.name); + while(auto act = battle.next()) { + auto& [enemy, wants_to, cost, host_behavior] = *act; + + fmt::println(">>>>> entity: {} wants to {} cost={} and has {} HP and {} damage", + enemy.entity, wants_to, + cost, enemy.combat->hp, + enemy.combat->damage); + + switch(host_behavior) { + case BattleHostState::agree: + fmt::println("HOST and PLAYER requests match {}, doing it.", + wants_to); + break; + case BattleHostState::disagree: + fmt::println("REBELIOUS ACT: {}", wants_to); + battle.$player_requests.clear(); + break; + case BattleHostState::not_host: + if(wants_to == "kill_enemy") { + enemy.combat->attack(host_combat); + } } + + fmt::println("<<<<<<<<<<<<<<<<"); } - fmt::println("<<<<<<<<<<<<<<<<"); + REQUIRE(!battle.next()); } - - REQUIRE(!battle.next()); }