From e2a5149788ffa90f31000e6934f9b3099edf7add Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Tue, 20 Oct 2026 13:05:51 +0200 Subject: [PATCH] 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 --- puga/contd.py | 79 +++++++++++++++++++++++++++++++------------------- tools/contd.py | 27 ++++++++++------- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/puga/contd.py b/puga/contd.py index 2e6acad..1ee9a68 100644 --- a/puga/contd.py +++ b/puga/contd.py @@ -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, } diff --git a/tools/contd.py b/tools/contd.py index 34a555c..15c3a8a 100644 --- a/tools/contd.py +++ b/tools/contd.py @@ -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__":