From 80b8fa8bf968409ecf9734dba51e045e18f53604 Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Thu, 8 Oct 2026 10:42:23 +0200 Subject: [PATCH] Add tools/planet.py: rank planets for a resource with real founding cost Ranks every planet with a given extraction factor using FIO's bulk planet data, pricing founding cost straight from FIO's own BuildRequirements (real environment-extras materials for that specific planet, not guessed temperature/pressure thresholds - PRUNplanner's own calculations don't model those either). Takes --from an existing base to report jump distance via tools/route.py, since pure-factor ranking alone is misleading: the best-factor planets found for FEO were all 500-2600+ ly away, confirming Vulcan (merely decent factor, same system as Deimos) was still the right practical pick. Co-Authored-By: Claude Sonnet 5 --- puga/planet.py | 27 +++++++++++++++++++++++++++ tools/planet.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 puga/planet.py create mode 100644 tools/planet.py diff --git a/puga/planet.py b/puga/planet.py new file mode 100644 index 0000000..afc0786 --- /dev/null +++ b/puga/planet.py @@ -0,0 +1,27 @@ +"""Rank planets by extraction factor for a resource, with founding cost (environment extras +from FIO's own BuildRequirements - authoritative, not guessed from temperature/pressure +thresholds, which PRUNplanner's own calculations don't model either).""" +from . import fio + + +def candidates(resource_ticker: str, min_factor: float = 0.0): + """All planets with this resource, sorted by factor descending. Each: PlanetNaturalId, + PlanetName, factor, resource_type, plus the raw planet record (temperature/pressure/ + gravity/fertility/faction/BuildRequirements/SystemId etc).""" + mats = {m["Ticker"]: m["MaterialId"] for m in fio.materials()} + mid = mats.get(resource_ticker.upper()) + if not mid: + raise ValueError(f"unknown material ticker {resource_ticker!r}") + out = [] + for p in fio.planets_full(): + for r in p.get("Resources", []): + if r["MaterialId"] == mid and r["Factor"] >= min_factor: + out.append({ + "natural_id": p["PlanetNaturalId"], "name": p["PlanetName"], + "factor": r["Factor"], "resource_type": r["ResourceType"], + "temperature": p["Temperature"], "pressure": p["Pressure"], "gravity": p["Gravity"], + "fertility": p["Fertility"], "faction": p.get("FactionCode"), + "system_id": p["SystemId"], "build_requirements": p.get("BuildRequirements", []), + }) + out.sort(key=lambda x: -x["factor"]) + return out diff --git a/tools/planet.py b/tools/planet.py new file mode 100644 index 0000000..d5531ae --- /dev/null +++ b/tools/planet.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Rank planets for a resource, with founding cost (environment extras straight from FIO's +BuildRequirements - real game data, not guessed thresholds) and optional jump distance from +a base you already have. + + tools/planet.py FEO --top 10 + tools/planet.py LST --min-factor 0.5 --from ZV-759c --top 5 +""" +import argparse, sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from puga import config, market, planet, route + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("resource", help="raw resource ticker, e.g. LST, FEO, SEA") + ap.add_argument("--min-factor", type=float, default=0.0) + ap.add_argument("--top", type=int, default=10) + ap.add_argument("--from", dest="from_", help="planet/system to measure jump distance from (e.g. an existing base)") + ap.add_argument("--cx", default=config.DEFAULT_CX) + a = ap.parse_args() + + rows = planet.candidates(a.resource, a.min_factor) + if not rows: + print(f"no planets found with {a.resource.upper()} (factor >= {a.min_factor})") + return + rows = rows[:a.top] + + src = route.resolve_system(a.from_) if a.from_ else None + graph = route.load_graph() if src else None + + for p in rows: + env_cost = 0.0 + env_items = [] + for br in p["build_requirements"]: + w = market.walk(br["MaterialTicker"], a.cx, br["MaterialAmount"], "buy") + env_cost += w["total"] + env_items.append(f"{br['MaterialAmount']}{br['MaterialTicker']}") + dist = "" + if src: + r = route.shortest_path(src, p["system_id"], graph=graph) if p["system_id"] != src else {"jumps": 0, "distance_ly": 0.0} + dist = f" {r['jumps']} jumps ({r['distance_ly']:.0f} ly)" if r else " unreachable" + print(f"{p['natural_id']:8} {p['name']:16} factor {p['factor']:.3f} T {p['temperature']:6.1f}C P {p['pressure']:.2f}bar " + f"G {p['gravity']:.2f} fert {p['fertility']:5.2f} faction {p['faction'] or '-':4} env-cost {env_cost:8,.0f} AIC ({', '.join(env_items) or 'none'}){dist}") + + +if __name__ == "__main__": + main()