From bf15c709ea2069a59d687cd2472367eb8d01d396 Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Sat, 17 Oct 2026 19:03:51 +0200 Subject: [PATCH] Add planet environment-extra materials to capex calcs; buildcost.py tool Every building needs a planet-hazard material bundle on top of its normal BuildingCosts (SEA for low pressure, HSE for high pressure, MGC for low gravity, BL for high gravity, INS for cold, TSH for heat - area-scaled where applicable), confirmed against live in-game build panels on Nike. FIO's own BuildRequirements field is an unreliable superset across hazard types, not a direct per-building list - don't use it directly. - puga/planet.py::environment_extras(): the real threshold table. - tools/buildcost.py: full construction material list for one building, optionally on a given planet. - Wired into whatif.py/buy.py (via simulate(planet=...)) and scan.py's own bcost(), so their capex numbers stop silently omitting this. - docs/mechanics.md: the rule, plus a separate forum-sourced note on the CX order-cancellation penalty. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 17 ++++++++++++++++ docs/mechanics.md | 30 ++++++++++++++++++++++++++++ docs/roadmap.md | 3 +++ puga/planet.py | 31 ++++++++++++++++++++++++++--- puga/simulate.py | 10 +++++++--- tools/buildcost.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++ tools/buy.py | 13 ++++++++---- tools/scan.py | 10 +++++++++- tools/whatif.py | 2 +- 9 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 tools/buildcost.py 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():