Add buy/sell tools, library docs, and permit/HQ fixes
- tools/buy.py: cash-aware shopping list (construction gap vs empire state's built buildings, plus N days of NET operating stock via simulate() so self-produced inputs net against consumption), priced at real order-book fill cost, checked against cash/reserve, flags multi-trip, binary-searches an affordable size when short - tools/sell.py: where to post an ask vs hitting the bids now, with expected clear time from 30-day traded-volume percentiles - docs/library.md: module map and patterns for using puga.* directly instead of the CLI, plus the "simulate() only knows what the plan lists" gotcha that a first version of buy.py hit - tools/simulate.py: fix --no-hq being a no-op; tools/state.py: permits_total_override for when the in-game HQ screen disagrees with FIO's MaximumPermits - docs/mechanics.md: HQ display vs PRUNplanner's HQ flag, workforce arrival sources - CLAUDE.md, docs/roadmap.md updated Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+110
@@ -0,0 +1,110 @@
|
||||
# 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).
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user