#include #include "../src/modules/mc_specific/TrainBuilderModule.h" #include #include #include #include /** * TrainBuilderModule Tests * * These tests validate the TrainBuilderModule implementation independently * without requiring the full game engine to be running. * * Test Coverage: * 1. Configuration loading (3 wagons) * 2. Balance calculation (lateral + longitudinal) * 3. Performance malus (speed + fuel) * 4. Cargo weight updates balance * 5. Hot-reload state preservation * 6. Damage updates wagon health */ class TrainBuilderModuleTest : public ::testing::Test { protected: void SetUp() override { // Create test configuration with 3 wagons config = std::make_unique("config"); // Wagons node auto wagonsNode = std::make_unique("wagons"); // Locomotive (centered, front) auto locoNode = std::make_unique("locomotive"); locoNode->setString("type", "locomotive"); locoNode->setDouble("health", 100.0); locoNode->setDouble("armor", 50.0); locoNode->setDouble("weight", 20000.0); locoNode->setDouble("capacity", 0.0); auto locoPos = std::make_unique("position"); locoPos->setDouble("x", 0.0); locoPos->setDouble("y", 0.0); locoPos->setDouble("z", 0.0); locoNode->setChild("position", std::move(locoPos)); wagonsNode->setChild("locomotive", std::move(locoNode)); // Cargo wagon (left side) auto cargoNode = std::make_unique("cargo_1"); cargoNode->setString("type", "cargo"); cargoNode->setDouble("health", 80.0); cargoNode->setDouble("armor", 30.0); cargoNode->setDouble("weight", 5000.0); cargoNode->setDouble("capacity", 10000.0); auto cargoPos = std::make_unique("position"); cargoPos->setDouble("x", -5.0); cargoPos->setDouble("y", 0.0); cargoPos->setDouble("z", 0.0); cargoNode->setChild("position", std::move(cargoPos)); wagonsNode->setChild("cargo_1", std::move(cargoNode)); // Workshop wagon (right side) auto workshopNode = std::make_unique("workshop_1"); workshopNode->setString("type", "workshop"); workshopNode->setDouble("health", 70.0); workshopNode->setDouble("armor", 20.0); workshopNode->setDouble("weight", 8000.0); workshopNode->setDouble("capacity", 5000.0); auto workshopPos = std::make_unique("position"); workshopPos->setDouble("x", 5.0); workshopPos->setDouble("y", 0.0); workshopPos->setDouble("z", 0.0); workshopNode->setChild("position", std::move(workshopPos)); wagonsNode->setChild("workshop_1", std::move(workshopNode)); config->setChild("wagons", std::move(wagonsNode)); // Balance thresholds auto thresholdsNode = std::make_unique("balanceThresholds"); thresholdsNode->setDouble("lateral_warning", 0.2); thresholdsNode->setDouble("longitudinal_warning", 0.3); config->setChild("balanceThresholds", std::move(thresholdsNode)); // Create IIO instance io = grove::createIntraIOInstance("test_module"); // Create module module = std::make_unique(); } void TearDown() override { module->shutdown(); module.reset(); io.reset(); config.reset(); } std::unique_ptr config; std::shared_ptr io; std::unique_ptr module; }; // Test 1: 3 wagons load correctly TEST_F(TrainBuilderModuleTest, LoadsThreeWagonsFromConfig) { module->setConfiguration(*config, io.get(), nullptr); const auto& wagons = module->getWagons(); ASSERT_EQ(wagons.size(), 3) << "Should load 3 wagons from config"; // Verify locomotive auto* loco = module->getWagon("locomotive"); ASSERT_NE(loco, nullptr); EXPECT_EQ(loco->type, "locomotive"); EXPECT_FLOAT_EQ(loco->weight, 20000.0f); EXPECT_FLOAT_EQ(loco->health, 100.0f); EXPECT_FLOAT_EQ(loco->armor, 50.0f); // Verify cargo auto* cargo = module->getWagon("cargo_1"); ASSERT_NE(cargo, nullptr); EXPECT_EQ(cargo->type, "cargo"); EXPECT_FLOAT_EQ(cargo->weight, 5000.0f); EXPECT_FLOAT_EQ(cargo->capacity, 10000.0f); // Verify workshop auto* workshop = module->getWagon("workshop_1"); ASSERT_NE(workshop, nullptr); EXPECT_EQ(workshop->type, "workshop"); EXPECT_FLOAT_EQ(workshop->weight, 8000.0f); EXPECT_FLOAT_EQ(workshop->capacity, 5000.0f); } // Test 2: Balance calculation correct (lateral + longitudinal) TEST_F(TrainBuilderModuleTest, CalculatesBalanceCorrectly) { module->setConfiguration(*config, io.get(), nullptr); auto balance = module->getBalance(); // Total weight: 20000 (loco) + 5000 (cargo) + 8000 (workshop) = 33000 // Left weight: 5000 (cargo at x=-5) // Right weight: 20000 (loco at x=0) + 8000 (workshop at x=5) = 28000 // Lateral offset: (28000 - 5000) / 33000 = 23000 / 33000 ≈ 0.697 (right-heavy) // All wagons at z=0, so longitudinal should be 0 EXPECT_NEAR(balance.longitudinalOffset, 0.0f, 0.01f) << "Longitudinal should be balanced"; EXPECT_GT(balance.lateralOffset, 0.5f) << "Should be right-heavy"; EXPECT_LT(balance.lateralOffset, 1.0f) << "Lateral offset should be < 1.0"; // Balance score should be non-zero due to lateral imbalance EXPECT_GT(balance.balanceScore, 0.0f) << "Balance score should indicate imbalance"; } // Test 3: Malus applied correctly TEST_F(TrainBuilderModuleTest, AppliesPerformanceMalus) { module->setConfiguration(*config, io.get(), nullptr); auto balance = module->getBalance(); // With imbalance, speed should be reduced EXPECT_LT(balance.speedMalus, 1.0f) << "Speed should be reduced due to imbalance"; EXPECT_GT(balance.speedMalus, 0.5f) << "Speed malus should not be too extreme"; // Fuel consumption should be increased EXPECT_GT(balance.fuelMalus, 1.0f) << "Fuel consumption should increase with imbalance"; EXPECT_LT(balance.fuelMalus, 1.5f) << "Fuel malus should not be too extreme"; // Relationship: higher balance score = lower speed, higher fuel float expectedSpeedMalus = 1.0f - (balance.balanceScore * 0.5f); float expectedFuelMalus = 1.0f + (balance.balanceScore * 0.5f); EXPECT_NEAR(balance.speedMalus, expectedSpeedMalus, 0.01f) << "Speed malus formula incorrect"; EXPECT_NEAR(balance.fuelMalus, expectedFuelMalus, 0.01f) << "Fuel malus formula incorrect"; } // Test 4: Cargo weight updates balance TEST_F(TrainBuilderModuleTest, CargoWeightUpdatesBalance) { module->setConfiguration(*config, io.get(), nullptr); // Get initial balance auto initialBalance = module->getBalance(); // Simulate cargo being added (resource:inventory_changed) auto inventoryMsg = std::make_unique("inventory_changed"); inventoryMsg->setString("resource_id", "scrap_metal"); inventoryMsg->setInt("total", 5000); // 5000 units = 5000kg at 1kg/unit inventoryMsg->setInt("delta", 5000); io->publish("resource:inventory_changed", std::move(inventoryMsg)); // Process the message auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // Get new balance auto newBalance = module->getBalance(); // Balance should have changed due to cargo weight EXPECT_NE(initialBalance.balanceScore, newBalance.balanceScore) << "Balance should change when cargo is added"; // Cargo wagons should have weight auto* cargo = module->getWagon("cargo_1"); ASSERT_NE(cargo, nullptr); EXPECT_GT(cargo->cargoWeight, 0.0f) << "Cargo wagon should have cargo weight"; } // Test 5: Hot-reload state preservation TEST_F(TrainBuilderModuleTest, PreservesStateOnHotReload) { module->setConfiguration(*config, io.get(), nullptr); // Simulate some cargo auto inventoryMsg = std::make_unique("inventory_changed"); inventoryMsg->setString("resource_id", "scrap_metal"); inventoryMsg->setInt("total", 3000); inventoryMsg->setInt("delta", 3000); io->publish("resource:inventory_changed", std::move(inventoryMsg)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // Get state before reload auto state = module->getState(); auto balanceBefore = module->getBalance(); size_t wagonCountBefore = module->getWagons().size(); // Create new module instance (simulating hot-reload) auto newModule = std::make_unique(); newModule->setConfiguration(*config, io.get(), nullptr); newModule->setState(*state); // Verify state restored EXPECT_EQ(newModule->getWagons().size(), wagonCountBefore) << "Wagon count should be preserved"; auto balanceAfter = newModule->getBalance(); EXPECT_NEAR(balanceAfter.balanceScore, balanceBefore.balanceScore, 0.001f) << "Balance score should be preserved"; EXPECT_NEAR(balanceAfter.lateralOffset, balanceBefore.lateralOffset, 0.001f) << "Lateral offset should be preserved"; EXPECT_NEAR(balanceAfter.speedMalus, balanceBefore.speedMalus, 0.001f) << "Speed malus should be preserved"; newModule->shutdown(); } // Test 6: Damage updates wagon health TEST_F(TrainBuilderModuleTest, DamageUpdatesWagonHealth) { module->setConfiguration(*config, io.get(), nullptr); auto* cargo = module->getWagon("cargo_1"); ASSERT_NE(cargo, nullptr); float initialHealth = cargo->health; float armor = cargo->armor; // Simulate damage to cargo wagon auto damageMsg = std::make_unique("damage_received"); damageMsg->setString("target", "cargo_1"); damageMsg->setDouble("damage", 50.0); // 50 damage io->publish("combat:damage_received", std::move(damageMsg)); // Process the message auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // Verify health reduced // Effective damage = 50 - (30 armor * 0.5) = 50 - 15 = 35 float expectedHealth = initialHealth - 35.0f; EXPECT_NEAR(cargo->health, expectedHealth, 0.1f) << "Wagon health should be reduced by effective damage"; } // Test 7: Add/Remove wagon operations TEST_F(TrainBuilderModuleTest, AddAndRemoveWagons) { module->setConfiguration(*config, io.get(), nullptr); ASSERT_EQ(module->getWagons().size(), 3); // Add a new wagon mc::Wagon newWagon; newWagon.id = "cargo_2"; newWagon.type = "cargo"; newWagon.health = 80.0f; newWagon.maxHealth = 80.0f; newWagon.armor = 30.0f; newWagon.weight = 5000.0f; newWagon.capacity = 10000.0f; newWagon.cargoWeight = 0.0f; newWagon.totalWeight = 5000.0f; newWagon.position.x = -10.0f; newWagon.position.y = 0.0f; newWagon.position.z = 0.0f; EXPECT_TRUE(module->addWagon(newWagon)); EXPECT_EQ(module->getWagons().size(), 4); // Verify wagon added auto* addedWagon = module->getWagon("cargo_2"); ASSERT_NE(addedWagon, nullptr); EXPECT_EQ(addedWagon->type, "cargo"); // Remove wagon EXPECT_TRUE(module->removeWagon("cargo_2")); EXPECT_EQ(module->getWagons().size(), 3); // Verify wagon removed auto* removedWagon = module->getWagon("cargo_2"); EXPECT_EQ(removedWagon, nullptr); } // Test 8: Capacity tracking TEST_F(TrainBuilderModuleTest, TracksCargoCapacity) { module->setConfiguration(*config, io.get(), nullptr); // Total capacity: cargo (10000) + workshop (5000) = 15000 float totalCapacity = module->getTotalCargoCapacity(); EXPECT_FLOAT_EQ(totalCapacity, 15000.0f); // Initially no cargo float usedCapacity = module->getTotalCargoUsed(); EXPECT_FLOAT_EQ(usedCapacity, 0.0f); // Add cargo auto inventoryMsg = std::make_unique("inventory_changed"); inventoryMsg->setString("resource_id", "scrap_metal"); inventoryMsg->setInt("total", 2000); inventoryMsg->setInt("delta", 2000); io->publish("resource:inventory_changed", std::move(inventoryMsg)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // Used capacity should increase usedCapacity = module->getTotalCargoUsed(); EXPECT_GT(usedCapacity, 0.0f); EXPECT_LE(usedCapacity, totalCapacity); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }