Initial PuGa toolkit: data layer, econ, depth-aware scan, state sync, plan push

- puga/: cached FIO + PRUNplanner clients, market view with order-book walk,
  econ formulas ported from PRUNplanner (tested against its suite and live FIO),
  saturation model v1 (reviewed by Opus)
- tools/: scan (depth-aware), price, book, chain, state sync, plan_push
  (dry run default, [PuGa]-prefixed plans only), legacy prun_scan/prun_cxarb
- docs/: mechanics (PRUNplanner is source of truth), roadmap, decisions,
  saturation design, archived handoff
- secrets stay in .env (gitignored); ref/ holds PRUNplanner source (ignored)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 23:00:21 +02:00
co-authored by Claude Sonnet 5
commit 7a538cb300
35 changed files with 1692 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
"""Expected values copied from PRUNplanner's own test suite (ref/frontend/src/tests/features/planning/calculations)."""
import pytest
from puga import econ
HAB_COSTS = {'HB1': 50283.96581293734, 'HB2': 48183.93380367031, 'HB3': 171236.78856075153, 'HB4': 478742.2978713045,
'HB5': 881886.0033487707, 'HBB': 78602.69810873509, 'HBC': 191175.66459235144,
'HBM': 759867.355670017, 'HBL': 1210568.4635058648}
REQ = {'pioneer': 100, 'settler': 390, 'technician': 490}
@pytest.mark.parametrize("cap,req,l1,l2,exp", [
(100, 100, False, False, 0.7944444444444446), (50, 100, False, False, 0.3972222222222223),
(100, 50, False, False, 0.7944444444444446), (100, 100, True, False, 0.8666666666666668),
(100, 100, False, True, 0.9166666666666667), (100, 100, True, True, 1)])
def test_tier_efficiency(cap, req, l1, l2, exp):
assert econ.tier_efficiency(cap, req, l1, l2) == pytest.approx(exp)
@pytest.mark.parametrize("n,exp", [(-5, 0), (100, 0), (1, 0.0306), (2, 0.0696), (3, 0.1248), (4, 0.1974), (5, 0.284)])
def test_expert_bonus(n, exp):
assert econ.expert_bonus(n) == exp
def test_workforce_factor():
b = dict(pioneers=40, settlers=30, technicians=20, engineers=10, scientists=5)
eff = dict(pioneer=.5, settler=.25, technician=.4, engineer=.1, scientist=1.5)
assert econ.workforce_factor(b, eff) == pytest.approx(0.419047619047619)
b = dict(pioneers=100, settlers=50, technicians=20, engineers=10, scientists=5)
eff = dict(pioneer=1.25, settler=1.625, technician=.4, engineer=.1, scientist=1.5)
assert econ.workforce_factor(b, eff) == pytest.approx(1.204054054054054)
def test_faction_bonus():
assert econ.faction_multiplier("HORTUS", "AGRICULTURE", 1, 3) == pytest.approx(1.14)
assert econ.faction_multiplier("MORIA", "METALLURGY", 20, 21) == pytest.approx(1.0438095238095237)
assert econ.faction_multiplier("FOO", "AGRICULTURE", 1, 3) is None
assert econ.faction_multiplier("ANTARES", "METALLURGY", 1, 2) is None # Antares is electronics only
def test_hwp_without_technicians_under_metallurgy_cogc():
"""HWP 40 settlers + 10 technicians; technicians absent: workforce 0.8 x COGC 1.25 = 1.0 (before expert/HQ)."""
hwp = dict(settlers=40, technicians=10)
tier = dict(settler=econ.tier_efficiency(40, 40, True, True), technician=0.0)
total, el = econ.building_efficiency(hwp, tier, expertise="METALLURGY", cogc="METALLURGY")
assert el["WORKFORCE"] == pytest.approx(0.8) and total == pytest.approx(1.0)
def test_extraction_matches_live_deimos():
"""Planet data ZV-759c: ALO factor 0.4 (MINERAL), O 0.3 (GASEOUS), H2O 0.2 (LIQUID). APEX/PRUNplanner chips: 28 ALO, 18 O, 14 H2O per day.
Live FIO EXT order: 14 ALO per 12.008 h = 28/day."""
assert econ.daily_extraction(0.4, "MINERAL") == pytest.approx(28.0)
assert econ.daily_extraction(0.3, "GASEOUS") == pytest.approx(18.0)
assert econ.daily_extraction(0.2, "LIQUID") == pytest.approx(14.0)
t, amt = econ.extraction_cycle("MINERAL", 28.0)
assert amt == 14 and t == pytest.approx(12 * 3600e3)
def test_production_io_flux_smelter():
r = dict(time_ms=14.9167 * 3600e3, inputs={"ALO": 6, "FLX": 1, "C": 1, "O": 1}, outputs={"AL": 4})
io = econ.production_io([r], efficiency=1.0, n_buildings=5)
assert io["out"]["AL"] == pytest.approx(4 * 5 * 24 / 14.9167, rel=1e-3) # ~32 AL/day for 5 SME at 100%
def test_workforce_consumption_luxury_gating():
d = econ.workforce_consumption("pioneer", 100, 100, lux1=True, lux2=False)
assert d == {"DW": 4, "RAT": 4, "OVE": 0.5, "PWO": 0.2}
assert econ.workforce_consumption("pioneer", 100, 50, False, False)["DW"] == 2
def test_hab_optimizer_matches_prunplanner():
r = econ.optimize_habs(REQ, HAB_COSTS, "cost", max_area=135)
assert r["habs"] == {"HB1": 1, "HB2": 4, "HB3": 5} and r["cost"] == pytest.approx(1099203.64383138)
r = econ.optimize_habs(REQ, HAB_COSTS, "area")
assert r["area"] == 118
def test_matches_live_fio_smelter_efficiency():
"""Deimos SME, FIO /production 2026-09-18: Efficiency 1.3360869884. All pioneers, both luxuries met, Metallurgy COGC,
2 metallurgy experts, condition 0.9993602633."""
tier = dict(pioneer=econ.tier_efficiency(400, 370, True, True))
total, el = econ.building_efficiency(dict(pioneers=50), tier, expertise="METALLURGY", cogc="METALLURGY",
experts={"METALLURGY": 2}, condition=0.9993602633476257)
# model 1.33614 vs FIO 1.33609: agrees to 0.004%; residual unexplained (rounding of expert/condition in game)
assert total == pytest.approx(1.3360869884490967, rel=1e-4)
+21
View File
@@ -0,0 +1,21 @@
from puga import market, fio
def _book(monkeypatch):
monkeypatch.setattr(fio, "order_book", lambda m, c, r=False: {
"SellingOrders": [{"ItemCount": 10, "ItemCost": 110}, {"ItemCount": 5, "ItemCost": 100}],
"BuyingOrders": [{"ItemCount": 4, "ItemCost": 90}, {"ItemCount": 10, "ItemCost": 80}],
})
def test_buy_walks_asks_ascending(monkeypatch):
_book(monkeypatch)
r = market.walk("X", "AI1", 10, "buy")
assert r["total"] == 5 * 100 + 5 * 110 and r["worst"] == 110 and not r["short"]
def test_sell_walks_bids_descending_and_reports_shortfall(monkeypatch):
_book(monkeypatch)
r = market.walk("X", "AI1", 20, "sell")
assert r["filled"] == 14 and r["short"] and r["worst"] == 80
assert r["total"] == 4 * 90 + 10 * 80
+36
View File
@@ -0,0 +1,36 @@
import importlib.util, sys
from pathlib import Path
import pytest
spec = importlib.util.spec_from_file_location("plan_push", Path(__file__).resolve().parent.parent / "tools" / "plan_push.py")
pp_tool = importlib.util.module_from_spec(spec)
spec.loader.exec_module(pp_tool)
R = [
{"recipe_id": "SME#6xALO 1xO 1xC=>3xAL", "building_ticker": "SME", "inputs": [{"material_ticker": t} for t in ("ALO", "O", "C")], "outputs": [{"material_ticker": "AL"}]},
{"recipe_id": "SME#6xALO 1xO 1xC 1xFLX=>4xAL", "building_ticker": "SME", "inputs": [{"material_ticker": t} for t in ("ALO", "O", "C", "FLX")], "outputs": [{"material_ticker": "AL"}]},
]
def test_resolve_by_ticker_sets_distinguishes_flux():
assert pp_tool.resolve_recipe("ALO,FLX,C,O=>AL", "SME", R).endswith("=>4xAL")
assert pp_tool.resolve_recipe("ALO,C,O=>AL", "SME", R).endswith("=>3xAL")
assert pp_tool.resolve_recipe("EXT#ALO", "EXT", R) == "EXT#ALO"
with pytest.raises(ValueError):
pp_tool.resolve_recipe("ALO=>AL", "SME", R)
def _spec(**kw):
s = dict(name="[PuGa] x", planet="ZV-759c", permits=1, cogc="METALLURGY", buildings=[{"building": "SME", "amount": 1, "recipes": ["ALO,C,O=>AL"]}])
s.update(kw)
return s
def test_guardrail_requires_prefix_and_validates():
ok = pp_tool.build_payload(_spec(), R, {"SME"})
assert ok["plan_cogc"] == "METALLURGY" and len(ok["plan_data"]["experts"]) == 9 and len(ok["plan_data"]["workforce"]) == 5
for bad in (dict(name="My plan"), dict(planet="ZV759c"), dict(permits=4), dict(cogc="FOO")):
with pytest.raises(ValueError):
pp_tool.build_payload(_spec(**bad), R, {"SME"})
with pytest.raises(ValueError):
pp_tool.build_payload(_spec(buildings=[{"building": "ZZZ", "amount": 1}]), R, {"SME"})
+40
View File
@@ -0,0 +1,40 @@
import math
from puga import saturation as s, market, fio
def test_tref_uses_min_and_poisson_floor():
assert s.tref(60, 40) == 40
assert s.tref(15, 13) < 13 # low volume gets a lower bound
assert s.tref(0, 0) == 0
def test_n_out_penalises_queue_and_thin_demand():
free = s.n_out(100, 0, 10_000, 5) # no queue, ample demand
assert math.isclose(free, 0.25 * 100 / 5)
assert s.n_out(100, 7000, 10_000, 5) < free # 70 days of queue cuts it
assert s.n_out(100, 0, 100, 5) < free # demand only 1 day of flow cuts it
assert s.n_out(0, 0, 100, 5) == 0
def test_thin_flag():
assert s.is_thin(1.3, 100, 5) and not s.is_thin(60, 500, 5)
def test_p_patient_clamps_and_falls_to_bid_when_oversupplied():
asks = [(100, 50), (110, 50), (120, 50)]
assert s.p_patient(asks, 10, 20, bid=90, vwap7=105, vwap30=105, ask=100) == 90 # we out-produce flow: sell at bid
p = s.p_patient(asks, 30, 1, 90, 200, 200, 100) # ample flow: capped by ask level 100
assert p == 100
def test_walk_treats_mm_null_count_as_unlimited(monkeypatch):
monkeypatch.setattr(fio, "order_book", lambda m, c, r=False: {
"SellingOrders": [], "BuyingOrders": [{"ItemCount": 4, "ItemCost": 90}, {"ItemCount": None, "ItemCost": 80}]})
r = market.walk("X", "AI1", 1000, "sell")
assert r["filled"] == 1000 and not r["short"] and r["worst"] == 80
def test_effective_supply_ignores_stale_far_asks():
asks = [(100, 10), (110, 20), (150, 1500)]
assert s.effective_supply(asks, 100) == 30 # 1500 units at 1.5x are not competition
assert s.effective_supply(asks, None) == 1530