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

Patches

Patches are the cells of the world grid, one per integer coordinate. Each patch has its own fields, and patch updates apply local rules to every patch: neighborhood counts, state changes, diffusion.

Chapter

Patches

Open the matching course chapter

The world grid

A MathPets world is a finite rectangle of integer-coordinate patches. world: declares the bounds, and the bounds are inclusive on both ends: x: -25..25 spans 51 patches. Widths and heights may be even or odd. The origin (0, 0) is a real patch only when the range crosses zero.

Inside a patch update, the current patch's integer coordinates are px and py. The world bounds min-x, max-x, min-y, and max-y are available in every expression.

Pets, the moving agents of the next chapter, have continuous coordinates x and y. These names do not overlap with the patch coordinates; reading one in the wrong context is a compile-time error.

The grid exists even when a model gives patches no data. If a model declares no patch fields and never changes a patch's state, wall, label, or color, the board draws neither patch cells nor grid lines. Declaring any patch field makes the patch layer visible, even if every value stays at its default.

world:  x: -25..25  y: -18..18  topology: box setup:  patches:    where (px = min-x):      state := burning

Topology

Topology sets what happens at the edges of the world: box (both axes hard), torus (both wrap), wrap-x (x wraps, y is hard), or wrap-y (y wraps, x is hard). It applies to every coordinate lookup, neighborhood, distance, and movement command.

On a wrapped axis, coordinates are taken modulo the axis width, max - min + 1. Distance, heading, and radius reporters use the shortest path, so radius queries and neighborhoods span the seam, and a pet that moves off one edge comes back on the other.

On a hard axis, patch-at(x, y) with an out-of-range coordinate returns nothing, neighborhood queries at an edge return fewer patches (at a corner of a box world, count neighbors8 = 3), and pet motion stops at the edge.

world:  topology: torus  x: -24..24  y: -16..16 # radius and nearest queries cross the seam on a torusstep:  flockers:    let nearby = flockers in-radius vision    turn-towards(average-heading(nearby), 5)    forward(1)

Fields and state

patches: at the top level declares the patch fields. Every patch starts with the declared initial value. Inside a patch update, field names refer to the current patch, with no prefix.

The field named state is special. If it is an enum, each symbol becomes a named visual state for styling. A numeric state is allowed too; it registers no discrete states and is meant for a continuous color scale. The field is optional, since style rules can target any declared field.

State symbols are ordinary values in expressions, and they can be used as bare predicates in where clauses: where burning means where (state = burning). A bare boolean field name works the same way.

patches:  state: empty | tree | burning | burned = empty  protected: boolean = false  heat: number = 0 # state name as bare predicatepatches where burning:  state := burned # state name as expression valuepatches where (state in [tree, burning] and not protected):  heat := heat + 1

Patch updates

A patch update is patches: followed by an indented body that runs once for each patch. patches where (...): restricts it to matching patches. The body can contain assignments, nested where branches, let bindings, and match blocks.

Inside the block, bare field names refer to the current patch. To write several fields of another target at once, use target := : with an indented field map.

A region can serve as the update header. patches in-radius r around (x, y): runs the body on the patches within distance r of (x, y), and in-square selects the patches within r on both axes. Inside an agent update the around (...) clause can be left out, and the region is centered on the current agent. Topology applies to the region.

# update every patchpatches:  state := empty  chemical := 0 # filtered updatepatches where (tree and py > 0):  heat := heat + 1  state := burning # spatial region (circle around 0.6 * max-x, 0)patches in-radius 5 around (0.6 * max-x, 0):  food-source-number := 1  state := food

Neighbors

neighbors4 is the four orthogonal neighbors of the current patch and neighbors8 all eight. Both are agentsets: you can count them, filter them with where, and test them with any. To sum or average a field over a neighborhood, use sum(field, patches around(px, py, r)), a disk of Euclidean radius r that includes the center patch; Lenia builds its rings this way.

For a specific direction, neighbor-at(direction).field reads one neighbor, and neighbors-at(dir, dir, ...) returns a selected subset. The direction symbols are top-left, top, top-right, left, right, bottom-left, bottom, and bottom-right.

Topology applies here too. On a torus a corner patch still has 8 neighbors, across the seams; on a box it has 3.

step staged:  patches:    let live = count neighbors8 where alive    state := dead     where alive:      where (live in [2, 3]):        state := alive     otherwise:      where (live = 3):        state := alive # directed readspatches where (py = max-y - tick - 1):  let left-live = neighbor-at(top-left).state = live  let center-live = neighbor-at(top).state = live  let right-live = neighbor-at(top-right).state = live

Coordinate lookup

patch-at(x, y).field reads or assigns a field on the patch at an explicit coordinate, after applying topology. On a hard axis, an out-of-range coordinate yields no patch, and reading .field from the missing patch is a runtime error, so guard the read.

The usual guard is a def that checks the bounds and returns a fallback. Turing Diffusion samples its Laplacian this way, passing the center value as the fallback, which gives a zero-flux boundary.

defs:  sample-u(col: number, row: number, fallback: number): number:    if (col < min-x or col > max-x or row < min-y or row > max-y) then fallback else patch-at(col, row).u patches:  let center = u  let horizontal = sample-u(px + 1, py, center) + sample-u(px - 1, py, center)  let vertical = sample-u(px, py + 1, center) + sample-u(px, py - 1, center)  u := clamp(center + diffuse-u * (horizontal + vertical - 4 * center), 0, 1)

Diffusion

diffuse(field, rate) moves part of a numeric patch field to neighboring patches. The rate is a fraction in [0, 1]: each patch sends rate / 8 of its value to each of its eight neighbors and keeps the rest, so with a full set of neighbors it keeps 1 - rate. At a box edge or next to a wall, the shares for missing neighbors stay on the patch, so the total is conserved. All shares are computed from the old values before any are written.

diffuse is a statement at the top level of a step: section, next to patch update blocks rather than inside one. Ants diffuses its chemical this way, then applies evaporation in a patch update.

step:  ants:    where released:      ...   diffuse(chemical, diffusion-rate / 100)  patches:    chemical := chemical * (100 - evaporation-rate) / 100

Walls

Every patch carries a built-in wall boolean (default false, no declaration needed). Walls separate the two sides: forward stops a pet before it enters a wall patch, follow-patch-gradient never steers toward one, and diffuse sends nothing into or out of a wall. Teleports (set-position, move-to) ignore walls.

Walls can be set in code like any field, painted by clicking the board when the model's brush: section toggles them, or stamped from a preset: a walls: list in a .petpreset entry is a text map, one string per patch row from top to bottom and centered on the world, where # marks a wall. Maze stores its layouts this way.

setup:  patches:    where (px = 0 and py > min-y + 4):      wall := true brush:  wall := not wall

Aggregates

count reports a number, any reports a boolean, and sum, mean, min, and max aggregate a numeric report (...) expression. All of them accept a where filter, and around (x, y) limits the query to the eight patches around a patch coordinate (from a pet, pass patch.px, patch.py).

count and any work on any agentset, including neighborhoods and let-bound queries. The report (...) forms take patches, a breed or link breed name (optionally filtered with where), or patches-in inside a zone update. Use them in monitors, stop conditions, and update blocks.

monitors:  living-trees: number = count patches where tree  fire-active: boolean = any patches where burning  average-heat: number = mean patches report (heat)  hottest: number = max patches report (heat) # scoped to neighborspatches:  let crowded = count neighbors8 where alive >= 6  state := if (crowded) then dead else state # scoped to the eight patches around the patch a pet stands onlet food-nearby = count patches where food around (patch.px, patch.py)

Zones

A zone breed tiles the world with squares of patches. Declare it in zones: with a side length, size n, and fields of its own. Zones are agents: they can be counted, filtered, and updated like patches. If the world's width or height is not a multiple of n, the zones in the last column or row are smaller.

Inside a zone update, patches-in is the patches contained by the current zone. Inside a patch or pet update, zone-at(zones) finds the zone that contains the current agent, and zone-at(zones, x, y) resolves an explicit coordinate.

Use zones to compute a regional summary once instead of once per patch, or to give regions state of their own. Hierarchical Cellular Automaton uses 4×4 clusters and 12×12 districts, and each patch reads both through zone-at.

zones:  zone clusters size 4:    live-count: number = 0    state: growth | stable | cool = stable step:  clusters:    live-count := count patches-in where (living)    state := stable     where (live-count >= cluster-threshold):      state := growth   patches:    cluster-state := zone-at(clusters).state

Models that use this

These library models are built from patch updates, and each one uses a different part of this chapter.