MathPets language
Pets
Moving agents grouped by breed. They carry continuous coordinates, read and write the patch under their feet, sense ahead, and call shared actions.
Breeds
A breed is a named class of moving agents. Declare it inside pets: using just its name. The explicit pet breed-name: form is also accepted when the declaration benefits from being spelled out.
A breed declaration is followed by an indented body of typed field declarations. Each individual pet of that breed carries those fields. A breed can also nest an actions: block of reusable mutating behavior (see the actions section below).
The breed name is itself a usable agentset. Anywhere an agentset is expected — counts, queries, updates — you can refer to ants or flockers by name.
pets: ants: state: searching | carrying = searching released: boolean = false scouts: state: idle | returning = idle cargo: number = 0 monitors: carrying-ants: number = count ants where (released and carrying)Built-in fields
Every pet carries four runtime fields the engine maintains: id (an integer index assigned at creation), x and y (continuous, real-valued position), and heading (degrees in [0, 360)). A boolean hidden field can be set to omit the pet from the rendered world without removing it from queries.
These names do not overlap with patch coordinates px and py; reading the wrong one inside a patch or pet update is a compile-time error.
# stable per-pet index — useful as a deterministic orderingants: released := id < min(tick + 1, population) hidden := not released # continuous positionsetup: flockers: set-random-position heading := random(360)The patch property
patch is the canonical way to talk about the patch under the current pet.
Read with patch.field. Write a single field with patch.field := value, or write several with the block form patch := :followed by an indented field map. Topology is applied to the pet's continuous coordinates before the patch is resolved.
patchis a context-bound view, not a first-class value: you can't pass it to a function or store it in a let, only read its fields or assign to them. No breed, zone, or memory declaration may use the name patch.
termites: match state, patch.has-chip: wandering, true: patch := : state: empty has-chip: false state := carrying forward(20) dropping, false: patch := : state: chip has-chip: true state := leaving jump-awayCreating pets
create breed-name count makes new pets in setup or in a step. The new pets get consecutive integer ids and default field values. Their position is (0, 0) and their heading is 0 until you set them.
Position assignment happens in a follow-up breed update. set-random-position drops the pet on a random patch. set-position(x, y) places it at an explicit coordinate. The earlier set-random-position() paren form still parses; the no-paren form is the canonical shape, since actions have always used the bare-name form.
scatter spread drops the pet uniformly inside a square of side spread centered on its current position. scatter spread from agent centers the square on another agent, and scatter spread from x, y on a point — handy for spawning a colony around its home.
setup: create ants population ants: set-position(0, 0) heading := random(360) state := searching hidden := true create scouts 10 scouts: set-random-position heading := random(360) create workers 30 workers: scatter 4 from one-of queens heading := random(360)Breed updates
A breed update runs the indented body once per pet of that breed. Use breed-name: for every pet, or breed-name where (...): to filter. Inside the block, bare field names refer to the current pet; patch.field refers to the patch under it.
Nested where branches and match arms work the way they do in patch updates — they branch per current pet.
match is especially useful for state-machine pets. The inline form breed-name match expr: elides the wrapping update block when the body is only match arms — see the termites example.
# filtered updateants where released: follow-patch-gradient(chemical, 0.05, 2) forward(1) # inline match on a state machinetermites match state, patch.has-chip: wandering, true: state := carrying forward(20) wandering, false: wiggle carrying, true: state := droppingMovement
forward(distance) moves the current pet forward along its heading. turn(degrees) rotates relative to the current heading — positive values turn left, negative right.
face(target) points the pet at a patch or another pet. face(target, percent) turns only partway, where percent is in [0, 100]. turn-towards(target, max) and turn-away(target, max) each limit the turn to max degrees, making smooth steering simple.
set-position(x, y) and move-to(target) are the teleport forms. Topology applies to every motion command: on a torus world a forward step crosses the seam cleanly; on a box world the position clamps to the edge.
flockers: let flockmates = flockers in-radius vision where (count flockmates > 0): let nearest-flockmate = nearest flockmates where (distance-to(nearest-flockmate) < minimum-separation): turn-away(nearest-flockmate, max-separate-turn) otherwise: turn-towards(average-heading(flockmates), max-align-turn) turn-towards(average-heading-towards(flockmates), max-cohere-turn) forward(1)Sensing ahead
A pet can read the patch grid relative to its own heading. patch-ahead(distance) gives the patch distance in front; patch-left-and-ahead(angle, distance) and patch-right-and-ahead(angle, distance) offset the look by angle degrees.
can-move(distance) is the boolean predicate: would a forward(distance)stay inside the world? On wrapped axes it's always true; on hard axes it's false near the edge.
ants: where (can-move(1)): let ahead = patch-ahead(1) where (ahead.state = food): face(ahead) forward(1) state := carrying turn(180)Following gradients
follow-patch-gradient(field) samples a numeric patch field at three positions — ahead, ahead-left, ahead-right — turns toward the strongest uphill side, and wiggles when no side is stronger. It is the canonical primitive for chemotaxis, scent-following, and any "walk uphill" behavior.
Optional min and max bounds gate the current patch value before following: follow-patch-gradient(chemical, 0.05, 2) only follows when chemical is in [0.05, 2], and otherwise wiggles. This keeps ants from chasing infinitesimal traces or saturating on the strongest source.
ants: where released: match state: searching: where (patch.state = food): state := carrying patch.state := empty turn(180) otherwise: follow-patch-gradient(chemical, 0.05, 2) forward(1) carrying: where (patch.nest): state := searching turn(180) otherwise: patch.chemical := patch.chemical + 60 follow-patch-gradient(nest-scent) forward(1)Breed queries
breed-name in-radius r is the standard nearby-pet query. Inside a pet update, it returns the pets of that breed within r of the current pet — excluding the current pet itself.
nearest agentset picks the closest single agent. It accepts a where filter for conditional searches. pet-at(breed, x, y).field looks up one breed member by the patch at an explicit coordinate.
The same count, any, sum, mean, min, and maxaggregates from the patches chapter apply to breeds. Use them in monitors, in stop conditions, or to ask "how many of my breed are near me right now?".
step: flockers: let flockmates = flockers in-radius vision flockmate-count := count flockmates where (flockmate-count > 0): let nearest-flockmate = nearest flockmates let nearest-distance = distance-to(nearest-flockmate) ... # look up a pet by coordinatelet neighbor-cell = pet-at(cells, x - 1, y).stateCrowd helpers
one-of agentset picks a single random member, deterministic for the current seed and call site. Combine with other breed-name to exclude self: one-of other peopleis "a random other person".
distance-to(target) and towards(target) compute the topology-aware distance and heading to a patch or pet. The pair average-heading(query) and average-heading-towards(query) summarize a crowd: the first is the mean of their own headings, the second is the mean direction from self toward each of them.
any sheep here and count sheep here are the same-patch population checks — here filters a breed to the current patch and composes with the whole query surface. It is the natural primitive for predator/prey contact rules.
# stochastic link partnersetup: people: where (random-float(100) < link-chance): let partner = one-of other people create-link-with(contacts, partner) # same-patch predationwolves: where (any sheep here): eat-sheepActions
Actions are class-level mutating behavior — reusable blocks of commands and assignments. Two scopes: actions declared under pets: actions:are callable from every breed's update, while actions declared inside a breed block are only callable from that breed.
An action body looks exactly like an agent update body. It expands inline at the call site under the current pet, mutating its fields, its patch, and calling movement commands.
Actions may declare typed parameters (wiggle(spread: number):); arguments evaluate once in the caller's context. They have no return value — for pure value-producing calculations, use a def instead (see the Essentials chapter). The rule of thumb: defs produce values for expressions; actions execute behavior for the current pet.
pets: actions: random-move: turn(floor(random(50))) turn(-floor(random(50))) forward(1) sheep: state: alive = alive energy: number = 0 actions: eat-grass: energy := energy + sheep-gain-from-food patch := : has-grass: false state: bare step: sheep: random-move where (patch.has-grass): eat-grassLifecycle commands
hatch(breed)creates a child pet of the named breed at the current pet's position, copying fields where they match. die removes the current pet from the world (no parens — zero-argument commands are bare names, matching the action calling convention).
kill(ref) removes the referenced pet. Predation composes from the query surface: pick a victim with one-of sheep here, guard on the reference (killing nobody is a runtime error), then kill it. Wolf-Sheep Predation uses exactly this shape.
wolves: random-move energy := energy - 1 let prey = one-of sheep here where (prey): kill(prey) energy := energy + wolf-gain-from-food where (energy < 0): die otherwise: where (random-float(100) < wolf-reproduce): energy := energy / 2 hatch(wolves)Models that use this
Four catalog models cover the breed surface, from pure motion to full lifecycle commands.
Model
Flocking
Pure pet motion: radius queries over flockmates, average headings, and turn-towards steering with no patch state at all.
Open model →Model
Ants
MathPets reading and writing the patch under their feet: chemical trails, gradient following, and observer-level diffusion.
Open model →Model
Wolf Sheep Predation
Lifecycle commands in earnest: hatch, die, kill, and energy budgets driving predator-prey population cycles.
Open model →Model
Termites
A tiny pick-up/put-down state machine: each termite carries one wood chip, and piles emerge from repeated local behavior.
Open model →