Write your first policy
After connecting to the server, make a small Python player. Save this as policies/my_agent.py. It uses the bundled example's priority rules so the first milestone is a working agent:
import json
import sys
from legal_first import choose
for line in sys.stdin:
message = json.loads(line)
# Each message contains view, topology, and new visible event batches.
command = choose(message["view"])
print(json.dumps(command), flush=True)
choose returns a protocol command or None when there is nothing to do. Replace it with your own decision rule once the first match works. Keep stdout for JSON responses; write diagnostics to stderr. The process stays alive across decisions, so ordinary Python state can hold your agent's memory.
Understand the input and output
All policies ultimately read GameView and write the server's Command JSON
schema. The authoritative documentation and schemas are served at /v1/schema
and /v1/topology with authentication; a server checkout also includes
docs/api.md and docs/protocol.schema.json. Protocol version is currently 1.
Stdio adapters
Add an entry in policies/registry.toml with transport = "stdio" and an argv
array. Commands run from the research root. Supported placeholders are
{python}, {root}, {server}, {base}, {game}, {token_file}, and {name}.
Never interpolate shell command strings. Compile Rust or TypeScript policies
before registering a run; point the command at the resulting executable.
The harness starts one persistent process per seat. Each line on stdin is:
{"protocol":1,"view":{"id":"…","version":8,"game":{}},"topology":{},"events":[]}
The abbreviated objects above are complete typed protocol data at runtime.
Return one JSON line containing a Command, or null if there is no action:
{"type":"act","action":{"type":"roll"}}
Flush stdout after every response; send diagnostics to stderr. Responses have a
16 KiB limit and a preregistered decision deadline. policies/legal_first.py is a
working example. The process retains its own memory across decisions. events
contains newly delivered batches for that participant, including directed
communications it may see. Empty redacted batches still advance the independent
history cursor. A snapshot version is not an event cursor.
The harness owns each seat's authenticated REST client, snapshots, envelope UUID,
expected version, and retries. A transport ambiguity retries the same entire
envelope. A conflict fetches fresh state and asks the policy again. Only the
server determines legality, trades, random draws, and the winner. legal_actions
contains useful finite actions but is not exhaustive for all discard/trade
quantities. Policies may emit other legal commands.
Independent network processes
transport = "remote" starts a process that owns its HTTP/WebSocket connection.
The harness prejoins the seat and writes its token to the supplied private token
file. The process must reuse that identity, connect its WebSocket (so readiness
is visible), wait for start, and exit after a terminal state. Each player has a
distinct token. The existing fast and eta Rust runners implement this contract.
Remote policies can act simultaneously; the server resolves ordering. Stdio decisions are scheduled sequentially by seat. Record this transport difference when studying negotiation timing or fairness. Native runtime stderr and all credentials stay in ignored private directories and never enter public archives.
Public replay encoding
Topology uses integer pointy-hex coordinates. Scale x by sqrt(3) relative to y
to draw regular hexagons. A hex byte stores its dice total in the high nibble and
terrain in the low nibble: 0 desert, then brick/lumber/wool/grain/ore as 1–5.
Buildings use 0 empty, 1–4 settlements, 5–8 cities; owner is (value - 1) % 4.
Road values are null or seat index. Player points are public midgame; the server
reveals final scoring at the terminal state.
Next: play a complete game
Continue to 3. Play against computer policies to register this script and choose opponents.