/** * EventModuleTest - Independent validation tests * * Tests the EventModule in isolation without requiring the full game. * Validates core functionality: event triggering, choices, outcomes, state preservation. */ #include "../src/modules/core/EventModule.h" #include #include #include #include #include // Simple assertion helper #define TEST_ASSERT(condition, message) \ if (!(condition)) { \ std::cerr << "[FAILED] " << message << std::endl; \ return false; \ } else { \ std::cout << "[PASSED] " << message << std::endl; \ } // Mock TaskScheduler (not used in EventModule but required by interface) class MockTaskScheduler : public grove::ITaskScheduler { public: void scheduleTask(const std::string& taskType, std::unique_ptr taskData) override {} int hasCompletedTasks() const override { return 0; } std::unique_ptr getCompletedTask() override { return nullptr; } }; /** * Helper: Create minimal event config for testing */ std::unique_ptr createTestEventConfig() { auto config = std::make_unique("config"); auto events = std::make_unique("events"); // Event 1: Simple event with time condition auto event1 = std::make_unique("test_event_1"); event1->setString("title", "Test Event 1"); event1->setString("description", "First test event"); event1->setInt("cooldown", 60); auto conditions1 = std::make_unique("conditions"); conditions1->setInt("game_time_min", 100); event1->setChild("conditions", std::move(conditions1)); auto choices1 = std::make_unique("choices"); auto choice1a = std::make_unique("choice_a"); choice1a->setString("text", "Choice A"); auto outcomes1a = std::make_unique("outcomes"); auto resources1a = std::make_unique("resources"); resources1a->setInt("supplies", 10); resources1a->setInt("fuel", -5); outcomes1a->setChild("resources", std::move(resources1a)); choice1a->setChild("outcomes", std::move(outcomes1a)); choices1->setChild("choice_a", std::move(choice1a)); auto choice1b = std::make_unique("choice_b"); choice1b->setString("text", "Choice B"); auto outcomes1b = std::make_unique("outcomes"); auto flags1b = std::make_unique("flags"); flags1b->setString("test_flag", "completed"); outcomes1b->setChild("flags", std::move(flags1b)); choice1b->setChild("outcomes", std::move(outcomes1b)); choices1->setChild("choice_b", std::move(choice1b)); event1->setChild("choices", std::move(choices1)); events->setChild("test_event_1", std::move(event1)); // Event 2: Event with resource requirements and chained event auto event2 = std::make_unique("test_event_2"); event2->setString("title", "Test Event 2"); event2->setString("description", "Second test event"); auto conditions2 = std::make_unique("conditions"); auto resourceMin = std::make_unique("resource_min"); resourceMin->setInt("supplies", 50); conditions2->setChild("resource_min", std::move(resourceMin)); event2->setChild("conditions", std::move(conditions2)); auto choices2 = std::make_unique("choices"); auto choice2a = std::make_unique("choice_chain"); choice2a->setString("text", "Chain to Event 3"); auto outcomes2a = std::make_unique("outcomes"); outcomes2a->setString("trigger_event", "test_event_3"); choice2a->setChild("outcomes", std::move(outcomes2a)); choices2->setChild("choice_chain", std::move(choice2a)); event2->setChild("choices", std::move(choices2)); events->setChild("test_event_2", std::move(event2)); // Event 3: Chained event auto event3 = std::make_unique("test_event_3"); event3->setString("title", "Test Event 3 (Chained)"); event3->setString("description", "Third test event triggered by chain"); auto conditions3 = std::make_unique("conditions"); event3->setChild("conditions", std::move(conditions3)); auto choices3 = std::make_unique("choices"); auto choice3a = std::make_unique("choice_end"); choice3a->setString("text", "End Chain"); auto outcomes3a = std::make_unique("outcomes"); auto resources3a = std::make_unique("resources"); resources3a->setInt("supplies", 100); outcomes3a->setChild("resources", std::move(resources3a)); choice3a->setChild("outcomes", std::move(outcomes3a)); choices3->setChild("choice_end", std::move(choice3a)); event3->setChild("choices", std::move(choices3)); events->setChild("test_event_3", std::move(event3)); config->setChild("events", std::move(events)); return config; } /** * Test 1: Event triggers when conditions met */ bool test_event_trigger_conditions() { std::cout << "\n=== Test 1: Event Trigger Conditions ===" << std::endl; // Create module and mock IO auto module = std::make_unique(); auto io = std::make_unique("EventModule"); MockTaskScheduler scheduler; // Configure module auto config = createTestEventConfig(); module->setConfiguration(*config, io.get(), &scheduler); // Subscribe to event:triggered io->subscribe("event:triggered"); // Update game state with time < 100 (condition not met) auto stateUpdate1 = std::make_unique("state_update"); stateUpdate1->setInt("game_time", 50); io->publish("game:state_update", std::move(stateUpdate1)); auto processInput1 = std::make_unique("input"); processInput1->setDouble("deltaTime", 0.016); module->process(*processInput1); // Should NOT trigger event TEST_ASSERT(io->hasMessages() == 0, "Event not triggered when condition not met"); // Update game state with time >= 100 (condition met) auto stateUpdate2 = std::make_unique("state_update"); stateUpdate2->setInt("game_time", 100); io->publish("game:state_update", std::move(stateUpdate2)); auto processInput2 = std::make_unique("input"); processInput2->setDouble("deltaTime", 0.016); module->process(*processInput2); // Should trigger event TEST_ASSERT(io->hasMessages() > 0, "Event triggered when condition met"); auto msg = io->pullMessage(); TEST_ASSERT(msg.topic == "event:triggered", "Correct topic"); TEST_ASSERT(msg.data->getString("event_id", "") == "test_event_1", "Correct event_id"); TEST_ASSERT(msg.data->getString("title", "") == "Test Event 1", "Correct title"); TEST_ASSERT(msg.data->hasChild("choices"), "Choices included"); return true; } /** * Test 2: Choices apply outcomes correctly */ bool test_choice_outcomes() { std::cout << "\n=== Test 2: Choice Outcomes ===" << std::endl; auto module = std::make_unique(); auto io = std::make_unique("EventModule"); MockTaskScheduler scheduler; auto config = createTestEventConfig(); module->setConfiguration(*config, io.get(), &scheduler); io->subscribe("event:triggered"); io->subscribe("event:choice_made"); io->subscribe("event:outcome"); // Trigger event manually auto triggerRequest = std::make_unique("trigger"); triggerRequest->setString("event_id", "test_event_1"); io->publish("event:trigger_manual", std::move(triggerRequest)); auto processInput1 = std::make_unique("input"); processInput1->setDouble("deltaTime", 0.016); module->process(*processInput1); // Consume triggered event TEST_ASSERT(io->hasMessages() > 0, "Event triggered"); io->pullMessage(); // event:triggered // Make choice A (resources outcome) auto choiceRequest = std::make_unique("choice"); choiceRequest->setString("event_id", "test_event_1"); choiceRequest->setString("choice_id", "choice_a"); io->publish("event:make_choice", std::move(choiceRequest)); auto processInput2 = std::make_unique("input"); processInput2->setDouble("deltaTime", 0.016); module->process(*processInput2); // Check choice_made published TEST_ASSERT(io->hasMessages() > 0, "choice_made published"); auto msg1 = io->pullMessage(); TEST_ASSERT(msg1.topic == "event:choice_made", "Correct topic"); // Check outcome published TEST_ASSERT(io->hasMessages() > 0, "outcome published"); auto msg2 = io->pullMessage(); TEST_ASSERT(msg2.topic == "event:outcome", "Correct topic"); TEST_ASSERT(msg2.data->hasChild("resources"), "Resources included"); auto resourcesNode = msg2.data->getChildReadOnly("resources"); TEST_ASSERT(resourcesNode != nullptr, "Resources node exists"); TEST_ASSERT(resourcesNode->getInt("supplies", 0) == 10, "supplies delta correct"); TEST_ASSERT(resourcesNode->getInt("fuel", 0) == -5, "fuel delta correct"); return true; } /** * Test 3: Resource deltas applied correctly */ bool test_resource_deltas() { std::cout << "\n=== Test 3: Resource Deltas ===" << std::endl; auto module = std::make_unique(); auto io = std::make_unique("EventModule"); MockTaskScheduler scheduler; auto config = createTestEventConfig(); module->setConfiguration(*config, io.get(), &scheduler); io->subscribe("event:triggered"); io->subscribe("event:outcome"); // Set game state with resources auto stateUpdate = std::make_unique("state_update"); stateUpdate->setInt("game_time", 200); auto resources = std::make_unique("resources"); resources->setInt("supplies", 100); resources->setInt("fuel", 50); stateUpdate->setChild("resources", std::move(resources)); io->publish("game:state_update", std::move(stateUpdate)); auto processInput1 = std::make_unique("input"); processInput1->setDouble("deltaTime", 0.016); module->process(*processInput1); // Trigger and make choice auto triggerRequest = std::make_unique("trigger"); triggerRequest->setString("event_id", "test_event_1"); io->publish("event:trigger_manual", std::move(triggerRequest)); module->process(*processInput1); io->pullMessage(); // event:triggered auto choiceRequest = std::make_unique("choice"); choiceRequest->setString("event_id", "test_event_1"); choiceRequest->setString("choice_id", "choice_a"); io->publish("event:make_choice", std::move(choiceRequest)); module->process(*processInput1); // Verify outcome contains deltas while (io->hasMessages() > 0) { auto msg = io->pullMessage(); if (msg.topic == "event:outcome") { TEST_ASSERT(msg.data->hasChild("resources"), "Resources in outcome"); auto resourcesNode = msg.data->getChildReadOnly("resources"); TEST_ASSERT(resourcesNode->getInt("supplies", 0) == 10, "Positive delta"); TEST_ASSERT(resourcesNode->getInt("fuel", 0) == -5, "Negative delta"); return true; } } TEST_ASSERT(false, "Outcome message not found"); return false; } /** * Test 4: Flags set correctly */ bool test_flags() { std::cout << "\n=== Test 4: Flags ===" << std::endl; auto module = std::make_unique(); auto io = std::make_unique("EventModule"); MockTaskScheduler scheduler; auto config = createTestEventConfig(); module->setConfiguration(*config, io.get(), &scheduler); io->subscribe("event:triggered"); io->subscribe("event:outcome"); // Trigger event auto triggerRequest = std::make_unique("trigger"); triggerRequest->setString("event_id", "test_event_1"); io->publish("event:trigger_manual", std::move(triggerRequest)); auto processInput = std::make_unique("input"); processInput->setDouble("deltaTime", 0.016); module->process(*processInput); io->pullMessage(); // event:triggered // Make choice B (flags outcome) auto choiceRequest = std::make_unique("choice"); choiceRequest->setString("event_id", "test_event_1"); choiceRequest->setString("choice_id", "choice_b"); io->publish("event:make_choice", std::move(choiceRequest)); module->process(*processInput); // Find outcome message while (io->hasMessages() > 0) { auto msg = io->pullMessage(); if (msg.topic == "event:outcome") { TEST_ASSERT(msg.data->hasChild("flags"), "Flags included"); auto flagsNode = msg.data->getChildReadOnly("flags"); TEST_ASSERT(flagsNode != nullptr, "Flags node exists"); TEST_ASSERT(flagsNode->getString("test_flag", "") == "completed", "Flag value correct"); return true; } } TEST_ASSERT(false, "Outcome message not found"); return false; } /** * Test 5: Chained events (trigger_event outcome) */ bool test_chained_events() { std::cout << "\n=== Test 5: Chained Events ===" << std::endl; auto module = std::make_unique(); auto io = std::make_unique("EventModule"); MockTaskScheduler scheduler; auto config = createTestEventConfig(); module->setConfiguration(*config, io.get(), &scheduler); io->subscribe("event:triggered"); io->subscribe("event:outcome"); // Set game state with enough resources auto stateUpdate = std::make_unique("state_update"); stateUpdate->setInt("game_time", 0); auto resources = std::make_unique("resources"); resources->setInt("supplies", 100); stateUpdate->setChild("resources", std::move(resources)); io->publish("game:state_update", std::move(stateUpdate)); auto processInput = std::make_unique("input"); processInput->setDouble("deltaTime", 0.016); module->process(*processInput); // Trigger event 2 auto triggerRequest = std::make_unique("trigger"); triggerRequest->setString("event_id", "test_event_2"); io->publish("event:trigger_manual", std::move(triggerRequest)); module->process(*processInput); TEST_ASSERT(io->hasMessages() > 0, "Event 2 triggered"); auto msg1 = io->pullMessage(); TEST_ASSERT(msg1.data->getString("event_id", "") == "test_event_2", "Event 2 triggered"); // Make choice that chains to event 3 auto choiceRequest = std::make_unique("choice"); choiceRequest->setString("event_id", "test_event_2"); choiceRequest->setString("choice_id", "choice_chain"); io->publish("event:make_choice", std::move(choiceRequest)); module->process(*processInput); // Should trigger event 3 bool foundChainEvent = false; while (io->hasMessages() > 0) { auto msg = io->pullMessage(); if (msg.topic == "event:triggered" && msg.data->getString("event_id", "") == "test_event_3") { foundChainEvent = true; TEST_ASSERT(msg.data->getString("title", "") == "Test Event 3 (Chained)", "Chained event correct"); } } TEST_ASSERT(foundChainEvent, "Chained event triggered"); return true; } /** * Test 6: Hot-reload state preservation */ bool test_hot_reload_state() { std::cout << "\n=== Test 6: Hot-Reload State Preservation ===" << std::endl; auto module1 = std::make_unique(); auto io = std::make_unique("EventModule"); MockTaskScheduler scheduler; auto config = createTestEventConfig(); module1->setConfiguration(*config, io.get(), &scheduler); // Trigger an event auto triggerRequest = std::make_unique("trigger"); triggerRequest->setString("event_id", "test_event_1"); io->publish("event:trigger_manual", std::move(triggerRequest)); auto processInput = std::make_unique("input"); processInput->setDouble("deltaTime", 0.016); module1->process(*processInput); // Extract state auto state = module1->getState(); TEST_ASSERT(state != nullptr, "State extracted"); // Create new module and restore state auto module2 = std::make_unique(); module2->setConfiguration(*config, io.get(), &scheduler); module2->setState(*state); // Verify state preserved auto health = module2->getHealthStatus(); TEST_ASSERT(health->getBool("has_active_event", false) == true, "Active event preserved"); TEST_ASSERT(health->getString("active_event_id", "") == "test_event_1", "Active event ID preserved"); return true; } /** * Test 7: Multiple active events (should only have one active) */ bool test_multiple_events() { std::cout << "\n=== Test 7: Multiple Events ===" << std::endl; auto module = std::make_unique(); auto io = std::make_unique("EventModule"); MockTaskScheduler scheduler; auto config = createTestEventConfig(); module->setConfiguration(*config, io.get(), &scheduler); io->subscribe("event:triggered"); // Trigger event 1 auto triggerRequest1 = std::make_unique("trigger"); triggerRequest1->setString("event_id", "test_event_1"); io->publish("event:trigger_manual", std::move(triggerRequest1)); auto processInput = std::make_unique("input"); processInput->setDouble("deltaTime", 0.016); module->process(*processInput); TEST_ASSERT(io->hasMessages() == 1, "First event triggered"); io->pullMessage(); // Consume // Try to trigger event 2 while event 1 is active auto triggerRequest2 = std::make_unique("trigger"); triggerRequest2->setString("event_id", "test_event_2"); io->publish("event:trigger_manual", std::move(triggerRequest2)); module->process(*processInput); // Should NOT trigger second event while first is active // (Actually, manual trigger bypasses this check, but auto-trigger should respect it) auto health = module->getHealthStatus(); TEST_ASSERT(health->getBool("has_active_event", false) == true, "Has active event"); return true; } /** * Main test runner */ int main() { std::cout << "==============================================\n"; std::cout << "EventModule Independent Validation Tests\n"; std::cout << "==============================================\n"; bool allPassed = true; allPassed &= test_event_trigger_conditions(); allPassed &= test_choice_outcomes(); allPassed &= test_resource_deltas(); allPassed &= test_flags(); allPassed &= test_chained_events(); allPassed &= test_hot_reload_state(); allPassed &= test_multiple_events(); std::cout << "\n==============================================\n"; if (allPassed) { std::cout << "ALL TESTS PASSED\n"; std::cout << "==============================================\n"; return 0; } else { std::cout << "SOME TESTS FAILED\n"; std::cout << "==============================================\n"; return 1; } }