Core Modules (game-agnostic, reusable for WarFactory): - ResourceModule: Inventory, crafting system (465 lines) - StorageModule: Save/load with pub/sub state collection (424 lines) - CombatModule: Combat resolver, damage/armor/morale (580 lines) - EventModule: JSON event scripting with choices/outcomes (651 lines) MC-Specific Modules: - GameModule v2: State machine + event subscriptions (updated) - TrainBuilderModule: 3 wagons, 2-axis balance, performance malus (530 lines) - ExpeditionModule: A→B expeditions, team management, events integration (641 lines) Features: - All modules hot-reload compatible (state preservation) - Pure pub/sub architecture (zero direct coupling) - 7 config files (resources, storage, combat, events, train, expeditions) - 7 test suites (GameModuleTest: 12/12 PASSED) - CMakeLists.txt updated for all modules + tests Total: ~3,500 lines of production code + comprehensive tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
511 lines
19 KiB
C++
511 lines
19 KiB
C++
/**
|
|
* 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 <grove/JsonDataNode.h>
|
|
#include <grove/IntraIO.h>
|
|
#include <iostream>
|
|
#include <cassert>
|
|
#include <memory>
|
|
|
|
// 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<grove::IDataNode> taskData) override {}
|
|
int hasCompletedTasks() const override { return 0; }
|
|
std::unique_ptr<grove::IDataNode> getCompletedTask() override { return nullptr; }
|
|
};
|
|
|
|
/**
|
|
* Helper: Create minimal event config for testing
|
|
*/
|
|
std::unique_ptr<grove::JsonDataNode> createTestEventConfig() {
|
|
auto config = std::make_unique<grove::JsonDataNode>("config");
|
|
auto events = std::make_unique<grove::JsonDataNode>("events");
|
|
|
|
// Event 1: Simple event with time condition
|
|
auto event1 = std::make_unique<grove::JsonDataNode>("test_event_1");
|
|
event1->setString("title", "Test Event 1");
|
|
event1->setString("description", "First test event");
|
|
event1->setInt("cooldown", 60);
|
|
|
|
auto conditions1 = std::make_unique<grove::JsonDataNode>("conditions");
|
|
conditions1->setInt("game_time_min", 100);
|
|
event1->setChild("conditions", std::move(conditions1));
|
|
|
|
auto choices1 = std::make_unique<grove::JsonDataNode>("choices");
|
|
|
|
auto choice1a = std::make_unique<grove::JsonDataNode>("choice_a");
|
|
choice1a->setString("text", "Choice A");
|
|
auto outcomes1a = std::make_unique<grove::JsonDataNode>("outcomes");
|
|
auto resources1a = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("choice_b");
|
|
choice1b->setString("text", "Choice B");
|
|
auto outcomes1b = std::make_unique<grove::JsonDataNode>("outcomes");
|
|
auto flags1b = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("test_event_2");
|
|
event2->setString("title", "Test Event 2");
|
|
event2->setString("description", "Second test event");
|
|
|
|
auto conditions2 = std::make_unique<grove::JsonDataNode>("conditions");
|
|
auto resourceMin = std::make_unique<grove::JsonDataNode>("resource_min");
|
|
resourceMin->setInt("supplies", 50);
|
|
conditions2->setChild("resource_min", std::move(resourceMin));
|
|
event2->setChild("conditions", std::move(conditions2));
|
|
|
|
auto choices2 = std::make_unique<grove::JsonDataNode>("choices");
|
|
|
|
auto choice2a = std::make_unique<grove::JsonDataNode>("choice_chain");
|
|
choice2a->setString("text", "Chain to Event 3");
|
|
auto outcomes2a = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("test_event_3");
|
|
event3->setString("title", "Test Event 3 (Chained)");
|
|
event3->setString("description", "Third test event triggered by chain");
|
|
|
|
auto conditions3 = std::make_unique<grove::JsonDataNode>("conditions");
|
|
event3->setChild("conditions", std::move(conditions3));
|
|
|
|
auto choices3 = std::make_unique<grove::JsonDataNode>("choices");
|
|
|
|
auto choice3a = std::make_unique<grove::JsonDataNode>("choice_end");
|
|
choice3a->setString("text", "End Chain");
|
|
auto outcomes3a = std::make_unique<grove::JsonDataNode>("outcomes");
|
|
auto resources3a = std::make_unique<grove::JsonDataNode>("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<EventModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("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<grove::JsonDataNode>("state_update");
|
|
stateUpdate1->setInt("game_time", 50);
|
|
io->publish("game:state_update", std::move(stateUpdate1));
|
|
|
|
auto processInput1 = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("state_update");
|
|
stateUpdate2->setInt("game_time", 100);
|
|
io->publish("game:state_update", std::move(stateUpdate2));
|
|
|
|
auto processInput2 = std::make_unique<grove::JsonDataNode>("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<EventModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("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<grove::JsonDataNode>("trigger");
|
|
triggerRequest->setString("event_id", "test_event_1");
|
|
io->publish("event:trigger_manual", std::move(triggerRequest));
|
|
|
|
auto processInput1 = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("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<grove::JsonDataNode>("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<EventModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("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<grove::JsonDataNode>("state_update");
|
|
stateUpdate->setInt("game_time", 200);
|
|
auto resources = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("input");
|
|
processInput1->setDouble("deltaTime", 0.016);
|
|
module->process(*processInput1);
|
|
|
|
// Trigger and make choice
|
|
auto triggerRequest = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("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<EventModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("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<grove::JsonDataNode>("trigger");
|
|
triggerRequest->setString("event_id", "test_event_1");
|
|
io->publish("event:trigger_manual", std::move(triggerRequest));
|
|
|
|
auto processInput = std::make_unique<grove::JsonDataNode>("input");
|
|
processInput->setDouble("deltaTime", 0.016);
|
|
module->process(*processInput);
|
|
io->pullMessage(); // event:triggered
|
|
|
|
// Make choice B (flags outcome)
|
|
auto choiceRequest = std::make_unique<grove::JsonDataNode>("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<EventModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("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<grove::JsonDataNode>("state_update");
|
|
stateUpdate->setInt("game_time", 0);
|
|
auto resources = std::make_unique<grove::JsonDataNode>("resources");
|
|
resources->setInt("supplies", 100);
|
|
stateUpdate->setChild("resources", std::move(resources));
|
|
io->publish("game:state_update", std::move(stateUpdate));
|
|
|
|
auto processInput = std::make_unique<grove::JsonDataNode>("input");
|
|
processInput->setDouble("deltaTime", 0.016);
|
|
module->process(*processInput);
|
|
|
|
// Trigger event 2
|
|
auto triggerRequest = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("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<EventModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("EventModule");
|
|
MockTaskScheduler scheduler;
|
|
|
|
auto config = createTestEventConfig();
|
|
module1->setConfiguration(*config, io.get(), &scheduler);
|
|
|
|
// Trigger an event
|
|
auto triggerRequest = std::make_unique<grove::JsonDataNode>("trigger");
|
|
triggerRequest->setString("event_id", "test_event_1");
|
|
io->publish("event:trigger_manual", std::move(triggerRequest));
|
|
|
|
auto processInput = std::make_unique<grove::JsonDataNode>("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<EventModule>();
|
|
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<EventModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("EventModule");
|
|
MockTaskScheduler scheduler;
|
|
|
|
auto config = createTestEventConfig();
|
|
module->setConfiguration(*config, io.get(), &scheduler);
|
|
|
|
io->subscribe("event:triggered");
|
|
|
|
// Trigger event 1
|
|
auto triggerRequest1 = std::make_unique<grove::JsonDataNode>("trigger");
|
|
triggerRequest1->setString("event_id", "test_event_1");
|
|
io->publish("event:trigger_manual", std::move(triggerRequest1));
|
|
|
|
auto processInput = std::make_unique<grove::JsonDataNode>("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<grove::JsonDataNode>("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;
|
|
}
|
|
}
|