CLAUDE.md/docs/roadmap.md/docs/library.md: register tools/shiptrip.py, tools/shipcost.py, tools/lmscan.py, and puga/shipping.py/puga/contracts.py per this project's usual pattern of recording new tools where every other tool is listed. docs/mechanics.md: new "Gateway (GTW) jump transit time" section - our own measured GTW segment durations scale ~linearly with parsec distance (~20 min/parsec across 3 distinct distances), contradicting a 2024 pre-launch forum comment about a possibly-fixed transit time. Also notes the handbook-confirmed facts not modelled anywhere yet: one-way gateway links and per-gateway max jump volume. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
Using puga as a library (not the CLI)
The tools/*.py scripts cover the common questions. For anything more involved — a custom
what-if, a one-off cost breakdown with wages and capex, a market check the CLI doesn't expose —
import the modules directly and write a short script inline. This is normal; most answers in
this project's history came from a python - <<EOF ... EOF block, not a tool flag.
. .venv/bin/activate # or use .venv/bin/python directly
python - <<'EOF'
from puga import market, econ, fio, prunplanner as pp, saturation as sat
...
EOF
Modules and what they give you
puga.fio— public FIO REST, cached.exchange_all(),order_book(mat, cx)(full book,BuyingOrders/SellingOrders),recipes(),buildings(),materials(),planets_full(),workforce_needs().own(path, ttl=0)for the player's authenticated data (/sites/{u},/storage/{u},/production/{u}— has liveEfficiency/Conditionper building,/workforce/{u}/{planet},/ship/ships/{u},/ship/flights/{u}).me()gives the configured username.puga.prunplanner—exchanges()(has vwap_daily/7d/30d, traded volume — richer than FIO's book totals),recipes()/buildings()(different field names thanfio's:building_ticker,material_ticker, snake_case),_g(path, ttl)for any other/data/...GET (e.g._g(f"/data/planet/{id}/", 3600)for fertility, resources, COGC, build requirements).request(method, path, body)for authenticated calls (Api-Key) — reads are always fine; writes must go throughtools/plan_push.py's guardrails, don't callrequest("POST"/"PUT"/"DELETE", "/planning/plan/...")directly.puga.market—snapshot(refresh=False)returns{(ticker, cx): Quote}merging FIO book totals with PRUNplanner's VWAP/volume; this is the one-stop source for prices, use it instead of callingfio/prunplannerseparately.walk(mat, cx, qty, side, refresh=False)walks the live order book for a real fill price (side="buy"hits asks,"sell"hits bids; handles market-maker orders withItemCount: Noneas unlimited).uni30(snap, tk)= PRUNplanner's "Universe 30D" price basis (volume-weighted 30d VWAP across all CX).puga.econ— pure formulas, no I/O, ported from PRUNplanner and tested against its own suite plus live FIO:tier_efficiency,workforce_consumption,building_efficiency,production_io,daily_extraction/extraction_cycle,optimize_habs,CONSUMPTION(the per-tier need table),HAB_CAP/HAB_AREA,TIERS. This is what you use for "what if I understaff this building" or "what's the real wage bill for N heads" — the CLI tools only expose fixed slices of this.puga.saturation—tref,n_out,p_patient,effective_supply,is_thin: the depth/persistence primitives behindscan.py. Reuse these instead of re-deriving a "how many buildings can the market take" estimate by hand.puga.simulate—simulate(plan, recipes, buildings, resources, fertility, price, ...): the PRUNplanner-simulator replica. Reuse this for anything plan-shaped rather than reimplementing efficiency/workforce/flows again.puga.route—load_graph(),resolve_system(query)(planet/system natural id or name -> SystemId),shortest_path(src, dst, graph=None)(Dijkstra by straight-line ly over FIO's/systemstarsjump graph; returns jumps, distance_ly, path). Not a PRUNplanner feature, built for this project since travel isn't modelled anywhere else. CLI:tools/route.py.puga.shipping—cruise_leg_fuel(v0),jump_fuel(v1),charge_fuel(v1),gateway_hop_cost(lock_fee_amount, lock_fee_currency),landing_fuel(planet_natural_id)(lookup only,Noneif unmeasured),trip_fuel(...)(one-way total, shared by the CLIs),leg_cost(src, dst, sf_price, ff_price, v0=1.0, v1=1.0)(one-way AIC cost; usesKNOWN_GATEWAY_ROUTESwhen the pair has a measured gateway chain, else the direct-jump graph;Noneif neither applies),round_trip_cost(...)(there-and-back via twoleg_costcalls,Noneif either leg is unpriceable or carries a non-AIC fee - used bytools/network.py's freight cost): real ship flight fuel model, linear in the fuel-usage (v0) / reactor-usage (v1) sliders, additive/no cross-term. Scoped to this account's one hull/reactor (BP-STRT-0000, both ships confirmed identical); see the module docstring for confirmed-vs-open caveats before trusting a number. CLIs:tools/shiptrip.py(ship+from+to, auto-computes everything viapuga.route),tools/shipcost.py(explicit jumps/sliders/gateway-fees, for when you already know the breakdown). Raw calibration data:empire/data/shipping_calibration/.puga.contracts—scan(planet, direction, sf_price, ff_price, v0=1.0, v1=1.0): live Local Market hauling ads (FIO/localmarket/shipping/{source,destination}), each enriched with real one-way fuel cost (puga.shipping.leg_cost) and margin. CLI:tools/lmscan.py.puga.undervalued—manufacturing_candidates(),current_margin(ticker, recipe, cx),scan_current(cx, min_vol=5.0): manufactured goods priced below implied production cost, for a buy-and-hold-for-reversion trade, not a build decision. Backtested expected return is ~0.10-0.24%/day - far below production's 14-29%/day, only worth excess idle capital. CLI:tools/undervalued.py. Sharespuga.history.daily_priceswithtools/history.py.puga.planet—candidates(resource_ticker, min_factor=0.0): every planet with that resource, sorted by extraction factor, each with temperature/pressure/gravity/fertility/faction andbuild_requirements(FIO's own founding-cost materials for that specific planet — real data, not guessed environment thresholds). CLI:tools/planet.py, pass--froman existing base or pure-factor ranking is misleading (best-factor planets are often thousands of ly away).puga.config—get(key)reads.env;state_path()returns the realempire/state/company.yamlif synced, else the tracked example.
Patterns that come up a lot
Price a basket at real fill cost (construction lists, working stock, a chain's inputs):
from puga import market, fio
mat = {m["Ticker"]: m for m in fio.materials()}
def price(items, cx="AI1"):
tot = wt = 0
for t, q in items.items():
w = market.walk(t, cx, q, "buy"); tot += w["total"]; wt += q * mat[t]["Weight"]
return tot, wt
Building construction cost incl. MCG, using either fio.buildings() (Ticker,
BuildingCosts, AreaCost) or prunplanner.buildings() (building_ticker, costs,
area_cost) — pick one client and stay consistent within a script, the field names differ.
Efficiency of a hypothetical building, e.g. staffing/COGC/expert what-ifs:
from puga import econ
eff, elements = econ.building_efficiency(
dict(pioneers=40, settlers=10), {"pioneer": 1.0, "settler": 1.0},
expertise="METALLURGY", cogc="METALLURGY", experts={"METALLURGY": 2},
faction="ANTARES", permits_used=1, permits_total=2)
Live production truth (skip the model, read the game's own number):
from puga import fio
for line in fio.own(f"/production/{fio.me()}", ttl=0):
print(line["Type"], line["Efficiency"], line["Condition"])
A recipe's daily flows for N buildings:
io = econ.production_io([dict(time_ms=r["TimeMs"], inputs=..., outputs=...)], eff, n_buildings)
puga.simulate.simulate() only knows what the plan tells it
simulate() (and anything built on it, e.g. tools/buy.py) computes material flows from the
plan_data's own building list — it has no idea what else exists at the base beyond the built
dict used for the construction-gap check. Pass a plan covering everything whose production or
consumption matters, not just the piece you're adding, or a self-sufficient material will look
like something you need to buy (a standalone HWP plan says "buy 42.8 AL/day"; the full base+HWP
plan nets that against the smelters to ~0). This bit a first version of tools/buy.py; caught by
comparing it against the by-hand restock calc, not by a unit test (the pure functions were right,
the plan file chosen was wrong).
Multi-base chains: puga.network
For a decision that spans two or more bases (mine LST at Nike, ship it, turn it into BSE at
Deimos), don't price the transferred material at market on both ends — that double counts a trade
that never happens. puga.network.combine() simulates each base independently (each base's own
profit is still correct market-priced), then nets any transferred material between a producer's
surplus and a consumer's need, charging only the real freight for what's actually moved; leftover
surplus is still validly sold at market by the source, leftover deficit still validly bought by the
destination — those are already right in each base's own numbers, so combine() only adds the one
correction neither side has. CLI: tools/network.py <chain.yaml>, spec format and a worked example
(Nike LST -> Deimos PP2/BSE) in plans/chains/.
Two gotchas specific to this: (1) each base's plan must be self-contained (see above) - Nike's plan
needs its OWN housing even though Deimos's doesn't compete for it; (2) a NEW base's plan always
prices a fresh core module unless the founding source (e.g. a Core Module Kit) actually covers it -
mark it with cm_free: true on that base in the chain spec (mirrors tools/simulate.py --cm-free),
or the phantom ~200k core-module cost craters an otherwise-fine ROI. (3) each base's reported
profit/day is for its WHOLE plan, not the marginal addition - for "is this new building worth it",
also simulate the base without the addition and diff, same as tools/simulate.py's built-vs-planned
new_capex logic but applied to profit instead of capex.
Multi-base state: storage and ships are now tagged by location
Once a 2nd base exists, empire/state/company.yaml's storage list has multiple STORE entries
(one per base) and must be filtered by planet (a field tools/state.py now adds, from FIO's
AddressableId matching a site's SiteId) - summing all STORE entries blindly (as tools/buy.py
did briefly) mixes bases' inventories together. Similarly each entry in ships now carries a
flight dict (origin, destination, eta_ms) from live /ship/flights, keyed to the ship via
ShipId (not StlFuelStoreId, despite that looking like a plausible match - verified 2026-09-28).
tools/runway.py's inbound_ships()/on_hand() are the reference implementation for scoping to
one base correctly; reuse them rather than re-deriving this.
Conventions to keep
- Cache TTLs matter: market data is cached ~15 min, static game data ~24h (
puga/cache.py). Passrefresh=Trueif you just changed something in-game and need it now. fiouses PascalCase ticker/field names (MaterialTicker,TimeMs);prunplanneruses snake_case (material_ticker,time_ms). Don't mix them in the same dict without converting.- Never call
prunplanner.requestwith a write verb outsidetools/plan_push.py— the[PuGa]name guard and dry-run default live there, not in the client. - One-off scripts like these are fine to throw away; if a calculation gets reused more than once or two, promote it into a function in the relevant module instead of re-deriving it in a shell heredoc each time.