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.

Download this page as Markdown