MathPets language
Pets
Pets are the moving agents, grouped by breed. Each has continuous coordinates and a heading, can read and write the patch under it, and can call actions defined for its breed or for all pets.
Breeds
A breed is a named kind of pet. Declare it inside pets: by its name alone; the longer form pet breed-name: is also accepted.
The breed name is followed by an indented list of field declarations, and every pet of the breed carries those fields. A breed can also contain an actions: block of reusable behavior (see Actions below).
The breed name is also an agentset. Wherever an agentset is expected (counts, queries, update blocks) you can use 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 has four built-in fields: id (an integer assigned at creation, counting from 0 and never reused within a run), x and y (a real-valued position), and heading (degrees in [0, 360); as in NetLogo, 0 points north, 90 east, and angles increase clockwise). Setting the boolean hidden field removes the pet from the drawing but not 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 patch under the current pet. It is available wherever a pet is the current agent (breed updates, action bodies, match arms) and nowhere else: not in patch or zone updates, defs, monitors, or stop conditions.
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 not a value you can pass around: you can't give it to a function or store it in a let, only read or assign its fields. No breed, patch field, 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 ids and their declared initial field values, and start at (0, 0) with heading 0.
Add along x in -20..20 to space the new pets evenly along an axis, both endpoints included, or along y for a vertical line. A single pet goes at the midpoint. An optional indented block runs for each new pet after placement; in it, the read-only index counts from zero within the batch, independently of pet ids. The block also works without an axis clause.
setup: create beads 41 along x in -20..20: y := 5 * sin(x) size := if (index = 0) then 2 else 1This samples the function at evenly spaced x coordinates; it does not space pets by distance along the curve. Counts and range endpoints can be expressions, and descending ranges work. Coordinates follow the world's wrap or clamp rules.
Positions can also be set in a later breed update. set-random-position puts the pet at a uniformly random point in the world, and set-position(x, y) at a given point. Commands without arguments are written without parentheses, like actions; set-random-position() is still accepted.
scatter spread places the pet uniformly at random 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. The sample below uses it to place workers around a queen.
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)Directions and paths
Inside a pet block, create beads 8 along heading 90 from self every 4 places eight new pets in a line east of the current pet, four units apart. The first is one interval from the origin pet, which is not counted among the eight. Headings are degrees, with 0 north and 90 east.
For unequal lengths or different angles, declare segments. Each segment creates one pet at its endpoint. The optional final each: block initializes each new pet.
pivots: create bobs along path from self: segment upper-length heading (180 - upper-angle) segment lower-length heading (180 - lower-angle) each: mechanics.body(if (index = 0) then 1 else lower-mass) create-link-with(rods, previous)In the initializer, origin is the pet given after from, previous is the preceding new pet (the origin, for the first), and index counts from zero. All three are read-only. Axis placement also supplies previous, which is nobody for the first pet; guard its first link with where (previous).
Segment headings are absolute, not relative to the previous segment. The whole path is computed once before any initializer runs, so moving an earlier pet during initialization does not bend the rest. Lengths and spacing must be positive and finite. Coordinates are clamped or wrapped as usual, so leave enough room for the distances you want. Placement creates no links or physics bodies by itself.
Double Pendulum builds its arms this way.
Breed updates
A breed update runs the indented body once for each pet of the breed, visiting pets in a random order that is reshuffled every tick. Use breed-name: for every pet, or breed-name where (...): to filter. Inside the block, bare field names refer to the current pet and patch.field to the patch under it.
Nested where branches and match arms work as they do in patch updates, branching separately for each pet.
match suits pets that behave as state machines. The inline form breed-name match expr: drops the wrapping update block when the body contains only match arms, as in the Termites excerpt below.
# 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 along its heading, in patch widths; the path is checked for walls every half unit. turn(degrees) rotates the heading clockwise, so negative values turn counter-clockwise.
face(target) points the pet at a patch or another pet. face(target, degrees) points at the target and then adds the offset: face(foe, 180) faces directly away. turn-towards(target, max) and turn-away(target, max) turn toward or away from the target by at most max degrees. The target can be an agent or a heading, such as average-heading(flockmates).
set-position(x, y) and move-to(target) teleport the pet and ignore walls. Topology applies to all motion: on a torus a pet crosses the seam, and on a box it stops at 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 patches relative to its own heading. patch-ahead(distance) is the patch distance units in front; patch-left-and-ahead(angle, distance) and patch-right-and-ahead(angle, distance) look angle degrees to either side. Beyond a hard edge they return nothing.
can-move(distance) reports whether forward(distance) would complete: it is false if the end point lies beyond a hard edge or a wall patch blocks the path.
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) compares a numeric patch field one unit ahead, ahead-left, and ahead-right (45° to either side). If a side patch holds more than the one straight ahead, the pet turns 45° toward the larger side. Then it always wiggles: a random right turn and a random left turn, each under 40°. Wall patches never win the comparison. Ants and Slime use it for trail following.
Optional min and max bounds gate on the value under the pet: follow-patch-gradient(chemical, 0.05, 2) steers only when chemical is in [0.05, 2), and otherwise only wiggles. In Ants this makes a searching ant ignore faint traces and stop steering once it is on a strong trail.
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 finds nearby pets. Inside a pet update it returns the pets of that breed within distance r of the current pet, excluding the current pet itself.
nearest agentset picks the closest agent, measured through the topology, and accepts a where filter. pet-at(breed, x, y).field looks up one member of a breed on the patch at a coordinate. Reading a field when no pet is there is a runtime error, so where a pet may be missing, test pet-at(...) first.
The count, any, sum, mean, min, and max aggregates from the Patches chapter work on breeds too. To count the pets near the current one, bind the radius query with let and count the local, as the sample does.
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 agentsetpicks a random member, or nothing if the set is empty. It draws from the model's seeded random stream, so a pinned seed reproduces it. Use other breed-name to exclude the current pet: one-of other people is a random person other than this one.
Add weighted-by (expression) to make the chance proportional to each candidate's weight. For example, one-of other people weighted-by (degree + 1) gives someone with four connections five chances and an isolated person one. Fields in the expression belong to the candidate;mine refers to the asker. Filters and radius queries still compose before the weight clause.
Weights must be numeric. Zero, negative, and nonfinite weights count as zero; if every candidate has zero weight, selection is uniform. An empty selection returns nobody. Weighted picks use the model seed, so reset replays the same choices. The olderroulette-one-of agentset report (weight) form remains supported.
distance-to(target) and towards(target) give the distance and heading to a patch or pet, measured through the topology. average-heading(query)is the circular mean of the members' headings, and average-heading-towards(query) the mean direction from the current pet to each member. For an empty set, both return the pet's own heading.
any sheep here and count sheep here check the current patch: herefilters a breed to the current agent's patch and combines with the rest of the query syntax, as in one-of sheep here. Predator-prey contact rules are built on it.
# stochastic link partnersetup: people: where (random-float(100) < link-chance): let partner = one-of other people create-link-with(contacts, partner) # preferential link partnerpeople: with partner = one-of other people where (degree > 0) weighted-by (degree): create-link-with(contacts, partner) # same-patch predationwolves: where (any sheep here): eat-sheepActions
Actions are named, reusable blocks of commands and assignments. Actions declared under pets: actions:can be called from any breed's update; actions declared inside a breed block only from that breed's.
An action body is written like an agent update body and is expanded inline where it is called, so it acts on the current pet: its fields, its patch, and its movement.
Actions may declare typed parameters (wiggle(spread: number):); arguments are evaluated once, in the caller's context. An action without parameters is declared and called without parentheses. Actions return nothing; for a calculation that returns a value, use a def (see Essentials). Defs produce values for expressions; actions change 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 pet of the named breed that copies the current pet's fields, position, and heading, then turns to a random heading and moves forward 1. die removes the current pet and every link attached to it. It takes no parentheses: zero-argument commands are bare names, like actions.
kill(ref) removes the referenced pet and its links. For predation, pick a victim with one-of sheep here, check that one was found (killing nobody is a runtime error), then kill it. Wolf Sheep Predation does this with the shorthand with prey = one-of sheep here:, which binds the pet and runs its block only if one was found.
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 library models, from motion alone to birth and death.
Model
Flocking
Motion only: separation, alignment, and cohesion from a radius query, average headings, and bounded turns, with no patch state.
Open model →Model
Ants
Pets reading and writing the patch under them: chemical trails, gradient following, and diffusion at the top level of the step.
Open model →Model
Wolf Sheep Predation
hatch, die, and kill driven by energy: wolves lose energy every tick and gain it by eating sheep, and reproducing halves a parent's energy.
Open model →Model
Termites
A pick-up and put-down state machine written as one inline match on state and patch.has-chip. A termite carries at most one wood chip.
Open model →