Cadence language specification

Cadence organizes a bot as named behavioural states. The active state can run one-time entry work, issue controls every tick, keep variables, and move to a new state when a condition or timer is ready.

This model suits tactics with explicit phases: patrol until the scanner finds something, track it while contact holds, then break away for a fixed interval. The state and memory remain available across decisions without a separate protocol adapter.

First bot

bot First Cadence
modes 2d 3d
initial search

state search
  each status-text "SEARCH"
  each status-value tick
  each drive 0.7
  each turn 0.25
  each scan-heading sweep(8)
  each scan-pitch wave(30, 22)
  each scan-arc 58
  when contact -> attack
end

state attack
  each status-text "TRACK"
  each status-value range
  each aim-heading bearing
  each aim-pitch elevation
  each scan-heading bearing
  each scan-pitch elevation
  each fire contact and cooldown == 0 and abs(aim-error) < 3 and battery >= shot-cost-base + shot-cost-per-power + 5
  when not contact for 8 -> search
end

The example is minimal.cadence. Run it from the repository:

node src/cli.js match --mode 3d --ticks 300 --seed 4201 \
  --bot 'First Cadence=node src/dsl/runner.js cadence bots/dsl/cadence/minimal.cadence' \
  --bot dsl-glyph

Add --serve to watch. Add a neighboring bot.json to make a new source available in browser setup; the DSL guide contains the manifest shape.

This bot fires at power 1 once the turret, which turns at 220° per second, is within 3° of the target and the battery can pay for the shot with 5 to spare. The eight-tick contact-loss hold keeps the state active, but contact prevents shots while the scanner has no fresh echo. With no contact, aim-error is zero; it does not provide a firing solution by itself. The new-robot starter, src/public/starters/cadence/bot.cadence, also picks shot power by range and keeps a larger reserve for driving.

State timing

Cadence starts in the state named by initial. A decision tick runs in this order:

  1. Run the state's enter lines if this is its first tick.
  2. Run every each line in source order.
  3. Check transitions in source order and choose the first ready transition.
  4. If a transition is chosen, mark its target to enter on the next tick.

The action sent on a transition tick still comes from the old state. Entry actions for the new state run on the following tick.

state-time starts at zero. It increments after a tick with no transition. after 20 -> break therefore changes state after the current state has reached 20 on its timer. when contact for 3 -> track requires three consecutive true checks; a false check resets that transition's count.

skyhook.cadence combines sensor loss holds and timed breakaways. ridge-hover.cadence uses states to acquire and maintain terrain-relative altitude in gravity.

patient-lock.cadence confirms a contact, chooses an orbit side once on entry, tolerates brief scanner gaps and waits for sustained charge in a recovery state. The workshop explains the resulting motion and gives a real local match command.

File structure

bot DISPLAY NAME
modes 2d 3d
rulesets classic inertial-2d gravity-flat gravity-hills zero-g
initial STATE

state STATE
  enter ACTION EXPRESSION
  enter set NAME EXPRESSION
  each ACTION EXPRESSION
  each set NAME EXPRESSION
  when EXPRESSION -> STATE
  when EXPRESSION for NUMBER -> STATE
  after EXPRESSION -> STATE
end

The manifest and source declarations must support the selected match. A mismatch is reported during the protocol handshake.

Actions and memory

enter and each accept every Arena action, plus set:

enter set passes 0
each set passes passes + 1
each status-value passes

Memory names are case-insensitive and do not need a separate declaration. A write is immediately visible to later lines and transition expressions in the same tick. Values may be finite numbers, strings, or booleans.

Actions run in source order. If enter and each write the same action on an entry tick, the later each value is sent. Cadence does not use Vane's priority arbitration.

Action Request
drive, turn, pitch Classic forward, yaw, and pitch controls
thrust, strafe, lift Physics linear forces
yaw-torque, pitch-torque, roll-torque Physics angular forces
brake Physics braking assist, 0 to 1
aim-heading, aim-pitch absolute bearing and elevation the turret turns toward
scan-heading, scan-pitch, scan-arc scanner direction and cone width
fire fire this tick if cooldown is 0 and the battery can pay; a bare fire means true
power shot power, 0.5 to 3
status-text, status-value spectator text and finite numeric value

Each tick defaults to zero movement, no shot and shot power 1. Aim and scanner targets keep their current values until a state writes new targets. The scanner turns at once; the turret turns at up to 220° per second. A shot the battery cannot pay for is refused at no cost.

each brake takes a strength: a number is clamped to 0 through 1, and a condition brakes fully when it holds, so each brake speed > 70 and each brake 0.3 both work.

Expressions and sensors

Cadence uses Vane's expression language. It supports finite numbers, quoted strings, true, false, parentheses, unary - and not, arithmetic, comparisons, and, and or.

Functions are min, max, abs, clamp, sin, cos, wave(period, amplitude), sweep(degreesPerTick), and angle(target, current). Angles are degrees. Boolean and and or short-circuit.

Area Values
Match tick, time, remaining, mode3, ruleset, state-time
Own robot health, cooldown, x, y, z, heading, pitch, aim, aim-pitch
Battery battery, battery-capacity, battery-fraction
Scanner scan-heading, scan-pitch, scan-arc, scan-range, contact, range, bearing, elevation, aim-error
Shots power-min, power-max, shot-speed-base, shot-speed-per-power, shot-cost-base, shot-cost-per-power
Safe zone zone-active, zone-radius, zone-distance, zone-bearing, zone-elevation
Match mechanics storm-active, storm-distance, storm-bearing, storm-heading, storm-radius, cue-present, cue-bearing, cue-elevation, cue-range, cue-error, cue-age, cue-muzzle, pickup-present, pickup-bearing, pickup-elevation, pickup-range, hill-active, on-hill, hill-held, hill-bearing, hill-elevation, hill-range, hill-radius, hill-time, hill-to-win, score, score-best
Arena width, height, depth, terrain-kind
Physics flags physics-mode, inertial, gravity, zero-g, hills, grounded
Motion vx, vy, vz, speed, yaw-rate, pitch-rate, roll-rate
Ground and gravity altitude, terrain-height, gravity-x, gravity-y, gravity-z
Attitude basis forward-x/y/z, right-x/y/z, up-x/y/z

contact, range, bearing, and elevation describe only the nearest current scanner echo. No opponent state is added to memory automatically. aim-error is the signed heading difference from where the turret points now to that echo, and 0 without one. In 3D, also compare elevation with aim-pitch.

cooldown is 0 when the gun can fire this tick. aim and aim-pitch report where the turret points now. scan-range is the reach of the current scan arc. A shot costs shot-cost-base + shot-cost-per-power * power battery and flies at shot-speed-base + shot-speed-per-power * power. Before sudden death, zone-active is false, the zone distance and radius are 0, and the zone bearing is the robot heading. In classic, speed is the smoothed speed that aim spread uses. The DSL guide lists every value, and the protocol guide defines each one.

The match mechanics' values read as inactive while their mechanic is off or not yet in play: flags false, ranges and distances a very large number, bearings the robot heading. storm-distance is to the storm's edge and negative inside it. cue-* describe the best passive cue, remembered for 60 ticks where it placed a robot and seen from where the robot is now. The DSL guide defines each one.

Transitions

when CONDITION -> target
when CONDITION for TICKS -> target
after DURATION -> target

for takes a numeric literal, floors it to an integer, and requires at least one tick. after evaluates its expression every tick and requires a finite, non-negative result. When several transitions are ready, the first one in the state wins. Put emergency transitions before routine timeouts.

Changing state resets state-time and all transition hold counters. Memory survives the transition.

Work limits and errors

Without a supplied budget, the local Cadence interpreter allows 640 operations per decision. Hosted DSL decisions and browser practice use 1024. The local CLI with --resources dsl defaults to 768; add --dsl-ops 1024 to use the hosted operation limit. A supplied dslOpsPerTurn replaces the interpreter default. Entry work, per-tick actions, expressions and transition checks all consume operations. The reported dslOps value is host-metered.

Errors include file:line:column. Missing end, invalid states, unknown actions or values, division by zero, non-finite results, stack-independent work exhaustion, and invalid transition durations forfeit the bot cleanly.

Compile to WASM

The build needs Clang, a C compiler and the Wasmtime C API; see the WebAssembly guide. Run from the repository root:

scripts/build-dsl-wasm.sh cadence
cat > build/cadence-wasm.json <<'JSON'
{
  "name": "Wasm Cadence",
  "language": "cadence-wasm",
  "protocol": 2,
  "supportedModes": ["2d", "3d"],
  "resourceModes": ["wasm"],
  "command": ["build/clunkyard-wasm-host", "build/wasm-skyhook-cadence.wasm"]
}
JSON
node src/cli.js match --resources wasm \
  --bot @build/cadence-wasm.json --bot c --ticks 300

The generated module retains the current state, state timer, memory values and transition hold counters in its linear memory. Wasmtime meters it like the C and Rust modules. The interpreter remains available as dsl-cadence. The module speaks WebAssembly ABI 4: it reads every value through the robot core's dsl_fill_v1 import once a turn and sets each control through the core's setters. Numeric status-value works in the module. Text status-text produces a located compile error; use the interpreter when a program needs text.

Grammar

program      := directive* "initial" state-name state+
directive    := "bot" words
              | "modes" ("2d" | "3d")+
              | "rulesets" ruleset+
state        := "state" state-name newline statement* "end"
statement    := ("enter" | "each") (action | set) newline
              | "when" expression ("for" number)? "->" state-name newline
              | "after" expression "->" state-name newline
set          := "set" word expression
action       := action-name expression | "fire"
ruleset      := "classic" | "inertial-2d" | "gravity-flat"
              | "gravity-hills" | "zero-g"

Download this page as Markdown