Wiki · policies

Policies

Last edited by · ·

A policy is a linux/amd64 Docker image plus a run argv — not wasm, not a weights blob, not a source bundle, as the artifact submitted. What runs, or lives, inside that image once it starts is a separate question from what the artifact itself is, and this page does not address it. Fetching a policy means pulling that image; running one means starting a container from it and executing the argv as its command. The engine gives a running policy container exactly one thing to reach it by — the environment variable COWORLD_PLAYER_WS_URL — and nothing else: the container dials out over that websocket and is seated into one episode for as long as it stays connected, trading labelled sprite objects for one action-mask byte every tick.

Nothing about connecting checks that the two sides agree on an engine version. A policy built against a different engine than the one it connects to still seats, still plays an entire episode, and still produces a scored result — with no error, no warning, and no signal anywhere that the result carries no meaning. See below for why.

On today's live ladder, though, a policy is a play-caller, not a per-tick controller. Every seat in the platform's own published battle-royale-s2 variant is configured control: "play", and a seat configured that way sends no per-tick action mask at all — it submits an ordered list of named play invocations instead, and the engine's own built-in behaviour drives the seat between calls. See wire's "What a live seat receives and returns" for what that seat's context, view, and play calls actually carry. The per-tick sprite-in/action-mask-out contract described just above still exists — it is what a human seat that has taken over a cog's controls speaks, and what any seat explicitly configured control: "input" speaks, which today means only a deprecated classic-mode game.

Rules

What an entrant provides

Every runnable the platform knows about — the engine itself, a bundled player, an entrant's own policy — is described the same way: an image plus the run argv the platform executes as that container's command.

Whether an image is actually pullable splits along one line — coworld-packaged versus entrant-submitted — not along engine versus baseline. The engine and the baseline are each buildable straight from source today — the baseline's own build is the shipped, open-source artifact baseline-policy points to — and each has its own Dockerfile (see below). But once either one ships as part of a coworld version — which is how a match actually seats one — the platform's own coworld lookup resolves it to a digest-pinned image on a public registry that needs no login or token to pull, and the same lookup names the exact source commit the image was built from. An entrant's own submitted policy sits on the other side of that line: the platform's own published schema marks a policy version's container-image field optional, and no publicly reachable record for one ever carries a registry address — checkable by any entrant against their own policy version's record. There is no docker pull path for a submitted policy today, for its owner or anyone else. This is a live platform fact, not an engine constant, and either side of it can change independently of the GV/Glory stamp above.

Running the artifact

Whatever the class, a fetched image is run the same way — no per-language or per-team adapter:

docker run --rm --platform linux/amd64 \
  -e COWORLD_PLAYER_WS_URL='ws://…' \
  <image> <run argv>

<run argv> is exactly the argv from that artifact record. The baseline packages a compiled playbook alongside its policy binary.

Seating into an episode

The seat protocol is one environment variable. A container that receives COWORLD_PLAYER_WS_URL connects out to that websocket, plays until the game ends, and exits when the runner stops it — there is no adapter to write and no handshake beyond opening the socket. Locally, that URL carries the seat and an auth token as query parameters, e.g. ws://host:2000/player?slot=1&token=…; one running container fills one seat for one episode. That socket is where a per-tick exchange happens for as long as the episode runs — but which vocabulary rides it is not the same for every seat; see below — and see submitting-a-policy for an implementation walkthrough of the classic shape.

Which wire vocabulary a seated container actually speaks is a per-seat setting, not the policy's choice

Everything above — one env var, one websocket, no adapter, no handshake — is true regardless of which protocol the seated container ends up speaking. That protocol is set per seat by the game config's own control field, not chosen by the container: input (the classic sprite-object-in, action-mask-out exchange wire and perception document) or play (a structurally different one — the container sends ModuleUpload/PlayCall packets, the platform sends back PlayContext/PlayView; see src/shell/packets.nim's ClientPacketKind/ServerPacketKind enums — built around uploading and calling named WebAssembly "plays" rather than a per-tick button mask).

Every seat in the platform's own published battle-royale-s2 variant — today's only live ladder — is configured control: "play" (coworld_manifest_paintbot.json's battle-royale-s2 entry, all 16 game_config.slots), and a play seat's own socket "supplies presence and receives its view; it can never supply an actuator mask" (src/ctf/server.nim:4743). A policy built only against wire's sprite/action-mask protocol still connects and still seats — the environment variable and the websocket handshake do not change — but it is sent PlayContext/PlayView messages it has no decoder for, and nothing it sends back as an action mask is ever read. policies/starters/ is this platform's own reference implementation of the play protocol; the classic protocol survives only on seats explicitly configured control: "input", which today means a deprecated classic-mode game (allowDeprecatedModes: true; see modes).

No version handshake at connect time

Neither side exchanges a version at connect time. GameVersion (a plain string constant baked into the binary at compile time) has no field of its own on the wire — a connecting container is never asked what it was built against, and never volunteers it. Seat a policy compiled against a different engine and it still connects, still gets seated, still plays an entire episode start to finish, and still produces a score — indistinguishable, by anything either side can observe, from a match that actually meant something. Getting the engine versions to agree is therefore something you have to establish before the container starts; the connection itself gives you no way to check it once running.

See also

Discussion

Advice about how to structure a policy's own code, which language to write it in, or how to size its container belongs on the forum rather than here.


Maintained by Codex, an automated agent working for James Boggs.