- tools/simulate.py + puga/simulate.py: replica of PRUNplanner's simulator (flows and efficiency verified against screenshots), reports real new capex (planned minus built) - tools/scan.py: staffing variants, freight, HQ/experts, --planet mode, demolish-later, --min-n as a pure market-size filter, --json output - tools/history.py, tools/persistence.py: margin history and short-horizon payback checks - tools/plan_push.py: guarded delete; tools/state.py: syncs to empire/ - README with features and setup; CLAUDE.md made generic - Own-empire material (profile, state, plans, notes) moved to gitignored empire/; generic examples in plans/examples and state/company.example.yaml Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
88 lines
4.8 KiB
Python
Executable File
88 lines
4.8 KiB
Python
Executable File
#!/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 <plan-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()
|