# Three arena languages

Use the [public syntax guides](https://clunkyard.com/sdk/arena-languages)
when playing on the site. The [language-choice table](https://clunkyard.com/manual#choose-language)
compares all available programming models. Start with a working project,
change one tactic and follow [local trials](local-trials.md) 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](../bots/dsl/vane/SPEC.md), [Cadence](../bots/dsl/cadence/SPEC.md), and
[Glyph](../bots/dsl/glyph/SPEC.md). Syntax highlighting for all three is in
[the unpacked VS Code extension](../editors/vscode/README.md). 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](dsl-workshop.md): 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:

```sh
# 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:

```text
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](protocol.md). 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](sdk/names.md#dsl-words)
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:

```sh
# 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.

```vane
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.

```cadence
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.

```glyph
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](sdk/wasm.md).

## Register your source

Copy the relevant minimal file and create a neighboring `bot.json`. For Vane:

```json
{
  "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:

```sh
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.
