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.