Three arena languages
Use the public syntax guides when playing on the site. The language-choice table compares all available programming models. Start with a working project, change one tactic and follow local trials to compare real matches and inspect replays.
Clunkyard includes three small languages for writing bots without building a protocol adapter. They are different programming models, not three spellings of the same script:
| Language | Think in | Persistent state | How competing decisions resolve |
|---|---|---|---|
| Vane | prioritized reactions | none by design | higher-priority rule owns each action field; lower rules fill gaps |
| Cadence | named behavioural states | current state, timers, and variables | the active state's actions run; its first ready transition wins |
| Glyph | values flowing through a stack | declared memory cells | words execute in order and later writes replace earlier action values |
All three read exactly the same legal sensor observation as every other bot.
They cannot see hidden opponents or call JavaScript, the shell, the
filesystem, or the network. The shared adapter compiles the source before the
protocol handshake and executes it with finite instruction, stack, and
call-depth budgets. A compile or runtime error includes file:line:column;
through the normal match runner, the bot forfeits cleanly and the other robots
continue.
Each bundled language now has a complete reference beside its runnable source: Vane, Cadence, and Glyph. Syntax highlighting for all three is in the unpacked VS Code extension. This is useful when editing the WebDAV robot workspace with VS Code or another compatible editor.
Run the included bots
For compact tactics that emphasize each language's own model, use the arena-language workshop: layered reactions in Vane, patient state changes in Cadence and a composable persistent filter in Glyph.
No install or build step is needed beyond Node.js 20 or newer:
# Watch all three in true 3D
npm run arena -- match --mode 3d --seed 31100 \
--bot dsl-vane --bot dsl-cadence --bot dsl-glyph --serve
# Headless 2D duel and saved replay
npm run arena -- match --mode 2d --seed 42 \
--bot dsl-vane --bot dsl-glyph --replay replays/my-dsl-duel.json
# Mixed-language round robin
npm run arena -- tournament --mode 3d --rounds 3 \
--bot dsl-vane --bot dsl-cadence --bot dsl-glyph
Every source begins with a display name and supported match modes:
bot My New Robot
modes 2d 3d
rulesets classic inertial-2d gravity-flat gravity-hills zero-g
Declare only modes 2d or modes 3d when a design intentionally supports one
modality. The source and its manifest should agree. In 2D, vertical actions
are accepted but the arena safely ignores them.
rulesets is also explicit. Omit it to accept every current ruleset, or list
only the motion models the program was designed for. The included physics
examples make narrow declarations so an inertial braking demonstration cannot
accidentally enter a Classic match.
Shared observations and actions
Expressions and stack words can read tick, time, remaining, health,
cooldown (0 when the gun can fire this tick), x, y, z, heading,
pitch, aim and aim-pitch (where the turret points now), scan-heading,
scan-pitch, scan-arc, width, height, and depth. mode3 is true in
3D. contact says whether the nearest current echo exists; when it does,
range, bearing, and elevation describe that echo. With no echo, range is
very large, bearing is your heading, and elevation is zero.
Each action sets one protocol 2 control:
| DSL action | Protocol field | Meaning |
|---|---|---|
drive / drive! |
throttle |
forward or reverse, normally -1 to 1 |
turn / turn! |
turn |
horizontal steering, normally -1 to 1 |
pitch / pitch-turn! |
pitchTurn |
climb or dive in 3D |
aim-heading / aim! |
aim |
absolute bearing the turret turns toward, in degrees |
aim-pitch / aim-pitch! |
aimPitch |
absolute elevation the turret turns toward |
scan-heading / scan-heading! |
scanHeading |
absolute sensor bearing |
scan-pitch / scan-pitch! |
scanPitch |
absolute sensor elevation |
scan-arc / scan-arc! |
scanArc |
sensor cone width |
fire / fire! |
fire |
fire this tick if the gun is ready and the battery can pay |
power / power! |
power |
shot power, 0.5 to 3 (default 1): heavier is slower and harder-hitting |
status-text / status-text! |
status.text |
short spectator message |
status-value / status-value! |
status.value |
numeric spectator value |
The engine remains authoritative and clamps controls to legal ranges. A
control the program does not set this tick leaves the turret and scanner where
they are and sets movement to 0, and any shot uses power 1. Status text
accepts a quoted string; its display limits are defined in the protocol
guide. Status text is a compile error in the -wasm builds;
status-value works there.
Rules v2 (protocol 2) adds these values:
| Value | Meaning |
|---|---|
battery, battery-capacity |
battery now and its capacity |
battery-fraction |
battery / battery-capacity, 0 to 1 |
scan-range |
how far the scanner reaches at its current arc |
aim-error |
signed heading degrees from where the turret points now to the nearest echo; 0 with no echo. In 3D, also compare elevation with aim-pitch |
zone-active |
true once the sudden-death safe zone exists |
zone-radius |
its radius; 0 before sudden death |
zone-distance |
straight-line distance to its centre; 0 before sudden death |
zone-bearing, zone-elevation |
direction of its centre; your heading and 0 before sudden death |
power-min, power-max |
the legal shot power range |
shot-speed-base, shot-speed-per-power |
shot speed is shot-speed-base + shot-speed-per-power * power |
shot-cost-base, shot-cost-per-power |
a shot costs shot-cost-base + shot-cost-per-power * power from the battery (fire-base and fire-per-power still work) |
Rules v2's match mechanics add these values. Each reads as inactive when its
mechanic is off or not yet in play: flags false, ranges and distances a very
large number (1e12, as range without an echo), bearings your heading, and
the rest 0.
| Value | Meaning |
|---|---|
storm-active |
true once the storm has entered |
storm-distance |
distance from you to its edge, counting only x and y; negative inside it |
storm-bearing |
direction of its centre |
storm-heading, storm-radius |
the way it travels, and its radius |
cue-present |
true while the best passive cue is remembered: this tick's nearest (so most accurate) cue, else the last one heard, for 60 ticks |
cue-bearing, cue-elevation, cue-range |
where that cue placed a robot, seen from where you are now |
cue-error |
the cue's bearing and elevation standard deviation in degrees, when it was heard |
cue-age |
ticks since it was heard |
cue-muzzle |
true when the cue was a muzzle flash: someone fired |
pickup-present |
true while a pickup is on the field |
pickup-bearing, pickup-elevation, pickup-range |
the nearest pickup, measured as the arena does (a sphere flying in zero-g and 3D classic, otherwise only x and y count; elevation 0 then) |
hill-active |
true once the hill has appeared |
on-hill |
true inside the hill |
hill-held |
true while you hold it alone |
hill-bearing, hill-elevation, hill-range, hill-radius |
its centre, measured like pickups, and its radius |
hill-time, hill-to-win |
seconds you have held it, and seconds more that win |
score, score-best |
your time-limit score and the best other robot's |
A cue has no robot id and a bearing good to about 3° per 100 units of range,
so it says which way to look, not where to shoot. The -wasm builds read the
same values from the robot core, through WebAssembly ABI 4. A program's own
name wins over any of these: a Cadence memory value once it is set, and a
Glyph memory value or word, so a robot written before them keeps its meaning.
The combat values come from the hello's gun and battery blocks:
power-min is hello.gun.power.min, shot-cost-base is
hello.gun.cost.base, and so on. docs/sdk/names.md
says where every word reads.
Physics rules add self-only values: vx, vy, vz, speed, yaw-rate,
pitch-rate, roll-rate, the forward-*, right-*, and up-* attitude
basis, gravity-z, terrain-height, altitude, and grounded. In classic,
speed is the smoothed speed the aim spread uses (vx, vy and vz stay
0); a program compiled to WebAssembly has no such input and reads 0 there.
Predicates inertial, gravity, hills, and zero-g make a dual-ruleset
program easy to read. Physics actions are thrust, strafe, lift,
yaw-torque, pitch-torque, roll-torque, and brake; Glyph spells their
writing words with !, such as thrust! and brake!. brake is a strength
from 0 to 1: a number is clamped to that range, and a condition brakes fully
when it holds (brake speed > 70) and not at all when it does not.
These are runnable, deliberately small demonstrations:
# Accelerate, then explicitly brake in 2D
npm run arena -- match --mode 2d --ruleset inertial-2d \
--bot "Brake Vane=node src/dsl/runner.js vane bots/dsl/vane/inertia-brake.vane" \
--bot dsl-cadence --serve
# Hold a terrain-relative altitude over flat ground or hills
npm run arena -- match --mode 3d --ruleset gravity-hills \
--bot "Ridge Hover=node src/dsl/runner.js cadence bots/dsl/cadence/ridge-hover.cadence" \
--bot dsl-vane --serve
# Burn, release thrust, and keep coasting in zero gravity
npm run arena -- match --mode 3d --ruleset zero-g \
--bot "Orbital Coast=node src/dsl/runner.js glyph bots/dsl/glyph/orbital-coast.glyph" \
--bot dsl-glyph --serve
Classic drive, turn, and pitch remain useful in a physics match: they
map to forward thrust, yaw torque, and pitch torque. An explicitly authored
physics action owns its corresponding axis. Aim and scan fields remain
absolute targets; inertia applies to the chassis, not the turret or sensor
head. The scanner turns at once and the turret at 220° per second, so fire on
abs(aim-error) < 3 rather than on contact alone.
Vane: prioritized reactive rules
Vane is for “if this is true now, request these controls.” Every tick it checks all rules from highest priority to lowest. The first matching rule to write a particular control owns that control for the tick. Lower matching rules still fill controls that no higher rule wrote. This makes an emergency rule able to replace movement without duplicating the targeting rules.
bot First Vane
modes 2d 3d
rule target priority 100 when contact
aim-heading bearing
aim-pitch elevation
scan-heading bearing
scan-pitch elevation
scan-arc 20
power clamp((range - shot-speed-base) / shot-speed-per-power, power-min, power-max)
fire cooldown == 0 and abs(aim-error) < 3 and battery >= shot-cost-base + shot-cost-per-power * power-max
end
rule roam priority 10 when true
drive 0.5
turn 0.25
scan-heading sweep(8)
scan-pitch wave(30, 22)
scan-arc 40
end
The target rule narrows the scan to hold the contact, picks the heaviest shot
that covers the range in about a second, and fires only once the turret is on
target with enough battery for any shot. Save this as bots/my-vane/bot.vane.
Vane expressions support parentheses, not, arithmetic, comparisons, and,
and or. Functions are min, max, abs, clamp, sin, cos,
wave(period, amplitude), sweep(degreesPerTick), angle(target, current)
and atan2(y, x) (degrees; atan2(350 - y, 500 - x) is the heading to the
arena's centre). Trigonometric arguments are degrees. The fuller
bots/dsl/vane/harrier.vane adds a higher-priority close-range evasion rule.
Cadence: persistent state choreography
Cadence is for tactics with phases. One named state is active at a time.
enter actions run on its first tick, each actions run every tick, and the
first declared ready transition selects the next state for the following tick.
set writes persistent memory. A when transition may need to remain true
for several ticks; after uses time spent in the current state.
bot First Cadence
modes 2d 3d
initial search
state search
enter set sweeps 0
each set sweeps sweeps + 1
each drive 0.5
each turn 0.25
each scan-heading sweep(8)
each scan-pitch wave(30, 22)
each scan-arc 40
when contact -> attack
end
state attack
each aim-heading bearing
each aim-pitch elevation
each scan-heading bearing
each scan-pitch elevation
each scan-arc 20
each set shot clamp((range - shot-speed-base) / shot-speed-per-power, power-min, power-max)
each power shot
each fire contact and cooldown == 0 and abs(aim-error) < 3 and battery >= shot-cost-base + shot-cost-per-power * shot + 5
when not contact for 8 -> search
after 180 -> search
end
Cadence uses the same safe expression language as Vane. The additional
state-time value counts ticks in the active state. Transitions are checked
in source order, so put urgent conditions first. See
bots/dsl/cadence/skyhook.cadence for a patrol, lock and timed breakaway
design, with separate states for the force rulesets and the safe zone.
Glyph: concatenative stack tactics
Glyph is a tiny concatenative language: literals and sensors push values;
words consume and produce them. : and ; define a named word. memory
declares a persistent cell, read with name@ and written with name!.
Conditionals consume the value on top of the stack.
bot First Glyph
modes 2d 3d
memory pace 0
memory shot 1
: target
bearing dup aim! scan-heading!
elevation dup aim-pitch! scan-pitch!
20 scan-arc!
range shot-speed-base - shot-speed-per-power / power-min power-max clamp
dup shot! power!
cooldown 0 =
aim-error abs 3 < and
battery shot-cost-base shot-cost-per-power shot@ * + 5 + >= and
fire!
;
: roam
pace@ 0.05 + dup pace! sin 0.3 * turn!
0.5 drive!
heading tick 8 * + scan-heading!
mode3 if tick 30 / sin 22 * scan-pitch! then
40 scan-arc!
;
contact if target else roam then
Arithmetic words are + - * / % neg abs min max clamp clamp1 sin cos angle;
comparisons are = == != < <= > >=; logic is and or not; stack tools are
dup drop swap over; and control flow is if ... else ... then (the else
branch is optional). Division by zero, stack underflow, excessive recursion,
or exceeding the per-turn work budget is a located runtime error. The fuller
bots/dsl/glyph/loom.glyph composes search and engagement words and remembers
its weaving phase across ticks.
Operation limits
Hosted DSL decisions and browser practice use 1024 interpreter operations per
decision. The local CLI's --resources dsl defaults to 768; set
--resources dsl --dsl-ops 1024 to compare tactics under the hosted operation
limit. This sets the operation budget; it does not reproduce every hosted
sandbox or match policy.
Without a supplied budget, local interpreter defaults are 512 operations for
Vane, 640 for Cadence and 1024 for Glyph. These are the defaults used by the
CLI's --resources open, whose name describes the local resource policy.
The website's Open class uses its own hosted policy. A supplied
dslOpsPerTurn replaces the interpreter default. WebAssembly builds use
fuel and import fees instead of interpreter operation counts; see the
WebAssembly guide.
Register your source
Copy the relevant minimal file and create a neighboring bot.json. For Vane:
{
"name": "My Vane",
"language": "Vane DSL",
"protocol": 2,
"strategy": "my reactive experiment",
"supportedModes": ["2d", "3d"],
"supportedRulesets": ["classic", "inertial-2d", "gravity-flat", "gravity-hills", "zero-g"],
"command": ["node", "src/dsl/runner.js", "vane", "bots/my-vane/bot.vane"]
}
Use cadence or glyph as the runner's language argument for those sources.
The directory name becomes the roster id, so the manifest above is selected as
--bot my-vane. Confirm it and run it:
npm run arena -- roster
npm run arena -- match --mode 3d \
--bot my-vane --bot dsl-cadence --serve
The browser's Stage match setup automatically lists the new manifest and
filters it by supportedModes and supportedRulesets. These programs are
still trusted local source: the bounded language prevents host escape, while
the Node adapter itself is a normal local process and the match runner is not
an operating-system sandbox.