Fix ThreatCostCalculator: per-unit division, scrap fallback, fixpoint, staggered-recipe max
This commit is contained in:
@@ -107,3 +107,62 @@ TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]")
|
||||
double threat = calculateShipThreatCost(table, cfg, "nonexistent_ship", {});
|
||||
CHECK(threat == Approx(0.0));
|
||||
}
|
||||
|
||||
// Fix 6: scrap-consuming recipes are a fallback only.
|
||||
// iron_ingot has a scrap-free smelter recipe, so the scrap_iron recipe must
|
||||
// be excluded. iron_ingot threat must not be inflated by the scrap path.
|
||||
TEST_CASE("ThreatCostCalculator: scrap-consuming recipe excluded when scrap-free recipe exists", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// scrap_iron recipe: duration=1.0, 1 scrap (threat=1.0) -> 1 iron_ingot.
|
||||
// That would give 1.0 + 1.0*1 = 2.0 per unit — but it must be excluded
|
||||
// because the scrap-free iron_ingot smelter recipe (threat=4.0) exists.
|
||||
// iron_ingot threat stays at 4.0.
|
||||
CHECK(table.itemThreat.at("iron_ingot") == Approx(4.0));
|
||||
|
||||
// The pure reprocessing-only item (advanced_alloy) must still be resolved
|
||||
// via the reprocessing path.
|
||||
CHECK(table.itemThreat.count("advanced_alloy") == 1u);
|
||||
}
|
||||
|
||||
// Fix 7: per-unit item threat divides by output amount.
|
||||
// dual_wire: assembler, 1 iron_ore -> 2 dual_wire, duration 3.0.
|
||||
// Per-unit threat = (3.0 + iron_ore(1.0)*1) / 2 = 4.0 / 2 = 2.0.
|
||||
TEST_CASE("ThreatCostCalculator: per-unit division by output amount", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("dual_wire") == Approx(2.0));
|
||||
}
|
||||
|
||||
// Fix 8: fixpoint resolution — items downstream of reprocessing-only items
|
||||
// must be resolved after the reprocessing pass re-enables the non-reprocessing
|
||||
// pass.
|
||||
// downstream_product: assembler, 1 advanced_alloy -> 1, duration 2.0.
|
||||
// advanced_alloy = 80.0 (reprocessing-only).
|
||||
// downstream_product = 2.0 + 80.0*1 = 82.0.
|
||||
TEST_CASE("ThreatCostCalculator: downstream-of-reprocessing item resolves via fixpoint", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("advanced_alloy") == Approx(80.0));
|
||||
CHECK(table.itemThreat.at("downstream_product") == Approx(82.0));
|
||||
}
|
||||
|
||||
// Fix 9: max rule across staggered recipes — item is committed only once
|
||||
// every eligible recipe for it is computable.
|
||||
// staggered_item has two recipes:
|
||||
// cheap: 1 iron_ore (1.0) + 1.0 s = 2.0 (resolves early)
|
||||
// expensive: 1 circuit_board (28.0) + 1.0 s = 29.0 (resolves later)
|
||||
// expected threat = max(2.0, 29.0) = 29.0, not 2.0.
|
||||
TEST_CASE("ThreatCostCalculator: staggered recipes committed only when all computable", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("staggered_item") == Approx(29.0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user