contd.py: fix condition direction, separate costs/rewards/freight legs

PAYMENT/REPUTATION always go to the player; PROVISION_SHIPMENT (a cost) and
PICKUP/DELIVERY_SHIPMENT (a freight leg the player may or may not fly) depend
on the condition's own Party field vs the contract's Party, not a flat
assumption that every material condition is something the player buys. Net
now correctly adds COMEX_PURCHASE_PICKUP rewards instead of only subtracting
provisioning costs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 13:05:51 +02:00
co-authored by Claude Sonnet 5
parent bf15c709ea
commit e2a5149788
2 changed files with 65 additions and 41 deletions
+49 -30
View File
@@ -3,19 +3,24 @@ offers shown in APEX's CONTD panel, not the public Local Market hauling ads in p
Only OPEN contracts are real choices - exactly one of the pool of (usually 3) offers can be accepted;
CANCELLED/REJECTED/FULFILLED/PARTIALLY_FULFILLED are history or already-committed work.
Condition vocabulary seen in this account's full history (2026-10-16): PAYMENT, REPUTATION,
PROVISION_SHIPMENT and COMEX_PURCHASE_PICKUP (a material+qty+address you must supply - these are the
only ones with a real market buy-fresh cost), PICKUP_SHIPMENT/DELIVERY_SHIPMENT (logistics legs, no
separate cost beyond the material), and open-ended ones this module deliberately does NOT price
because their real cost depends on context this API can't see (a ship already flying that route, a
planet you were about to survey anyway, a ship already damaged): EXPLORATION (Address = planet to
survey), REPAIR_SHIP, HEADQUARTERS_UPGRADE, BASE_CONSTRUCTION, CONTRIBUTION, WAIT,
INCREASE_SATISFACTION. Judging whether one of those is actually cheap right now is the caller's job.
Each condition's own `Party` field ("CUSTOMER" or "PROVIDER") says who performs it, checked
against the contract's own top-level `Party` field (the player's role in that contract - usually
"CUSTOMER"). Condition vocabulary, direction confirmed against real contract JSON (2026-10-17):
PAYMENT/REPUTATION (always granted TO the player by the PROVIDER, regardless of which `Party` the
condition lists), PROVISION_SHIPMENT (the player's `Party` supplies this material - a real COST,
buy-fresh-priced), COMEX_PURCHASE_PICKUP (the partner already bought this via Comex - a REWARD
handed to the player, sell-valued), PICKUP_SHIPMENT/DELIVERY_SHIPMENT (a logistics leg performed
by whichever `Party` it lists - free to the player when that's the PROVIDER's leg, e.g. their
transporter collecting what you provisioned; a real freight cost via `puga.shipping` when it's the
player's own leg, not priced by this module, flagged with weight/volume/address instead), and
open-ended ones this module deliberately does NOT price because their real cost depends on context
this API can't see (a ship already flying that route, a planet you were about to survey anyway, a
ship already damaged): EXPLORATION (Address = planet to survey), REPAIR_SHIP,
HEADQUARTERS_UPGRADE, BASE_CONSTRUCTION, CONTRIBUTION, WAIT, INCREASE_SATISFACTION. Judging
whether one of those is actually cheap right now is the caller's job.
"""
from . import fio, market
MATERIAL_TYPES = {"PROVISION_SHIPMENT", "COMEX_PURCHASE_PICKUP"}
def open_contracts():
me = fio.me()
@@ -24,36 +29,48 @@ def open_contracts():
def evaluate(contract: dict, cx: str) -> dict:
"""Payment, priced material requirements (buy-fresh cost at `cx` - assumes none already in free
stock, check storage for materials you may already hold), and any unpriced/contextual conditions
flagged separately for the caller to judge."""
"""Payment, materials the contract hands TO the player (COMEX_PURCHASE_PICKUP, sell-valued),
materials the player must supply (PROVISION_SHIPMENT on the player's own Party, buy-fresh
cost), freight legs the player must personally fly (PICKUP/DELIVERY_SHIPMENT on the player's
own Party - weight/volume flagged, not priced), and any other unpriced/contextual conditions -
all for the caller to weigh. net = payment + reward_value - cost_value (freight excluded,
flagged separately)."""
my_party = contract.get("Party") # the player's role in THIS contract, e.g. "CUSTOMER"
payment = 0.0
reputation = False
requirements = [] # (ticker, qty, address)
flags = [] # (type, address) for conditions this module won't price
rewards, costs, freight_legs = [], [], []
flags = []
deadline_days = 0.0
for cond in contract["Conditions"]:
t = cond["Type"]
mine = cond.get("Party") == my_party
if t == "PAYMENT" and cond.get("Amount"):
payment += cond["Amount"]
elif t == "REPUTATION":
reputation = True
elif t in MATERIAL_TYPES and cond.get("MaterialTicker"):
requirements.append((cond["MaterialTicker"], cond["MaterialAmount"], cond.get("Address")))
elif t not in MATERIAL_TYPES | {"PAYMENT", "REPUTATION", "PICKUP_SHIPMENT", "DELIVERY_SHIPMENT"}:
elif t == "COMEX_PURCHASE_PICKUP" and cond.get("MaterialTicker"):
rewards.append((cond["MaterialTicker"], cond["MaterialAmount"], cond.get("Address")))
elif t == "PROVISION_SHIPMENT" and cond.get("MaterialTicker") and mine:
costs.append((cond["MaterialTicker"], cond["MaterialAmount"], cond.get("Address")))
elif t in ("PICKUP_SHIPMENT", "DELIVERY_SHIPMENT") and mine:
freight_legs.append((t, cond.get("Address"), cond.get("Weight"), cond.get("Volume")))
elif t not in ("PAYMENT", "REPUTATION", "COMEX_PURCHASE_PICKUP", "PROVISION_SHIPMENT",
"PICKUP_SHIPMENT", "DELIVERY_SHIPMENT"):
flags.append((t, cond.get("Address")))
dl = cond.get("DeadlineDurationMs")
if dl:
deadline_days = max(deadline_days, dl / 86400000)
material_cost = 0.0
short = False
priced = []
for tick, qty, addr in requirements:
r = market.walk(tick, cx, qty, "buy")
priced.append({"ticker": tick, "qty": qty, "address": addr, "cost": r["total"], "short": r["short"]})
material_cost += r["total"]
short = short or r["short"]
def price_all(items, side):
total, priced = 0.0, []
for tick, qty, addr in items:
r = market.walk(tick, cx, qty, side)
priced.append({"ticker": tick, "qty": qty, "address": addr, "value": r["total"], "short": r["short"]})
total += r["total"]
return total, priced
reward_value, priced_rewards = price_all(rewards, "sell")
cost_value, priced_costs = price_all(costs, "buy")
return {
"id": contract["ContractLocalId"],
@@ -61,9 +78,11 @@ def evaluate(contract: dict, cx: str) -> dict:
"payment": payment,
"reputation": reputation,
"deadline_days": deadline_days,
"requirements": priced,
"material_cost": material_cost,
"net": payment - material_cost,
"short": short,
"rewards": priced_rewards,
"reward_value": reward_value,
"costs": priced_costs,
"cost_value": cost_value,
"freight_legs": freight_legs,
"net": payment + reward_value - cost_value,
"flags": flags,
}
+16 -11
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python3
"""List this account's OPEN CONTD contract offers (the NPC logistics-officer pool shown in APEX's
CONTD panel - usually 3 at a time, only one acceptable) with payment, priced material requirements,
and net. Judging the actual best deal is NOT this tool's job for anything it flags as unpriced
(EXPLORATION, REPAIR_SHIP, etc. - see puga/contd.py's docstring) or for situational cheapness (a ship
already flying that route, material already in free/uncommitted storage) - it reports the breakdown,
the caller reasons about context.
CONTD panel - usually 3 at a time, only one acceptable) with payment, materials received/supplied,
freight legs the player must personally fly, and net (see puga/contd.py's docstring for exactly
which condition types are costs vs rewards vs the player's own job). Judging the actual best deal
is NOT this tool's job for anything it flags as unpriced (EXPLORATION, REPAIR_SHIP, etc.) or for
a freight leg's real fuel cost (flagged with weight/volume, price via puga.shipping yourself) - it
reports the breakdown, the caller reasons about context.
puga contd
puga contd --cx AI1
@@ -30,18 +31,22 @@ def main():
print(f"\n{r['id']} partner={r['partner']} deadline {r['deadline_days']:.1f}d"
+ (" +reputation" if r["reputation"] else ""))
print(f" payment: {r['payment']:,.2f} AIC")
for req in r["requirements"]:
flag = " SHORT: book can't fill this qty" if req["short"] else ""
print(f" requires: {req['ticker']} x{req['qty']:g} @ {req['address']} buy-fresh cost {req['cost']:,.0f}{flag}")
for rew in r["rewards"]:
print(f" receives: {rew['ticker']} x{rew['qty']:g} @ {rew['address']} market value {rew['value']:,.0f}")
for cost in r["costs"]:
flag = " SHORT: book can't fill this qty" if cost["short"] else ""
print(f" must supply: {cost['ticker']} x{cost['qty']:g} @ {cost['address']} buy-fresh cost {cost['value']:,.0f}{flag}")
for ltype, addr, wgt, vol in r["freight_legs"]:
print(f" YOUR FREIGHT LEG: {ltype} @ {addr} {wgt:g}t / {vol:g}m3 - price via puga.shipping, not included in net")
for ftype, addr in r["flags"]:
print(f" UNPRICED CONDITION: {ftype}" + (f" @ {addr}" if addr else "") + " - context-dependent, judge separately")
print(f" material cost: {r['material_cost']:,.0f} -> net (payment - buy-fresh cost): {r['net']:,.0f}")
print(f" reward value: {r['reward_value']:,.0f} cost value: {r['cost_value']:,.0f} -> net (payment + reward - cost): {r['net']:,.0f}")
priceable = [c for c in contracts if not contd.evaluate(c, a.cx)["flags"]]
if len(priceable) == len(contracts):
best = max(contracts, key=lambda c: contd.evaluate(c, a.cx)["net"])
print(f"\nby buy-fresh net alone, best priced: {best['ContractLocalId']}"
" - but check storage for materials you may already hold before trusting this")
print(f"\nby payment+reward net alone, best priced: {best['ContractLocalId']}"
" - unpriced/flagged conditions (freight legs, exploration, etc.) still need judgment")
if __name__ == "__main__":