Add simulator replica, persistence, planet scan, README; split empire state out of the repo

- 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>
This commit is contained in:
2026-09-19 00:11:02 +02:00
co-authored by Claude Sonnet 5
parent 7a538cb300
commit 3bbf524ebb
26 changed files with 778 additions and 259 deletions
+18 -8
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env python3
"""Turn a YAML plan spec (plans/*.yaml) into a PRUNplanner plan, via the Api-Key API.
DEFAULT IS DRY RUN. Guardrails (docs/decisions.md): only plans named '[PuGa] ...' are created/updated; never delete;
--update refuses if the existing plan's name does not start with '[PuGa]'. Show the dry-run to Dominik and get a yes before --apply.
DEFAULT IS DRY RUN. Guardrails (docs/decisions.md): only plans named '[PuGa] ...' are created/updated; deletes only [PuGa] plans and only when asked;
--update refuses if the existing plan's name does not start with '[PuGa]'. Show the dry-run to the user and get a yes before --apply.
tools/plan_push.py list
tools/plan_push.py plans/deimos_bhp.yaml # dry run: validated payload summary
tools/plan_push.py plans/deimos_bhp.yaml --json # full JSON payload
tools/plan_push.py plans/deimos_bhp.yaml --apply # create (after user says yes)
tools/plan_push.py plans/deimos_bhp.yaml --apply --update <uuid>
tools/plan_push.py plans/examples/base_plus_hwp.yaml # dry run: validated payload summary
tools/plan_push.py plans/examples/base_plus_hwp.yaml --json # full JSON payload
tools/plan_push.py plans/examples/base_plus_hwp.yaml --apply # create (after user says yes)
tools/plan_push.py plans/examples/base_plus_hwp.yaml --apply --update <uuid>
tools/plan_push.py delete <uuid> # only [PuGa] plans, only when the user asks
Spec: name, planet (natural id), permits, cogc (e.g. METALLURGY or null), hq, experts {METALLURGY: 2}, lux {pioneer: [true,true]},
infrastructure {HB1: 4, HB2: 1}, buildings: [{building: SME, amount: 5, recipes: ["ALO,FLX,C,O=>4AL"|"AL,STL,HE=>BHP"|"EXT#ALO"]}]
@@ -79,7 +80,8 @@ def build_payload(spec: dict, recipes: list[dict], building_tickers: set[str]) -
def main():
ap = argparse.ArgumentParser()
ap.add_argument("spec", help="plans/*.yaml or 'list'")
ap.add_argument("spec", help="plans/*.yaml, 'list', or 'delete'")
ap.add_argument("target", nargs="?", help="uuid for 'delete'")
ap.add_argument("--apply", action="store_true", help="actually write to PRUNplanner (creates a new plan)")
ap.add_argument("--update", metavar="UUID", help="with --apply: update this existing [PuGa] plan instead of creating")
ap.add_argument("--json", action="store_true")
@@ -89,6 +91,14 @@ def main():
for p in pp.request("GET", "/planning/plan/"):
print(p["uuid"], p.get("plan_name"), p.get("planet_natural_id"))
return
if a.spec == "delete":
# delete is allowed only on request, and only for plans this tool made ([PuGa] prefix).
cur = pp.request("GET", f"/planning/plan/{a.target}/")
if not str(cur.get("plan_name", "")).startswith(PREFIX):
sys.exit(f"REFUSED: '{cur.get('plan_name')}' is not a {PREFIX} plan")
pp.request("DELETE", f"/planning/plan/{a.target}/")
print("DELETED:", cur["plan_name"], a.target)
return
spec = yaml.safe_load(Path(a.spec).read_text())
payload = build_payload(spec, pp.recipes(), {b["building_ticker"] for b in pp.buildings()})
d = payload["plan_data"]
@@ -100,7 +110,7 @@ def main():
if a.json:
print(json.dumps(payload, indent=1))
if not a.apply:
print("\nDRY RUN: nothing sent. Re-run with --apply after Dominik confirms.")
print("\nDRY RUN: nothing sent. Re-run with --apply after the user confirms.")
return
if a.update:
cur = pp.request("GET", f"/planning/plan/{a.update}/")