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:
@@ -373,31 +373,3 @@ in `requirements.md` and the git history). Still open:
|
||||
one matching deposit tile). Touches REQ-BLD-MINER ("every asteroid
|
||||
tile is equivalent" no longer holds), REQ-GW-ASTEROID-EXPAND /
|
||||
REQ-EXP-*, `world.toml`, and `visuals.toml`.
|
||||
6. **Scrap-consuming recipes as threat fallback only.** Amend
|
||||
REQ-THREAT-ITEM: recipes that take scrap as an input participate in
|
||||
an item's threat computation only if no scrap-free recipe (miner,
|
||||
smelter, or assembler) produces that item — mirroring the existing
|
||||
rule for the reprocessing path. Otherwise the scrap→ingot smelter
|
||||
recipe would inflate the basic materials' threat via the
|
||||
max-across-recipes rule, poisoning every downstream value.
|
||||
7. **Per-unit item threat.** Amend REQ-THREAT-ITEM and
|
||||
`ThreatCostCalculator`: a recipe's threat is divided by its output
|
||||
amount, so item threat is production-seconds *per unit*. Currently a
|
||||
recipe producing 2 copper_wire per run assigns each wire the full
|
||||
run's threat, double-pricing multi-output items and everything
|
||||
downstream of them.
|
||||
8. **Fixpoint resolution in ThreatCostCalculator.** Items downstream of
|
||||
reprocessing-only items (e.g. capital parts built from the scrap-only
|
||||
input) never resolve, because resolution stops after the reprocessing
|
||||
pass instead of iterating; their consumers silently drop the missing
|
||||
materials, so capital hull threat is currently underestimated (found
|
||||
by `tools/threat_report.py`, which implements the correct fixpoint).
|
||||
9. **Max rule across staggered recipes in ThreatCostCalculator.** An
|
||||
item is committed at the first iteration where *any* of its recipes
|
||||
resolves, taking the max only over the recipes resolvable at that
|
||||
point. A shallow shortcut recipe (e.g. steel plate from raw ore)
|
||||
resolves one iteration earlier than the base path and wins, silently
|
||||
underpricing the item and everything downstream — violating the
|
||||
"shortcuts are pure rewards" rule. Fix: commit an item's threat only
|
||||
once every eligible recipe for it is computable (as
|
||||
`tools/threat_report.py` does), with a fallback for recipe cycles.
|
||||
|
||||
Reference in New Issue
Block a user