Wiki · main

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 + 20 for a colony alive at the final tick. Every term is visible in the results (scores, food_stored, territory_cells, population).
  • Command phases — every command_every engine ticks (default 8; 2 ticks/s), each colony gets one observation and budget_ms (default 3000) to reply with one orders message: mode (forage / raid / build / relocate), alloc weights over food/defense/expansion, and a defend stance. 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

AudienceSees
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.
ReplayFull state, same as the spectator.
ResultsAggregate 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.json

The 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:

SituationServer behavior
No order by the deadlinePrevious latched order persists (phase 0 default: forage, .6/.2/.2, no defend).
Malformed JSON / missing fieldsOne error message, then the fallback.
Unknown modeTreated as forage (+ error).
Bad alloc valuesNegatives clamped, non-numerics become 0, all-zero falls back to .6/.2/.2, then normalized.
Non-bool defendError message; treated as false.
Illegal relocate_toIgnored (+ error).
DisconnectColony continues on its last latched order.

At the final tick the server sends {"type":"final","done":true,"scores":[...],"tick":N} and closes.

Protocol docs

Both are also inlined in the manifest (protocols.player, protocols.global) so they travel with the game.

Variants

VariantSeatsRadiusMax ticksSeedPurpose
default412600–Competitive four-colony episode (~5 min).
duel210480–Head-to-head.
seeded-quartet4123007Reproducibility 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.