#!/usr/bin/env python3 """Read a plan the way PRUNplanner's simulator does: efficiencies, workforce, material I/O, profit. Source: a local spec (plans/*.yaml) or the plan stored in his PRUNplanner account (--uuid; picks up his UI edits). tools/simulate.py plans/examples/base_plus_hwp.yaml tools/simulate.py --uuid --basis ask --cx AI1 Prices: --basis real (buy at ask, sell at 7d VWAP at --cx: what a patient trader gets) | uni30 (default: volume-weighted 30d VWAP across all exchanges, = PRUNplanner 'Universe 30D') | vwap30 | vwap7 | ask | bid | mid at --cx.""" import argparse, sys from pathlib import Path import yaml sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from puga import ROOT, config, market, prunplanner as pp from puga.simulate import simulate sys.path.insert(0, str(Path(__file__).resolve().parent)) import plan_push def main(): ap = argparse.ArgumentParser() ap.add_argument("spec", nargs="?") ap.add_argument("--uuid") ap.add_argument("--cx", default=config.DEFAULT_CX) ap.add_argument("--basis", default="uni30", choices=["real", "uni30", "vwap30", "vwap7", "ask", "bid", "mid"]) ap.add_argument("--off", default="", help="building tickers whose recipes are switched off (quantity 0), e.g. EXT,SME") ap.add_argument("--no-faction", action="store_true") ap.add_argument("--cm-free", action="store_true", help="treat the core module as free/already there (e.g. new base where CM cost is ignored)") ap.add_argument("--no-hq", action="store_true", help="ignore hq: true from empire/state/company.yaml") 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") off = {x.strip() for x in a.off.split(",") if x.strip()} for b in plan["plan_data"]["buildings"]: if b["name"] in off: for ar in b["active_recipes"]: ar["amount"] = 0 planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600) snap = market.snapshot() def price(t, side="both"): if a.basis == "real": q = snap.get((t, a.cx)) return None if not q else (q.ask if side == "buy" else (q.vwap7 or q.vwap30 or q.bid)) if a.basis == "uni30": return market.uni30(snap, t) or ((snap.get((t, a.cx)) or market.Quote(t, a.cx)).ask) q = snap.get((t, a.cx)) if not q: return None v = {"vwap30": q.vwap30 or q.vwap7 or q.ask, "vwap7": q.vwap7 or q.vwap30 or q.ask, "ask": q.ask, "bid": q.bid, "mid": (q.ask + q.bid) / 2 if q.ask and q.bid else None}[a.basis] return v import yaml as _y st = _y.safe_load(config.state_path().read_text()) faction = None if a.no_faction else (st.get("company") or {}).get("faction") perm = (st.get("permits", {}).get("used", 1), st.get("permits", {}).get("total", 2)) if not a.no_hq and st.get("hq"): plan["plan_corphq"] = True built = next((b.get("buildings", {}) for b in st.get("bases", []) if b.get("planet") == plan["planet_natural_id"]), {}) if a.cm_free: built = {**built, "CM": 1} r = simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm, built=built) print(f"{plan['plan_name']} {plan['planet_natural_id']} COGC {plan.get('plan_cogc')} prices: {a.cx} {a.basis}") print(f"Area {r['area']:.0f}/500 Profit/day {r['profit']:,.0f} gross {r['gross']:,.0f} degradation {r['degradation']:,.0f} plan cost {r['plan_cost']:,.0f} ROI {r['roi_days'] and round(r['roi_days'], 2)} d") nc = r["new_capex"] print(f"NEW CAPEX = planned minus already built ({built or 'nothing built'}): {nc:,.0f} -> payback {nc / r['profit']:.2f} d = {100 * r['profit'] / nc:.1f}%/day" if r["profit"] > 0 and nc > 0 else ("NEW CAPEX: nothing to build" if nc <= 0 else "NEW CAPEX: profit <= 0")) print("\nWORKFORCE need supply open eff%") for t, w in r["workforce"].items(): if w["need"] or w["supply"]: print(f" {t:10} {w['need']:5.0f} {w['supply']:6.0f} {w['open']:5.0f} {w['eff']*100:7.2f}") print("\nBUILDINGS") for b in r["buildings"]: print(f" {b['amount']:3} x {b['building']:4} eff {b['efficiency']*100:7.2f}% {b['recipes']}") print(f"\n{'MATERIAL':8} {'in/d':>9} {'out/d':>9} {'delta':>9} {'price':>7} {'value/d':>10}") for tk, f in sorted(r["flows"].items(), key=lambda kv: -abs(kv[1]["value"])): print(f"{tk:8} {f['inp']:9.2f} {f['out']:9.2f} {f['delta']:9.2f} {f['price']:7.0f} {f['value']:10.0f}") if r["missing_prices"]: print("no price for:", r["missing_prices"]) if __name__ == "__main__": main()