derive threat cost dynamically
This commit is contained in:
@@ -19,5 +19,6 @@ add_files(
|
||||
BlueprintSerializerTest.cpp
|
||||
ModuleConfigTest.cpp
|
||||
ShipModuleTest.cpp
|
||||
ThreatCostCalculatorTest.cpp
|
||||
RecipeSchematicTest.cpp
|
||||
)
|
||||
|
||||
@@ -40,7 +40,6 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||
CHECK(armor.materials[0].amount == 2);
|
||||
CHECK(armor.playerProductionLevel == 1);
|
||||
CHECK(armor.productionTimeSeconds == Approx(3.0));
|
||||
CHECK(armor.threatCost == Approx(2.0));
|
||||
CHECK(armor.fillColor == "#808080");
|
||||
CHECK(armor.glyph == "A");
|
||||
REQUIRE(armor.statModifiers.size() == 1);
|
||||
|
||||
110
src/test/ThreatCostCalculatorTest.cpp
Normal file
110
src/test/ThreatCostCalculatorTest.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "ThreatCostCalculator.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("iron_ore") == Approx(1.0));
|
||||
CHECK(table.itemThreat.at("copper_ore") == Approx(1.5));
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// iron_ingot: duration 2.0 + iron_ore(1.0) * 2 = 4.0
|
||||
CHECK(table.itemThreat.at("iron_ingot") == Approx(4.0));
|
||||
// copper_ingot: duration 2.5 + copper_ore(1.5) * 2 = 5.5
|
||||
CHECK(table.itemThreat.at("copper_ingot") == Approx(5.5));
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// circuit_board has three non-reprocessing recipes:
|
||||
// 5.0 + iron_ingot(4.0)*3 + copper_ingot(5.5)*2 = 28.0
|
||||
// 3.0 + copper_ingot(5.5)*3 = 19.5
|
||||
// 6.0 + iron_ingot(4.0)*5 = 26.0
|
||||
// max = 28.0
|
||||
CHECK(table.itemThreat.at("circuit_board") == Approx(28.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: scrap threat from cheapest ship", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// Cheapest ship by scrap_drop is interceptor (scrap_drop=2).
|
||||
// Interceptor threat: 10 + iron_ingot(4)*3 + circuit_board(28)*1
|
||||
// + laser_cannon(5 + iron_ingot(4)*1) = 10 + 12 + 28 + 9 = 59.0
|
||||
// scrapThreat = 59.0 / 2 = 29.5
|
||||
CHECK(table.scrapThreat == Approx(29.5));
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// advanced_alloy: reprocessing recipe with scrap*5, duration 3.0, probability 0.1
|
||||
// (29.5 * 5 + 3.0) / 0.1 = 1505.0
|
||||
CHECK(table.itemThreat.at("advanced_alloy") == Approx(1505.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// interceptor: 10 + iron_ingot(4)*3 + circuit_board(28)*1 + laser_cannon(5 + 4*1) = 59.0
|
||||
double interceptorThreat = calculateShipThreatCost(
|
||||
table, cfg, "interceptor", cfg.ships.ships[0].defaultModules);
|
||||
CHECK(interceptorThreat == Approx(59.0));
|
||||
|
||||
// salvage_ship (no default modules): 10 + iron_ingot(4)*4 = 26.0
|
||||
double salvageThreat = calculateShipThreatCost(
|
||||
table, cfg, "salvage_ship", cfg.ships.ships[2].defaultModules);
|
||||
CHECK(salvageThreat == Approx(26.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// interceptor base: 10 + iron_ingot(4)*3 + circuit_board(28)*1 = 50.0
|
||||
// + armor_plate: 3 + iron_ingot(4)*2 = 11.0
|
||||
// + sensor_booster: 2 + circuit_board(28)*1 = 30.0
|
||||
// total = 50.0 + 11.0 + 30.0 = 91.0
|
||||
std::vector<PlacedModule> modules;
|
||||
PlacedModule armor;
|
||||
armor.moduleId = "armor_plate";
|
||||
modules.push_back(armor);
|
||||
PlacedModule sensor;
|
||||
sensor.moduleId = "sensor_booster";
|
||||
modules.push_back(sensor);
|
||||
|
||||
double threat = calculateShipThreatCost(table, cfg, "interceptor", modules);
|
||||
CHECK(threat == Approx(91.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
double threat = calculateShipThreatCost(table, cfg, "nonexistent_ship", {});
|
||||
CHECK(threat == Approx(0.0));
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "ShipsConfig.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "ThreatCostCalculator.h"
|
||||
#include "WaveSystem.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
@@ -201,25 +202,16 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
|
||||
REQUIRE(foundEnemyShip);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]")
|
||||
TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
const GameConfig cfg = loadConfig();
|
||||
|
||||
// Run long enough for several waves.
|
||||
const int limit = static_cast<int>(secondsToTicks(120.0));
|
||||
for (int i = 0; i < limit; ++i)
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
sim.tick();
|
||||
const double cost = calculateShipThreatCost(cfg.threatCosts, cfg,
|
||||
def.id, def.defaultModules);
|
||||
CHECK(cost > 0.0);
|
||||
}
|
||||
|
||||
sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
|
||||
[&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f)
|
||||
{
|
||||
if (!f.isEnemy) { return; }
|
||||
// salvage_ship and repair_ship have cost_formula = "0" and must not spawn.
|
||||
REQUIRE(si.schematicId != "salvage_ship");
|
||||
REQUIRE(si.schematicId != "repair_ship");
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user