grf-football
Last edited by · ·
grf-football
Eleven-a-side association football in a continuous 2D physics world. Twenty-two wheeled cogs and one ball on an 84 m × 54 m pitch; the ball goes out (throw-ins, corners, goal kicks); goals win the match. It reimplements the spirit of Google Research Football — 11 v 11, a nineteen-action discrete vocabulary, and unseated players run by the engine's built-in AI — as a new physics game written for this coworld.
A policy is just a prompt. A seat does not drive a joystick. Every ten seconds of match time it issues ONE order for its shirt — a role, an intent, a target point, an on-ball choice, sprint and tackle permissions — and a deterministic control layer executes it for the next ten seconds. Twenty-four turns, one order each.
+27 m
RED ┌───────────────────────────────────┐ BLUE
goal ▐│ RED-6 RED-10 (o) BLUE-9 │▌ goal
▐│ RED-7 BLUE-6 BLUE-7│▌
└───────────────────────────────────┘
−27 m
- docs/RULES.md — the pitch, the physics, the nineteen actions, the restart table, the scoring and the determinism contract.
- docs/PROTOCOL.md — the runtime contract, the seat
protocol, the
COWLDFTBreplay bytes and the results document. - docs/COACHING.md — how to write a grf-football prompt.
Seats and scoring
num_agents is 8: four seats a side, one outfield shirt each (RED-10,
RED-9, RED-7, RED-6 and the blue mirror). The other seven shirts a side,
keeper included, are the built-in AI. Team zero-sum and margin-sensitive:
score(seat) = 0.5 + 0.5 · clamp((goals_you − goals_them) / 3, −1, +1)
3–0 or better = 1.000; 1–0 = 0.667; any draw = 0.500. Higher is better; the eight scores always sum to 4.000.
Playing
One image, two entrypoints, every policy env-switched:
# the game
docker run --rm -e COGAME_CONFIG_URI=file:///coworld/config.json \
coworld-grf-football:latest /bin/grf-football
# an LLM seat
docker run --rm -e COWORLD_PLAYER_WS_URL=ws://game:8080/player?slot=0 \
-e PLAYER_PROMPT="Play possession football and make the pitch big…" \
coworld-grf-football:latest /bin/grf-football-player
# a scripted seat
docker run --rm -e COWORLD_PLAYER_WS_URL=ws://game:8080/player?slot=1 \
-e PLAYER_SCRIPTED=zonal \
coworld-grf-football:latest /bin/grf-football-player
zonal is the reference baseline (hold the zone, support the ball, press when
it is close) and also the fallback whenever a decision call fails twice.
gegenpress is the second filler — press everywhere, sprint always — which
fades on stamina around the third minute, so the ladder has a spread.
Watching
The replay ships as a static wasm bundle, never a pod:
tools/build_replay_viewer.sh builds replay-viewer/grf_football_replay.nim —
which imports the same src/grf_football/sim.nim the native server ran —
through the pinned emscripten/emsdk:4.0.15 container, and the browser
re-simulates the recorded action log and checks the recorded gameHash every
tick.
The broadcast chrome shows: the score bug (goals, shots on target, possession), the clock with the half and turn, a plain-language match feed carrying the seats' own notes and shouts, a ball trail tinted by the last toucher with a shadow while the ball is airborne, pass arcs and shot streaks, a goal celebration, an instant slow-mo goal replay, a possession bar, and the seated-shirt rings that say at a glance which four cogs a side are being played by a policy.
Layout
src/grf_football.nim game entrypoint (seed randomisation lives here)
src/grf_football_player.nim every policy: registers, then idles
src/grf_football/
sim_types.nim consts (incl. GameVersion), types, the action byte
trig.nim the committed SinQ12 table, isqrt, bradsOfVectorI
pitch.nim geometry, the out tests, the restart spots
sim.nim the integer physics core and the step loop
builtin_ai.nim the fourteen unseated shirts and the safe option
control.nim order -> 22 action bytes (integer only)
directives.nim view coordinates, rune truncation, the parser
baselines.nim the zonal and gegenpress scripted policies
llm.nim the credential ladder and transport
decide.nim the turn engine: ONE parallel batch of eight
server.nim mummy HTTP/ws, the COGAME_* contract, the loop
replays.nim the COWLDFTB codec, keyframes, the scan
global.nim the board renderer
broadcast.nim the chrome JSON channel and the record fold-back
rig_art.nim the wheeled-rig compositor and the shirt chips
replay-viewer/ the emscripten entry point and the worker
client/ the broadcast chrome (kept from coworld-ctf)
tools/ build hooks, forensics, CI helpers
tests/ the determinism gate and thirteen suites around it
Building and testing
The repo builds with nimby:
nimby --global sync nimby.lock
nim r --path:src tests/test_determinism.nim
CI runs every tests/*.nim twice — debug (range and overflow checks) and
-d:release — plus a raw-Docker episode smoke and the wasm bundle build with
the native↔wasm determinism gate. See .github/workflows/ci.yml.
Design notes live in docs/plans/. The lineage is
Metta-AI/coworld-ctf (paintbot):
the game loop, the per-tick replays, the static wasm viewer, the broadcast
chrome and the CI wiring are its, kept; the football rules replace the arena
rules.