Fix ThreatCostCalculator: per-unit division, scrap fallback, fixpoint, staggered-recipe max (action items 6-9)
Four algorithm fixes to bring ThreatCostCalculator.cpp into agreement with
tools/threat_report.py and the newly amended REQ-THREAT-ITEM semantics:
6. Scrap-consuming recipes as threat fallback only. Non-reprocessing recipes
that take scrap as an input are excluded from an item's threat computation
whenever at least one scrap-free recipe (miner/smelter/assembler) produces
that item. Previously the scrap_smelting recipe (1 scrap → 1 iron_ingot)
would have inflated iron_ingot's threat via the max rule.
7. Per-unit item threat. computeRecipeThreatPerUnit() now divides by the
recipe's output amount, so multi-output recipes price each unit correctly.
Example: copper_wire (1 copper_ingot, 1 s, output 2) is now 1.5, not 3.
8. Fixpoint resolution. The resolution loop now alternates the non-reprocessing
pass and the reprocessing pass until neither makes progress, rather than
running the reprocessing pass once at the end. Items downstream of
reprocessing-only items (voidsteel_plate, capital_core, capital hulls,
drone_hangar_module) now resolve correctly.
9. Max rule across staggered recipes. An item is committed only once every
eligible recipe producing it is computable, so a shallow shortcut recipe
(e.g. shortcut_steel_plate: 3 iron_ore → 1 steel_plate, resolvable one
iteration earlier) cannot undercut the expensive base path. A deadlock
fallback (require_all_recipes=False) handles potential recipe cycles.
docs/requirements.md: REQ-THREAT-ITEM amended for per-unit division, the
scrap-fallback rule, and order-independence via fixpoint.
docs/progression_design.md: action items 6-9 removed (completed); remaining
items 1-5 renumbered unchanged.
tools/threat_report.py: NOTE updated — C++ now matches Python semantics.
bin/test/data/config/recipes.toml: four minimal test recipes added (one per
fix: scrap_iron, dual_wire, downstream_product, staggered_item_{cheap,expensive}).
src/test/ThreatCostCalculatorTest.cpp: four new TEST_CASEs covering each fix.
Expected values with the live config (bin/app/data/config) verified by
threat_report.py: iron_ingot 2, copper_wire 1.5, steel_plate 7, control_chip
12, voidsteel_plate 141, capital_core 240; fitted ships 10.5/47/99/233.5/
354.5/722.5/1491.5/1436.5. All 378 test cases pass.
This commit is contained in:
@@ -92,3 +92,60 @@ duration_seconds = 3.0
|
||||
item = "advanced_alloy"
|
||||
amount = 1
|
||||
probability = 0.1
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Extra recipes for ThreatCostCalculator unit tests (fixes 6-9)
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# Fix 6: scrap-consuming smelter recipe for iron_ingot. Because iron_ingot
|
||||
# already has a scrap-free smelter recipe above, this recipe must be excluded
|
||||
# from iron_ingot's threat computation.
|
||||
[[recipe]]
|
||||
id = "scrap_iron"
|
||||
building = "smelter"
|
||||
inputs = [{item = "scrap", amount = 1}]
|
||||
outputs = [{item = "iron_ingot", amount = 1}]
|
||||
duration_seconds = 1.0
|
||||
|
||||
# Fix 7: a recipe that produces 2 items per cycle. Per-unit threat must
|
||||
# divide by the output amount.
|
||||
# dual_wire: (duration=3.0 + iron_ore(1.0)*1) / 2 = 4.0 / 2 = 2.0 per unit.
|
||||
[[recipe]]
|
||||
id = "dual_wire"
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ore", amount = 1}]
|
||||
outputs = [{item = "dual_wire", amount = 2}]
|
||||
duration_seconds = 3.0
|
||||
|
||||
# Fix 8: an item downstream of a reprocessing-only item (advanced_alloy).
|
||||
# advanced_alloy is resolved only by the reprocessing pass; downstream_product
|
||||
# can only resolve in a non-reprocessing pass that runs AFTER the reprocessing
|
||||
# pass, requiring proper fixpoint iteration.
|
||||
# downstream_product: 2.0 + advanced_alloy(80.0)*1 = 82.0
|
||||
[[recipe]]
|
||||
id = "downstream_product"
|
||||
building = "assembler"
|
||||
inputs = [{item = "advanced_alloy", amount = 1}]
|
||||
outputs = [{item = "downstream_product", amount = 1}]
|
||||
duration_seconds = 2.0
|
||||
|
||||
# Fix 9: two recipes producing the same staggered_item. The cheap recipe
|
||||
# resolves before circuit_board is known; the expensive one requires
|
||||
# circuit_board. The item must be committed only once BOTH are computable,
|
||||
# so the result is max(cheap, expensive).
|
||||
# staggered_item_cheap: 1.0 + iron_ore(1.0)*1 = 2.0 (resolves early)
|
||||
# staggered_item_expensive: 1.0 + circuit_board(28.0)*1 = 29.0 (resolves later)
|
||||
# expected: max = 29.0
|
||||
[[recipe]]
|
||||
id = "staggered_item_cheap"
|
||||
building = "assembler"
|
||||
inputs = [{item = "iron_ore", amount = 1}]
|
||||
outputs = [{item = "staggered_item", amount = 1}]
|
||||
duration_seconds = 1.0
|
||||
|
||||
[[recipe]]
|
||||
id = "staggered_item_expensive"
|
||||
building = "assembler"
|
||||
inputs = [{item = "circuit_board", amount = 1}]
|
||||
outputs = [{item = "staggered_item", amount = 1}]
|
||||
duration_seconds = 1.0
|
||||
|
||||
Reference in New Issue
Block a user