From 88dae754ca9f1945c3296d1ed2c1a2408b061d3b Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Fri, 9 Oct 2026 10:36:05 +0200 Subject: [PATCH] whatif.py: model demolition correctly - reclaim materials aren't a dead loss Was wrong earlier this session claiming demolition gives nothing back. FIO's /rain/userplanetbuildingreclaimables confirms partial material return (observed 50-90% depending on material, not a fixed fraction - looked up live, not approximated). --remove now prices the real reclaim for anything actually built and nets it against new capex, so a demolish-and-reinvest question gets the true net cash picture instead of double-penalizing the removed building. Used this to check "demolish BHP's HWP, build a 2nd CHP or POL instead" - both are clearly net losers even counting the ~57,750 AIC reclaim credit (net capex still positive, and marginal profit is negative either way) - dominated by just adding the new building without removing anything, since nothing scarce actually forces the swap. Co-Authored-By: Claude Sonnet 5 --- tools/whatif.py | 63 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/tools/whatif.py b/tools/whatif.py index dacf0cc..024f1dc 100644 --- a/tools/whatif.py +++ b/tools/whatif.py @@ -10,10 +10,11 @@ session - e.g. PP2 overloading pioneer housing after POL+CHP already maxed it). tools/whatif.py before.yaml --add POL:1 # reuses POL's existing recipe in before.yaml """ import argparse, copy, sys +from collections import Counter from pathlib import Path import yaml sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from puga import config, market, prunplanner as pp +from puga import config, fio, market, prunplanner as pp from puga.simulate import simulate sys.path.insert(0, str(Path(__file__).resolve().parent)) @@ -59,6 +60,34 @@ def apply_infra(spec: dict, infra: str): inf[ticker] = inf.get(ticker, 0) + int(amount) +def reclaim_value(ticker: str, qty: int, built_count: int, cx: str): + """Sell value of the materials you'd actually get back demolishing `qty` of a building you + already own `built_count` of - real per-site data from FIO's own reclaimables endpoint + (demolition is NOT a dead loss: observed ~50-90%% of original materials back, varies by + material, not a fixed fraction - don't approximate it, look it up). None if you don't + actually own one (can't reclaim a hypothetical building).""" + if built_count <= 0 or qty <= 0: + return None + u = fio.me() + rec = fio.own(f"/rain/userplanetbuildingreclaimables/{u}", ttl=3600) + totals = Counter() + for x in rec: + if x["BuildingId"].startswith(f"{ticker}-"): + totals[x["Material"]] += x["Amount"] + if not totals: + return None + value = 0.0 + items = {} + for mat, amt in totals.items(): + q = round(amt / built_count * min(qty, built_count)) # amt is already summed over all built_count units + if q <= 0: + continue + w = market.walk(mat, cx, q, "sell") + items[mat] = q + value += w["total"] + return {"value": value, "items": items} + + def run(spec, recipes, blds, price, faction, perm, built): plan = plan_push.build_payload(spec, recipes, {b["building_ticker"] for b in blds}) planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600) @@ -122,15 +151,35 @@ def main(): d_profit = r_a["profit"] - r_b["profit"] d_capex = r_a["new_capex"] - r_b["new_capex"] + before_counts = Counter(b["building"] for b in before_spec.get("buildings", []) for _ in range(b["amount"])) + after_counts = Counter(b["building"] for b in after_spec.get("buildings", []) for _ in range(b["amount"])) + reclaim_total = 0.0 + reclaim_lines = [] + for ticker in before_counts: + removed_qty = before_counts[ticker] - after_counts.get(ticker, 0) + if removed_qty > 0: + r = reclaim_value(ticker, removed_qty, built.get(ticker, 0), a.cx) + if r: + reclaim_total += r["value"] + items = ", ".join(f"{q}{m}" for m, q in r["items"].items()) + reclaim_lines.append(f" {ticker} x{removed_qty}: {r['value']:,.0f} AIC ({items})") + print(f"BEFORE profit/day {r_b['profit']:,.0f} new_capex {r_b['new_capex']:,.0f}") print(f"AFTER profit/day {r_a['profit']:,.0f} new_capex {r_a['new_capex']:,.0f}") - print(f"\nMARGINAL profit/day {d_profit:,.0f} new_capex {d_capex:,.0f}", end="") - if d_capex > 0 and d_profit > 0: - print(f" ROI/day {100*d_profit/d_capex:.2f}% payback {d_capex/d_profit:.1f} d") - elif d_capex <= 0: - print(" (no new capex)") + if reclaim_lines: + print(f"\nRECLAIMED MATERIALS (demolishing what's removed, sell value, real FIO data):") + for line in reclaim_lines: + print(line) + print(f" total: {reclaim_total:,.0f} AIC") + net_capex = d_capex - reclaim_total + net_label = f" net cash GAINED {-net_capex:,.0f}" if net_capex < 0 and reclaim_total else (f" net new capex {net_capex:,.0f}" if reclaim_total else "") + print(f"\nMARGINAL profit/day {d_profit:,.0f} new_capex {d_capex:,.0f}" + net_label, end="") + if net_capex > 0 and d_profit > 0: + print(f" ROI/day {100*d_profit/net_capex:.2f}% payback {net_capex/d_profit:.1f} d") + elif net_capex <= 0 and d_profit > 0: + print(" (no net new capex after reclaim)" if reclaim_total else " (no new capex)") else: - print(" (marginal profit <= 0 - not worth it)") + print(" (marginal profit <= 0 - not worth it, regardless of reclaim)") print("\nWORKFORCE (after) need supply open eff%") newly_overloaded = []