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>
349 lines
12 KiB
C++
349 lines
12 KiB
C++
/**
|
|
* ResourceModuleTest - Independent validation tests
|
|
*
|
|
* Tests the ResourceModule in isolation without requiring the full game.
|
|
* Validates core functionality: inventory, crafting, state preservation.
|
|
*/
|
|
|
|
#include "../src/modules/core/ResourceModule.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 ResourceModule 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; }
|
|
};
|
|
|
|
/**
|
|
* Test 1: Add and remove resources
|
|
*/
|
|
bool test_add_remove_resources() {
|
|
std::cout << "\n=== Test 1: Add/Remove Resources ===" << std::endl;
|
|
|
|
// Create module and mock IO
|
|
auto module = std::make_unique<ResourceModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("ResourceModule");
|
|
MockTaskScheduler scheduler;
|
|
|
|
// Create minimal config
|
|
auto config = std::make_unique<grove::JsonDataNode>("config");
|
|
auto resources = std::make_unique<grove::JsonDataNode>("resources");
|
|
|
|
auto scrapMetal = std::make_unique<grove::JsonDataNode>("scrap_metal");
|
|
scrapMetal->setInt("maxStack", 100);
|
|
scrapMetal->setDouble("weight", 1.5);
|
|
scrapMetal->setInt("baseValue", 10);
|
|
scrapMetal->setInt("lowThreshold", 15);
|
|
resources->setChild("scrap_metal", std::move(scrapMetal));
|
|
|
|
config->setChild("resources", std::move(resources));
|
|
|
|
// Initialize module
|
|
module->setConfiguration(*config, io.get(), &scheduler);
|
|
|
|
// Subscribe to inventory_changed events
|
|
io->subscribe("resource:inventory_changed");
|
|
|
|
// Test adding resource
|
|
auto addRequest = std::make_unique<grove::JsonDataNode>("add_request");
|
|
addRequest->setString("resource_id", "scrap_metal");
|
|
addRequest->setInt("quantity", 50);
|
|
io->publish("resource:add_request", std::move(addRequest));
|
|
|
|
// Process
|
|
auto processInput = std::make_unique<grove::JsonDataNode>("input");
|
|
processInput->setDouble("deltaTime", 0.016);
|
|
module->process(*processInput);
|
|
|
|
// Check event was published
|
|
TEST_ASSERT(io->hasMessages() > 0, "inventory_changed event published");
|
|
auto msg = io->pullMessage();
|
|
TEST_ASSERT(msg.topic == "resource:inventory_changed", "Correct topic");
|
|
TEST_ASSERT(msg.data->getString("resource_id", "") == "scrap_metal", "Correct resource");
|
|
TEST_ASSERT(msg.data->getInt("delta", 0) == 50, "Delta is 50");
|
|
TEST_ASSERT(msg.data->getInt("total", 0) == 50, "Total is 50");
|
|
|
|
// Test removing resource
|
|
auto removeRequest = std::make_unique<grove::JsonDataNode>("remove_request");
|
|
removeRequest->setString("resource_id", "scrap_metal");
|
|
removeRequest->setInt("quantity", 20);
|
|
io->publish("resource:remove_request", std::move(removeRequest));
|
|
|
|
module->process(*processInput);
|
|
|
|
TEST_ASSERT(io->hasMessages() > 0, "inventory_changed event published");
|
|
msg = io->pullMessage();
|
|
TEST_ASSERT(msg.data->getInt("delta", 0) == -20, "Delta is -20");
|
|
TEST_ASSERT(msg.data->getInt("total", 0) == 30, "Total is 30");
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Test 2: Crafting system (1 input -> 1 output)
|
|
*/
|
|
bool test_crafting() {
|
|
std::cout << "\n=== Test 2: Crafting System ===" << std::endl;
|
|
|
|
auto module = std::make_unique<ResourceModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("ResourceModule");
|
|
MockTaskScheduler scheduler;
|
|
|
|
// Create config with resources and recipe
|
|
auto config = std::make_unique<grove::JsonDataNode>("config");
|
|
|
|
// Resources
|
|
auto resources = std::make_unique<grove::JsonDataNode>("resources");
|
|
auto scrapMetal = std::make_unique<grove::JsonDataNode>("scrap_metal");
|
|
scrapMetal->setInt("maxStack", 100);
|
|
scrapMetal->setDouble("weight", 1.5);
|
|
scrapMetal->setInt("baseValue", 10);
|
|
resources->setChild("scrap_metal", std::move(scrapMetal));
|
|
|
|
auto repairKit = std::make_unique<grove::JsonDataNode>("repair_kit");
|
|
repairKit->setInt("maxStack", 20);
|
|
repairKit->setDouble("weight", 2.0);
|
|
repairKit->setInt("baseValue", 50);
|
|
resources->setChild("repair_kit", std::move(repairKit));
|
|
|
|
config->setChild("resources", std::move(resources));
|
|
|
|
// Recipes
|
|
auto recipes = std::make_unique<grove::JsonDataNode>("recipes");
|
|
auto repairKitRecipe = std::make_unique<grove::JsonDataNode>("repair_kit_basic");
|
|
repairKitRecipe->setDouble("craftTime", 1.0); // 1 second for test
|
|
|
|
auto inputs = std::make_unique<grove::JsonDataNode>("inputs");
|
|
inputs->setInt("scrap_metal", 5);
|
|
repairKitRecipe->setChild("inputs", std::move(inputs));
|
|
|
|
auto outputs = std::make_unique<grove::JsonDataNode>("outputs");
|
|
outputs->setInt("repair_kit", 1);
|
|
repairKitRecipe->setChild("outputs", std::move(outputs));
|
|
|
|
recipes->setChild("repair_kit_basic", std::move(repairKitRecipe));
|
|
config->setChild("recipes", std::move(recipes));
|
|
|
|
// Initialize
|
|
module->setConfiguration(*config, io.get(), &scheduler);
|
|
io->subscribe("resource:*");
|
|
|
|
// Add scrap metal
|
|
auto addRequest = std::make_unique<grove::JsonDataNode>("add_request");
|
|
addRequest->setString("resource_id", "scrap_metal");
|
|
addRequest->setInt("quantity", 10);
|
|
io->publish("resource:add_request", std::move(addRequest));
|
|
|
|
auto processInput = std::make_unique<grove::JsonDataNode>("input");
|
|
processInput->setDouble("deltaTime", 0.016);
|
|
module->process(*processInput);
|
|
|
|
// Clear messages
|
|
while (io->hasMessages() > 0) { io->pullMessage(); }
|
|
|
|
// Start craft
|
|
auto craftRequest = std::make_unique<grove::JsonDataNode>("craft_request");
|
|
craftRequest->setString("recipe_id", "repair_kit_basic");
|
|
io->publish("resource:craft_request", std::move(craftRequest));
|
|
|
|
module->process(*processInput);
|
|
|
|
// Check craft_started event
|
|
bool craftStartedFound = false;
|
|
while (io->hasMessages() > 0) {
|
|
auto msg = io->pullMessage();
|
|
if (msg.topic == "resource:craft_started") {
|
|
craftStartedFound = true;
|
|
TEST_ASSERT(msg.data->getString("recipe_id", "") == "repair_kit_basic", "Correct recipe");
|
|
}
|
|
}
|
|
TEST_ASSERT(craftStartedFound, "craft_started event published");
|
|
|
|
// Simulate crafting time (1 second = 1000ms / 16ms = ~63 frames)
|
|
for (int i = 0; i < 70; ++i) {
|
|
module->process(*processInput);
|
|
}
|
|
|
|
// Check craft_complete event
|
|
bool craftCompleteFound = false;
|
|
while (io->hasMessages() > 0) {
|
|
auto msg = io->pullMessage();
|
|
if (msg.topic == "resource:craft_complete") {
|
|
craftCompleteFound = true;
|
|
TEST_ASSERT(msg.data->getString("recipe_id", "") == "repair_kit_basic", "Correct recipe");
|
|
TEST_ASSERT(msg.data->getInt("repair_kit", 0) == 1, "Output produced");
|
|
}
|
|
}
|
|
TEST_ASSERT(craftCompleteFound, "craft_complete event published");
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Test 3: Hot-reload state preservation
|
|
*/
|
|
bool test_state_preservation() {
|
|
std::cout << "\n=== Test 3: State Preservation (Hot-Reload) ===" << std::endl;
|
|
|
|
auto module1 = std::make_unique<ResourceModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("ResourceModule");
|
|
MockTaskScheduler scheduler;
|
|
|
|
// Create minimal config
|
|
auto config = std::make_unique<grove::JsonDataNode>("config");
|
|
auto resources = std::make_unique<grove::JsonDataNode>("resources");
|
|
|
|
auto scrapMetal = std::make_unique<grove::JsonDataNode>("scrap_metal");
|
|
scrapMetal->setInt("maxStack", 100);
|
|
scrapMetal->setDouble("weight", 1.5);
|
|
scrapMetal->setInt("baseValue", 10);
|
|
resources->setChild("scrap_metal", std::move(scrapMetal));
|
|
|
|
config->setChild("resources", std::move(resources));
|
|
|
|
// Initialize first module
|
|
module1->setConfiguration(*config, io.get(), &scheduler);
|
|
|
|
// Add some inventory
|
|
auto addRequest = std::make_unique<grove::JsonDataNode>("add_request");
|
|
addRequest->setString("resource_id", "scrap_metal");
|
|
addRequest->setInt("quantity", 42);
|
|
io->publish("resource:add_request", std::move(addRequest));
|
|
|
|
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 successfully");
|
|
|
|
// Verify state content
|
|
TEST_ASSERT(state->hasChild("inventory"), "State has inventory");
|
|
auto inventoryNode = state->getChildReadOnly("inventory");
|
|
TEST_ASSERT(inventoryNode != nullptr, "Inventory node exists");
|
|
TEST_ASSERT(inventoryNode->getInt("scrap_metal", 0) == 42, "Inventory value preserved");
|
|
|
|
// Create new module (simulating hot-reload)
|
|
auto module2 = std::make_unique<ResourceModule>();
|
|
module2->setConfiguration(*config, io.get(), &scheduler);
|
|
|
|
// Restore state
|
|
module2->setState(*state);
|
|
|
|
// Query inventory to verify
|
|
auto queryRequest = std::make_unique<grove::JsonDataNode>("query_request");
|
|
io->publish("resource:query_inventory", std::move(queryRequest));
|
|
|
|
module2->process(*processInput);
|
|
|
|
// Check inventory report
|
|
bool inventoryReportFound = false;
|
|
while (io->hasMessages() > 0) {
|
|
auto msg = io->pullMessage();
|
|
if (msg.topic == "resource:inventory_report") {
|
|
inventoryReportFound = true;
|
|
TEST_ASSERT(msg.data->getInt("scrap_metal", 0) == 42, "Inventory restored correctly");
|
|
}
|
|
}
|
|
TEST_ASSERT(inventoryReportFound, "inventory_report event published");
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Test 4: Config loading validation
|
|
*/
|
|
bool test_config_loading() {
|
|
std::cout << "\n=== Test 4: Config Loading ===" << std::endl;
|
|
|
|
auto module = std::make_unique<ResourceModule>();
|
|
auto io = std::make_unique<grove::IntraIO>("ResourceModule");
|
|
MockTaskScheduler scheduler;
|
|
|
|
// Create full config
|
|
auto config = std::make_unique<grove::JsonDataNode>("config");
|
|
|
|
// Multiple resources
|
|
auto resources = std::make_unique<grove::JsonDataNode>("resources");
|
|
|
|
auto res1 = std::make_unique<grove::JsonDataNode>("resource_a");
|
|
res1->setInt("maxStack", 50);
|
|
res1->setDouble("weight", 1.0);
|
|
res1->setInt("baseValue", 5);
|
|
resources->setChild("resource_a", std::move(res1));
|
|
|
|
auto res2 = std::make_unique<grove::JsonDataNode>("resource_b");
|
|
res2->setInt("maxStack", 100);
|
|
res2->setDouble("weight", 2.0);
|
|
res2->setInt("baseValue", 10);
|
|
resources->setChild("resource_b", std::move(res2));
|
|
|
|
config->setChild("resources", std::move(resources));
|
|
|
|
// Multiple recipes
|
|
auto recipes = std::make_unique<grove::JsonDataNode>("recipes");
|
|
|
|
auto recipe1 = std::make_unique<grove::JsonDataNode>("craft_a_to_b");
|
|
recipe1->setDouble("craftTime", 5.0);
|
|
|
|
auto inputs1 = std::make_unique<grove::JsonDataNode>("inputs");
|
|
inputs1->setInt("resource_a", 2);
|
|
recipe1->setChild("inputs", std::move(inputs1));
|
|
|
|
auto outputs1 = std::make_unique<grove::JsonDataNode>("outputs");
|
|
outputs1->setInt("resource_b", 1);
|
|
recipe1->setChild("outputs", std::move(outputs1));
|
|
|
|
recipes->setChild("craft_a_to_b", std::move(recipe1));
|
|
config->setChild("recipes", std::move(recipes));
|
|
|
|
// Initialize
|
|
module->setConfiguration(*config, io.get(), &scheduler);
|
|
|
|
// Check health status reflects loaded config
|
|
auto health = module->getHealthStatus();
|
|
TEST_ASSERT(health != nullptr, "Health status available");
|
|
TEST_ASSERT(health->getString("status", "") == "healthy", "Module healthy");
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Main test runner
|
|
*/
|
|
int main() {
|
|
std::cout << "========================================" << std::endl;
|
|
std::cout << "ResourceModule Independent Tests" << std::endl;
|
|
std::cout << "========================================" << std::endl;
|
|
|
|
int passed = 0;
|
|
int total = 4;
|
|
|
|
if (test_add_remove_resources()) passed++;
|
|
if (test_crafting()) passed++;
|
|
if (test_state_preservation()) passed++;
|
|
if (test_config_loading()) passed++;
|
|
|
|
std::cout << "\n========================================" << std::endl;
|
|
std::cout << "Results: " << passed << "/" << total << " tests passed" << std::endl;
|
|
std::cout << "========================================" << std::endl;
|
|
|
|
return (passed == total) ? 0 : 1;
|
|
}
|