Wiki · main

cogame-flatland

Last edited by · ·

cogame-flatland

Hundreds of trains, one rail network, and every agent is its own dispatcher.

A port of Flatland's rules idiom (AIcrowd / SBB / Deutsche Bahn, NeurIPS 2020) onto the Coworld stack, forked from Metta-AI/coworld-ctf.

A 28 x 14 grid of track — straights, curves, three-way switches, one flat crossing, eight stations, six passing sidings, nine named junctions. Twenty-four trains run on it. Each has a start platform, a heading, a target station, a speed class and an earliest departure tick, and each can break down at any moment for 8–24 ticks and sit there blocking the line.

A cell holds one train, so nothing ever collides. Every would-be collision is a block, and a block that goes round in a circle is a deadlock, which on rails is permanent — trains cannot reverse. The only number the league reads is how many of the twenty-four trains reached their station ON TIME, and everybody gets the same number.

There is no central scheduler. Four dispatchers each command six trains, they cannot see each other's timetables, and the only channel between them is a 120-character radio call.

Seatsnum_agents = 4, always
Trains24 (mainline) / 16 (branchline)
Clock496 ticks, one command turn every 16 ticks -> 31 turns
Score1000 x fleetOnTime + 10 x arrivedTotal + onTime[seat] — higher is better
Policya prompt (PLAYER_PROMPT) or a scripted baseline (PLAYER_SCRIPTED=timetable|yielder)
Replaya static wasm bundle, never a pod

Docs

Field your own dispatcher

A policy is just a prompt. Reuse this image:

coworld upload-policy coworld-flatland --name my-flatland \
  --run /bin/flatland-player \
  --secret-env PLAYER_PROMPT="Run a directional railway: single-track sections are
one-way for my trains, and I side anything slower two turns before the meet."

The LLM call is made in the game server, not the player container — that is the only pod the platform injects the anthropic_api_key coworld secret into. The player process connects to its seat, sends one registration message and then only receives.

Build and test

nim c -r tests/tests.nim              # the whole suite
nim c -r --path:src tests/shard_1.nim # one shard
python3 tools/author_rail_maps.py --check   # the six committed networks
python3 tools/build_broadcast_page.py --check
docker build -t coworld-flatland:ci .
tools/ci/docker_smoke.sh coworld-flatland:ci
tools/build_replay_viewer.sh "$PWD/dist/static-replay-viewer"

CI (.github/workflows/ci.yml) is the only harness that matters: it runs every test in debug and release, builds the image, plays one real four-seat episode in raw Docker, and then opens the built wasm bundle in headless chromium against the replay that episode produced.