#include "../src/modules/core/CombatModule.h" #include #include #include #include #include #include /** * CombatModule Test Suite * * Tests the game-agnostic combat resolver with various scenarios * Uses generic "units" and "combatants" (not game-specific entities) */ // Mock IO for testing class TestIO { public: std::vector>> publishedMessages; void publish(const std::string& topic, std::unique_ptr data) { std::cout << "[TestIO] Published: " << topic << std::endl; publishedMessages.push_back({topic, std::move(data)}); } void clear() { publishedMessages.clear(); } }; // Helper: Create combat config std::unique_ptr createCombatConfig() { auto config = std::make_unique("config"); // Formulas auto formulas = std::make_unique("formulas"); formulas->setDouble("hit_base_chance", 0.7); formulas->setDouble("armor_damage_reduction", 0.5); formulas->setDouble("cover_evasion_bonus", 0.3); formulas->setDouble("morale_retreat_threshold", 0.2); config->setChild("formulas", std::move(formulas)); // Combat rules auto rules = std::make_unique("combatRules"); rules->setInt("max_rounds", 20); rules->setDouble("round_duration", 1.0); rules->setBool("simultaneous_attacks", true); config->setChild("combatRules", std::move(rules)); return config; } // Helper: Create combatant std::unique_ptr createCombatant( const std::string& id, float firepower, float armor, float health, float accuracy, float evasion ) { auto combatant = std::make_unique(id); combatant->setString("id", id); combatant->setDouble("firepower", firepower); combatant->setDouble("armor", armor); combatant->setDouble("health", health); combatant->setDouble("accuracy", accuracy); combatant->setDouble("evasion", evasion); return combatant; } // Helper: Create combat request std::unique_ptr createCombatRequest( const std::string& combatId, const std::vector>& attackers, const std::vector>& defenders, float environmentCover = 0.0f ) { auto request = std::make_unique("combat_request"); request->setString("combat_id", combatId); request->setString("location", "test_arena"); request->setDouble("environment_cover", environmentCover); request->setDouble("environment_visibility", 1.0); // Add attackers auto attackersNode = std::make_unique("attackers"); for (size_t i = 0; i < attackers.size(); ++i) { auto [id, firepower, armor, health, accuracy, evasion] = attackers[i]; auto combatant = createCombatant(id, firepower, armor, health, accuracy, evasion); attackersNode->setChild("attacker_" + std::to_string(i), std::move(combatant)); } request->setChild("attackers", std::move(attackersNode)); // Add defenders auto defendersNode = std::make_unique("defenders"); for (size_t i = 0; i < defenders.size(); ++i) { auto [id, firepower, armor, health, accuracy, evasion] = defenders[i]; auto combatant = createCombatant(id, firepower, armor, health, accuracy, evasion); defendersNode->setChild("defender_" + std::to_string(i), std::move(combatant)); } request->setChild("defenders", std::move(defendersNode)); return request; } // Test 1: Module initialization void test_module_initialization() { std::cout << "\n=== Test 1: Module Initialization ===" << std::endl; CombatModule* module = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module->setConfiguration(*config, &io, nullptr); assert(module->getType() == "CombatModule"); assert(module->isIdle() == true); module->shutdown(); delete module; std::cout << "PASS: Module initialized correctly" << std::endl; } // Test 2: Combat resolves with victory void test_combat_victory() { std::cout << "\n=== Test 2: Combat Victory ===" << std::endl; CombatModule* module = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module->setConfiguration(*config, &io, nullptr); // Create strong attackers vs weak defenders std::vector> attackers = { {"attacker_1", 50.0f, 20.0f, 100.0f, 0.9f, 0.1f}, // High firepower, armor, accuracy {"attacker_2", 50.0f, 20.0f, 100.0f, 0.9f, 0.1f} }; std::vector> defenders = { {"defender_1", 10.0f, 5.0f, 50.0f, 0.5f, 0.1f}, // Weak units }; auto combatRequest = createCombatRequest("combat_1", attackers, defenders); // Simulate combat request io.publish("combat:request_start", std::move(combatRequest)); // Process multiple rounds auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.0); bool combatEnded = false; int rounds = 0; while (!combatEnded && rounds < 25) { module->process(*input); rounds++; // Check for combat:ended message while (io.hasMessages() > 0) { auto msg = io.pullMessage(); if (msg.topic == "combat:ended") { bool victory = msg.data->getBool("victory", false); std::string outcome = msg.data->getString("outcome_reason", ""); std::cout << "Combat ended: victory=" << victory << ", outcome=" << outcome << std::endl; assert(victory == true); combatEnded = true; } } } assert(combatEnded == true); assert(module->isIdle() == true); module->shutdown(); delete module; std::cout << "PASS: Combat resolved with victory" << std::endl; } // Test 3: Armor reduces damage void test_armor_damage_reduction() { std::cout << "\n=== Test 3: Armor Damage Reduction ===" << std::endl; CombatModule* module = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module->setConfiguration(*config, &io, nullptr); // Attacker with moderate firepower vs high armor defender std::vector> attackers = { {"attacker_1", 30.0f, 10.0f, 100.0f, 1.0f, 0.0f}, // Perfect accuracy, no evasion }; std::vector> defenders = { {"defender_1", 10.0f, 40.0f, 100.0f, 0.5f, 0.0f}, // Very high armor }; auto combatRequest = createCombatRequest("combat_2", attackers, defenders); io.publish("combat:request_start", std::move(combatRequest)); // Process multiple rounds auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.0); int totalDamage = 0; int rounds = 0; bool combatEnded = false; while (!combatEnded && rounds < 25) { module->process(*input); rounds++; while (io.hasMessages() > 0) { auto msg = io.pullMessage(); if (msg.topic == "combat:round_complete") { int damage = msg.data->getInt("attacker_damage_dealt", 0); totalDamage += damage; std::cout << "Round " << rounds << ": damage=" << damage << std::endl; } else if (msg.topic == "combat:ended") { combatEnded = true; } } } // With armor_damage_reduction=0.5, damage = 30 - (40 * 0.5) = 10 per hit // High armor should reduce damage significantly std::cout << "Total damage dealt: " << totalDamage << " over " << rounds << " rounds" << std::endl; assert(totalDamage > 0); // Some damage should be dealt assert(combatEnded == true); module->shutdown(); delete module; std::cout << "PASS: Armor correctly reduces damage" << std::endl; } // Test 4: Hit probability calculation void test_hit_probability() { std::cout << "\n=== Test 4: Hit Probability ===" << std::endl; CombatModule* module = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module->setConfiguration(*config, &io, nullptr); // Very low accuracy attacker std::vector> attackers = { {"attacker_1", 50.0f, 10.0f, 100.0f, 0.1f, 0.0f}, // 10% accuracy {"attacker_2", 50.0f, 10.0f, 100.0f, 0.1f, 0.0f}, {"attacker_3", 50.0f, 10.0f, 100.0f, 0.1f, 0.0f}, }; std::vector> defenders = { {"defender_1", 10.0f, 5.0f, 200.0f, 0.5f, 0.5f}, // High evasion }; auto combatRequest = createCombatRequest("combat_3", attackers, defenders); io.publish("combat:request_start", std::move(combatRequest)); // Process rounds auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.0); int hits = 0; int rounds = 0; bool combatEnded = false; while (!combatEnded && rounds < 25) { module->process(*input); rounds++; while (io.hasMessages() > 0) { auto msg = io.pullMessage(); if (msg.topic == "combat:round_complete") { int damage = msg.data->getInt("attacker_damage_dealt", 0); if (damage > 0) hits++; } else if (msg.topic == "combat:ended") { combatEnded = true; } } } std::cout << "Hits: " << hits << " out of " << rounds << " rounds (low accuracy)" << std::endl; // With very low accuracy and high evasion, most attacks should miss assert(hits < rounds); // Not all rounds should have hits module->shutdown(); delete module; std::cout << "PASS: Hit probability works correctly" << std::endl; } // Test 5: Casualties applied correctly void test_casualties() { std::cout << "\n=== Test 5: Casualties ===" << std::endl; CombatModule* module = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module->setConfiguration(*config, &io, nullptr); // Balanced combat std::vector> attackers = { {"attacker_1", 40.0f, 10.0f, 50.0f, 0.8f, 0.1f}, {"attacker_2", 40.0f, 10.0f, 50.0f, 0.8f, 0.1f}, }; std::vector> defenders = { {"defender_1", 40.0f, 10.0f, 50.0f, 0.8f, 0.1f}, }; auto combatRequest = createCombatRequest("combat_4", attackers, defenders); io.publish("combat:request_start", std::move(combatRequest)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.0); int totalCasualties = 0; bool combatEnded = false; int rounds = 0; while (!combatEnded && rounds < 25) { module->process(*input); rounds++; while (io.hasMessages() > 0) { auto msg = io.pullMessage(); if (msg.topic == "combat:ended") { // Count casualties from both sides if (msg.data->hasChild("attacker_casualties")) { auto casualties = msg.data->getChildReadOnly("attacker_casualties"); if (casualties) { totalCasualties += casualties->getChildNames().size(); } } if (msg.data->hasChild("defender_casualties")) { auto casualties = msg.data->getChildReadOnly("defender_casualties"); if (casualties) { totalCasualties += casualties->getChildNames().size(); } } std::cout << "Total casualties: " << totalCasualties << std::endl; combatEnded = true; } } } assert(combatEnded == true); assert(totalCasualties > 0); // Some casualties should occur module->shutdown(); delete module; std::cout << "PASS: Casualties tracked correctly" << std::endl; } // Test 6: Morale retreat void test_morale_retreat() { std::cout << "\n=== Test 6: Morale Retreat ===" << std::endl; CombatModule* module = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module->setConfiguration(*config, &io, nullptr); // Weak defenders should retreat std::vector> attackers = { {"attacker_1", 60.0f, 20.0f, 100.0f, 0.9f, 0.1f}, {"attacker_2", 60.0f, 20.0f, 100.0f, 0.9f, 0.1f}, {"attacker_3", 60.0f, 20.0f, 100.0f, 0.9f, 0.1f}, }; std::vector> defenders = { {"defender_1", 10.0f, 5.0f, 30.0f, 0.5f, 0.1f}, // Low health {"defender_2", 10.0f, 5.0f, 30.0f, 0.5f, 0.1f}, }; auto combatRequest = createCombatRequest("combat_5", attackers, defenders); io.publish("combat:request_start", std::move(combatRequest)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.0); bool retreatOccurred = false; bool combatEnded = false; int rounds = 0; while (!combatEnded && rounds < 25) { module->process(*input); rounds++; while (io.hasMessages() > 0) { auto msg = io.pullMessage(); if (msg.topic == "combat:ended") { std::string outcome = msg.data->getString("outcome_reason", ""); std::cout << "Combat outcome: " << outcome << std::endl; if (outcome == "defender_retreat" || outcome == "attacker_retreat") { retreatOccurred = true; } combatEnded = true; } } } assert(combatEnded == true); // Note: Retreat is probabilistic based on morale, so we just check combat ended std::cout << "Retreat occurred: " << (retreatOccurred ? "yes" : "no") << std::endl; module->shutdown(); delete module; std::cout << "PASS: Morale retreat system works" << std::endl; } // Test 7: Hot-reload state preservation void test_hot_reload() { std::cout << "\n=== Test 7: Hot-Reload State Preservation ===" << std::endl; CombatModule* module1 = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module1->setConfiguration(*config, &io, nullptr); // Start combat std::vector> attackers = { {"attacker_1", 40.0f, 10.0f, 100.0f, 0.8f, 0.1f}, }; std::vector> defenders = { {"defender_1", 40.0f, 10.0f, 100.0f, 0.8f, 0.1f}, }; auto combatRequest = createCombatRequest("combat_6", attackers, defenders); io.publish("combat:request_start", std::move(combatRequest)); // Process one round auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.0); module1->process(*input); // Get state auto state = module1->getState(); // Create new module (simulate hot-reload) CombatModule* module2 = new CombatModule(); module2->setConfiguration(*config, &io, nullptr); module2->setState(*state); // Verify state assert(module2->getType() == "CombatModule"); module1->shutdown(); module2->shutdown(); delete module1; delete module2; std::cout << "PASS: Hot-reload state preservation works" << std::endl; } // Test 8: Multiple simultaneous combats void test_multiple_combats() { std::cout << "\n=== Test 8: Multiple Simultaneous Combats ===" << std::endl; CombatModule* module = new CombatModule(); auto config = createCombatConfig(); grove::IntraIOManager io; module->setConfiguration(*config, &io, nullptr); // Start multiple combats std::vector> attackers = { {"attacker_1", 50.0f, 10.0f, 100.0f, 0.9f, 0.1f}, }; std::vector> defenders = { {"defender_1", 10.0f, 5.0f, 50.0f, 0.5f, 0.1f}, }; auto combat1 = createCombatRequest("combat_a", attackers, defenders); auto combat2 = createCombatRequest("combat_b", attackers, defenders); io.publish("combat:request_start", std::move(combat1)); io.publish("combat:request_start", std::move(combat2)); // Process both combats auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.0); int combatsEnded = 0; int rounds = 0; while (combatsEnded < 2 && rounds < 30) { module->process(*input); rounds++; while (io.hasMessages() > 0) { auto msg = io.pullMessage(); if (msg.topic == "combat:ended") { std::string combatId = msg.data->getString("combat_id", ""); std::cout << "Combat ended: " << combatId << std::endl; combatsEnded++; } } } assert(combatsEnded == 2); assert(module->isIdle() == true); module->shutdown(); delete module; std::cout << "PASS: Multiple simultaneous combats work" << std::endl; } int main() { std::cout << "======================================" << std::endl; std::cout << " CombatModule Test Suite" << std::endl; std::cout << " Game-Agnostic Combat Resolver" << std::endl; std::cout << "======================================" << std::endl; try { test_module_initialization(); test_combat_victory(); test_armor_damage_reduction(); test_hit_probability(); test_casualties(); test_morale_retreat(); test_hot_reload(); test_multiple_combats(); std::cout << "\n======================================" << std::endl; std::cout << " ALL TESTS PASSED!" << std::endl; std::cout << "======================================" << std::endl; return 0; } catch (const std::exception& e) { std::cerr << "\nTEST FAILED: " << e.what() << std::endl; return 1; } }