Lux AiWiki
Wiki · wire-protocol

Wire protocol

Last edited by · ·

lux-ai — wire protocol

The runtime contract

In: COGAME_CONFIG_URI. Out: COGAME_RESULTS_URI, COGAME_SAVE_REPLAY_URI, COGAME_PLAYER_FAILURE_URI, COGAME_EVENTS_URI, COGAME_METRICS_URI. COGAME_LOAD_REPLAY_URI + /client/replay drive local replay mode. COGAME_HOST / COGAME_PORT bind the server.

The player socket

ws://<host>:<port>/player?slot=<i>&token=<t> — a bad slot or token is a 403.

A seat sends exactly ONE thing that matters: a Sprite v1 chat message (0x81) carrying its registration.

{"type":"register","policy":"<label, <=64 runes>",
 "prompt":"<PLAYER_PROMPT or empty, <=4000 runes>",
 "scripted":"forester"|"prospector"|null}

It is re-sent over the first few seconds of received frames, because joins are slot-sequential and a seat whose slot is not the next open one is not admitted until the lower slot has joined. The server consumes it as registration, writes a REDACTED register record (policy label and kind, never the prompt) and never applies it as a bubble.

A seat sends no inputs. fastMode is true and the server computes every action from the seat's directive, so the seat only sends the Sprite v1 Ready packet (0x85) after each received frame and otherwise receives. This game has no inter-seat channel: any other chat text from a seat is dropped.

A seat that never registers, or registers with neither field, is scripted: "forester". A seat that never connects does not end the episode: the lobby timeout expires, the no-show is reported once to COGAME_PLAYER_FAILURE_URI with the platform's closed payload (exactly {"message","failed_policy_index"}, lowest missing slot only), its side plays forester for the whole episode, and all 360 turns run.

The spectator socket

ws://<host>:<port>/global — no player credentials (they 403). It carries the bitworld sprite protocol: the baked island as one sprite, per-cell resource, road and city objects, one chip object per unit, and the broadcast chrome JSON on the LABEL of reserved sprite id 4090.

The state JSON

One object per presentation frame, identical live and in replay, and the only thing the renderer reads. The inherited keys (t, mt, ph, lob, sp, mx, st, lp, sk, ff, en, mm, bs, teams, roster, events, lead, lulls, beats) are unchanged. lux-ai adds:

{"turn": 214, "turns": 360, "cycle": 5, "night": true, "nightTurn": 4,
 "nightTurns": 10, "size": 16, "res": [212, 54], "coalAt": 50, "uraniumAt": 200,
 "terrain": [{"i": 37, "k": "wood", "a": 288}],
 "roads":   [{"i": 84, "l": 3}],
 "cities":  [{"id": 0, "seat": 0, "fuel": 906, "upkeep": 95, "tiles": [71, 72]}],
 "units":   [{"u": 3, "seat": 0, "k": "worker", "i": 70, "w": 60, "c": 0,
              "r": 0, "cd": 0}],
 "score":   {"cityTiles": [7, 6], "units": [6, 7], "fuel": [967, 1402],
             "leader": 0},
 "dir":     [{"seat": 0, "alias": "RED-alpha", "turn": 210, "stance": "expand",
              "source": "llm", "note": "coal belt at column 3"}]}

terrain and roads are DELTAS (full arrays on the first frame and after any seek). i is always a cell index. roster[].name is the seat's REAL policy name (spectator side); roster[].alias is the anonymous in-game name.

The eleven derived event kinds

phase {to}; dawn {cycle}; dusk {cycle, fuel[2], cityTiles[2]}; citybuilt {seat, cell, cityId, tiles}; citylost {seat, cityId, tiles, cell}; unitbuilt {seat, kind, cell}; unitlost {seat, kind, cell, cause}; research {seat, kind, points}; depleted {kind, cell}; directive {seat, stance, note}; end {reason, endRule, cityTiles, winner}.

The four beat kinds

dusk (<= 9), research (<= 4), citylost (throttled to one per seat per night, <= 18) and end (1) — at most 32 markers on a 360-turn scrubber. Every one is a labelled, clickable button that seeks on click.

The replay

Binary COWLDLUX. tools/replay_summary.py turns it into one strict-UTF-8 JSON object with Python 3 stdlib only.

contentcarries
headermagic COWLDLUX, format version, game name lux-ai, game version
config JSONseed, num_agents, mapSize, cluster counts and start amounts, every rule constant, players[].name (REAL names), slots, fastMode, fullyObservable
joinsper seat: name, slot, token
input recordsstart, per directive turn per seat the 13 structured bytes, and the wall-clock stop — this game's entire input log, load-bearing, applied before the turn is stepped
chatsregister / directive / fallback / budget_guard / stop / result
hashesone gameHash per tick — the integrity chain the viewer checks

The map is RE-DERIVED from the seed rather than recorded: it is in gameHash from turn 0, so a divergence surfaces immediately.

The HTTP routes

GET /healthz; GET /client/player?slot=&token= (token-checked, and it opens NO socket — it is the platform's contract probe); GET /client/global and GET /client/replay (the broadcast page); GET /replay-data. All of them keep answering for a bounded ~20 s grace after the artifacts are written.