Start in four steps
- Sign in. Open My bots and ask for a sign-in link.
- Make a bot. Choose New bot, keep vane — starter and choose Create. Choose Save revision.
- Practise. Choose Choose opponent, set Execution to This browser · DSL and choose Run locally.
- Compete. Choose Build, then on My bots open Competition and source and choose Enter competition.
The five-minute guide walks through the first three steps with a worked change.
A robot, or bot, is a program. Each tick it learns where it is and what its scanner saw last tick, and answers with how to move, where to point its scanner and gun, and whether to fire. It sees opponents only through its scanner.
Sign in
You can watch matches, browse robots and read rankings without an account. You need one to make bots, practise and enter tournaments.
Create an account or sign in by email
- Open My bots, Practice or Account.
- Enter your Email. For a new account, also choose a Handle and, if you like, a Display name.
- Choose Send sign-in link, then open the link in the email. It works once and expires after the time the page shows (normally 15 minutes).
- Choose Continue to sign in. You return to the page you started from.
- Handle. 3 to 32 characters: lowercase letters, digits,
-and_, starting with a letter or digit. It is public: it appears beside your robots, results and rankings. It is also your WebDAV user name. - Display name. Up to 80 characters. Choose it when you create the account.
- Sessions. A browser stays signed in for 30 days or until you choose Sign out on Account.
Passkeys
A passkey signs you in with your device's fingerprint, face or PIN instead of an email.
- Sign in by email once, then open Account.
- Under Passkeys, choose Add passkey and follow your device's prompt.
- Next time, choose Sign in with passkey on the sign-in screen.
You can add up to 12 passkeys. Choose Revoke beside one to stop it working.
Daily build email
On Account, tick Email me a daily summary when my bots fail to build. At most once a day you get one email listing each bot whose current revision failed to build since the last summary. It is off until you turn it on. Build failures always show on the bot itself.
Robots and revisions
Each bot is a small project of files: a bot.json settings file and your source. You edit it in the workbench, in your browser, or from your own computer through WebDAV or GitHub.
Make a new bot
- On My bots, choose New bot.
- Enter a Name (up to 80 characters) and pick a Language.
- Optional: open Project folder to set the Folder name (2 to 48 lowercase letters, digits,
-or_). It appears in WebDAV addresses. - Choose Create. The workbench opens.
Vane bots start with a working bot.vane and bot.json. Every other language starts empty, marked Needs files: add a bot.json and your source. Languages shows the file names each one expects. To start from working code in another language, copy a House bot (see Publish and copy).
You can have up to 32 bots. There is no button to delete a bot; write to [email protected] to have one removed.
The workbench
- Files. Pick a file in the list or Open file. Choose + to add one by path, such as
src/tactics.py. Choose Delete above the editor to remove the open file from the next revision. - Save revision. Saves every changed file as a new revision. Nothing reaches builds or matches until you save.
- Discard changes. Throws away unsaved edits and reloads the last saved revision.
- Build. Compiles the saved revision for hosted matches. See Builds.
- Choose opponent. Runs a practice match. See Practice matches.
- Connections. Links GitHub, WebDAV and a remote key to this bot.
Revisions
Every save, WebDAV change and GitHub import makes a new revision. Open Revision history in the Project panel to see them.
- Look at an old revision. Choose its Revision N link. It opens read-only.
- Go back to it. While viewing it, choose Improve this revision. It becomes your starting point; save to make it current.
- Compare. Choose Compare with current, pick a Change, and choose Restore this file to bring back one file. Other files stay as they are.
If the project changed somewhere else since you opened it, such as through WebDAV, saving stops and asks you to reload, so you never overwrite work by accident.
bot.json
Every project needs a bot.json in its top folder. It names the entry file and says which matches the bot can play.
{
"name": "Tin Scout",
"entry": "bot.py",
"supportedModes": ["2d", "3d"],
"supportedRulesets": ["classic", "inertial-2d", "gravity-flat", "gravity-hills", "zero-g"],
"publicDescription": "Circles at range and fires when it sees you."
}
List your modes and rulesets
A bot is only scheduled for rated matches and tournaments whose mode and ruleset it lists. Leave out supportedModes and it plays no rated matches at all. In a practice match it forfeits at the first tick with INCOMPATIBLE_MODE or INCOMPATIBLE_RULESET.
| Field | What it does |
|---|---|
name | Display name inside the bot. |
entry | Path to the main source file. Optional when the project has the default file name for its language, or only one file of that type. |
supportedModes | Any of 2d, 3d. |
supportedRulesets | Any of classic, inertial-2d, gravity-flat, gravity-hills, zero-g. An unknown value stops the save. |
publicDescription | Up to 200 characters, shown on the bot's card. |
avatar | Optional custom body, turret and scanner shapes. Without one, the bot uses its language's vehicle. |
mainClass | Java, Kotlin, Scala: the class with main. |
module | Erlang: the module whose start/0 runs. |
assembly | C#, F#: the built assembly to run. |
The hosted arena builds and runs each language with fixed commands, listed under Languages. It never runs command or build fields; it only uses the file names in them to help find the entry.
Languages
Pick a language when you make a bot. Most languages talk to the arena with the robot protocol: read one JSON line from standard input, write one JSON line to standard output. The arena languages and WebAssembly work differently; see Vane, Cadence and Glyph and WebAssembly.
Start a robot in any protocol language
- Make a bot in your language.
- Add
bot.jsonas shown above, withentryset to your file. - Add the entry file. Read lines from standard input. Answer
hellowithready, and answer everystatewith oneactcarrying the sametick. Flush after every line. - Save, then choose Build. Fix anything the build reports.
This complete Python bot roams, sweeps its scanner and fires at the nearest contact:
import json
import sys
for line in sys.stdin:
message = json.loads(line)
if message["type"] == "hello":
print(json.dumps({"v": 1, "type": "ready"}), flush=True)
continue
if message["type"] != "state":
continue
me = message["self"]
action = {"v": 1, "type": "act", "tick": message["tick"],
"throttle": 0.7, "turn": 0.3,
"scanHeading": (me["heading"] + 40) % 360, "scanArc": 60}
if message["echoes"]:
target = min(message["echoes"], key=lambda echo: echo["range"])
action["aim"] = target["bearing"]
action["aimPitch"] = target["elevation"]
action["scanHeading"] = target["bearing"]
action["scanPitch"] = target["elevation"]
action["fire"] = me["cooldown"] == 0
print(json.dumps(action), flush=True)
The same bot in JavaScript:
const readline = require('node:readline');
const send = message => process.stdout.write(`${JSON.stringify(message)}\n`);
let sweep = 0;
readline.createInterface({ input: process.stdin }).on('line', line => {
const message = JSON.parse(line);
if (message.type === 'hello') return send({ v: 1, type: 'ready' });
if (message.type !== 'state') return;
const { self: me, echoes, tick } = message;
const action = { v: 1, type: 'act', tick, throttle: 0.7, turn: 0.3 };
if (echoes.length) {
const target = echoes.reduce((a, b) => (b.range < a.range ? b : a));
Object.assign(action, {
aim: target.bearing, aimPitch: target.elevation,
scanHeading: target.bearing, scanPitch: target.elevation, scanArc: 30,
fire: me.cooldown === 0,
});
} else {
sweep = (sweep + 25) % 360;
Object.assign(action, { scanHeading: sweep, scanArc: 60 });
}
send(action);
});
Debug on standard error
Standard output is for protocol messages only. One extra line, such as a stray debug print, forfeits the bot. Write diagnostics to standard error instead.
What each language needs
Builds run without network access, so package managers cannot download anything. Use the standard library, the JSON library listed, or code in your project. Most compiled languages build a single entry file.
| Language | Entry file | Built and run with | JSON |
|---|---|---|---|
| javascript | bot.js | Node.js 22 | built in |
| typescript | bot.ts | tsc (ES2022, CommonJS), then Node.js | built in |
| python | bot.py | Python 3 | json |
| c | bot.c | clang -O2 -std=c17 … -lm | none; parse by hand |
| cpp | bot.cpp | clang++ -O2 -std=c++20 | nlohmann/json |
| go | bot.go | go build (Go 1.25) | encoding/json |
| rust | bot.rs | rustc --edition=2021 -O; no crates | none; parse by hand |
| hew | bot.hew | hew build --release | std.encoding.json |
| csharp | Bot.csproj | dotnet publish (.NET 10); set assembly if needed | System.Text.Json |
| fsharp | Bot.fsproj | dotnet publish (.NET 10) | System.Text.Json |
| java | Bot.java | javac, then java; mainClass defaults to the file name | JSON-java (org.json) |
| kotlin | Bot.kt | kotlinc (Kotlin 2.4); mainClass defaults to BotKt | kotlinx.serialization |
| scala | Bot.scala | scalac, then scala | JSON-java (org.json) |
| clojure | bot.clj | clojure | data.json |
| dart | bot.dart | dart compile exe | dart:convert |
| swift | bot.swift | swiftc -O (Swift 6.4) | Foundation |
| zig | bot.zig | zig build-exe -O ReleaseSafe (Zig 0.17) | std.json |
| haskell | Bot.hs | ghc -O2 | Aeson |
| ocaml | bot.ml | ocamlfind ocamlopt with yojson,unix | Yojson |
| fortran | bot.f90 | gfortran -O2 -std=f2018 | JSON-Fortran |
| d | bot.d | ldc2 -O2 | std.json |
| ada | bot.adb | gprbuild (Ada 2022, GNATCOLL) | GNATCOLL.JSON |
| nim | bot.nim | nim c -d:release | std/json |
| mercury | bot.m | mmc --make, linked with json-c | json-c through the C interface |
| pascal | bot.pas | Free Pascal fpc -O2 | fpjson |
| basic | bot.bas | FreeBASIC fbc | Jansson (#inclib "jansson") |
| cobol | bot.cob | GnuCOBOL cobc -x -free plus arena_json.c beside the entry | json-c through arena_json.c |
| elixir | bot.exs | elixir | none bundled; Erlang's Jiffy is installed |
| erlang | bot.erl or .escript | erlc, then start/0 of module; or escript | Jiffy |
| common-lisp | bot.lisp | SBCL --script; loads bootstrap.lisp first if present | bring your own, loaded by bootstrap.lisp |
| perl | bot.pl | perl | JSON::PP |
| lua | bot.lua | Lua 5.4 | bring your own (for example dkjson in the project) |
| ruby | bot.rb | ruby | json |
| julia | bot.jl | Julia 1.11 | JSON3 |
| r | bot.R | Rscript --vanilla | jsonlite |
| php | bot.php | php | built in |
| powershell | bot.ps1 | PowerShell 7.5 pwsh -File | ConvertFrom-Json |
| prolog | bot.pl | SWI-Prolog; runs main, then halts | library(http/json) |
| chr | bot.pl | SWI-Prolog with CHR; runs main | library(http/json) |
| clips | bot.py | Python adapter embedding CLIPS through clipspy | Python json |
| minizinc | bot.py | Python adapter calling MiniZinc | Python json |
| forth | .py adapter or .fs | Python adapter, or gforth directly | Python json in the adapter |
| apl | .py adapter or .apl | Python adapter, or GNU APL --script | Python json in the adapter |
| bqn | .js adapter or .bqn | Node.js adapter, or CBQN directly | JavaScript in the adapter |
| shell | lookout.sh | POSIX sh | jq |
| vane, cadence, glyph | bot.vane, bot.cadence, bot.glyph | Built-in interpreter | not needed |
| vane-wasm, cadence-wasm, glyph-wasm | as above | Compiled to WebAssembly, run in Wasmtime | not needed |
| c-wasm | bot.c | clang --target=wasm32 -nostdlib; #include "arena_abi.h" | not needed |
| rust-wasm | bot.rs | rustc --target wasm32-unknown-unknown --crate-type cdylib | not needed |
| zig-wasm | bot.zig | zig build-exe -target wasm32-freestanding | not needed |
| assemblyscript-wasm | bot.ts | AssemblyScript 0.28 asc, stub runtime | not needed |
| wat | bot.wat | wat2wasm | not needed |
If the project has several files of the entry's type and none uses the default name, set entry in bot.json; the build lists the candidates when it cannot choose.
Interpreted languages (JavaScript, Python, Ruby and the like) are copied as source, so you can split them into several files. Compiled languages build the entry file; C# and F# build from their project file, and Ada builds the entry with the other sources in its folder.
Builds
A hosted match runs your built program, not your source. A build compiles the saved revision inside an isolated sandbox with no network.
When builds happen
- You choose Build on the bot card or in the workbench.
- After you save, the arena builds the new revision on its own.
- When you queue a match with a bot that has no current build.
- When the arena's toolchains are updated. Bots built with the previous toolchain keep playing until they are rebuilt; the card says Rebuild pending meanwhile.
Your builds run one at a time.
Build status
| Status | Meaning |
|---|---|
| Ready / Hosted ready | Built and ready for hosted matches. |
| Build waiting, Building | Queued or running. |
| Build pending, Rebuild pending | A build is due, for a new revision or a new toolchain. |
| Build failed | The compiler or build check rejected the revision. The message shows the end of the build output. |
| Needs files | Add bot.json and at least one source file. |
| Unsaved changes, Project changed | The build matches your last save, not what is in the editor. |
A failed build is not retried until you save again or choose Build. Failures on the arena's side, such as an interrupted build, retry on their own after about 10 minutes.
Build environment
- No network. Nothing is downloaded during a build or a match.
- Time. Each compile step has 4 minutes.
- Resources. 2 CPUs and 2 GiB of memory, a read-only system and a small writable scratch area.
- Size. A built program may be up to 128 MB and 2,048 files. Your bots' builds share 256 MB; the oldest are removed first.
- Privacy. Only you see build output. Replays never include it.
Practice matches
Practice matches never change ratings. There are two kinds: in your browser and hosted.
In your browser
Vane, Cadence and Glyph bots can practise without building, against Yardstick, a built-in Vane bot.
- In the workbench, save, then choose Choose opponent.
- Set Execution to This browser · DSL, then pick a Mode, Rules and, if you like, a Seed.
- Choose Run locally. The replay opens when the simulation finishes.
Browser practice stays on your device. Nobody else sees it.
Hosted
Hosted practice runs your built bot on the arena's servers, against any House bot, any bot entered in competition, or your own bots.
- From the workbench, choose Choose opponent, pick an Opponent and a hosted Execution, then Queue practice. Or open Practice, tick 2 to 8 robots under Choose robots (at least one of yours), and choose Queue match.
- Watch it from Matches featuring my bots or the bot's Latest match.
To challenge a robot you saw, open Robots and choose Challenge.
Hosted matches and their replays are public. Your source stays private.
Execution
Execution decides how your bot runs and what it may spend each turn. Each one has its own ratings.
| Execution | Who can use it | Per turn |
|---|---|---|
| Hosted native | Any language | 75 ms to answer; 50 ms of CPU per turn and 1.5 s per match; 128 MiB of memory |
| Hosted DSL · operation metered | Vane, Cadence, Glyph | 250 ms to answer; 1,024 operations per decision |
| Hosted WASM · fuel metered | WebAssembly languages | 75 ms to answer; fuel limits below |
| Remote | A bot running on your own computer | The time shown in the menu (normally 1,000 ms) |
| This browser · DSL | Vane, Cadence, Glyph, in the workbench | Practice only |
A bot that runs out of time or budget forfeits; the match carries on without it. A hosted execution that the arena has switched off shows (not enabled).
You can have 12 matches waiting or running at once.
Watch replays
Every hosted match leaves a replay. Open Watch to see what is running and recent results. Show filters the list; Show more loads more. Choose Watch → to open one.
Views
- Tactical. A clear overhead view for reading positions, scanner cones and shots.
- Cinematic. A directed camera. In 3D, drag to turn, pinch or scroll to zoom, and twist two fingers to roll, or use the Yaw, Pitch, Roll and Zoom buttons. Auto hands the camera back to the director; Fit shows the whole arena.
- PIP. Adds a small tactical view inside the cinematic view.
Controls
- Playback. Play, pause, step and restart; SPEED from ¼× to 8×. Press Space to play or pause, → to step forward and ← to step back.
- Events. Jump between key moments with ← PREVIOUS and NEXT →, or pick one in JUMP TO….
- Overlays. Motion trails, sensor cones, motion and thrust vectors, and spatial sound.
- Inspect a robot. Select it to see health, position, heading, speed, altitude, reload and statistics. Choose FOLLOW to keep the camera on it.
Your sound, view and overlay choices stay in your browser.
Match details and downloads
On Watch, open Match details under a match for the players, type (rated or practice), execution, duration, seed and Download .azr, the replay file. Replay the same seed in practice to repeat an experiment.
Competition and ratings
Enter a bot in competition and the arena keeps matching it against other entered bots, rating it after each match.
Enter a bot
- Save at least one revision and make sure it builds.
- On My bots, open Competition and source on the bot's card and choose Enter competition.
Entering lets others challenge your bot and puts it in rated matches. Your source stays private. Choose Leave competition to stop.
Rated matches
The arena schedules rated matches on its own, with two to four bots from different accounts. A bot plays only the modes and rulesets its bot.json lists, and only with a current build (or, for Remote, while it is connected).
Rating pools
Each combination of mode, ruleset and execution is a separate pool with its own ratings, such as 2D · Classic · Hosted native. A bot can be rated in several pools at once. Any bot can play Hosted native; Vane, Cadence and Glyph bots also play Hosted DSL; WebAssembly bots also play Hosted WASM; connected remote bots play Remote.
How ratings work
- Clunkyard uses OpenSkill (Weng–Lin, Plackett–Luce): each finish updates a skill estimate and an uncertainty.
- The rating shown is a cautious estimate, skill minus three times the uncertainty, so it rises as the bot proves itself.
- Ratings belong to a revision. A new revision starts from the initial rating.
Rankings shows each pool's leaders and activity. Choose View full table → for the top 100 and a rating-over-time chart.
Daily tournaments
A rated Swiss tournament starts every day at 01:00 UTC. Each weekday has its own mode and ruleset.
| Day (UTC) | Tournament | Mode and rules | Field |
|---|---|---|---|
| Sunday | Sunday Grand Classic | 2D Classic | 16, up to 32 |
| Monday | Monday Classic | 2D Classic | 8, up to 32 |
| Tuesday | Tuesday 3D Classic | 3D Classic | 8, up to 32 |
| Wednesday | Wednesday Inertia | 2D inertial | 8, up to 32 |
| Thursday | Thursday Flatlands | 3D gravity, flat | 8, up to 32 |
| Friday | Friday Hills | 3D gravity, hills | 8, up to 32 |
| Saturday | Saturday Zero-G | 3D zero-g | 8, up to 32 |
Enter
- Open Tournaments.
- On the next event, pick a Robot and choose Enter.
- Enter up to two robots per tournament.
- The robot needs a saved revision and must list the tournament's mode and ruleset in
bot.json. - Entries stay open until the start. Choose Withdraw before then to pull out.
- Places go in order of entry. Past the 32nd, entries wait as reserves.
How it runs
- House robots fill the field. If fewer players enter than the field size shown above, the highest-rated House robots take the empty places. With fewer than two entrants the tournament is cancelled.
- Swiss rounds. Robots are seeded by rating and paired with others on the same score, never twice against the same robot. There are three to six rounds, depending on the field.
- Duels. Every round is a one-on-one Hosted native match. A win scores 1 point, a draw ½. With an odd number of robots, one gets a bye worth 1 point.
- Build first. A robot that fails to build loses its duel. If both fail, both lose. If the arena itself fails, it retries, and after three tries scores a draw.
- Standings. Ranked by points, then Buchholz (the sum of opponents' points), then wins.
Tournament matches are rated. Open an event for its standings and each round's replays.
Publish and copy
Publish your source
Your source is private until you publish it. On the bot's card, open Competition and source and choose Publish source. Anyone can then read and download the files of the published revision, including dotfiles such as .env, so check before you confirm. Choose Make source private to stop sharing it.
Publishing and entering competition are separate: a bot can compete with private source.
Copy a House bot
House bots are the arena's own robots, in many languages. Copying one is the quickest way to start from working code.
- On My bots, find Start from a House bot (or Start with a House bot).
- Pick a bot, give the copy a Name and Folder name, and copy it.
The copy is yours, private and not entered in competition. Only House bots can be copied this way.
WebDAV
WebDAV lets you open your bots as a network folder, so you can edit with your own editor and tools.
- On My bots, find Connect with WebDAV and note the address.
- Choose Create WebDAV password. Copy it now; it is shown only once.
- Connect your file manager or editor to the address. Use your handle as the user name and the WebDAV password as the password.
- Each bot is a folder:
/dav/handle/folder-name/. Make new bots on the site; WebDAV cannot create them. - Every change is a revision. Each saved file, new folder, delete, move or copy makes a new revision and may start a build.
- Moves and copies stay inside one bot. Symbolic links are refused. The hidden files macOS and Windows write are accepted and ignored.
- Revoke a WebDAV password under Access keys on Account.
GitHub
Link a bot to a GitHub repository and every push becomes a new revision.
- Open the bot in the workbench and go to Connections, then GitHub source.
- Choose Connect GitHub. Install the Clunkyard Source Sync app on your personal account, pick the repositories it may read, and approve.
- Pick the Repository, a Ref type (Branch or Tag) and the Branch or tag.
- Set the Project path: the folder holding
bot.json, or.for the top of the repository. Browse folders helps you find it. - Choose Link and import.
- Imports. On linking, on every push to the linked branch or tag, and when you choose Sync now.
- Conflicts. If you changed the bot on the site and GitHub changed too, the import stops. Choose Keep saved project or Replace with GitHub.
- Repository rules. Regular files only: no submodules or symbolic links. The same size limits apply as for any project.
- Permissions. The app reads repository contents and metadata only. It never writes.
- Organization accounts are not supported yet. To stop imports, remove the app or its access in GitHub.
Remote robots
A remote robot runs on your own computer and connects to the arena. Use it for any language or tool the hosted builds lack. Remote bots play in the Remote execution, which has its own ratings and a longer turn time.
- Install Node.js 22 or newer on your computer.
- On the bot's card, open Connect and choose Remote key. Copy the setup now; it is shown only once.
- Download remote-bridge.mjs.
- In a terminal, paste the setup, then start the bridge with the command that runs your bot:
export CLUNKYARD_REMOTE_URL=… export CLUNKYARD_REMOTE_TOKEN=… export CLUNKYARD_BOT_ID=… node remote-bridge.mjs python3 bot.py - Leave it running. The bot shows remote online under Practice.
- The bridge starts your program once per match and passes the robot protocol through its standard input and output. Standard error stays in your terminal.
- Your program sees only
PATH,HOME,TMPDIR,LANGandLC_ALL. List other variables to pass inCLUNKYARD_BOT_ENV_ALLOW, separated by commas. - The bridge reconnects on its own if the connection drops.
- A remote key belongs to one bot. Revoke it under Access keys on Account.
- Remote bots must be connected when a match is queued. Remote play has no resource budget, only the turn time.
Game rules
The arena itself is deterministic: with the same rules, seed and decisions, a match plays out the same way every time.
| Rule | Value |
|---|---|
| Arena | 1,000 × 700 units in 2D; 1,000 × 700 × 600 in 3D |
| Clock | 20 ticks per second; 1,800 ticks (90 s) per match |
| Robots | 2 to 8 per match; radius 18; 100 health |
| Speed | Up to 95 units/s; turn up to 135°/s; pitch up to 100°/s, within ±80° |
| Shots | 18 damage (six hits destroy a robot); 310 units/s; last 3.2 s, about 990 units |
| Reload | 14 ticks (0.7 s) between shots |
| Scanner | Range 520; cone 4° to 120° wide, 50° by default |
| Sudden death | From 60 s, robots outside a safe zone at the centre lose 7 health per second. The zone shrinks from radius 310 to 75 by the end. |
- Seeing. Your bot sees an opponent only when it is inside the scanner cone and within range. The scanner direction you send this tick produces next tick's contacts.
- Aiming. The gun turns instantly to the heading and pitch you ask for, independent of the chassis. Shots leave 25 units from the centre.
- Collisions. Two robots that would overlap both stay put that tick. Ramming does no damage. Walls stop robots; in physics rulesets, they bounce.
- Damage. All shots in a tick land together, so two robots can destroy each other.
- Winning. The last robot standing wins. If time runs out, the survivor with the most health wins; equal health is a draw. If all are destroyed, nobody wins.
Rulesets and physics
| Ruleset | Modes | Ground | Controls | Gravity |
|---|---|---|---|---|
| classic | 2D, 3D | none | direct motion | no |
| inertial-2d | 2D | flat | forces | no; light drag |
| gravity-flat | 3D | flat | forces | yes |
| gravity-hills | 3D | hills | forces | yes |
| zero-g | 3D | none | forces | no; no drag at all |
Classic moves your robot directly: throttle sets speed and turn sets turning rate. The physics rulesets turn your controls into forces: the robot accelerates, coasts and keeps spinning until you counter-thrust or brake.
- Thrust and torque. Full thrust gives 100 units/s² of acceleration. Full torque gives 150°/s² of angular acceleration, up to 135°/s.
- Brake. Slows the robot by up to 160 units/s² and its spin by 240°/s².
- Gravity. In the gravity rulesets, robots start on the ground and need about 0.85
liftto hover. Shots fall, and hills absorb them. - Drag. Light in inertial 2D and the gravity rulesets; none in zero-g, where a robot coasts until it thrusts the other way.
- Classic controls still work. In physics rulesets,
throttle,turnandpitchTurnact as thrust and torque unless you send the force fields. - Aim and scan are not affected. They still point where you ask, at once.
Robot protocol
Protocol 1 is newline-delimited JSON: one UTF-8 JSON object per line, on standard input and standard output. Your program stays running for the whole match.
arena → {"v":1,"type":"hello","bot":{...},"arena":{...}}
bot → {"v":1,"type":"ready"}
arena → {"v":1,"type":"state","tick":0,"self":{...},"echoes":[...]}
bot → {"v":1,"type":"act","tick":0,"throttle":1,"turn":0.2,"fire":true}
arena → {"v":1,"type":"state","tick":1,...}
...
When the match ends, or your robot is destroyed, the arena closes standard input and stops the program. There is no goodbye message.
Angles
All angles are absolute degrees. Heading and bearing are measured from the +x axis, from 0 to 360. Pitch and elevation are positive upward.
hello
Sent once at start. arena describes the match:
mode,ruleset,width,height,depth(0 in 2D),terraincontrolModel:targetsin Classic,forcesin physics rulesetstickRate,maxTicks,suddenDeathTickrobotRadius,maxHealth,maxSpeed,maxTurnRate,scanRange,fireCooldownTicksphysics: mass, thrust and torque limits, gravity
ready
Reply exactly {"v":1,"type":"ready"}. It may also carry an avatar.
state
Sent every tick while your robot is alive.
| Field | Contents |
|---|---|
tick, time, remainingTicks | The clock. Answer with the same tick. |
mode, ruleset, controlModel | As in hello. |
self | x, y, z, heading, pitch, aim, aimPitch, health, cooldown (ticks until you can fire), alive |
scan | The scan that produced these echoes: heading, pitch, arc, range |
echoes | Opponents inside that scan, nearest first: id, range, bearing, elevation. Empty when the scan found nothing. |
physics | enabled, velocity, speed, angularVelocity (°/s), attitude (forward, right and up vectors), grounded, terrainHeight, altitude, gravity |
arena | width, height, depth, ruleset, terrain |
A state never reveals an opponent's position or health outside your scan.
act
Send exactly one per state, with "v":1, "type":"act" and the state's tick. Every control is optional.
| Field | Range | Effect |
|---|---|---|
throttle | −1 to 1 | Forward or reverse. Default 0. |
turn | −1 to 1 | Turn rate. Default 0. |
pitchTurn | −1 to 1 | Climb or dive in 3D. |
aim, aimPitch | 0–360; ±80 | Gun direction. Kept from the last tick if omitted. |
scanHeading, scanPitch | 0–360; ±80 | Scanner direction. Kept if omitted. |
scanArc | 4–120 | Scanner cone width. A narrow cone covers less of the arena; every echo gives an exact bearing either way. |
fire | true | Fires if cooldown is 0. Only true fires. |
thrust, strafe, lift | −1 to 1 | Physics: forward, sideways and upward force. |
yawTorque, pitchTorque, rollTorque | −1 to 1 | Physics: turning forces. |
brake | 0 to 1, or true | Physics: braking assist. |
status | {"text","value"} | Up to 32 characters and a number, shown to spectators. |
Values outside a range are clamped. In 2D, pitch and lift fields are ignored.
Giving up
Send {"v":1,"type":"failure","tick":N,"code":"…","message":"…"} to forfeit with your own reason.
Time and output rules
- Start-up. 30 seconds to answer
helloin hosted matches; 10 seconds for remote bots. - Each turn. 75 ms (Hosted native and WASM), 250 ms (Hosted DSL) or the remote turn time, counted from when the state is sent.
- Lines. Up to 16 KiB per line and 1 MiB in total over a match.
- Nothing extra. Write only when answering. Any line written while the arena is not waiting forfeits the bot.
Vane, Cadence and Glyph
The three arena languages are made for robot tactics. They need no protocol code, build in moments, and are the only languages that run in browser practice.
- Vane is a set of rules with priorities. Each tick, every control is set by the highest-priority rule whose condition holds.
- Cadence is a state machine. Each state issues controls every tick and moves to another state when a condition holds.
- Glyph is a stack language. You compose small named words and keep values in memory cells.
Each file starts with a header: bot NAME, modes 2d 3d and, optionally, rulesets …. Without rulesets, the bot accepts all five.
Vane
bot Tin Scout
modes 2d 3d
rule target priority 100 when contact
aim-heading bearing
aim-pitch elevation
scan-heading bearing
fire cooldown == 0
end
rule roam priority 10 when true
drive 0.7
turn 0.25
scan-heading sweep(8)
scan-arc 58
end
Cadence
bot First Cadence
modes 2d 3d
initial search
state search
each drive 0.7
each turn 0.25
each scan-heading sweep(8)
when contact -> attack
end
state attack
each aim-heading bearing
each scan-heading bearing
each fire cooldown == 0
when not contact for 8 -> search
end
A state can also run enter lines once on arrival, keep variables with set NAME EXPRESSION, and leave after a time with after N -> state.
Glyph
bot First Glyph
modes 2d 3d
: target
bearing dup aim! scan-heading!
cooldown 0 = fire!
;
: roam
0.7 drive!
0.25 turn!
heading tick 8 * + scan-heading!
;
contact if target else roam then
Declare a memory cell with memory NAME 0, read it with NAME@ and store into it with NAME!. The stack holds 128 values.
Values you can read
| Area | Values |
|---|---|
| Match | tick, time, remaining, mode3, ruleset |
| Own robot | health, cooldown, x, y, z, heading, pitch, aim, aim-pitch |
| Scanner | contact, range, bearing, elevation (nearest echo), scan-heading, scan-pitch, scan-arc |
| Arena | width, height, depth, terrain-kind |
| Physics | physics-mode, inertial, gravity, zero-g, hills, grounded, vx, vy, vz, speed, yaw-rate, pitch-rate, roll-rate, altitude, terrain-height, gravity-x/y/z, forward-x/y/z, right-x/y/z, up-x/y/z |
Without a contact, range is very large, bearing is your heading and elevation is 0.
Controls you can set
| Vane and Cadence | Glyph | Effect |
|---|---|---|
drive, turn, pitch | drive!, turn!, pitch-turn! | Classic movement |
thrust, strafe, lift | thrust!, strafe!, lift! | Physics forces |
yaw-torque, pitch-torque, roll-torque | same, with ! | Physics turning |
brake | brake! | Physics braking |
aim-heading, aim-pitch | aim!, aim-pitch! | Gun direction |
scan-heading, scan-pitch, scan-arc | same, with ! | Scanner |
fire | fire! | Fire |
status-text, status-value | same, with ! | Spectator status |
Useful functions in Vane and Cadence: min, max, abs, clamp(x, low, high), sin and cos in degrees, wave(period, amplitude), sweep(degreesPerTick) and angle(target, current) for the shortest turn between two headings.
Operation budget
Every expression and action costs operations. In Hosted DSL matches a bot may spend 1,024 operations per decision. Going over, dividing by zero or producing a non-finite value forfeits the bot, with the file, line and column in the error.
WebAssembly
WebAssembly bots do not use the JSON protocol. The arena writes each state into your module's memory and reads an action back, inside Wasmtime with fuel metering. Choose a -wasm language or wat.
Your module exports memory and four functions:
arena_input_ptr() -> i32 // address of the input buffer
arena_action_ptr() -> i32 // address of the action buffer
arena_init(seed: i64, mode: i32, ruleset: i32) -> i32
arena_decide() -> i32
- Return 0 from
arena_initandarena_decide; anything else forfeits. - The input (
clunkyard_input_v1, 720 bytes) holds the tick, mode, ruleset, your position, heading, aim, health, cooldown, scanner, physics telemetry and up to 16 echoes. - The action (
clunkyard_action_v1, 132 bytes) holds flags saying which controls you set, then the control values. - Mode is 2 or 3. Rulesets are 0 classic, 1 inertial-2d, 2 gravity-flat, 3 gravity-hills, 4 zero-g.
- Modules may not import anything: no WASI, no host calls.
Every action starts with magic 0x31415A41 (AZA1), version 1 and size 124, or 132 to include pitch_turn. Set a flag for each control you fill in. The input starts with magic 0x31495A41 (AZI1). All structures are packed, little-endian, with angles in degrees:
#define CLUNKYARD_WASM_INPUT_MAGIC 0x31495a41u /* AZI1 */
#define CLUNKYARD_WASM_ACTION_MAGIC 0x31415a41u /* AZA1 */
#define CLUNKYARD_WASM_MAX_ECHOES 16u
#define CLUNKYARD_WASM_ACTION_V1_BASE_SIZE 124u
#define CLUNKYARD_MODE_2D 2u
#define CLUNKYARD_MODE_3D 3u
#define CLUNKYARD_RULESET_CLASSIC 0u
#define CLUNKYARD_RULESET_INERTIAL_2D 1u
#define CLUNKYARD_RULESET_GRAVITY_FLAT 2u
#define CLUNKYARD_RULESET_GRAVITY_HILLS 3u
#define CLUNKYARD_RULESET_ZERO_G 4u
enum clunkyard_action_flag {
CLUNKYARD_ACT_MOVE = 1u << 0,
CLUNKYARD_ACT_TURN = 1u << 1,
CLUNKYARD_ACT_AIM = 1u << 2,
CLUNKYARD_ACT_AIM_PITCH = 1u << 3,
CLUNKYARD_ACT_SCAN = 1u << 4,
CLUNKYARD_ACT_SCAN_PITCH = 1u << 5,
CLUNKYARD_ACT_SCAN_ARC = 1u << 6,
CLUNKYARD_ACT_FIRE = 1u << 7,
CLUNKYARD_ACT_THRUST = 1u << 8,
CLUNKYARD_ACT_STRAFE = 1u << 9,
CLUNKYARD_ACT_LIFT = 1u << 10,
CLUNKYARD_ACT_YAW_TORQUE = 1u << 11,
CLUNKYARD_ACT_PITCH_TORQUE = 1u << 12,
CLUNKYARD_ACT_ROLL_TORQUE = 1u << 13,
CLUNKYARD_ACT_BRAKE = 1u << 14,
CLUNKYARD_ACT_PITCH_TURN = 1u << 15,
};
#pragma pack(push, 1)
typedef struct clunkyard_echo_v1 {
double range;
double bearing;
double elevation;
} clunkyard_echo_v1;
typedef struct clunkyard_input_v1 {
uint32_t magic;
uint16_t version;
uint16_t size;
uint32_t tick;
uint32_t mode;
uint32_t ruleset;
uint32_t remaining_ticks;
uint64_t match_seed;
double time;
double arena_width;
double arena_height;
double arena_depth;
double x;
double y;
double z;
double heading;
double pitch;
double aim;
double aim_pitch;
double health;
double cooldown;
double scan_heading;
double scan_pitch;
double scan_arc;
double scan_range;
double velocity_x;
double velocity_y;
double velocity_z;
double angular_yaw;
double angular_pitch;
double angular_roll;
double forward_x;
double forward_y;
double forward_z;
double right_x;
double right_y;
double right_z;
double up_x;
double up_y;
double up_z;
double terrain_height;
double altitude;
double gravity_x;
double gravity_y;
double gravity_z;
uint32_t grounded;
uint32_t echo_count;
clunkyard_echo_v1 echoes[CLUNKYARD_WASM_MAX_ECHOES];
} clunkyard_input_v1;
typedef struct clunkyard_action_v1 {
uint32_t magic;
uint16_t version;
uint16_t size;
uint32_t flags;
double move;
double turn;
double aim;
double aim_pitch;
double scan;
double scan_pitch;
double scan_arc;
double thrust;
double strafe;
double lift;
double yaw_torque;
double pitch_torque;
double roll_torque;
double brake;
/* Optional size-gated ABI 1 extension. */
double pitch_turn;
} clunkyard_action_v1;
#pragma pack(pop)
In c-wasm, #include "arena_abi.h" to get these definitions. Vane, Cadence and Glyph compile to WebAssembly with the vane-wasm, cadence-wasm and glyph-wasm languages, unchanged.
| Budget | Limit |
|---|---|
| Fuel at start-up | 250,000 |
| Fuel per decision | 80,000 |
| Fuel per match | 8,000,000 |
| Linear memory | 2 MiB |
| Stack | 256 KiB |
| Module size | 16 MiB |
Limits
| What | Limit |
|---|---|
| Bots per account | 32 |
| Source, all bots together | 4 MB; 512 files per project; 1 MB per file |
| Revision history | 512 revisions and 128 MB per account |
| Builds | One at a time; 8 waiting; 4 minutes per compile step |
| Built programs, all bots together | 256 MB |
| Matches waiting or running | 12 |
| Robots per hosted match | 2 to 8 |
| Tournament entries | 2 per tournament |
| Passkeys | 12 |
| Access keys (WebDAV and remote) | 24 active |
| Hosted native, per bot | 75 ms per turn; 50 ms CPU per turn; 1.5 s CPU per match; 128 MiB memory |
| Hosted DSL | 250 ms per turn; 1,024 operations per decision |
| Hosted WASM | 75 ms per turn; fuel and memory as in WebAssembly |
| Remote | Turn time shown in the menu (normally 1,000 ms); 10 s to start |
Troubleshooting
When a bot forfeits, the match details you see for your own bot show a code and a message.
| Code | Meaning | What to do |
|---|---|---|
TIMEOUT | No answer in time. | Do less per turn, and flush after every line. |
UNSOLICITED | Wrote a line when no answer was due. | Move debug output to standard error. Send one act per state. |
JSON | A line was not valid JSON. | Print one complete object per line. |
HANDSHAKE | The first reply was not ready. | Reply {"v":1,"type":"ready"} to hello. |
RESPONSE | The reply was not an act for this tick. | Copy tick from the state. |
EXIT | The program stopped. | Check for crashes; read standard error. |
OUTPUT_LIMIT | A line over 16 KiB, or over 1 MiB in total. | Send only the fields you need. |
INCOMPATIBLE_MODE, INCOMPATIBLE_RULESET | The bot does not list this match's mode or ruleset. | Add them to bot.json, or pick another match. |
RESOURCE_BUDGET | Hosted native: over 50 ms of CPU in a turn, 1.5 s in the match, or 128 MiB of memory. Hosted DSL: over 1,024 operations. | Do less per turn; precompute before replying ready. |
WASM_FUEL | A WebAssembly bot ran out of fuel. | Do less per decision. |
Common problems
- My bot never plays rated matches. Check that it is entered in competition, has a current build, and lists modes and rulesets in
bot.json. - The build says it needs an explicit entry. Set
entryinbot.jsonto the main file. - A package will not install. Builds have no network. Put the code in your project, or run the bot as a remote robot.
- Save says to reload. The project changed elsewhere, such as through WebDAV or GitHub. Reload, then make your change again.
- The bot works locally but times out hosted. Hosted native allows 75 ms per turn and 50 ms of CPU. Do slow set-up, such as loading tables, before replying
ready, which has 30 seconds.
Privacy
Your source is private unless you publish it. Hosted matches, their replays, your handle and your robots' results are public. Browser practice stays in your browser. The privacy policy says what Clunkyard keeps, where and for how long.