# Build a tactic in the arena languages

These three small robots show how a language's programming model shapes its
motion and decisions. Each source runs in Classic 2D. Read it, run a match,
then change one decision and replay the same seed to see its effect.

| Robot | Language idea | What to watch | First experiment |
| --- | --- | --- | --- |
| [Layered Guard](#layered-guard-source) | Independent prioritized policies claim individual controls | It keeps aiming while boundary steering or a battery reserve takes priority | Raise the reserve threshold from 20 to 30; compare firing gaps and battery |
| [Patient Lock](#patient-lock-source) | Entry actions, held conditions and persistent phases | It commits to one orbit side, keeps a brief lost lock and waits for sustained charge before attacking again | Change the six-tick lost-contact hold; compare lock retention and time spent searching |
| [Filtered Orbit](#filtered-orbit-source) | Words transform values; memory makes a reusable controller | Hull steering changes gradually while turret tracking stays responsive | Change the slew step from 0.08 to 0.04; compare turn smoothness and tracking lag |

## Run a real match

Install Node.js 20 or later with npm. Create a project for the example you want
to try; no additional compiler is needed for these three languages:

```sh
npx @clunkyard/cli new vane layered-guard
cd layered-guard
```

Replace `bot.vane` with the complete [Layered Guard source](#layered-guard-source)
below. In `bot.json`, set `supportedModes` to `["2d"]` and
`supportedRulesets` to `["classic"]`, matching this example's declarations.
Keep the other manifest fields. Then run:

```sh
npx @clunkyard/cli match 742 2d classic
npx @clunkyard/cli replay
```

Open the viewer address printed by the replay command. The match plays against
the bundled practice rival and saves `replays/trial-742.json` in your project.
To try Patient Lock or Filtered Orbit, use `new cadence patient-lock` or
`new glyph filtered-orbit`, replace `bot.cadence` or `bot.glyph` with its source
below, and make the same manifest edits.

Keep the seed and opponent fixed while comparing an edit, then try several
seeds. Watch damage, shots, battery and time spent without a contact alongside
the final placing. A smoother turn can also take longer to face danger.

If you already have the full arena source checkout, you can also run the three
examples against each other from its root:

```sh
node src/cli.js match --mode 2d --ruleset classic --ticks 600 --seed 742 \
  --resources dsl --dsl-ops 1024 \
  --bot @bots/dsl/vane/layered-guard.json \
  --bot @bots/dsl/cadence/patient-lock.json \
  --bot @bots/dsl/glyph/filtered-orbit.json \
  --serve
```

Open the viewer address printed by the CLI. Remove `--serve` for a batch run.

The examples concentrate on their controller. For storm escape, sudden-death
navigation and 3D force control, continue with the full
[new-robot starters](../src/public/starters) or the house robots
[Harrier](../bots/dsl/vane/harrier.vane),
[Skyhook](../bots/dsl/cadence/skyhook.cadence) and
[Loom](../bots/dsl/glyph/loom.glyph).

## Vane: combine policies without copying targeting

Layered Guard's boundary rule has priority 400 and writes movement. Its battery
reserve rule has priority 300 and writes a slow drive request and `fire false`.
The target rule at priority 100 can still write aim and scan. When all three
match, the boundary rule owns movement, the reserve rule vetoes the shot, and
the target rule tracks the contact. The patrol rule fills any remaining fields.

A `false` action claims a field just as a true one does. Omitting `fire` from
the reserve rule would leave the target rule free to fire. Each rule condition
is still checked every decision; priorities select controls rather than skip
the rest of the program.

Try adding a high-priority rule that writes only `scan-arc`. Observe how that
changes sensing without replacing locomotion or aiming. Use Vane when the
tactic reads naturally as several reactions that should coexist.

## Cadence: make commitment and patience explicit

Patient Lock requires two consecutive echoes before selecting `attack`.
The transition tick still uses `search` controls. On the following tick,
`attack` chooses an orbit side once with `enter set side ...`; hull rotation
does not keep changing that choice.

While attacking, each fresh contact updates `last-bearing`. Six consecutive
misses return it to search. Shorter gaps keep the last bearing for aiming and
scanning, but `contact` remains part of the fire condition: a stored bearing
does not become a new firing solution.

Low battery selects `recover`. It stops firing and slows down until the battery
stays above 35 for four decisions. The entry threshold is below 18. Those
separate thresholds and the held exit prevent repeated state changes around a
single cutoff. A timed transition also caps an attack at 100 state ticks.

Try inserting another transition above the timeout. The first ready transition
wins. Use Cadence when a decision needs a clear beginning, a duration or a
deliberate commitment that survives changing observations.

## Glyph: compose a controller from values

Filtered Orbit separates a desired course from the control that follows it:

```glyph
course steer-towards dup turn! status-value!
```

`course` leaves one heading. `steer-towards` converts it into a shortest signed
angle, scales and clamps it, then passes it through `slew`. That word moves
the stored steering value towards the desired value by at most 0.08 and leaves
the result on the stack. `dup` lets two consumers use it: the turn setter and
numeric spectator status.

Only the hull command passes through this filter. The turret and scanner
receive current contacts directly. Memory persists when contacts disappear;
the data stack starts empty on the next decision. The comments above words
describe their stack effects: `( desired -- filtered )` means one input and
one output.

Try calling `steer-towards` with a boundary-avoidance heading. The same filter
can serve several course sources without copying its state update. Use Glyph
when reusable transformations and explicit data flow make the controller
easier to reason about.

## Complete example sources

### Layered Guard source

Save this as `bot.vane`. [Repository source](../bots/dsl/vane/layered-guard.vane).

```vane
# Classic 2D workshop: compose independent steering, targeting and battery
# policies. Higher rules claim individual controls, not the whole decision.
bot Layered Guard
modes 2d
rulesets classic

rule boundary priority 400 when x < 70 or x > width - 70 or y < 70 or y > height - 70
  turn clamp(angle(atan2(height / 2 - y, width / 2 - x), heading) / 45, -1, 1)
  drive 0.5
end

# This veto owns fire even when target matches. It leaves aim and scan free.
rule reserve priority 300 when battery < 20
  fire false
  drive 0.2
end

rule target priority 100 when contact
  aim-heading bearing
  scan-heading bearing
  scan-arc 20
  power 1
  fire cooldown == 0 and abs(aim-error) < 3
  turn clamp(angle(bearing + 90, heading) / 45, -1, 1)
end

rule patrol priority 10 when true
  drive 0.5
  turn 0.2
  scan-heading sweep(12)
  scan-arc 40
end
```

### Patient Lock source

Save this as `bot.cadence`. [Repository source](../bots/dsl/cadence/patient-lock.cadence).

```cadence
# Classic 2D workshop: confirm a lock, commit to an orbit side, tolerate
# brief lost echoes and recover battery before starting another attack.
bot Patient Lock
modes 2d
rulesets classic
initial search

state search
  enter set last-bearing heading
  each set last-bearing contact * bearing + (not contact) * last-bearing
  each drive 0.4
  each turn 0.2
  each scan-heading contact * bearing + (not contact) * (scan-heading + 36)
  each scan-arc 40
  when battery < 18 -> recover
  when contact for 2 -> attack
end

state attack
  # Choose a side once, rather than reverse it whenever the hull turns.
  enter set side 1 - 2 * (angle(heading, bearing) < 0)
  each set last-bearing contact * bearing + (not contact) * last-bearing
  each drive 0.45
  each turn clamp(angle(last-bearing + side * 90, heading) / 45, -1, 1)
  each aim-heading last-bearing
  each scan-heading last-bearing
  each scan-arc 20 + 20 * (not contact)
  each power 1
  each fire contact and cooldown == 0 and abs(aim-error) < 3 and battery >= 20
  when battery < 18 -> recover
  when not contact for 6 -> search
  after 100 -> recover
end

state recover
  each drive 0.15
  each turn 0.12
  each scan-heading scan-heading + 36
  each scan-arc 40
  # Different entry/exit thresholds avoid thrashing between states.
  when battery > 35 for 4 -> search
end
```

### Filtered Orbit source

Save this as `bot.glyph`. [Repository source](../bots/dsl/glyph/filtered-orbit.glyph).

```glyph
# Classic 2D workshop: build a controller from value-transforming words.
# The turn command approaches its target by at most 0.08 each decision;
# that filter keeps its history even when a contact appears or disappears.
bot Filtered Orbit
modes 2d
rulesets classic
memory steering 0

# ( desired -- filtered ): store the new command and leave it for a setter.
: slew
  steering@ - 0.08 neg 0.08 clamp steering@ + dup steering!
;

# ( target-heading -- turn-command )
: steer-towards heading angle 45 / clamp1 slew ;

# ( -- target-heading ): leave a value, rather than write a control here.
: course
  contact if bearing 90 + else heading 18 + then
;

: target
  bearing dup aim! scan-heading!
  20 scan-arc!
  1 power!
  cooldown 0 = aim-error abs 3 < and battery 20 >= and fire!
;

course steer-towards dup turn! status-value!
battery 20 < if 0.2 else 0.45 then drive!
contact if target else heading tick 12 * + scan-heading! 40 scan-arc! then
```
