#!/usr/bin/env python3 """Days of runway per material before the base stalls, from the plan's real NET consumption (simulate()'s flows, so self-produced inputs like AL correctly don't count against you), the base's own storage, plus (by default) any ship currently flying TO this planet, auto-detected from empire state (tools/state.py tags each ship with its live flight destination). puga runway plans/examples/base_plus_hwp.yaml # base storage + inbound ships (default) puga runway plans/examples/base_plus_hwp.yaml --no-inbound # base storage only, ignore ships in flight """ import argparse, sys from pathlib import Path import yaml sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from puga import config, prunplanner as pp, market from puga.simulate import simulate sys.path.insert(0, str(Path(__file__).resolve().parent)) import plan_push from buy import stock_items # net-consumption-per-day logic, shared with `buy` def inbound_ships(state: dict, planet: str) -> list[str]: """Registrations of ships whose live flight destination is this planet (natural id substring match against the flight's destination string, e.g. '... - Deimos (ZV-759c)').""" out = [] for s in state.get("ships", []): f = s.get("flight") if f and f.get("destination") and planet in f["destination"]: out.append(s["reg"]) return out def on_hand(state: dict, planet: str, ships: list[str]) -> dict: """This planet's own STORE items plus the named ships' SHIP_STORE cargo, summed.""" totals: dict[str, float] = {} for s in state.get("storage", []): if (s.get("type") == "STORE" and s.get("planet") == planet) or (s.get("type") == "SHIP_STORE" and s.get("name") in ships): for tk, qty in s.get("items", {}).items(): totals[tk] = totals.get(tk, 0) + qty return totals def main(): ap = argparse.ArgumentParser() ap.add_argument("spec", nargs="?", help="plans/*.yaml describing the WHOLE base (see docs/library.md)") ap.add_argument("--uuid", help="read the plan from the PRUNplanner account instead") ap.add_argument("--no-inbound", action="store_true", help="ignore ships currently flying to this base; base storage only") ap.add_argument("--cx", default=config.DEFAULT_CX) a = ap.parse_args() recipes, blds = pp.recipes(), pp.buildings() if a.uuid: plan = pp.request("GET", f"/planning/plan/{a.uuid}/") elif a.spec: plan = plan_push.build_payload(yaml.safe_load(Path(a.spec).read_text()), recipes, {b["building_ticker"] for b in blds}) else: sys.exit("give a spec file or --uuid") st = yaml.safe_load(config.state_path().read_text()) if not st.get("hq"): plan["plan_corphq"] = False faction = st.get("company", {}).get("faction") perm = (st.get("permits", {}).get("used", 1), st.get("permits", {}).get("total", 2)) planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600) snap = market.snapshot() price = lambda t, side="buy": (lambda q: q.ask if q else None)(snap.get((t, a.cx))) r = simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm) ships = [] if a.no_inbound else inbound_ships(st, plan["planet_natural_id"]) have = on_hand(st, plan["planet_natural_id"], ships) daily = stock_items(r["flows"], days=1.0) # net units consumed per day, per material (self-production already netted) if not daily: print("nothing net-consumed by this plan; no runway to compute") return rows = sorted(((tk, have.get(tk, 0), rate, have.get(tk, 0) / rate) for tk, rate in daily.items()), key=lambda x: x[3]) ships_note = f" + inbound {', '.join(ships)}" if ships else "" print(f"RUNWAY (base storage{ships_note})\n{'material':8} {'have':>8} {'burn/day':>9} {'days':>7}") for tk, have, rate, days in rows: flag = " <- CRITICAL" if days < 1 else (" <- low" if days < 2 else "") print(f"{tk:8} {have:8.1f} {rate:9.2f} {days:7.2f}{flag}") if __name__ == "__main__": main()