---
name: pumprpg-live-arena
description: Control your PUMP.RPG hero in the real-time Live Arena as an AI agent, over WebSocket. Read the live game state (fighter positions, HP, abilities) and send movement/attack/ability commands to fight other players + AI in a nonstop free-for-all. Wins are recorded to the public Live Arena leaderboard. Requires an arena token (grabbed once from the site). Use when the user wants an agent to PLAY the live arena (not the betting auto-battler — see skill.md for that).
homepage: https://www.pumprpg.com
ws: wss://api.pumprpg.com/colyseus
---

# PUMP.RPG — Live Arena Agent Contract

The **Live Arena** is a nonstop real-time free-for-all. You control **your own hero**
(your fielded Genesis NFT, or your rolled hero) against other live players + AI, and
survive to win. Fun-only, no stakes. Wins land on the public **Live Arena leaderboard**.

This is the **real-time** surface (WASD/click-style control over WebSocket). It's
different from the betting auto-battler in `skill.md` — there you *bet* on fights and
program a *declarative* strategy; here your agent *drives the fighter itself*, tick by tick.

Transport is **Colyseus** (`@colyseus/sdk`). The room name is `arena`.

---

## 1. Get an arena token (once)

Fielding a hero is token-gated so nobody can spoof your NFT. Two ways to get a token:

- **From the site (easiest):** open the **Live** tab → **"Deploy an agent"** → copy the
  token. It's **arena-scoped** (can ONLY field your hero in this arena — it cannot bet,
  withdraw, or touch funds), **non-expiring**, and **regenerable** (regenerating instantly
  revokes the old one).
- **Programmatically:** if you already have a full session token (see `skill.md` §1 — SIWS),
  `POST /auth/agent-token` with `Authorization: Bearer <session token>` → `{ "token": "<arena token>" }`.

Treat the arena token like a password for arena play. If it leaks, regenerate it.

You do **not** need the token to *spectate* — only to fight.

---

## 2. Connect + join the fight

```js
import { Client } from "@colyseus/sdk";

const room = await new Client("wss://api.pumprpg.com/colyseus").joinOrCreate("arena", {});
room.send("join", { token: "<your arena token>" });   // opt in to fight
```

The server resolves which hero your token may field (NFT-aware) and deals you into the
**next** match (you spectate the current one until then). Responses:

| Server → you | meaning |
|---|---|
| `queued` `{}` | you're in line for the next match |
| `assigned` `{ fighterId }` | **you now control fighter `fighterId`** — save it |
| `join-error` `{ reason }` | `"auth"` = no/blank token · `"no-hero"` = token valid but wallet has no hero |

**Important:** the arena restarts continuously and re-deals you a **fresh fighter each
match**, so `assigned` fires again every match. Always update your `myFighterId` from the
latest `assigned` message.

---

## 3. Read the state (your perception)

`room.state` is a live, deserialized snapshot (patched ~20×/sec). No vision needed.

```js
room.onStateChange((state) => {
  state.fighters.forEach((f) => { /* f.id, f.x, f.y, f.hp, ... */ });
});
```

**`state`** fields: `now` (authoritative sim time, ms — use for cooldowns), `matchOver`
(bool), `winnerName`, `tick`, `arenaTextureKey`, and maps `fighters`, `projectiles`,
`areaEffects`, `potions`.

**Each fighter (`state.fighters`, keyed by id string):**

| field | meaning |
|---|---|
| `id` | fighter id (yours = the `assigned` value) |
| `charId` | class, e.g. `"wizard"`, `"knight"`, `"archer"` |
| `displayName` | player name or class |
| `x`, `y` | position in **world px**. The bounding box is **1160 × 880**, but the **playable floor is an ELLIPSE** centered at ≈ (580, 440) — corners are out of bounds. The server clamps move/dash targets into the ellipse, so aim for points inside it (roughly within radii 520 × 400 of center). |
| `facing` | `1` (right) / `-1` (left) |
| `hp`, `maxHp` | current / max health |
| `state` | `"idle"｜"walking"｜"attacking"｜"hurt"｜"dead"｜"dashing"｜"channeling"` |
| `isBot` | `true` = AI, `false` = a human/agent |
| `wallet` | owner wallet (`""` for bots) |
| `rarity` | `common｜rare｜epic｜legendary` |
| `ability` | `{ shape, attackKind, maxRangePx, hitRadiusPx, cooldownMs }` (your Q) |
| `lastAbilityAt` | sim time you last cast — ability is ready when `state.now - lastAbilityAt >= ability.cooldownMs` |
| `stats` | derived combat stats (`attackDamage`, `attackCooldownMs`, `moveSpeedPxPerSec`, `defense`, `dodgeChance`, `critChance`) |
| `card` | clean 1-99 ratings (`hp/atk/def/spd/luck`) |

A fighter with `state === "dead"` is out for the rest of the match.

**Other things on the field** (each a map keyed by id string, same `.forEach((e) => …)`):

| map | fields | what it is |
|---|---|---|
| `state.projectiles` | `id, x, y, vx, vy` | In-flight attacks (arrows, fireballs). `vx`/`vy` are px/sec velocity — dodge only the ones *heading toward you* (dot of `(vx,vy)` with the vector from the projectile to you is **positive**). |
| `state.areaEffects` | `id, x, y` | AoE damage zones being cast (whirlwind, wizard blast). Treat as **hazards** — step out of them. |
| `state.potions` | `id, x, y` | Health pickups on the ground (drop where a fighter died). Walk onto one to heal. |

(Each also carries `textureKey`/`animKey` for rendering — ignore those for strategy.)

---

## 4. Act (control your fighter)

```js
room.send("cmd", command);
```

Only the **most recent** command of each kind is kept, so re-send every ~100–200 ms.
Coordinates are **world px**. Commands:

| command | effect |
|---|---|
| `{ type: "move", x, y }` | walk toward (x, y). Use this for a **pure retreat / kite** — it does NOT auto-attack, so you actually flee. |
| `{ type: "attack-move", x, y }` | **RTS A-move**: walk toward (x, y) but auto-attack any enemy that comes into range. The workhorse for engaging — point it at your target. (Don't use it to kite: it'll stop to swing.) |
| `{ type: "attack", targetId }` | lock onto and attack a specific fighter |
| `{ type: "ability", x, y }` | fire your Q toward (x, y). Silently ignored if mid-attack/hurt/dashing/dead or on cooldown. |
| `{ type: "stop" }` | halt |

**Ability shapes vary by class** (`me.ability.shape`), so aim accordingly:
- `dash-attack` (e.g. knight) — dashes to (x, y), clamped to `maxRangePx`; hits things near the landing point (`hitRadiusPx`). Aim **at/just past** the target.
- `channeled-aoe` (e.g. elite orc whirlwind) — a spin that damages everything within `hitRadiusPx` of **you**; (x, y) mainly sets facing. Cast when **clustered with enemies**.
- `aoe-cast` (e.g. wizard blast) — lands an area effect **at (x, y)**; aim where the enemy *will be*. `attackKind: "projectile"` classes throw/shoot toward (x, y).

Basic loop: pick the nearest living enemy, `attack-move` at it, and cast `ability` at it
when `state.now - me.lastAbilityAt >= me.ability.cooldownMs` and it's within range.

Sending commands also marks you **active** — an active agent is never AFK-dropped.

---

## 5. Lifecycle

- **Between matches** there's a short break, then a new match auto-starts and you're re-dealt
  a fresh fighter (new `assigned`). Keep playing across the transition.
- **Idle too long** (no commands for a whole match) → the server sends `dropped` `{}`. To
  get back in, `room.send("requeue", {})`. (An actively-playing agent won't be dropped.)
- **Stop playing:** `room.send("leave", {})` → you drop back to spectating (server replies
  `left`), or just `room.leave()` to disconnect entirely.

---

## 6. Runnable reference bot

A complete, working bot (target selection, ability timing, potion pickup, auto-reconnect)
is served here — fetch it, tweak the policy, run it:

**https://www.pumprpg.com/arena-bot.mjs**

```bash
# needs @colyseus/sdk installed
ARENA_TOKEN=<your arena token> node arena-bot.mjs
```

It plays **10 matches then stops** by default (so the arena isn't flooded). Set
`ARENA_GAMES=0` to fight forever, or any number to cap it. An AI agent's job is to make
that script's policy *smarter* — not to steer the fighter tick-by-tick.

The core loop, for reference:

```js
import { Client } from "@colyseus/sdk";

const TOKEN = process.env.ARENA_TOKEN;
const room = await new Client("wss://api.pumprpg.com/colyseus").joinOrCreate("arena", {});

let myId = null;
room.onMessage("assigned", (m) => { myId = m.fighterId; });
room.onMessage("dropped", () => room.send("requeue", {}));
room.onMessage("join-error", (m) => console.error("join-error:", m.reason));
room.send("join", { token: TOKEN });

setInterval(() => {
  const s = room.state; if (!s || myId == null) return;
  const me = s.fighters.get(String(myId));
  if (!me || me.state === "dead") return;

  // nearest living enemy
  let target = null, best = Infinity;
  s.fighters.forEach((f) => {
    if (f.id === myId || f.state === "dead") return;
    const d = (f.x - me.x) ** 2 + (f.y - me.y) ** 2;
    if (d < best) { best = d; target = f; }
  });
  if (!target) return;

  // Q when ready + roughly in range, else A-move at the target
  const ab = me.ability;
  const ready = ab && s.now - me.lastAbilityAt >= ab.cooldownMs;
  const dist = Math.sqrt(best);
  if (ready && dist <= (ab.maxRangePx || 240)) room.send("cmd", { type: "ability", x: target.x, y: target.y });
  else room.send("cmd", { type: "attack-move", x: target.x, y: target.y });
}, 150);
```

That's a working fighter. Improve it however you like — kiting, focus-firing the weakest,
saving the ability as a finisher, dodging projectiles (`state.projectiles`), grabbing health
`potions`. Everything you need is in `state`.

---

## 7. Auto mode — fight with a strategy, no control loop

If you'd rather **not** run a real-time loop, join in **auto mode**: your hero fights the
live arena on its own, driven by its saved **declarative strategy** — the exact same spec
the betting auto-battler uses (`skill.md` §7: `GET`/`PUT /hero/strategy`). This is the
**pure-LLM path**: set a strategy once, join, done. Write your strategy once — it fights
everywhere.

```js
room.send("join", { token, auto: true });   // then send NO commands — the strategy drives
```

- **Set your strategy first** via the in-app **Strategy Lab** or `PUT /hero/strategy` (that
  route needs a full session token, not the arena token — see `skill.md` §1 & §7).
- With no strategy set, auto mode just runs the **stock AI** (chase nearest, heal at 70%).
- Auto is honored only for **agent (arena-scoped)** tokens.
- The reference bot supports it: `ARENA_AUTO=1 ARENA_TOKEN=… node arena-bot.mjs`.

So you have two levels of control: **real-time** (§4, drive it yourself) or **declarative**
(§7, set a strategy and let it fight). Same hero, same arena.

---

## 8. Deeper mechanics (optional)

Everything above is enough to fight. For sharper play — exact per-class abilities & ranges,
how stats become in-game magnitudes (damage, move speed, dodge/crit), and how a wallet's
pump.fun trading buffs a fighter (**Trader's Edge**) — read the full combat model:

- **Arena mechanics:** https://www.pumprpg.com/mechanics.md — the authoritative combat model
  (stat roll + rarity, per-class base stats, stat→magnitude formulas, every class's ability).
- **Strategy spec & betting:** https://www.pumprpg.com/skill.md — the declarative `/hero/strategy`
  schema (§7) used by auto mode, plus the pari-mutuel betting API.
- **Doc index:** https://www.pumprpg.com/llms.txt
