The DSL
The when and do fields accept a tiny expression language. It's deliberately small; the goal is one-line readability. Drop into a CodeBlock (next chapter) when you need anything more.
The full alphabetical reference is one click away inside the rule editor
- the ? DSL button opens a categorised popup with every function and
identifier. This chapter explains the shape of the language.
Operators
+ - * / % arithmetic
== != < <= > >= comparison (=== and !== work too, same meaning)
&& || ! boolean
? : ternary
= += -= *= /= assignment (only inside do)
; statement separator
Numeric helpers
random(min, max) float in [min, max), seeded RNG
random_int(min, max) integer in [min, max] inclusive
random_normal(mean, sd) Gaussian sample
chance(p) true with probability p
sqrt(x) abs(x) floor(x) round(x)
min(...) max(...) smallest / largest argument
Math.PI Math.atan2(y, x) ...
Use the seeded helpers (random, chance) where you can - they make the simulation reproducible across runs with the same seed. Math.random() works but breaks reproducibility.
Movement (agent rules)
move_forward(d) walk d patches along the current heading
turn(deg) rotate heading by deg degrees (negative = left)
turn_random() pick a uniform random heading
face(target) turn to face an agent or patch
set_position(x, y) teleport; wrapping rules apply
distance_to(target) distance to an agent or patch
Patches and agents nearby
patch() the patch under the current agent
patch_ahead(d) the patch d steps ahead in the current heading
patches_in(r) patches within radius r
neighbors() 8 surrounding patches (patch rules) /
nearby agents (agent rules)
neighbors4() 4 cardinal neighbour patches (Von Neumann)
agents_here() every agent currently on this patch (patch rules)
all_patches() every patch in the world (observer)
all_agents() every alive agent (observer)
agents_of_type('foo') every alive agent of that type
nearest('foo') the closest agent of type 'foo', or null
Agent lifecycle
create_agents('foo', n) spawn n agents at random positions (observer)
create_agents_at('foo', n,x,y) spawn n agents at exact (x, y) (observer)
spawn('foo') one new agent at the current spot (agent rule)
spawn_at('foo', x, y) one new agent at (x, y) (agent rule)
sprout('foo') one new agent at this patch (patch rule)
hatch(count?, init?) clone the current agent; run init(child) on each
die() remove self at end of tick
You don't have to write create_agents in a setup rule: every agent type has an initial_count field, and at setup we auto-spawn that many agents (at random positions) if no setup rule has already created any.
Diffusion (patch fields)
Two helpers spread a numeric patch property across its neighbours each tick - useful for chemical gradients, pheromone trails, heat, anything field-like. They live in observer scope, so they go on a go_rule:
diffuse('pheromone', 0.5) // 8-neighbour Moore stencil
diffuse4('heat', 0.2) // 4-neighbour Von Neumann stencil
The second argument is the fraction of each patch's value that gets shared out per tick.
Links (networks)
Agents can be connected by undirected links - a small network sits on top of the population. Create and inspect them with:
link(a, b) // observer or agent rules
unlink(a, b)
linked(a, b) // observer: bool
my_links() // agent: links touching this agent
link_neighbors() // agent: every agent I'm linked to
all_links() // observer: every link in the world
Aggregations
count, sum, mean, min, max operate on arrays. count accepts any array (agents, patches, anything); the others want numbers, so .map to project a property first:
count(agents_of_type('sheep')) // 47
count(agents_of_type('sheep').filter(a => a.energy > 5)) // 33
mean(agents_of_type('sheep').map(a => a.energy)) // 12.4
sum(all_patches().map(p => p.grass)) // 1280
A common slip is mean(agents_of_type('sheep').energy) — that reads .energy on the array and gets undefined. Always .map first.
Lambdas
Anonymous functions are allowed wherever an array method (.filter, .map, .sort, .reduce) or the ask helper needs a callback. Three shapes work:
x => x.energy > 0 single arg
(a, b) => a.energy - b.energy multi arg, e.g. sort comparator
() => spawn('child') zero arg, for ask(arr, () => ...)
ask(collection, fn) calls fn for each item; errors in one item don't stop the sweep.
me and globals
Inside agent or patch rules, me is the current entity. Bare names work when there's no name collision: energy = energy - 1 is the same as me.properties.energy = me.properties.energy - 1. Use me. when something else (a global, a function) shadows the name you wanted.
Sliders, switches, and choosers you've added on the Interface tab are available as bare identifiers - so a slider named sheep_repro shows up as just sheep_repro inside expressions.
When a rule fails
If a when or do body throws (compile error, undefined name, runtime exception), the rule is silently skipped for that agent/patch on that tick - one bad rule never crashes the simulation. The failure message is captured on RuntimeState.rule_errors and shown as a red ! error badge next to the rule in the Code tab. The badge disappears the moment the rule runs cleanly again.