Clunkyard manual

Using Clunkyard

Write a program that drives a robot, then watch it fight. Start with four steps, look up a task, or check the exact rules and messages in the reference.

Start in four steps

  1. Sign in. Open My bots and ask for a sign-in link.
  2. Make a bot. Choose New bot, keep vane — starter and choose Create. Choose Save revision.
  3. Practise. Choose Choose opponent, set Execution to This browser · DSL and choose Run locally.
  4. 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

  1. Open My bots, Practice or Account.
  2. Enter your Email. For a new account, also choose a Handle and, if you like, a Display name.
  3. 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).
  4. 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.

  1. Sign in by email once, then open Account.
  2. Under Passkeys, choose Add passkey and follow your device's prompt.
  3. 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

  1. On My bots, choose New bot.
  2. Enter a Name (up to 80 characters) and pick a Language.
  3. Optional: open Project folder to set the Folder name (2 to 48 lowercase letters, digits, - or _). It appears in WebDAV addresses.
  4. 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.

FieldWhat it does
nameDisplay name inside the bot.
entryPath to the main source file. Optional when the project has the default file name for its language, or only one file of that type.
supportedModesAny of 2d, 3d.
supportedRulesetsAny of classic, inertial-2d, gravity-flat, gravity-hills, zero-g. An unknown value stops the save.
publicDescriptionUp to 200 characters, shown on the bot's card.
avatarOptional custom body, turret and scanner shapes. Without one, the bot uses its language's vehicle.
mainClassJava, Kotlin, Scala: the class with main.
moduleErlang: the module whose start/0 runs.
assemblyC#, 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

  1. Make a bot in your language.
  2. Add bot.json as shown above, with entry set to your file.
  3. Add the entry file. Read lines from standard input. Answer hello with ready, and answer every state with one act carrying the same tick. Flush after every line.
  4. 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.

LanguageEntry fileBuilt and run withJSON
javascriptbot.jsNode.js 22built in
typescriptbot.tstsc (ES2022, CommonJS), then Node.jsbuilt in
pythonbot.pyPython 3json
cbot.cclang -O2 -std=c17 … -lmnone; parse by hand
cppbot.cppclang++ -O2 -std=c++20nlohmann/json
gobot.gogo build (Go 1.25)encoding/json
rustbot.rsrustc --edition=2021 -O; no cratesnone; parse by hand
hewbot.hewhew build --releasestd.encoding.json
csharpBot.csprojdotnet publish (.NET 10); set assembly if neededSystem.Text.Json
fsharpBot.fsprojdotnet publish (.NET 10)System.Text.Json
javaBot.javajavac, then java; mainClass defaults to the file nameJSON-java (org.json)
kotlinBot.ktkotlinc (Kotlin 2.4); mainClass defaults to BotKtkotlinx.serialization
scalaBot.scalascalac, then scalaJSON-java (org.json)
clojurebot.cljclojuredata.json
dartbot.dartdart compile exedart:convert
swiftbot.swiftswiftc -O (Swift 6.4)Foundation
zigbot.zigzig build-exe -O ReleaseSafe (Zig 0.17)std.json
haskellBot.hsghc -O2Aeson
ocamlbot.mlocamlfind ocamlopt with yojson,unixYojson
fortranbot.f90gfortran -O2 -std=f2018JSON-Fortran
dbot.dldc2 -O2std.json
adabot.adbgprbuild (Ada 2022, GNATCOLL)GNATCOLL.JSON
nimbot.nimnim c -d:releasestd/json
mercurybot.mmmc --make, linked with json-cjson-c through the C interface
pascalbot.pasFree Pascal fpc -O2fpjson
basicbot.basFreeBASIC fbcJansson (#inclib "jansson")
cobolbot.cobGnuCOBOL cobc -x -free plus arena_json.c beside the entryjson-c through arena_json.c
elixirbot.exselixirnone bundled; Erlang's Jiffy is installed
erlangbot.erl or .escripterlc, then start/0 of module; or escriptJiffy
common-lispbot.lispSBCL --script; loads bootstrap.lisp first if presentbring your own, loaded by bootstrap.lisp
perlbot.plperlJSON::PP
luabot.luaLua 5.4bring your own (for example dkjson in the project)
rubybot.rbrubyjson
juliabot.jlJulia 1.11JSON3
rbot.RRscript --vanillajsonlite
phpbot.phpphpbuilt in
powershellbot.ps1PowerShell 7.5 pwsh -FileConvertFrom-Json
prologbot.plSWI-Prolog; runs main, then haltslibrary(http/json)
chrbot.plSWI-Prolog with CHR; runs mainlibrary(http/json)
clipsbot.pyPython adapter embedding CLIPS through clipspyPython json
minizincbot.pyPython adapter calling MiniZincPython json
forth.py adapter or .fsPython adapter, or gforth directlyPython json in the adapter
apl.py adapter or .aplPython adapter, or GNU APL --scriptPython json in the adapter
bqn.js adapter or .bqnNode.js adapter, or CBQN directlyJavaScript in the adapter
shelllookout.shPOSIX shjq
vane, cadence, glyphbot.vane, bot.cadence, bot.glyphBuilt-in interpreternot needed
vane-wasm, cadence-wasm, glyph-wasmas aboveCompiled to WebAssembly, run in Wasmtimenot needed
c-wasmbot.cclang --target=wasm32 -nostdlib; #include "arena_abi.h"not needed
rust-wasmbot.rsrustc --target wasm32-unknown-unknown --crate-type cdylibnot needed
zig-wasmbot.zigzig build-exe -target wasm32-freestandingnot needed
assemblyscript-wasmbot.tsAssemblyScript 0.28 asc, stub runtimenot needed
watbot.watwat2wasmnot 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

StatusMeaning
Ready / Hosted readyBuilt and ready for hosted matches.
Build waiting, BuildingQueued or running.
Build pending, Rebuild pendingA build is due, for a new revision or a new toolchain.
Build failedThe compiler or build check rejected the revision. The message shows the end of the build output.
Needs filesAdd bot.json and at least one source file.
Unsaved changes, Project changedThe 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.

  1. In the workbench, save, then choose Choose opponent.
  2. Set Execution to This browser · DSL, then pick a Mode, Rules and, if you like, a Seed.
  3. 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.

  1. 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.
  2. 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.

ExecutionWho can use itPer turn
Hosted nativeAny language75 ms to answer; 50 ms of CPU per turn and 1.5 s per match; 128 MiB of memory
Hosted DSL · operation meteredVane, Cadence, Glyph250 ms to answer; 1,024 operations per decision
Hosted WASM · fuel meteredWebAssembly languages75 ms to answer; fuel limits below
RemoteA bot running on your own computerThe time shown in the menu (normally 1,000 ms)
This browser · DSLVane, Cadence, Glyph, in the workbenchPractice 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

  1. Save at least one revision and make sure it builds.
  2. 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)TournamentMode and rulesField
SundaySunday Grand Classic2D Classic16, up to 32
MondayMonday Classic2D Classic8, up to 32
TuesdayTuesday 3D Classic3D Classic8, up to 32
WednesdayWednesday Inertia2D inertial8, up to 32
ThursdayThursday Flatlands3D gravity, flat8, up to 32
FridayFriday Hills3D gravity, hills8, up to 32
SaturdaySaturday Zero-G3D zero-g8, up to 32

Enter

  1. Open Tournaments.
  2. 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.

  1. On My bots, find Start from a House bot (or Start with a House bot).
  2. 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.

  1. On My bots, find Connect with WebDAV and note the address.
  2. Choose Create WebDAV password. Copy it now; it is shown only once.
  3. 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.

  1. Open the bot in the workbench and go to Connections, then GitHub source.
  2. Choose Connect GitHub. Install the Clunkyard Source Sync app on your personal account, pick the repositories it may read, and approve.
  3. Pick the Repository, a Ref type (Branch or Tag) and the Branch or tag.
  4. Set the Project path: the folder holding bot.json, or . for the top of the repository. Browse folders helps you find it.
  5. 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.

  1. Install Node.js 22 or newer on your computer.
  2. On the bot's card, open Connect and choose Remote key. Copy the setup now; it is shown only once.
  3. Download remote-bridge.mjs.
  4. 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
  5. 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, LANG and LC_ALL. List other variables to pass in CLUNKYARD_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.

RuleValue
Arena1,000 × 700 units in 2D; 1,000 × 700 × 600 in 3D
Clock20 ticks per second; 1,800 ticks (90 s) per match
Robots2 to 8 per match; radius 18; 100 health
SpeedUp to 95 units/s; turn up to 135°/s; pitch up to 100°/s, within ±80°
Shots18 damage (six hits destroy a robot); 310 units/s; last 3.2 s, about 990 units
Reload14 ticks (0.7 s) between shots
ScannerRange 520; cone 4° to 120° wide, 50° by default
Sudden deathFrom 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

RulesetModesGroundControlsGravity
classic2D, 3Dnonedirect motionno
inertial-2d2Dflatforcesno; light drag
gravity-flat3Dflatforcesyes
gravity-hills3Dhillsforcesyes
zero-g3Dnoneforcesno; 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 lift to 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, turn and pitchTurn act 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), terrain
  • controlModel: targets in Classic, forces in physics rulesets
  • tickRate, maxTicks, suddenDeathTick
  • robotRadius, maxHealth, maxSpeed, maxTurnRate, scanRange, fireCooldownTicks
  • physics: 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.

FieldContents
tick, time, remainingTicksThe clock. Answer with the same tick.
mode, ruleset, controlModelAs in hello.
selfx, y, z, heading, pitch, aim, aimPitch, health, cooldown (ticks until you can fire), alive
scanThe scan that produced these echoes: heading, pitch, arc, range
echoesOpponents inside that scan, nearest first: id, range, bearing, elevation. Empty when the scan found nothing.
physicsenabled, velocity, speed, angularVelocity (°/s), attitude (forward, right and up vectors), grounded, terrainHeight, altitude, gravity
arenawidth, 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.

FieldRangeEffect
throttle−1 to 1Forward or reverse. Default 0.
turn−1 to 1Turn rate. Default 0.
pitchTurn−1 to 1Climb or dive in 3D.
aim, aimPitch0–360; ±80Gun direction. Kept from the last tick if omitted.
scanHeading, scanPitch0–360; ±80Scanner direction. Kept if omitted.
scanArc4–120Scanner cone width. A narrow cone covers less of the arena; every echo gives an exact bearing either way.
firetrueFires if cooldown is 0. Only true fires.
thrust, strafe, lift−1 to 1Physics: forward, sideways and upward force.
yawTorque, pitchTorque, rollTorque−1 to 1Physics: turning forces.
brake0 to 1, or truePhysics: 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 hello in 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

AreaValues
Matchtick, time, remaining, mode3, ruleset
Own robothealth, cooldown, x, y, z, heading, pitch, aim, aim-pitch
Scannercontact, range, bearing, elevation (nearest echo), scan-heading, scan-pitch, scan-arc
Arenawidth, height, depth, terrain-kind
Physicsphysics-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 CadenceGlyphEffect
drive, turn, pitchdrive!, turn!, pitch-turn!Classic movement
thrust, strafe, liftthrust!, strafe!, lift!Physics forces
yaw-torque, pitch-torque, roll-torquesame, with !Physics turning
brakebrake!Physics braking
aim-heading, aim-pitchaim!, aim-pitch!Gun direction
scan-heading, scan-pitch, scan-arcsame, with !Scanner
firefire!Fire
status-text, status-valuesame, 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_init and arena_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.

BudgetLimit
Fuel at start-up250,000
Fuel per decision80,000
Fuel per match8,000,000
Linear memory2 MiB
Stack256 KiB
Module size16 MiB

Limits

WhatLimit
Bots per account32
Source, all bots together4 MB; 512 files per project; 1 MB per file
Revision history512 revisions and 128 MB per account
BuildsOne at a time; 8 waiting; 4 minutes per compile step
Built programs, all bots together256 MB
Matches waiting or running12
Robots per hosted match2 to 8
Tournament entries2 per tournament
Passkeys12
Access keys (WebDAV and remote)24 active
Hosted native, per bot75 ms per turn; 50 ms CPU per turn; 1.5 s CPU per match; 128 MiB memory
Hosted DSL250 ms per turn; 1,024 operations per decision
Hosted WASM75 ms per turn; fuel and memory as in WebAssembly
RemoteTurn 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.

CodeMeaningWhat to do
TIMEOUTNo answer in time.Do less per turn, and flush after every line.
UNSOLICITEDWrote a line when no answer was due.Move debug output to standard error. Send one act per state.
JSONA line was not valid JSON.Print one complete object per line.
HANDSHAKEThe first reply was not ready.Reply {"v":1,"type":"ready"} to hello.
RESPONSEThe reply was not an act for this tick.Copy tick from the state.
EXITThe program stopped.Check for crashes; read standard error.
OUTPUT_LIMITA line over 16 KiB, or over 1 MiB in total.Send only the fields you need.
INCOMPATIBLE_MODE, INCOMPATIBLE_RULESETThe bot does not list this match's mode or ruleset.Add them to bot.json, or pick another match.
RESOURCE_BUDGETHosted 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_FUELA 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 entry in bot.json to 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.