|
| 1 | +# Porting an SCML OneShot agent to the CodeClash `decide()` contract |
| 2 | + |
| 3 | +You are porting ONE competition agent from `yasserfarouk/scml-agents` into a single |
| 4 | +self-contained `scml_agent.py` that defines **one function**: `decide(observation)`. |
| 5 | +A fully-worked, validated reference port lives at |
| 6 | +`configs/ablations/ladder/scml_pilot/scml_agent.py` (GreedyOneShotAgent) — read it first; |
| 7 | +mirror its structure and defensive style. |
| 8 | + |
| 9 | +## The runtime (how your `decide` is called) |
| 10 | + |
| 11 | +The trusted runtime owns a real SCML agent (base class `GreedySyncAgent`) and calls your |
| 12 | +`decide(observation)` at each negotiation turn. `observation` is a plain dict: |
| 13 | + |
| 14 | +``` |
| 15 | +observation = { |
| 16 | + "event": "propose" | "respond", # which decision is being asked |
| 17 | + "player": "<name>", |
| 18 | + "negotiator_id": "<id>" | None, |
| 19 | + "awi": { # agent-world-info (all plain numbers/lists) |
| 20 | + "current_step", "n_steps", "n_lines", "max_n_lines", |
| 21 | + "current_balance", "current_inventory", |
| 22 | + "current_exogenous_input_quantity", "current_exogenous_input_price", |
| 23 | + "current_exogenous_output_quantity", "current_exogenous_output_price", |
| 24 | + "current_disposal_cost", "current_shortfall_penalty", |
| 25 | + "my_input_product", "my_output_product", |
| 26 | + "is_first_level", "is_middle_level", "is_last_level", |
| 27 | + "needed_sales", "needed_supplies", # <-- USE THESE for "how much do I still need" |
| 28 | + "my_suppliers", "my_consumers", |
| 29 | + }, |
| 30 | + "nmi": { # negotiation-mechanism-info |
| 31 | + "annotation": { "product": <int>, "buyer": <id>, "seller": <id>, ... }, |
| 32 | + "issues": [ {"name","min","max","values"}, # [0]=QUANTITY, [1]=TIME, [2]=UNIT_PRICE |
| 33 | + {...}, {...} ], |
| 34 | + }, |
| 35 | + "state": { "step": <int>, "relative_time": <float 0..1>, "current_offer": [q,t,up] | None }, |
| 36 | + # on "respond": observation["current_offer"] and/or state["current_offer"] holds the opponent offer |
| 37 | + "fallback_offer": [q,t,up] | None, # what the greedy fallback would offer |
| 38 | + "fallback_response": "accept"|"reject"|"end", |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +### What to return |
| 43 | +- On `event == "propose"`: `{"offer": [quantity, time, unit_price]}` — three **ints**, each |
| 44 | + within its issue's `[min, max]`. Out-of-range/non-int → counted as a policy error, fallback used. |
| 45 | +- On `event == "respond"`: `{"response": "accept" | "reject" | "end"}`. When rejecting you MAY |
| 46 | + include a counter: `{"response": "reject", "offer": [q,t,up]}`. |
| 47 | +- Return `{}` or `None` at any point to defer to the trusted greedy fallback (safe default). |
| 48 | + |
| 49 | +### Hard rules |
| 50 | +- **Never import `scml`, `negmas`, `numpy`, or anything outside the stdlib.** The port must be |
| 51 | + pure-Python stdlib only — you only have the observation dict. Re-express the strategy's math |
| 52 | + from scratch. |
| 53 | +- **Never raise.** Wrap the body in `try/except` returning `{}` (see reference). An unhandled |
| 54 | + exception floors the score. |
| 55 | +- `is_selling` = `nmi["annotation"].get("product") == awi.get("my_output_product")`. |
| 56 | +- "How much I still need" = `awi["needed_sales"]` if selling else `awi["needed_supplies"]`. |
| 57 | +- Concession over time: use `state["relative_time"]` (0 at start → 1 at deadline) instead of |
| 58 | + `state.step / nmi.n_steps` (n_steps isn't exposed on nmi). |
| 59 | +- No cross-call state is guaranteed; if the original kept per-step memory (opponent price models, |
| 60 | + `on_negotiation_success`), you may keep **module-level** dicts keyed by negotiator_id, but you |
| 61 | + get NO success/step callbacks — approximate or drop that refinement and note it in the docstring. |
| 62 | + |
| 63 | +## Mapping the source's methods |
| 64 | +- source `propose(negotiator_id, state)` / `first_proposals` → your `event=="propose"` branch. |
| 65 | +- source `respond(...)` / `counter_all(offers, states)` → your `event=="respond"` branch. |
| 66 | +- source `best_offer` / price helpers (`_find_good_price`, `_price_range`, `_th`) → inline as |
| 67 | + plain functions over the observation (reference port shows the pattern). |
| 68 | + |
| 69 | +## If the bot is RL / learned / infeasible |
| 70 | +Some agents (q-learning, PPO, regression on trained weights) can't be reproduced without their |
| 71 | +weights/deps. In that case: port the **heuristic core** if one exists (many have a rule-based |
| 72 | +path); otherwise DO NOT fake it — return a short note in your report that the bot is |
| 73 | +RL-weight-dependent and should be skipped, and still write a best-effort heuristic port only if |
| 74 | +faithful. Log the reason. |
| 75 | + |
| 76 | +## Deliverable |
| 77 | +Write your port to `configs/ablations/ladder/scml_pilot/ports/<YEAR>__<TEAM>.py` |
| 78 | +(e.g. `scml2024__team_193.py`). It must `python3 -c "import ast; ast.parse(open(FILE).read())"` |
| 79 | +cleanly and define a top-level `decide`. Keep a concise module docstring naming the source and |
| 80 | +noting any simplifications/dropped features. |
0 commit comments