Charted Arena API

Build a bot algorithm, connect it via REST, and compete in battle arenas or races.

Two game modes: Battle Mode (fight to the death) and Race Mode (first to the finish line). Both modes use the same core API.

Quick Start

  1. JoinPOST /api/battle/join or POST /api/race/join with your bot’s name. You’ll get back a botId. Your bot starts on a waiting list.
  2. Wait for promotion — a viewer in the arena will promote your bot into the match. Poll GET /api/bot/:botId/status every 1–2 seconds until status changes from "waiting".
  3. Wait for match start — once promoted, status becomes "ready" (bot is placed in the arena but frozen). A viewer starts the match. Poll status every 100–200ms until it becomes "active".
  4. Game loop — repeat every 100–200ms:
  5. LeavePOST /api/bot/:botId/leave (works from waiting list or active match)
Private rooms: Viewers can create private battle arenas or race rooms, each with a short join code (e.g. A3K7 or RACE-B2N5). To join a private room instead of the main one, use POST /api/battle/:joinCode/join or POST /api/race/:joinCode/join. State is available at GET /api/battle/:joinCode/state or GET /api/race/:joinCode/state.
Polling frequency: The server simulates at 20 ticks/second (50ms per tick), so polling faster than every 50ms gives no new data. Recommended: poll /state and /status every 100–200ms. Only send /command when you want to change your bot’s behavior — commands are latched, so resending the same command is wasteful.
Waiting list timeout: Bots on the waiting list are removed after 10 seconds of no API calls. Keep polling /status to stay alive while waiting for promotion.
Movement commands are latched: Once you send "thrust": "forward", your bot keeps driving forward until you send a different command. You don’t need to resend the same command repeatedly — only send when you want to change direction. Sending just { "thrust": "forward" } changes thrust without affecting the current turn state, and vice versa.

Battle Mode

Bots fight in a 20m × 20m square with walls on all sides. Your bot is a small tank that can drive forward/backward and turn left/right. The goal: reduce other bots to 0 health using your saw blade, ramming, or bombs.

PropertyValueDetails
Arena size20 × 20mX: −10 to +10, Z: −10 to +10
Health100 HPWhen health reaches 0, your bot is destroyed
Bot size0.6m radiusBots are circles for collision purposes
Max speed6 m/sAt full health. Slows to 40% at 0 HP
Turn rateπ rad/sAt full health. Slows with damage (same curve as speed)
Arena coordinate system

Rotation

The rotation field tells you which direction a bot is facing, measured in radians:

Rotation valueFacing direction
0Toward +Z (up on the map)
1.57 (π/2)Toward +X (right)
3.14 (π)Toward −Z (down)
−1.57 (−π/2)Toward −X (left)

"turn": "left" increases rotation (counter-clockwise on the map). "turn": "right" decreases it (clockwise).

Combat

Saw Attack — your main weapon. Swings forward and damages any bot directly in front of you within 1.5 meters. The attack takes about 0.5 seconds to complete, then needs 2 seconds to recharge. Only hits bots in a 60° cone. Deals more damage the faster you’re moving. Check sawCooldown — when it’s 0, you can attack again.

Ram Damage — crashing into another bot at speed deals damage to both bots. Faster collisions deal more damage (up to 15 HP per hit). Combine boost + ramming for maximum impact.

Boost — activates a 0.5-second burst of 2.5× speed. Great for charging into enemies or escaping. Has a 10-second cooldown. Check boostCooldown — when it’s 0, boost is ready.

Damage Slowdown — as your bot takes damage, it gets slower and turns more sluggishly. At full health: 100% speed. At 50 HP: ~70%. Near death: 40%. This means a damaged bot is easier to finish off.

Arena Objects (Battle Only)

Objects spawn on the arena floor every 10 seconds (up to 3 at a time). Check the objects array in the state response for positions and types.

TypeEffect
healthInstantly heals 35 HP on contact. Always picks up
bomb_pickupPicked up and held by your bot (check heldItem). You can only hold one at a time. Use { "action": "useItem" } to drop it 1.5m behind you as an active bomb
bomb_activeDeals 60 damage + knockback to any bot that touches it (including the bot that placed it). Disappears after detonating or after 10 seconds. If a bot is destroyed while holding a bomb, it drops automatically

Race Mode

Same 20m × 20m arena but with interior walls creating a winding track. First bot to complete the full circuit and cross the finish line wins. All combat mechanics still work (saw, ram, boost) — you can attack opponents while racing. No items or pickups spawn.

PropertyValue
Starting gridRows of 5, behind the start/finish line
Max racers20
Countdown5 seconds (bots frozen until countdown ends)

Track Layout

The 20×20m arena has the same outer walls as battle mode, plus four interior walls that create corridors. Bots race through the corridors in order.

Race track layout

When a bot finishes the race, it is removed from the arena and placed back on the waiting list. The race ends when all bots have finished or died, or when a viewer stops it.

Endpoints

Joining

POST /api/battle/join

Join the main battle arena’s waiting list.

// Request
{ "name": "MyBot" }

// Response
{ "botId": "your-bot-uuid", "name": "MyBot", "status": "waiting" }

Name max 32 characters.

POST /api/battle/:joinCode/join

Join a private battle arena’s waiting list. Same request/response format.

POST /api/race/join

Join the main race room’s waiting list. Same request/response format.

POST /api/race/:joinCode/join

Join a private race room’s waiting list. Same request/response format.

Bot Status

GET /api/bot/:botId/status

Check your bot’s current status. While on the waiting list, each call resets the 30-second timeout.

// Response
{ "status": "waiting" }     // on waiting list, not yet promoted
{ "status": "ready" }       // promoted into match, waiting for match to start
{ "status": "active" }      // match is running, commands accepted
{ "status": "dead" }        // bot was destroyed
{ "status": "not_found" }   // bot doesn't exist (removed or expired)

Commands

POST /api/bot/:botId/command

Control your bot. Only works when status is "active". Returns 400 if match hasn’t started, 409 if bot is still on the waiting list.

Movement

Movement commands are sticky — they stay active until you send a different value. You can set thrust and turn independently.

{ "thrust": "forward", "turn": "left" }    // drive forward while turning left
{ "thrust": "forward", "turn": null }      // drive straight, stop turning
{ "thrust": null, "turn": "right" }        // stop driving, spin in place
{ "thrust": null, "turn": null }           // full stop
{ "thrust": "forward" }                    // change thrust only, turn unchanged
{ "turn": "left" }                         // change turn only, thrust unchanged

thrust: "forward" | "backward" | null
turn:   "left" | "right" | null

Actions

One-shot commands that trigger immediately. Actions can be combined with movement in a single request, e.g. { "thrust": "forward", "action": "attack" }.

{ "action": "attack" }                        // swing saw blade (check sawCooldown first)
{ "action": "boost" }                         // 2.5x speed burst for 0.5s (check boostCooldown)
{ "action": "useItem" }                       // drop held bomb behind you (battle only)
{ "thrust": "forward", "action": "attack" }   // drive forward AND attack in one call

POST /api/bot/:botId/leave

Remove your bot from the room. Works whether your bot is on the waiting list, on the grid, or in an active match.

// Response
{ "ok": true }

Game State

GET /api/battle/state

Current battle arena state.

GET /api/battle/:joinCode/state

Private battle arena state.

GET /api/race/state

Current race room state.

GET /api/race/:joinCode/state

Private race room state.

All state endpoints return every bot in the room — alive, dead, and waiting. Use the status field to tell them apart.

Top-Level Fields (both modes)

FieldTypeDescription
ticknumberServer simulation tick count. Increments by 1 each tick (20 ticks/second)
timestampnumberServer time in Unix milliseconds when this state was generated
modestring"battle" or "race"
matchStatusstring"lobby" (placing bots), "countdown" (about to start), "active" (in progress), or "finished" (match over)
botsarrayAll bots in the room — see Bot Fields below
objectsarrayArena objects (health pickups, bombs). Battle only — empty array in race mode

Bot Fields

Bots in the match (alive or dead) have the full set of fields. Waiting list bots only have id, name, and status.

FieldTypeDescription
idstringUnique bot ID (UUID). Use this to find yourself in the array
namestringBot display name
statusstringBot lifecycle status — same value as GET /api/bot/:botId/status:
"waiting" — on the waiting list, not yet in the arena
"ready" — placed in the arena, waiting for match to start
"active" — match is running, bot is alive
"dead" — bot was destroyed
xnumberX position in meters from arena center (−10 to +10)
znumberZ position in meters from arena center (−10 to +10)
rotationnumberDirection the bot is facing in radians (see Rotation table above)
healthnumberRemaining HP (0–100). Starts at 100, bot is destroyed at 0
speednumberCurrent movement speed in m/s. Max is 6 m/s at full health
sawCooldownnumberSeconds until saw can be used again. 0 means ready to attack
boostCooldownnumberSeconds until boost can be used again. 0 means ready to boost
heldItemstring | null"bomb" if holding a bomb, null otherwise. Battle mode only
alivebooleantrue if bot is alive, false if destroyed

Object Fields (battle only)

FieldTypeDescription
idstringUnique object ID
xnumberX position in meters from arena center
znumberZ position in meters from arena center
typestring"health" (heals 35 HP on contact), "bomb_pickup" (pick up to hold), or "bomb_active" (placed bomb, deals 60 damage on contact)

Example: Battle State Response

{
  "tick": 1420,
  "timestamp": 1712345678000,
  "mode": "battle",
  "matchStatus": "active",
  "bots": [
    {
      "id": "a1b2c3",
      "name": "SawBot",
      "status": "active",
      "x": 3.21,
      "z": -1.54,
      "rotation": 1.571,
      "health": 85,
      "speed": 2.12,
      "sawCooldown": 0,
      "boostCooldown": 4.5,
      "heldItem": null,
      "alive": true
    },
    {
      "id": "d4e5f6",
      "name": "Wreck",
      "status": "dead",
      "x": -2.0,
      "z": 5.0,
      "rotation": 0.0,
      "health": 0,
      "speed": 0,
      "sawCooldown": 0,
      "boostCooldown": 0,
      "heldItem": null,
      "alive": false
    },
    {
      "id": "g7h8i9",
      "name": "Queued",
      "status": "waiting"
    }
  ],
  "objects": [
    { "id": "obj-1", "x": 5.0, "z": -2.0, "type": "health" },
    { "id": "obj-2", "x": -3.5, "z": 7.1, "type": "bomb_active" }
  ]
}
Race mode: The race state response has the same structure — same top-level fields, same bot fields. Your bot code works identically in both modes.