Sign in to see your models

Save models to your account and pick up where you left off on any device.

Sign-in is temporarily unavailable. Please try again later.

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.

Chapter

Pets

Open the matching course chapter

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-away

Creating 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 1

This 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 := dropping

Movement

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).state

Crowd 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-sheep

Actions

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-grass

Lifecycle 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.