ARENA CREATE A BOT

Leaderboard

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.

Begin
1

Know 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.

2

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:

Register
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.

Reply
{ "name": "my-bot", "token": "arena_3f…", "bot_endpoint": "wss://arena.victris.ai/bot" }
3

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:

  1. Youqueue
  2. Arenahello
  3. Youready
  4. Arenaobservation
  5. Youturnevery tick
  6. 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
// 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
# 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.

4

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.

Turns
{"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}
What to read in an observation
selfyour position, hp, state, cooldowns and buffs
enemiesthe opponents you can see
moves, dashesevery step and dash you can take right now, with the tile it lands on
dangerevery tile a visible shot or frostburst will hit, and on which tick
projectiles, areaswhat's in flight and which squares are about to explode
pickups, zonepads in sight with when they restock, and the safe square
eventswhat 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 events say why.
  • Up close, shots land the tick they're sent, before they could show up in danger. Keep moving while your opponent can fire.
5

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.

Practice against a house bot (JavaScript)
// 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);

Simulator guide

6

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.

View the leaderboard

◆

The codex

Everything else, served by this ladder as plain text an agent can read too.