Add planet environment-extra materials to capex calcs; buildcost.py tool

Every building needs a planet-hazard material bundle on top of its normal
BuildingCosts (SEA for low pressure, HSE for high pressure, MGC for low
gravity, BL for high gravity, INS for cold, TSH for heat - area-scaled
where applicable), confirmed against live in-game build panels on Nike.
FIO's own BuildRequirements field is an unreliable superset across hazard
types, not a direct per-building list - don't use it directly.

- puga/planet.py::environment_extras(): the real threshold table.
- tools/buildcost.py: full construction material list for one building,
  optionally on a given planet.
- Wired into whatif.py/buy.py (via simulate(planet=...)) and scan.py's
  own bcost(), so their capex numbers stop silently omitting this.
- docs/mechanics.md: the rule, plus a separate forum-sourced note on the
  CX order-cancellation penalty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 19:03:51 +02:00
co-authored by Claude Sonnet 5
parent 6bd5d6cc69
commit bf15c709ea
9 changed files with 153 additions and 12 deletions
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Full construction material list for one building, optionally on a specific planet (adds its
environment-extra materials via puga.planet.environment_extras() - SEA/HSE/MGC/BL/INS/TSH).
puga buildcost HB1
puga buildcost EXT --planet ZV-194a
puga buildcost PP1 --planet ZV-194a --amount 1
"""
import argparse, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, fio, market, prunplanner as pp, planet as planet_mod
def main():
ap = argparse.ArgumentParser()
ap.add_argument("building", help="building ticker, e.g. HB1, EXT, PP1")
ap.add_argument("--planet", help="planet natural id - adds its environment-extra materials")
ap.add_argument("--amount", type=int, default=1)
ap.add_argument("--cx", default=config.DEFAULT_CX)
a = ap.parse_args()
blds = {b["Ticker"]: b for b in fio.buildings()}
b = blds.get(a.building.upper())
if not b:
sys.exit(f"unknown building {a.building!r}")
need: dict[str, float] = {c["CommodityTicker"]: c["Amount"] for c in b["BuildingCosts"]}
need["MCG"] = need.get("MCG", 0) + 4 * b["AreaCost"]
if a.planet:
planet = pp._g(f"/data/planet/{a.planet}/", 3600)
for tk, qty in planet_mod.environment_extras(planet, b["AreaCost"]).items():
need[tk] = need.get(tk, 0) + qty
print(f"# {b['Ticker']} on {planet['planet_name']} ({a.planet}): pressure {planet['pressure']:.3f}, "
f"gravity {planet['gravity']:.2f}, temp {planet['temperature']:.1f}C")
total = 0.0
for tk, qty in sorted(need.items()):
qty *= a.amount
cost = market.walk(tk, a.cx, qty, "buy")
px = cost["total"] if not cost["short"] else None
total += px or 0
print(f"{tk:4} {qty:8.1f} {px:>10,.0f} AIC" if px is not None else f"{tk:4} {qty:8.1f} (short on {a.cx})")
print(f"total: {total:,.0f} AIC" if total else "")
if __name__ == "__main__":
main()
+9 -4
View File
@@ -20,14 +20,16 @@ from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, fio, market, prunplanner as pp
from puga import planet as planet_mod
from puga.simulate import simulate
sys.path.insert(0, str(Path(__file__).resolve().parent))
import plan_push
def construction_items(plan: dict, buildings: list, built: dict) -> dict:
"""Materials needed for the plan's buildings/infra beyond what `built` (ticker -> count) already has."""
def construction_items(plan: dict, buildings: list, built: dict, planet: dict | None = None) -> dict:
"""Materials needed for the plan's buildings/infra beyond what `built` (ticker -> count) already has.
planet: optional, adds environment-extra materials (SEA/HSE/MGC/BL/INS/TSH) via planet.environment_extras()."""
bl = {b["building_ticker"]: b for b in buildings}
d = plan["plan_data"]
need: dict[str, float] = {}
@@ -39,6 +41,9 @@ def construction_items(plan: dict, buildings: list, built: dict) -> dict:
for c in info["costs"]:
need[c["material_ticker"]] = need.get(c["material_ticker"], 0) + c["material_amount"] * gap
need["MCG"] = need.get("MCG", 0) + 4 * info["area_cost"] * gap
if planet is not None:
for etk, qty in planet_mod.environment_extras(planet, info["area_cost"]).items():
need[etk] = need.get(etk, 0) + qty * gap
return need
@@ -101,9 +106,9 @@ def main():
perm = (st.get("permits", {}).get("used", 1), st.get("permits", {}).get("total", 2))
planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600)
price_ask = lambda t, side="buy": (lambda q: q.ask if q else None)(market.snapshot().get((t, a.cx)))
r = simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price_ask, faction, perm)
r = simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price_ask, faction, perm, planet=planet)
con = net_of_on_hand(construction_items(plan, blds, built), on_hand)
con = net_of_on_hand(construction_items(plan, blds, built, planet), on_hand)
stk = net_of_on_hand(stock_items(r["flows"], a.days), on_hand)
con_rows, con_total, con_w, con_v = price(con, a.cx)
stk_rows, stk_total, stk_w, stk_v = price(stk, a.cx)
+9 -1
View File
@@ -13,6 +13,7 @@ from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import ROOT, config, econ, fio, market, prunplanner as pp, saturation as sat
from puga import planet as pl
TIER_CODE = {"pioneer": "P", "settler": "S", "technician": "T", "engineer": "E", "scientist": "Sc"}
TIER_RANK = {"P": 0, "S": 1, "T": 2, "E": 3, "Sc": 4}
@@ -83,7 +84,14 @@ def main():
parts = [(x["Amount"], ask(x["CommodityTicker"])) for x in b["BuildingCosts"]]
if any(p is None for _, p in parts):
return None
return sum(n * p for n, p in parts) + a.mcg * b["AreaCost"] * mcg
cost = sum(n * p for n, p in parts) + a.mcg * b["AreaCost"] * mcg
if planet is not None:
extras = pl.environment_extras(planet, b["AreaCost"])
prices = {etk: ask(etk) for etk in extras}
if any(p is None for p in prices.values()):
return None
cost += sum(qty * prices[etk] for etk, qty in extras.items())
return cost
hab_head, hab_area = {}, {}
for tier, hab in (("pioneer", "HB1"), ("settler", "HB2"), ("technician", "HB3"), ("engineer", "HB4"), ("scientist", "HB5")):
+1 -1
View File
@@ -127,7 +127,7 @@ def marginal_freight(r_b, r_a, planet_id, cx, sf_price, ff_price, mats):
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)
return plan, simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm, built=built)
return plan, simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm, built=built, planet=planet)
def main():