diff --git a/CLAUDE.md b/CLAUDE.md index 353a898..4e228db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,24 @@ Run as `puga ...` (e.g. `puga scan --min-n 3`, `puga plan pull `); - `tools/shiptrip.py SHIP FROM TO [--v0 X --v1 Y]` the simple ship-trip tool: give a ship registration + start + stop, it works out jumps/charges/landing fuel itself via `tools/route.py`'s jump graph and reports fuel cost (defaults to max sliders). Direct-jump routes only - an unconnected pair means gateway-only, and it tells you exactly what to read off the live APEX Ship Flight Control screen (the LOCK fee - genuinely not available via FIO/PRUNplanner, confirmed by inspecting both) and what to run instead (`tools/shipcost.py`). Warns if the given ship's live specs (FIO `/ship/ships/{user}`) don't match the calibrated hull/reactor. - `tools/shipcost.py [--jumps N --charges N] [--v0 X --v1 Y] [--gateways N --lock-fee AMOUNT:CURRENCY] [--to PLANET]` the explicit/manual version: fuel/AIC cost of a ship trip from physics parameters you already know (jump count, slider values, gateway LOCK fees read off the live game) - use `tools/shiptrip.py` instead if you just want "ship X from A to B". Landing fuel is a measured-planet lookup via `--to`, independent of whether the route used jumps or gateways. Both tools scoped to this account's one hull/reactor (blueprint BP-STRT-0000, shared by both ships AVI-07JFB/AVI-07JFC); open caveats (v0 relative to current fuel load, landing fuel confounded with gravity/distance, no second hull to cross-check) are in `puga/shipping.py`'s module docstring - don't trust magnitudes beyond what's there. Raw calibration data (APEX DOM reads, 2026-10-09): `empire/data/shipping_calibration/`. Also wired into `tools/network.py`'s freight cost (direct-jump transfers use the real model; gateway transfers need an explicit `trip_cost` in the chain YAML, else a 9250 AIC fallback). Library: `puga/shipping.py` (`trip_fuel()`, `round_trip_cost()`). - `tools/parkcash.py [--min-spread X] [--min-trade-days N] [--rate R] [--top N]` where to park idle cash as a buy-low/sell-high position (not a build) while ships are busy or cash has no better use right now - scans every ticker for a big bid/ask spread AND real trading liquidity (trade-days/gap over a calendar window - a huge spread with few trading days is a stale-quote trap, confirmed by hand: AST showed a 130% spread but its resting ask had gone stale after a 60% real price crash). Round-trip wait/value is EMPIRICAL (`puga.history.empirical_touch`, same model as `tools/sell.py --tiers`), not an assumed random walk - a driftless-walk version was tried and backtested wrong (overstated reachability 2-4x vs 3 years of real data, see `docs/decisions.md`). This dropped the whole universe's top pick from ~94%/day to ~1%/day with real waits of 19-194+ days - treat any number from the old model as stale. `--rate` defaults to 0.0 (matches the idle-cash premise; pass your realized growth rate if this cash IS earmarked) and shows up as a separate `discounted_pct` column (margin% x real odds both legs ever happen) alongside the simple non-compounding `daily_pct` used for ranking. Library: `puga/parkcash.py`. +- `tools/contd.py [--cx AI1]` evaluates this account's OPEN CONTD contract offers (the NPC logistics-officer + pool in APEX's CONTD panel, usually 3 at a time, only one acceptable) - payment, each required + material priced at real buy-fresh cost, and net. Deliberately leaves conditions it can't price + (EXPLORATION, REPAIR_SHIP, HEADQUARTERS_UPGRADE, BASE_CONSTRUCTION, CONTRIBUTION, WAIT, + INCREASE_SATISFACTION - real cost depends on context the API can't see, e.g. a ship already flying + that route) as flagged, unpriced conditions, and doesn't account for materials you may already hold + in storage - it's a breakdown for the caller to judge, not a verdict. Library: `puga/contd.py`. - `tools/lmscan.py PLANET [--direction source|destination] [--min-margin X]` live Local Market hauling-ad scan (FIO, public/no-auth - real player-posted freight jobs, not the CONTD player-contract system), ranked by margin after real fuel cost (`puga.shipping.leg_cost`). One-way cost only - margin assumes the next job is picked up near the destination, not an empty flight home. A small measured gateway-fee table covers ANT<->HRT/BEN; other gateway routes can't be priced (LOCK fee isn't available via any API). Library: `puga/contracts.py`. +- `tools/buildcost.py BUILDING [--planet ID] [--amount N] [--cx AI1]` full construction material + list for one building, priced at real fill cost; `--planet` adds that planet's environment-extra + materials (SEA/HSE/MGC/BL/INS/TSH - threshold table in `docs/mechanics.md`). Also wired into + `whatif.py`/`buy.py`/`scan.py`'s own capex numbers via `simulate(planet=...)` / `scan.py`'s + `bcost()`. Library: `puga/planet.py::environment_extras()`. +- `tools/actpkg.py NAME TICKER:QTY [TICKER:QTY ...] [--cx AI1] [-o file.json]` emits a Refined PrUn + (browser extension, github.com/refined-prun/refined-prun) `XIT ACT` action-package JSON for a buy + list - paste into the extension's Import Action Package to pre-fill the in-game CXPO buy tile per + material (player still clicks the real buy button - semi-auto, not full auto). NAME must be + letters/digits/spaces only; punctuation is stripped automatically. Library: `puga/actpkg.py`. - `tools/price.py` prices across exchanges with VWAP, volume, fill price for a quantity; `tools/book.py` order-book ladder. - `tools/prun_scan.py`, `tools/prun_cxarb.py` legacy top-of-book scans; overstate thin markets. Baseline only. `tools/arb.py` replacement is on the roadmap. - `puga/econ.py` pure formulas ported from PRUNplanner (efficiency stack, workforce, extraction, production I/O, hab optimizer); `tests/test_econ.py` holds the reference values. diff --git a/docs/mechanics.md b/docs/mechanics.md index 1666635..c5363ec 100644 --- a/docs/mechanics.md +++ b/docs/mechanics.md @@ -75,6 +75,36 @@ storage fee - this only confirms placing the order doesn't require a prerequisit that storage itself is free. Also still unconfirmed: FX conversion fee size (a forum thread titled "remove FX transaction fee entirely" implies one exists, but no amount confirmed). +## Planet environment-extra materials (per community handbook, confirmed live against Nike) +FIO's `/planet/full/{id}` -> `BuildRequirements` lists a superset across hazard types and is NOT +a direct per-building material list - compute it instead from the planet's own Pressure/Gravity/ +Temperature/Surface fields against these threshold rules (source: Prosperous Universe Handbook, +building-costs page): + +| condition | extra material | quantity | +|---|---|---| +| rocky | MCG | area x 4 | +| gaseous | AEF | area / 3 | +| pressure < 0.25 atm | SEA | area x 1 | +| pressure > 2.0 atm | HSE | 1/building | +| gravity < 0.25g | MGC | 1/building | +| gravity > 2.5g | BL | 1/building | +| temp < -25C | INS | area x 10 | +| temp > 75C | TSH | 1/building | + +Applies per building (construction AND repair), area-scaled rows use the building's own `AreaCost`. +Implemented in `puga/planet.py::environment_extras()`, used by `tools/buildcost.py` and wired into +`whatif.py`/`buy.py`/`scan.py`'s own capex numbers. + +## CX order cancellation penalty (UNCONFIRMED numbers, forum-sourced) +Deleting too many unfilled/partially-filled CX orders on the same ticker within a day triggers a +fee (added per dev patch notes to deter bot-style requoting); a warning appears around the 4th +attempt. No confirmed threshold or fee formula - forum reports range from 3 to 5-6 cancels/ticker/ +day, fee said to scale with order size. Effect: rate-limits requoting, so effective spreads widen +and top-of-book gets sticky/stale - chasing the top spot is still possible within the free +cancel budget (~3-5/ticker/day per forum reports), just be aware each reprice spends one of those +before the fee kicks in. + ## HQ in game vs PRUNplanner's "HQ" flag (observed 2026-09-21) - The HQ screen (level 1, base permits 1 / 2) shows "Efficiency gains: Electronics 20.0%", and 23.3% for the next level (permits 1 / 3). That is exactly the faction bonus formula (Antares electronics 5% x 2 x (3 - 2 x used/total)), so the HQ display is the faction bonus. - PRUNplanner's plan flag "HQ" (x1.1 on every building) is NOT visible in live efficiencies (smelters 1.3361, HWP 1.0696 both match the model without it). Keep it off. The HQ upgrade adds a base permit and raises the faction bonus. diff --git a/docs/roadmap.md b/docs/roadmap.md index be239da..db659e5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,6 +4,9 @@ Open work only - what's already built/fixed is discoverable from `CLAUDE.md`'s T `docs/library.md`, `docs/decisions.md`, and module docstrings, not repeated here. ## Todo +- [ ] `tools/network.py`/`puga/network.py`'s `combine()` doesn't pass `planet` into `simulate()` + yet (environment-extra construction materials, `puga/planet.py::environment_extras()`) - + `whatif.py`/`buy.py`/`scan.py` already do. - [ ] `tools/arb.py`: inter-CX arbitrage at real fill size (replaces `prun_cxarb.py`, which overstated profit 7-11x on 2/4 top hits). - [ ] `tools/haul.py`: fuel cost / profit per trip (route.py has the jump graph). diff --git a/puga/planet.py b/puga/planet.py index afc0786..5e27358 100644 --- a/puga/planet.py +++ b/puga/planet.py @@ -1,8 +1,33 @@ -"""Rank planets by extraction factor for a resource, with founding cost (environment extras -from FIO's own BuildRequirements - authoritative, not guessed from temperature/pressure -thresholds, which PRUNplanner's own calculations don't model either).""" +"""Rank planets by extraction factor for a resource. Also holds environment_extras(): FIO's own +BuildRequirements field is an unreliable superset across hazard types (confirmed wrong in-game, +see docs/mechanics.md) - the real per-building material is computed from the planet's own +pressure/gravity/temperature/surface against the Prosperous Universe Handbook's threshold table, +which PRUNplanner's own calculations don't model either.""" from . import fio +# (condition check, ticker, qty per unit area, flat qty) - handbook building-costs table +ENV_RULES = [ + (lambda p: (p.get("pressure") or 0) < 0.25, "SEA", 1.0, 0.0), + (lambda p: (p.get("pressure") or 0) > 2.0, "HSE", 0.0, 1.0), + (lambda p: 0 < (p.get("gravity") or 1) < 0.25, "MGC", 0.0, 1.0), + (lambda p: (p.get("gravity") or 0) > 2.5, "BL", 0.0, 1.0), + (lambda p: (p.get("temperature") if p.get("temperature") is not None else 20) < -25, "INS", 10.0, 0.0), + (lambda p: (p.get("temperature") or 0) > 75, "TSH", 0.0, 1.0), +] + + +def environment_extras(planet: dict, area: float) -> dict[str, float]: + """Extra construction/repair materials a building needs beyond its normal BuildingCosts and + the universal MCG area cost (already handled elsewhere, e.g. simulate()'s mcg_per_area) - one + row per hazard the planet actually has (pressure/gravity/temperature; rocky-vs-gaseous MCG/AEF + swap not covered here). planet needs lowercase pressure/gravity/temperature keys, e.g. from + prunplanner._g('/data/planet/{id}/') or fio.planets_full() remapped to lowercase.""" + out: dict[str, float] = {} + for cond, tk, per_area, flat in ENV_RULES: + if cond(planet): + out[tk] = out.get(tk, 0.0) + per_area * area + flat + return out + def candidates(resource_ticker: str, min_factor: float = 0.0): """All planets with this resource, sorted by factor descending. Each: PlanetNaturalId, diff --git a/puga/simulate.py b/puga/simulate.py index 3f948d9..8545220 100644 --- a/puga/simulate.py +++ b/puga/simulate.py @@ -2,6 +2,7 @@ Input is a PRUNplanner plan JSON (as stored by the API, or built by tools/plan_push.build_payload). Pure given the data passed in. Validated against a screenshot of the real simulator (tests/test_simulate.py).""" from . import econ +from . import planet as planet_mod TIERS = econ.TIERS FARMS = {"FRM", "ORC"} @@ -10,8 +11,8 @@ DEGRADATION_DAYS = 180 # usePlanCalculation.ts: degradation/day = const def simulate(plan: dict, recipes: list[dict], buildings: list[dict], resources: list[dict], fertility: float, price, faction: str | None = None, permits: tuple[float, float] = (1, 2), mcg_per_area: float = 4.0, - built: dict[str, int] | None = None) -> dict: - """price(ticker, side) -> AIC per unit or None; side is "buy" (net consumed) or "sell" (net produced), like PRUNplanner BUY/SELL exchange preferences; construction uses "buy". resources: planet resources [{material_ticker, resource_type, daily_extraction}].""" + built: dict[str, int] | None = None, planet: dict | None = None) -> dict: + """price(ticker, side) -> AIC per unit or None; side is "buy" (net consumed) or "sell" (net produced), like PRUNplanner BUY/SELL exchange preferences; construction uses "buy". resources: planet resources [{material_ticker, resource_type, daily_extraction}]. planet: optional, adds environment-extra construction materials via planet.environment_extras() (SEA/HSE/MGC/BL/INS/TSH - needs lowercase pressure/gravity/temperature keys).""" d = plan["plan_data"] bl = {b["building_ticker"]: b for b in buildings} rec = {r["recipe_id"]: r for r in recipes} @@ -69,7 +70,10 @@ def simulate(plan: dict, recipes: list[dict], buildings: list[dict], resources: def unit_cost(tk): info = bl[tk] - return sum(c["material_amount"] * (price(c["material_ticker"], "buy") or 0) for c in info["costs"]) + mcg_per_area * info["area_cost"] * (price("MCG", "buy") or 0) + cost = sum(c["material_amount"] * (price(c["material_ticker"], "buy") or 0) for c in info["costs"]) + mcg_per_area * info["area_cost"] * (price("MCG", "buy") or 0) + if planet is not None: + cost += sum(qty * (price(etk, "buy") or 0) for etk, qty in planet_mod.environment_extras(planet, info["area_cost"]).items()) + return cost # Verified vs simulator screenshots: degradation = production buildings' construction cost / 180 (infrastructure excluded); # plan cost and area also include one core module (CM), added automatically. diff --git a/tools/buildcost.py b/tools/buildcost.py new file mode 100644 index 0000000..4125688 --- /dev/null +++ b/tools/buildcost.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Full construction material list for one building, optionally on a specific planet (adds its +environment-extra materials via puga.planet.environment_extras() - SEA/HSE/MGC/BL/INS/TSH). + + puga buildcost HB1 + puga buildcost EXT --planet ZV-194a + puga buildcost PP1 --planet ZV-194a --amount 1 +""" +import argparse, sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from puga import config, fio, market, prunplanner as pp, planet as planet_mod + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("building", help="building ticker, e.g. HB1, EXT, PP1") + ap.add_argument("--planet", help="planet natural id - adds its environment-extra materials") + ap.add_argument("--amount", type=int, default=1) + ap.add_argument("--cx", default=config.DEFAULT_CX) + a = ap.parse_args() + + blds = {b["Ticker"]: b for b in fio.buildings()} + b = blds.get(a.building.upper()) + if not b: + sys.exit(f"unknown building {a.building!r}") + + need: dict[str, float] = {c["CommodityTicker"]: c["Amount"] for c in b["BuildingCosts"]} + need["MCG"] = need.get("MCG", 0) + 4 * b["AreaCost"] + + if a.planet: + planet = pp._g(f"/data/planet/{a.planet}/", 3600) + for tk, qty in planet_mod.environment_extras(planet, b["AreaCost"]).items(): + need[tk] = need.get(tk, 0) + qty + print(f"# {b['Ticker']} on {planet['planet_name']} ({a.planet}): pressure {planet['pressure']:.3f}, " + f"gravity {planet['gravity']:.2f}, temp {planet['temperature']:.1f}C") + + total = 0.0 + for tk, qty in sorted(need.items()): + qty *= a.amount + cost = market.walk(tk, a.cx, qty, "buy") + px = cost["total"] if not cost["short"] else None + total += px or 0 + print(f"{tk:4} {qty:8.1f} {px:>10,.0f} AIC" if px is not None else f"{tk:4} {qty:8.1f} (short on {a.cx})") + print(f"total: {total:,.0f} AIC" if total else "") + + +if __name__ == "__main__": + main() diff --git a/tools/buy.py b/tools/buy.py index 73c6a6f..691b30c 100755 --- a/tools/buy.py +++ b/tools/buy.py @@ -20,14 +20,16 @@ from pathlib import Path import yaml sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from puga import config, fio, market, prunplanner as pp +from puga import planet as planet_mod from puga.simulate import simulate sys.path.insert(0, str(Path(__file__).resolve().parent)) import plan_push -def construction_items(plan: dict, buildings: list, built: dict) -> dict: - """Materials needed for the plan's buildings/infra beyond what `built` (ticker -> count) already has.""" +def construction_items(plan: dict, buildings: list, built: dict, planet: dict | None = None) -> dict: + """Materials needed for the plan's buildings/infra beyond what `built` (ticker -> count) already has. + planet: optional, adds environment-extra materials (SEA/HSE/MGC/BL/INS/TSH) via planet.environment_extras().""" bl = {b["building_ticker"]: b for b in buildings} d = plan["plan_data"] need: dict[str, float] = {} @@ -39,6 +41,9 @@ def construction_items(plan: dict, buildings: list, built: dict) -> dict: for c in info["costs"]: need[c["material_ticker"]] = need.get(c["material_ticker"], 0) + c["material_amount"] * gap need["MCG"] = need.get("MCG", 0) + 4 * info["area_cost"] * gap + if planet is not None: + for etk, qty in planet_mod.environment_extras(planet, info["area_cost"]).items(): + need[etk] = need.get(etk, 0) + qty * gap return need @@ -101,9 +106,9 @@ def main(): perm = (st.get("permits", {}).get("used", 1), st.get("permits", {}).get("total", 2)) planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600) price_ask = lambda t, side="buy": (lambda q: q.ask if q else None)(market.snapshot().get((t, a.cx))) - r = simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price_ask, faction, perm) + r = simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price_ask, faction, perm, planet=planet) - con = net_of_on_hand(construction_items(plan, blds, built), on_hand) + con = net_of_on_hand(construction_items(plan, blds, built, planet), on_hand) stk = net_of_on_hand(stock_items(r["flows"], a.days), on_hand) con_rows, con_total, con_w, con_v = price(con, a.cx) stk_rows, stk_total, stk_w, stk_v = price(stk, a.cx) diff --git a/tools/scan.py b/tools/scan.py index 1c5e4fa..690fbae 100755 --- a/tools/scan.py +++ b/tools/scan.py @@ -13,6 +13,7 @@ from pathlib import Path import yaml sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from puga import ROOT, config, econ, fio, market, prunplanner as pp, saturation as sat +from puga import planet as pl TIER_CODE = {"pioneer": "P", "settler": "S", "technician": "T", "engineer": "E", "scientist": "Sc"} TIER_RANK = {"P": 0, "S": 1, "T": 2, "E": 3, "Sc": 4} @@ -83,7 +84,14 @@ def main(): parts = [(x["Amount"], ask(x["CommodityTicker"])) for x in b["BuildingCosts"]] if any(p is None for _, p in parts): return None - return sum(n * p for n, p in parts) + a.mcg * b["AreaCost"] * mcg + cost = sum(n * p for n, p in parts) + a.mcg * b["AreaCost"] * mcg + if planet is not None: + extras = pl.environment_extras(planet, b["AreaCost"]) + prices = {etk: ask(etk) for etk in extras} + if any(p is None for p in prices.values()): + return None + cost += sum(qty * prices[etk] for etk, qty in extras.items()) + return cost hab_head, hab_area = {}, {} for tier, hab in (("pioneer", "HB1"), ("settler", "HB2"), ("technician", "HB3"), ("engineer", "HB4"), ("scientist", "HB5")): diff --git a/tools/whatif.py b/tools/whatif.py index 59f026d..63acc4d 100644 --- a/tools/whatif.py +++ b/tools/whatif.py @@ -127,7 +127,7 @@ def marginal_freight(r_b, r_a, planet_id, cx, sf_price, ff_price, mats): def run(spec, recipes, blds, price, faction, perm, built): plan = plan_push.build_payload(spec, recipes, {b["building_ticker"] for b in blds}) planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600) - return plan, simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm, built=built) + return plan, simulate(plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm, built=built, planet=planet) def main():