Wiki · wire-protocol

Wire protocol

Last edited by · ·

Wire protocol

The Coworld 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.

HTTP: GET /healthz, GET /client/player?slot=&token= (token-checked, and it does not open the player socket), GET /client/global, GET /client/replay, GET /client/<asset>, GET /replay-data, GET /reward. Websockets: /player?slot=<i>&token=<t> (403 on a bad slot or token, 409 on a duplicate) and /global.

The player socket

A seat sends one Sprite v1 chat frame (0x81) carrying its registration, re-sent ten times over the first ten seconds because joins are slot-sequential:

{"type":"register","policy":"<label, <=48 runes>",
 "prompt":"<PLAYER_PROMPT or empty, <=4000 runes>",
 "scripted":"sprawl"|"crown"|null}

A seat that registers with neither field, or never registers, is seated as sprawl. Any OTHER chat text from a seat is dropped: this game has no inter-seat channel of any kind — no chat, no radio, no say, no emote.

The seat then sends the Sprite v1 Ready packet (0x85) after each received frame, which is legitimate here because a seat sends no inputs: every decision is made in the game server, which calls the LLM once every eight turns with all living seats batched into ONE parallel request.

The game sends the seat:

{"type":"welcome","protocol":"gen-generals-io.player.v1","slot":0,
 "alias":"RED-alpha","turns":240,"directive_every":8}
{"type":"turn","turn":96,"of":240,"you":"RED-alpha","alive":true}
{"done":true,"result":{...the results document...}}

A seat frame carries nothing a seat may not know: its own alias, the turn and whether it is still standing. No board, no rival, and never a real policy name. An eliminated seat keeps receiving frames and exits 0 with everyone else — elimination is a game state, not a disconnection.

The spectator socket

/global speaks the Sprite v1 binary protocol: sprite definitions, object placements, a viewport and a layer, exactly as client/broadcast_core.js decodes them. The broadcast chrome rides as the LABEL of the reserved 1 × 1 sprite id 4090, which is what makes it survive a hosted replay.

One chrome object per presentation frame. The inherited keys (t, mt, ph, lob, sp, mx, st, lp, sk, ff, en, mm, teams, roster, events, lead, lulls, beats, over) are the starter's; gen-generals-io adds:

{"turn": 96, "turns": 240, "growthIn": 4, "growthEvery": 25,
 "w": 16, "h": 10,
 "cells": [{"i": 37, "k": "plain", "o": 0, "a": 54}],
 "gen":   [17, 28, -1, 142],
 "alive": [true, true, false, true],
 "stand": {"land": [31,27,0,34], "army": [210,188,0,156], "cities": [1,2,0,0]},
 "out":   [-1, -1, 71, -1],
 "outBy": [-1, -1, 1, -1],
 "plan":  [{"seat": 0, "turn": 96, "intent": "expand", "source": "llm",
            "note": "taking the middle before blue does"}]}

cells is a delta (the full array on the first frame and on every keyframe); i is a cell index and o a seat index or -1 for neutral. gen[s] is seat s's crown cell, or -1 once captured. The per-seat fog is NOT transmitted: the viewer derives visible[s] = owned ∪ 8-neighbours(owned) in the browser exactly as the sim does, which costs zero replay bytes and cannot drift.

Derived events — a closed enum of twelve kinds

phase, growth, claim, citytaken, tilelost, stackclash, generalspotted, generalcaptured, eliminated, plan, fallback, end.

Beats — the scrubber markers, and the only kinds the game block draws: citytaken, generalspotted, generalcaptured, end. Each is a labelled, clickable button that seeks on click.

The replay

Binary, magic COWLDGEN:

"COWLDGEN" | u16 formatVersion | u16 len + game name | u16 len + game version
           | u32 len + the resolved config JSON
           | records: u8 kind, u32 length, payload

Record kinds: 1 join, 2 plan input (load-bearing, re-applied before the turn it belongs to is stepped), 3 chat (register / plan / fallback / budget_guard / stop / result), 4 hash (u32 turn, u32 gameHash), 5 leave.

The board is re-derived from the seed rather than stored — it is in gameHash from turn 0, so a divergence surfaces immediately. Everything the viewer needs is in the bytes; no server is contacted except S3 for the file. tools/replay_summary.py (Python 3 stdlib only) prints one strict-UTF-8 JSON object describing a replay.