FORMICA
Last edited by · ·
FORMICA
FORMICA is an ant-colony free-for-all on a seeded hex map. Each seat is one colony: a nest, a stored-food budget that buys population, a squad of ants split across roles, pheromone trails, and a territory. Episodes are tick-based with discrete command phases at which each colony submits one order set. The winner is the colony with the highest score at the final tick.
Two bundled players ship with the game: Scripted Forager (a deterministic greedy baseline) and Daf-Jev Colony (a decider that asks a batched question set per phase, falling back to the scripted heuristic whenever its key or network is unavailable — the episode never depends on it).
Rules summary
- Score —
food_stored + 0.5 × territory_cells + 20for a colony alive at the final tick. Every term is visible in the results (scores,food_stored,territory_cells,population). - Command phases — every
command_everyengine ticks (default 8; 2 ticks/s), each colony gets one observation andbudget_ms(default 3000) to reply with one orders message:mode(forage / raid / build / relocate),allocweights over food/defense/expansion, and adefendstance. Orders are latched — the latest valid order persists, and a missed deadline just keeps the previous one. Event phases get a tighter budget (default 1200 ms). - Squads — the population is split across forage/defense/expansion by the normalized alloc; raid mode converts about a third of foragers into raiders that march on scouted nests and steal food.
- Pheromone — ants deposit trail at their cell (more when carrying); the field decays each tick and biases later foraging. Rain doubles the decay for 10 ticks.
- Territory — every ant stamps the cell it stands on; a cell stays held for 25 engine ticks after the last visit. Rival ants take a cell by standing on it.
- Events — on a seeded schedule: locust (40%: every colony loses 15% stored food and one immediate extra command phase runs on the tighter budget), rain (30%), or clear (30%).
- Death — a colony whose population reaches 0 is dead: no ants, orders ignored, score frozen. It still counts in the results.
Information boundaries
| Audience | Sees |
|---|---|
One seat (/player) | Own colony, ants, and full own pheromone; food within distance 3 of own ants; rival ants within distance 2; rival pheromone only as adjacent-cell scent; rival nests only after scouting (latched); all scores public. |
Spectator (/global) | Full state — territory, all ants, food, pheromone on phase frames. |
| Replay | Full state, same as the spectator. |
| Results | Aggregate colony stats per slot. |
Boundaries are enforced server-side; no hidden state is sent to a player client.
Run locally
From the repository root:
# Build the game image from the package and stamp the manifest
uv run coworld build --project coworlds-authored/formica --version 0.1.0
# Play interactively: prints one player link per seat, a global viewer
# link, and an admin link (pause / resume / tick rate)
uv run coworld play coworlds-authored/formica/dist/coworld_manifest.json
# Run a headless episode with the bundled players
uv run coworld run-episode coworlds-authored/formica/dist/coworld_manifest.json
# Watch a replay from a finished episode
uv run coworld replay coworlds-authored/formica/dist/coworld_manifest.json path/to/replay
# Certification smoke run
uv run coworld certify coworlds-authored/formica/dist/coworld_manifest.jsonThe episode starts once all scheduled players have connected or
player_connect_timeout_seconds (default 180) elapses.
Write a player
A player is any program that connects to the game container's
WS /player?slot=<slot>&token=<token> route. The platform passes the
seat's slot and token; with the Python websockets client, connect with
ping_timeout=None.
Each command phase the server sends one command observation (own
colony, own ants, visible food, nearby rivals, scent, threat) and the
player replies with one orders message:
{"type":"orders","mode":"forage","alloc":{"food":0.6,"defense":0.2,"expansion":0.2},"defend":false}One order per phase, latched. Malformed or missing orders never crash
the episode — the server applies a documented fallback and sends one
error message:
| Situation | Server behavior |
|---|---|
| No order by the deadline | Previous latched order persists (phase 0 default: forage, .6/.2/.2, no defend). |
| Malformed JSON / missing fields | One error message, then the fallback. |
| Unknown mode | Treated as forage (+ error). |
| Bad alloc values | Negatives clamped, non-numerics become 0, all-zero falls back to .6/.2/.2, then normalized. |
Non-bool defend | Error message; treated as false. |
Illegal relocate_to | Ignored (+ error). |
| Disconnect | Colony continues on its last latched order. |
At the final tick the server sends {"type":"final","done":true,"scores":[...],"tick":N}
and closes.
Protocol docs
- Player protocol:
game/docs/player_protocol_spec.md(observation fields, orders schema, fallbacks, hex addressing, mechanics). - Global / admin / replay:
game/docs/global_protocol_spec.md(spectator frames, admin commands, replay payload).
Both are also inlined in the manifest (protocols.player,
protocols.global) so they travel with the game.
Variants
| Variant | Seats | Radius | Max ticks | Seed | Purpose |
|---|---|---|---|---|---|
default | 4 | 12 | 600 | – | Competitive four-colony episode (~5 min). |
duel | 2 | 10 | 480 | – | Head-to-head. |
seeded-quartet | 4 | 12 | 300 | 7 | Reproducibility comparisons. |
Nests are placed on evenly spaced vertex directions of the hex board,
deterministic from the seat count and seed. With a seed set, identical
action trajectories reproduce identically; without one, the game mints a
fresh seed and records it as seed_used in the results and replay.