# 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 - < SystemId), `shortest_path(src, dst, graph=None)` (Dijkstra by straight-line ly over FIO's `/systemstars` jump 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.planet`** — `candidates(resource_ticker, min_factor=0.0)`: every planet with that resource, sorted by extraction factor, each with temperature/pressure/gravity/fertility/faction and `build_requirements` (FIO's own founding-cost materials for that specific planet — real data, not guessed environment thresholds). CLI: `tools/planet.py`, pass `--from` an 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 real `empire/state/company.yaml` if 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): ```python 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: ```python 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): ```python 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**: ```python 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 `, 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`). Pass `refresh=True` if you just changed something in-game and need it now. - `fio` uses PascalCase ticker/field names (`MaterialTicker`, `TimeMs`); `prunplanner` uses snake_case (`material_ticker`, `time_ms`). Don't mix them in the same dict without converting. - Never call `prunplanner.request` with a write verb outside `tools/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.