- puga/network.py + tools/network.py: model a multi-base chain (e.g. mine LST at one base, ship it, consume it at another). Simulates each base independently, nets a transferred material's producer-surplus against consumer-need, charges real freight only on what's moved, cm_free for a founding covered by a Core Module Kit. Worked example in plans/chains/. - puga/saturation.py: real price-band filtering. FIO's order_book NarrowPriceBandLow/High and WidePriceBandLow/High match APEX's own displayed Price Band exactly (verified live) - an order outside it is a stale artifact, not just uncompetitive. Added in_band() and effective_demand(); effective_supply() gained the same hard band filter alongside its existing soft vwap-proximity filter (renamed that param mult to free up `band`). Wired into tools/scan.py stage 2 in place of the raw, unfiltered demand figure. Was flagged as an unimplemented refinement in saturation-design.md since the original design review. - tools/sell.py: self-contained tranche split (aggressive tranche capped at a volume quantile, median normally or 80th pct with --tight when a payment is imminent and stockout risk outweighs margin; patient tranche priced under the next competitor tier). Instant-bid comparison moved behind --show-bid (off by default). - Docs and roadmap updated accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
132 lines
7.7 KiB
Markdown
132 lines
7.7 KiB
Markdown
# 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.
|
|
|
|
```sh
|
|
. .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 live `Efficiency`/`Condition` per
|
|
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 than `fio`'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 through `tools/plan_push.py`'s guardrails, don't call `request("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 calling `fio`/`prunplanner` separately. `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 with `ItemCount: None` as 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 behind `scan.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.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 <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.
|
|
|
|
## 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.
|