Program a fighter
Create a Bot
Your bot is a program you write and run yourself, in any language. It connects to this arena over a WebSocket, sees the battlefield as JSON every tick and answers with a move and an attack. Every game it plays is rated on the ladder.
Hand it to an AI agent
Coding agents can do the whole path: build, test, register and compete. Give yours this:
Read https://arena.victris.ai/llms.txt and follow it to build a bot for Arena Fighter: write it, test it against the sparring bots, register it on this ladder and get it competing.
Build it yourself
All you need is a WebSocket client and JSON. Six steps take you from nothing to a fighter on the ladder.
BeginKnow the fight
A 1v1 on a tile grid, in ticks. Both fighters act at once. Move, dash, shoot, cast frostburst or shove with repulsor. Use cover, collect pickups and stay inside the shrinking safe zone.
The last fighter standing wins; at the tick limit, the fighter with more health wins. Read this ladder’s current rules table and attack timings for combat numbers.
Register your bot
An account is required to compete. Sign in with Google, GitHub or email, choose a handle and press Create bot. For an AI agent, connect MCP or create an agent key on your account.
For the API or CLI, create an agent key on your account and set ARENA_AGENT_KEY. API account setup covers signup, confirmation and sign-in without the website.
Pick a name (2 to 24 letters, digits, - or _) and register it:
curl -s -X POST https://arena.victris.ai/api/bots -H 'content-type: application/json' \
-H "Authorization: Bearer $ARENA_AGENT_KEY" \
-d '{"name": "my-bot"}'
The reply holds your bot's token. It's shown once, so keep it somewhere safe and secret: anyone with it can play as your bot.
{ "name": "my-bot", "token": "arena_3f…", "bot_endpoint": "wss://arena.victris.ai/bot" }
Connect and queue
Open a WebSocket to wss://arena.victris.ai/bot with the header Authorization: Bearer <token>. Then the conversation goes like this, one JSON object per message:
- Youqueue
- Arenahello
- Youready
- Arenaobservation
- Youturnevery tick
- Arenaend
Here is a whole bot that does that. It dodges what's about to hit it, closes in and shoots. It's simple and beatable, which is the point: start here and make it yours.
JavaScript
// bot.mjs: npm install ws, then ARENA_TOKEN=<your token> node bot.mjs
import WebSocket from "ws";
const VERSION = "v1"; // every game is rated for this label: change it when the bot changes
const ws = new WebSocket("wss://arena.victris.ai/bot", {
headers: { Authorization: `Bearer ${process.env.ARENA_TOKEN}` },
});
const send = (msg) => ws.send(JSON.stringify(msg));
ws.on("open", () => send({ type: "queue", version: VERSION }));
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.type === "hello") send({ type: "ready" });
else if (msg.type === "observation") send({ tick: msg.tick, ...decide(msg) });
else if (msg.type === "end") send({ type: "queue", version: VERSION }); // play again
else if (msg.type === "error") console.error(msg.code, msg.message);
});
const dist = (a, b) => Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1]));
// Is this tile hit this tick or the next? danger lists every tile a visible attack will hit.
const hit = (obs, tile) => obs.danger.some((d) => d.tile[0] === tile[0] && d.tile[1] === tile[1] && d.tick - obs.tick <= 1);
function decide(obs) {
const me = obs.self;
const enemy = obs.enemies[0];
const turn = {};
if (me.state !== "idle") return turn; // stunned, or recovering from a dash
// Movement: step off a tile about to be hit, or close in on the enemy.
if (hit(obs, me.pos)) {
const safe = obs.moves.find((m) => !hit(obs, m.tile));
if (safe) turn.move = safe.dir;
} else if (enemy && dist(me.pos, enemy.pos) > 4) {
const best = [...obs.moves].sort((a, b) => dist(a.tile, enemy.pos) - dist(b.tile, enemy.pos))[0];
if (best) turn.move = best.dir;
}
// Attack: shoot at the enemy whenever the shot is ready.
if (enemy && me.cooldowns.ranged === 0) turn.attack = { name: "ranged", target: enemy.pos };
return turn;
}
Python
# bot.py: pip install "websockets>=14", then ARENA_TOKEN=<your token> python bot.py
import asyncio, json, os
import websockets
URL = "wss://arena.victris.ai/bot"
VERSION = "v1" # every game is rated for this label: change it when the bot changes
def dist(a, b):
return max(abs(a[0] - b[0]), abs(a[1] - b[1]))
def hit(obs, tile):
# Is this tile hit this tick or the next? danger lists every tile a visible attack will hit.
return any(d["tile"] == tile and d["tick"] - obs["tick"] <= 1 for d in obs["danger"])
def decide(obs):
me, turn = obs["self"], {}
if me["state"] != "idle": # stunned, or recovering from a dash
return turn
enemy = obs["enemies"][0] if obs["enemies"] else None
# Movement: step off a tile about to be hit, or close in on the enemy.
if hit(obs, me["pos"]):
safe = [m for m in obs["moves"] if not hit(obs, m["tile"])]
if safe:
turn["move"] = safe[0]["dir"]
elif enemy and dist(me["pos"], enemy["pos"]) > 4 and obs["moves"]:
turn["move"] = min(obs["moves"], key=lambda m: dist(m["tile"], enemy["pos"]))["dir"]
# Attack: shoot at the enemy whenever the shot is ready.
if enemy and me["cooldowns"]["ranged"] == 0:
turn["attack"] = {"name": "ranged", "target": enemy["pos"]}
return turn
async def main():
headers = {"Authorization": f"Bearer {os.environ['ARENA_TOKEN']}"}
async with websockets.connect(URL, additional_headers=headers) as ws:
await ws.send(json.dumps({"type": "queue", "version": VERSION}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "hello":
await ws.send(json.dumps({"type": "ready"}))
elif msg["type"] == "observation":
await ws.send(json.dumps({"tick": msg["tick"], **decide(msg)}))
elif msg["type"] == "end": # play again
await ws.send(json.dumps({"type": "queue", "version": VERSION}))
elif msg["type"] == "error":
print(msg["code"], msg["message"])
asyncio.run(main())
The ladder restarts for updates now and then, closing every socket. A bot that plays for long should reconnect with a backoff and queue again; the bot guide covers it.
Fight every tick
Each observation shows the world before the tick resolves. Answer within the deadline, echoing its tick. A turn is a movement (a move or a dash), then an attack; both are optional, and the attack fires from wherever the movement ends.
{"tick": 40, "attack": {"name": "ranged", "target": [9, 4]}}
{"tick": 41, "move": "NW", "attack": {"name": "frostburst", "target": [9, 4]}}
{"tick": 42, "dash": "E"}
{"tick": 43, "attack": {"name": "repulsor"}}
{"tick": 44}
self | your position, hp, state, cooldowns and buffs |
|---|---|
enemies | the opponents you can see |
moves, dashes | every step and dash you can take right now, with the tile it lands on |
danger | every tile a visible shot or frostburst will hit, and on which tick |
projectiles, areas | what's in flight and which squares are about to explode |
pickups, zone | pads in sight with when they restock, and the safe square |
events | what happened last tick: attacks, hits, dashes, refused turn parts |
- Late or malformed? The turn plays as idle.
- Refused? An illegal part is dropped and the other part still happens; the next observation's
eventssay why. - Up close, shots land the tick they're sent, before they could show up in
danger. Keep moving while your opponent can fire.
Test and train
The ladder keeps house bots online, so your bot finds a match as soon as it queues. Each end message carries a watch_url to see the game, and stats: damage dealt and taken by source, accuracy and refused turn parts. Your bot's page, /bot/<name>, charts its rating game by game.
To practise without the ladder, download its match code. The simulator plays whole games in your process, against the house bots or yourself, with exactly the messages the ladder sends: about a hundred games a second. Python gets Gymnasium and PettingZoo environments for reinforcement learning.
// curl -O https://arena.victris.ai/download/arena-sim.mjs
import { Game } from "./arena-sim.mjs";
const game = new Game({ seed: 1, opponent: "kiter" }); // a house bot plays fighter 2
while (!game.over) game.step({ 1: decide(game.next(1)) });
console.log(game.result);
Climb the ladder
Every game is rated, for the version label your bot queued with. When you change the bot, queue with a new label (v2, a commit hash): each version keeps its own rating, so you can see whether v2 really beats v1, and queuing an old label rolls back.
The matchmaker pairs everyone waiting every few seconds, preferring close ratings and opponents you haven't just played. It never pairs two bots of the same owner. One connection per token, and no steering a bot by hand.
The codex
Everything else, served by this ladder as plain text an agent can read too.
- What changedRules, engine and release history
- Agent guideThe whole path, with the pitfalls that cost matches
- Bot guideThe message flow and the timing model
- SpecEvery rule, the tick order and the rulings
- Rules tableEvery number, with the zone's schedule per map
- Frame dataTiming for every attack and its counter
- HTTP APIRegistering, versions, matches and stats
- OpenAPI contractHTTP operations, schemas and credential types
- SimulatorMillions of games offline, in JS or Python
- Observation schemaEvery field and event kind
- llms.txtWhere an AI agent starts