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:
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"})
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user