Can now run the full AI for all combatants and then sort by the action costs to make the action queue.

This commit is contained in:
Zed A. Shaw 2025-11-27 12:46:14 -05:00
parent d244106981
commit b48df3f4db
10 changed files with 104 additions and 59 deletions

View file

@ -36,8 +36,8 @@ tracy_build:
meson setup --wipe builddir --buildtype debugoptimized -Dtracy_enable=true -Dtracy:on_demand=true meson setup --wipe builddir --buildtype debugoptimized -Dtracy_enable=true -Dtracy:on_demand=true
meson compile -j 10 -C builddir meson compile -j 10 -C builddir
test: test: build
./builddir/runtests -d yes ./builddir/runtests -d yes "[combat-battle]"
run: build test run: build test
ifeq '$(OS)' 'Windows_NT' ifeq '$(OS)' 'Windows_NT'
@ -60,7 +60,7 @@ clean:
meson compile --clean -C builddir meson compile --clean -C builddir
debug_test: build debug_test: build
gdb --nx -x .gdbinit --ex run --ex bt --ex q --args builddir/runtests -e gdb --nx -x .gdbinit --ex run --ex bt --ex q --args builddir/runtests -e "[combat-battle]"
win_installer: win_installer:
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp' powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp'

View file

@ -82,7 +82,6 @@
"name": "use_healing", "name": "use_healing",
"cost": 1, "cost": 1,
"needs": { "needs": {
"have_item": true,
"have_healing": true, "have_healing": true,
"health_good": false "health_good": false
}, },
@ -133,6 +132,7 @@
"kill_enemy", "kill_enemy",
"collect_items", "collect_items",
"find_healing", "find_healing",
"run_away",
"use_healing"], "use_healing"],
"Enemy::actions": "Enemy::actions":
["find_enemy", "run_away", "kill_enemy", "use_healing"] ["find_enemy", "run_away", "kill_enemy", "use_healing"]

View file

@ -10,18 +10,30 @@ namespace combat {
int active = 0; int active = 0;
for(auto& [entity, enemy] : combatants) { for(auto& [entity, enemy] : combatants) {
enemy.ai.update(); enemy.ai->update();
active += enemy.ai.active(); active += enemy.ai->active();
if(enemy.ai.active()) { if(enemy.ai->active()) {
if(enemy.ai.wants_to("kill_enemy")) { for(auto& action : enemy.ai->plan.script) {
pending_actions.emplace_back(enemy, BattleAction::ATTACK); if(enemy.ai->wants_to("kill_enemy")) {
} else if(enemy.ai.wants_to("run_away")) { pending_actions.emplace_back(enemy, action, BattleAction::ATTACK);
pending_actions.emplace_back(enemy, BattleAction::ESCAPE); } 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);
}
} }
} }
} }
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 active > 0; return active > 0;
} }
@ -36,25 +48,25 @@ namespace combat {
void BattleEngine::dump() { void BattleEngine::dump() {
for(auto& [entity, enemy] : combatants) { for(auto& [entity, enemy] : combatants) {
fmt::println("\n\n###### ENTITY #{}", entity); fmt::println("\n\n###### ENTITY #{}", entity);
enemy.ai.dump(); enemy.ai->dump();
} }
} }
void BattleEngine::set(DinkyECS::Entity entity, const std::string& state, bool setting) { void BattleEngine::set(DinkyECS::Entity entity, const std::string& state, bool setting) {
dbc::check(combatants.contains(entity), "invalid combatant given to BattleEngine"); dbc::check(combatants.contains(entity), "invalid combatant given to BattleEngine");
auto& action = combatants.at(entity); auto& action = combatants.at(entity);
action.ai.set_state(state, setting); action.ai->set_state(state, setting);
} }
void BattleEngine::set_all(const std::string& state, bool 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); action.ai->set_state(state, setting);
} }
} }
void BattleEngine::queue(DinkyECS::Entity entity, BattleAction action) { 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");
auto& enemy = combatants.at(entity);
pending_actions.emplace_back(enemy, action); return combatants.at(entity);
} }
} }

View file

@ -8,17 +8,18 @@
namespace combat { namespace combat {
struct Combatant { struct Combatant {
DinkyECS::Entity entity; DinkyECS::Entity entity = DinkyECS::NONE;
ai::EntityAI &ai; ai::EntityAI* ai = nullptr;
components::Combat &combat; components::Combat* combat = nullptr;
}; };
enum class BattleAction { enum class BattleAction {
ATTACK, BLOCK, ESCAPE ATTACK, BLOCK, ESCAPE, OTHER
}; };
struct BattleResult { struct BattleResult {
Combatant &state; Combatant state;
ai::Action wants_to;
BattleAction action; BattleAction action;
}; };
@ -27,11 +28,11 @@ namespace combat {
std::vector<BattleResult> pending_actions; std::vector<BattleResult> pending_actions;
void add_enemy(Combatant ba); void add_enemy(Combatant ba);
Combatant& get_enemy(DinkyECS::Entity entity);
bool plan(); bool plan();
std::optional<BattleResult> next(); std::optional<BattleResult> next();
void dump(); void dump();
void set(DinkyECS::Entity entity, const std::string& state, bool setting); void set(DinkyECS::Entity entity, const std::string& state, bool setting);
void set_all(const std::string& state, bool setting); void set_all(const std::string& state, bool setting);
void queue(DinkyECS::Entity entity, BattleAction action);
}; };
} }

View file

@ -33,11 +33,10 @@ namespace boss {
switch(ev) { switch(ev) {
case MOUSE_CLICK: { case MOUSE_CLICK: {
$ui.mouse(mouse_pos.x, mouse_pos.y, guecs::NO_MODS); $ui.mouse(mouse_pos.x, mouse_pos.y, guecs::NO_MODS);
} break; } break;
case MOUSE_MOVE: { case MOUSE_MOVE: {
$ui.mouse(mouse_pos.x, mouse_pos.y, {1 << guecs::ModBit::hover}); $ui.mouse(mouse_pos.x, mouse_pos.y, {1 << guecs::ModBit::hover});
} } break;
break;
case MOUSE_DRAG: case MOUSE_DRAG:
dbc::log("mouse drag"); dbc::log("mouse drag");
break; break;
@ -64,12 +63,15 @@ namespace boss {
case BOSS_START: case BOSS_START:
state(State::END); state(State::END);
break; break;
case KEY_PRESS:
fmt::println("KEY_PRESS");
break;
case ATTACK: case ATTACK:
$ui.status(L"PLAYER TURN"); $ui.status(L"PLAYER TURN");
state(State::PLAYER_TURN); state(State::PLAYER_TURN);
break; break;
default: default:
// fmt::println("BOSS_FIGHT unknown event {}", (int)ev); fmt::println("BOSS_FIGHT:START unknown event {}", (int)ev);
break; break;
} }
} }
@ -82,11 +84,16 @@ namespace boss {
case BOSS_START: case BOSS_START:
state(State::END); state(State::END);
break; break;
case KEY_PRESS:
fmt::println("KEY_PRESS");
break;
case ATTACK: { case ATTACK: {
int attack_id = std::any_cast<int>(data);
fmt::println("Player attack: {}", attack_id);
$ui.status(L"PLAYER TURN"); $ui.status(L"PLAYER TURN");
const std::string& player_pos = run % 10 < 5 ? "player1" : "player2"; const std::string& player_pos = run % 10 < 5 ? "player1" : "player2";
$ui.move_actor("player", player_pos); $ui.move_actor("player", player_pos);
int attack_id = std::any_cast<int>(data);
boss::System::combat($world, $boss_id, attack_id); boss::System::combat($world, $boss_id, attack_id);
$ui.update_stats(); $ui.update_stats();
state(State::PLAYER_TURN); state(State::PLAYER_TURN);
@ -105,6 +112,9 @@ namespace boss {
case BOSS_START: case BOSS_START:
state(State::END); state(State::END);
break; break;
case KEY_PRESS:
fmt::println("KEY_PRESS");
break;
case ATTACK: { case ATTACK: {
$ui.status(L"BOSS TURN"); $ui.status(L"BOSS TURN");
const std::string &boss_at = run % 10 < 5 ? "boss5" : "boss6"; const std::string &boss_at = run % 10 < 5 ? "boss5" : "boss6";
@ -120,10 +130,8 @@ namespace boss {
} }
} }
void Fight::END(gui::Event ev, std::any data) { void Fight::END(gui::Event ev, std::any) {
// We need to clean up that world I think, but not sure how fmt::println("BOSS_FIGHT:END event {}", (int)ev);
(void)ev;
(void)data;
} }
void Fight::run_systems() { void Fight::run_systems() {

View file

@ -56,17 +56,17 @@ namespace boss {
auto& boss_ai = world->get<ai::EntityAI>(boss_id); auto& boss_ai = world->get<ai::EntityAI>(boss_id);
combat::BattleEngine battle; combat::BattleEngine battle;
battle.add_enemy({boss_id, boss_ai, boss_combat}); battle.add_enemy({boss_id, &boss_ai, &boss_combat});
battle.set_all("enemy_found", true); battle.set_all("enemy_found", true);
battle.set_all("in_combat", true); battle.set_all("in_combat", true);
battle.plan(); battle.plan();
while(auto act = battle.next()) { while(auto act = battle.next()) {
auto [enemy, enemy_action] = *act; auto [enemy, ai_action, enemy_action] = *act;
Events::Combat result { Events::Combat result {
player_combat.attack(enemy.combat), 0 player_combat.attack(*enemy.combat), 0
}; };
if(result.player_did > 0) { if(result.player_did > 0) {
@ -76,7 +76,7 @@ namespace boss {
} }
if(enemy_action == combat::BattleAction::ATTACK) { if(enemy_action == combat::BattleAction::ATTACK) {
result.enemy_did = enemy.combat.attack(player_combat); result.enemy_did = enemy.combat->attack(player_combat);
} }
// need to replicate this in the boss UI // need to replicate this in the boss UI

View file

@ -8,7 +8,11 @@ project('raycaster', 'cpp',
]) ])
# use this for common options only for our executables # use this for common options only for our executables
cpp_args=[] cpp_args=[
'-Wno-unused-parameter',
'-Wno-unused-function',
'-Wno-unused-variable',
]
link_args=[] link_args=[]
# these are passed as override_defaults # these are passed as override_defaults
exe_defaults = [ 'warning_level=2' ] exe_defaults = [ 'warning_level=2' ]

View file

@ -244,7 +244,7 @@ void System::combat(int attack_id) {
if(world.has<ai::EntityAI>(entity)) { if(world.has<ai::EntityAI>(entity)) {
auto& enemy_ai = world.get<ai::EntityAI>(entity); auto& enemy_ai = world.get<ai::EntityAI>(entity);
auto& enemy_combat = world.get<Combat>(entity); auto& enemy_combat = world.get<Combat>(entity);
battle.add_enemy({entity, enemy_ai, enemy_combat}); battle.add_enemy({entity, &enemy_ai, &enemy_combat});
} }
} }
@ -256,10 +256,10 @@ void System::combat(int attack_id) {
battle.dump(); battle.dump();
while(auto act = battle.next()) { while(auto act = battle.next()) {
auto [enemy, enemy_action] = *act; auto [enemy, ai_action, enemy_action] = *act;
Events::Combat result { Events::Combat result {
player_combat.attack(enemy.combat), 0 player_combat.attack(*enemy.combat), 0
}; };
if(result.player_did > 0) { if(result.player_did > 0) {
@ -267,7 +267,7 @@ void System::combat(int attack_id) {
} }
if(enemy_action == combat::BattleAction::ATTACK) { if(enemy_action == combat::BattleAction::ATTACK) {
result.enemy_did = enemy.combat.attack(player_combat); result.enemy_did = enemy.combat->attack(player_combat);
animation::animate_entity(world, enemy.entity); animation::animate_entity(world, enemy.entity);
} }

View file

@ -1,5 +1,6 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <iostream> #include <iostream>
#include <set>
#include "rituals.hpp" #include "rituals.hpp"
#include "battle.hpp" #include "battle.hpp"
#include "simplefsm.hpp" #include "simplefsm.hpp"
@ -17,41 +18,60 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") {
auto host_goal = ai::load_state("Host::final_state"); auto host_goal = ai::load_state("Host::final_state");
BattleEngine battle; BattleEngine battle;
DinkyECS::Entity player = 0;
ai::EntityAI player_ai("Host::actions", host_start, host_goal); DinkyECS::Entity host = 0;
components::Combat player_combat{100, 100, 20}; ai::EntityAI host_ai("Host::actions", host_start, host_goal);
battle.add_enemy({player, player_ai, player_combat}); components::Combat host_combat{100, 100, 20};
battle.add_enemy({host, &host_ai, &host_combat});
DinkyECS::Entity axe_ranger = 1; DinkyECS::Entity axe_ranger = 1;
ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal); ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal);
components::Combat axe_combat{100, 100, 20}; components::Combat axe_combat{100, 100, 20};
battle.add_enemy({axe_ranger, axe_ai, axe_combat}); battle.add_enemy({axe_ranger, &axe_ai, &axe_combat});
DinkyECS::Entity rat = 2; DinkyECS::Entity rat = 2;
ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal); ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal);
components::Combat rat_combat{10, 10, 2}; components::Combat rat_combat{10, 10, 2};
battle.add_enemy({rat, rat_ai, rat_combat}); battle.add_enemy({rat, &rat_ai, &rat_combat});
battle.set_all("enemy_found", true); battle.set_all("enemy_found", true);
battle.set_all("in_combat", true); battle.set_all("in_combat", true);
battle.set_all("tough_personality", true); battle.set_all("tough_personality", true);
battle.set_all("health_good", true); battle.set_all("health_good", true);
battle.set(rat, "tough_personality", false); battle.set(rat, "tough_personality", false);
battle.queue(player, BattleAction::ATTACK); battle.set(host, "have_healing", false);
battle.queue(player, BattleAction::BLOCK); battle.set(host, "health_good", false);
battle.set(host, "tough_personality", false);
battle.queue(player, BattleAction::ESCAPE);
battle.plan(); battle.plan();
while(auto act = battle.next()) { std::set<std::string> requests{
auto& [enemy, action] = *act; "use_healing",
"kill_enemy"
};
fmt::println("entity: {} wants to {} action={} and has {} HP and {} damage", while(auto act = battle.next()) {
enemy.entity, enemy.ai.wants_to(), auto& [enemy, wants_to, action] = *act;
int(action), enemy.combat.hp,
enemy.combat.damage); 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);
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);
}
}
fmt::println("<<<<<<<<<<<<<<<<");
} }
REQUIRE(!battle.next()); REQUIRE(!battle.next());

View file

@ -33,6 +33,7 @@ int main(int, char*[]) {
sf::RenderWindow window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Bossfight Testing Arena"); sf::RenderWindow window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Bossfight Testing Arena");
window.setVerticalSyncEnabled(VSYNC); window.setVerticalSyncEnabled(VSYNC);
if(FRAME_LIMIT) window.setFramerateLimit(FRAME_LIMIT); if(FRAME_LIMIT) window.setFramerateLimit(FRAME_LIMIT);
window.setPosition({0,0}); window.setPosition({0,0});
@ -63,7 +64,6 @@ int main(int, char*[]) {
while(world->has_event<Events::GUI>()) { while(world->has_event<Events::GUI>()) {
auto [evt, entity, data] = world->recv<Events::GUI>(); auto [evt, entity, data] = world->recv<Events::GUI>();
// FIX YOUR DAMN EVENTS
switch(evt) { switch(evt) {
case Events::GUI::ATTACK: case Events::GUI::ATTACK:
main->event(gui::Event::ATTACK, data); main->event(gui::Event::ATTACK, data);