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 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,24 @@ Run as `puga <tool> ...` (e.g. `puga scan --min-n 3`, `puga plan pull <uuid>`);
|
||||
- `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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
+28
-3
@@ -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,
|
||||
|
||||
+7
-3
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
+9
-4
@@ -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)
|
||||
|
||||
+9
-1
@@ -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")):
|
||||
|
||||
+1
-1
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user