project-mobile-command/tests/ResourceModuleTest.cpp
StillHammer 0953451fea Implement 7 modules: 4 core (game-agnostic) + 3 MC-specific
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>
2025-12-02 16:40:54 +08:00

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;
}