On this page

7. Programming turtles

Manual contents | Previous: Programming patches | Next: Grazers and food | Wandering turtles | NLTurtle reference

A turtle is one individual with position, heading, speed, appearance, and your own state. Begin with the complete Walkers tutorial. This chapter assumes aligned lifecycle and uses public properties rather than underscore-prefixed built-in Stores.

A turtle's hooks, in plain language

Hook Runs when Put here
constructor(id, main) Each turtle is created Preserve super(id, main); allocate stable custom fields/Stores
init() Once per turtle, after built-in defaults Register public updaters and one-time listeners
setup() After initial populations are ready, or after a birth batch is inserted Relationships and values requiring the populated world
turtleStep() The scheduler visits this turtle Sense, decide, and propose changes; preserve the base step
turtleShift() During the turtle shift phase Preserve super.turtleShift() for newborn bookkeeping if overriding
reset() An explicit local turtle reset Preserve super.reset() and restore custom local state

Omit hooks you do not need. The base init and setup are empty; the base step, shift, and reset are not. Do not call init/setup yourself. The constructor is too early to rely on assigned appearance, speed, heading, or position. Read those in init or later.

World reset creates new turtles. It does not reuse each old turtle by calling its reset hook. New turtles pass through constructor, defaults, init, and coordinated setup. If you also offer individual turtle reset, define the custom-state policy for that separate operation. Setup is not a per-tick hook.

7.1 Built-in state

Read Meaning
location Coordinate object with numeric x, y properties
x, y Current position, possibly fractional
col, row Floored position; use myPatch() for actual hex occupancy
theta Heading in radians; zero east, positive toward south
speed Movement distance parameter
color Numeric agent-palette index/value, not a CSS color
size Drawing size in world units
shape Runtime Shape.Circle or Shape.Rectangle, not a string
id Population identifier; do not use as a persistent identity across reset

Built-in properties distinguish initialization, immediate edits, and proposals:

this.color = 1;
this.currentColor = 0;
this.nextColor = 1;

color = changes current/next and the reset baseline. currentColor = changes current/next immediately without replacing the baseline. nextColor = changes only the pending value. The analogous heading, speed, size, and shape properties follow this pattern. An ordinary field such as this.age has no such automatic synchronization.

For position, prefer initLocation(x, y) for initialization: it initializes both stored location representations. moveTo(x, y) edits position immediately; moveNext(x, y) proposes the next position. Do not substitute x = ...: x/y are read accessors, not movement commands.

7.2 Choose who supplies movement

Project option Your responsibility
movement: "Trajectory" Supply speed/heading; base turtleStep schedules movement
movement: "Next_Location" Supply destination using moveNext/nextLocation
motion: "Continuous" Fractional positions permitted
motion: "Discrete" Built-in trajectory movement rounds speed and uses eight heading sectors

With trajectory movement, keep super.turtleStep() and do not also schedule another move unless deliberately overriding it. With Next_Location, keep the base call for appearance/heading updaters; no trajectory move is scheduled.

This method-body fragment moves one square-grid column to the right in a Next_Location model whose cell boundary is Wrap:

turtleStep() {
  super.turtleStep();
  const destination = this.main.patch(this.x + 1, this.y);
  this.moveNext(destination.col + 0.5, destination.row + 0.5);
}

This chooses a patch center deliberately. It is not a continuous-speed rule, and would clamp rather than wrap with an Edge patch lookup. Hex movement requires its specialized coordinates, not square offsets.

7.3 Turn before or after movement deliberately

nextTheta is pending heading. The trajectory base uses pending speed and pending heading when scheduling its move. Consequently, a heading proposal before the base call affects that move:

turtleStep() {
  this.nextTheta = Math.PI / 2;
  super.turtleStep();
}

These two fragments assume no installed thetaUpdater will overwrite the pending angle. In the same trajectory setup, moving the assignment after the base call affects the following move instead:

turtleStep() {
  super.turtleStep();
  this.nextTheta = Math.PI / 2;
}

At shift, both versions commit the southward heading. They differ in the destination already proposed. Do not infer motion from the new heading alone.

Public built-in updaters run inside the aligned base step after movement: heading, color, size, then shape. For example, in init:

this.colorUpdater = () => this.main.getBParam("HIGHLIGHT") ? 1 : 0;

This fragment needs a HIGHLIGHT toggle and two agent palette entries. Assign once; keep the base step; do not also call an underlying Store's autoNext(). A custom Store updater is different and remains explicitly evaluated by your code.

Turning helpers left(degrees) / lt(degrees) and right(degrees) / rt(degrees) return an angle; they do not assign it. For example:

this.nextTheta = this.right(15);

Unlike NetLogo rt 15, calling this.right(15) alone does not turn the turtle. toRadians(degrees) and toDegrees(radians) convert angular units.

7.4 Boundaries, sensing, and distances

Agent edge behavior is separate from cell boundary lookup. Walkers uses Wrap for both, small positive speeds, and no collision response. Bounce/Stop, collisions, and wall bounce involve the runtime's boundary/ collision machinery; use a checked example rather than assuming a lookup operation implements agent physics.

myPatch() finds current occupancy, including the hex index conversion. patch_ahead(distance) samples along the current heading; patch_ahead_next(distance) samples along the pending heading. In an Edge cell world they return null when the sampled point is outside. Check before reading a sampled patch:

const ahead = this.patch_ahead(0.5);
if (ahead !== null && ahead.color === 1) {
  this.nextTheta = this.right(90);
}

Use these for local sensing, not as arbitrary-many-world-wrap coordinate normalizers. They are different from Main patch lookup, which clamps at Edge.

For nearby turtles, use this.nhoodPatches(this.myPatch().block(1)) or nhoodCount(...). The caller is excluded; other turtles on the same patch are included by block, but not by blockX. Results use committed occupancy, not nextLocation. See the full neighborhood reference.

towardsxy(x, y) returns the direct angle to a point; assign its result to a heading property. distanceTo(other) returns direct coordinate distance. Neither promises a shortest toroidal/hex-geometric displacement. For a circular query with shortest wrapped distance use Main's repaired turtlesInRadius(patch, radius), then filter out self if needed. That query is centered on the supplied patch coordinate point, not automatically on the calling turtle's fractional position.

7.5 Appearance and custom state

An agent palette and a cell palette are separate. Changing turtle color does not recolor the occupied patch. Use patch.nextColor deliberately if your rule should mark the environment.

setVisible(false) hides a turtle; setVisible(true) restores its cached color. Hiding is not death and does not remove it from the population or local queries. Do not treat visibility as an epidemiological state.

Allocate custom Stores once per turtle, synchronize them once, and define their local reset policy if you use individual turtle reset. World reset instead discards turtles and constructs replacements. For a simple intentionally sequential counter, an ordinary field may suffice; it still needs explicit initialization and an intentional update order.

Built-in addColorChangeListener, addThetaChangeListener, addLocationChangeListener, and analogous size/shape/speed methods install Store listeners receiving (store, oldValue, newValue). Do not assume every notification means a changed value. One-time wiring belongs in init or the constructor, not repeatable setup. See listener cautions.

7.6 Births and deaths

Births require Project's agent configuration and an initialized population, even when its initial count is zero.

Main operation Child starts from
createTurtleFromParent(parent, customize) Parent class, location, and built-in appearance/motion values
createTurtleFromPatch(patch, Class, customize) Supplied class at patch center; population defaults
createTurtleFromSpecs(Class, customize) Supplied class at origin; population defaults, then explicit customization

For example, a one-time model action, not a line to run unconditionally each step:

this.main.createTurtleFromParent(this, child => {
  child.theta = this.right(30);
});

Child constructor/defaults/init happen before customization. Insertion and indexing are queued until shift; setup runs once after the birth batch is inserted. Do not call child init/setup yourself, or expect the live population count to increase immediately at the request line. The callback does not automatically copy arbitrary custom fields. If child init registers a heading updater, customization may set its initial heading but the updater remains installed.

this.die() marks the turtle as dying and queues removal. Return from your rule if it must do no more work:

if (this.color === 1) {
  this.die();
  return;
}

Removal happens at shift, while local occupancy queries hide dying turtles immediately. A newborn can be inserted but remain hidden from local queries until its base turtleShift() clears newborn bookkeeping. Preserve that base call when overriding the hook. Do not assume collection membership, local visibility, and opportunity to step begin at the same instant. Avoid slash visit modes until you need their intermediate insertion boundary.

Births can make a population grow exponentially. Add an explicit probability, resource condition, or population cap before putting births in a repeated rule. Also define who removes long-lived listener registrations owned by a dead or reset-discarded turtle; the runtime cannot infer arbitrary resource ownership.

7.7 Existing helpers not used as teaching shortcuts

Aligned lifecycle does not repair every old movement method:

These limitations are separate from the now-repaired neighborhood operators. Do not work around them with private Stores in a first model. The complete tutorials use only the public movement paths checked by their executable examples.

7.8 Worked example: sense an obstacle, stop, and turn

This example replaces the Patch and Turtle in Walkers, keeping its original Main and chart. It illustrates one local decision with visible consequences:

  1. Sample the patch at the proposed destination.
  2. If it is blocked, stay in place and turn right by 90 degrees.
  3. Otherwise, move forward.
  4. Show orange on a blocked step and blue on a clear step.

The orange color means the last decision was blocked, not that the turtle is standing on an obstacle. A blocked turtle checks its new direction on the following tick; it does not turn and try another move in the same tick.

Prepare Project

Keep aligned lifecycle, visitMode: "A", dt: 1, square topology, movement: "Trajectory", motion: "Continuous", and collisions: "None". Use Wrap for both cell boundary and agent edge. Change these settings:

Replace agent.start with the array below. Put the array itself in Project, not the variable declaration:

const patrolStart = [
  [0, 1.5, 1.5, 0, 0.6, 0, 1, 0]
];

The turtle starts at a patch center, facing east. The explicit list creates one turtle regardless of the previous agent.count. The original RIGHT chart still counts turtles in the right half; its range can be reduced to [0, 1].

Supply two obstacle patches

import {NLPatch} from "@nwk/api/nlpatch.js";

class ObstaclePatch extends NLPatch {
  setup() {
    const blocked = (this.col === 2 && this.row === 1) ||
                    (this.col === 1 && this.row === 3);
    this.currentColor = blocked ? 1 : 0;
  }
}

export default ObstaclePatch;

These are static obstacles. Patch setup reconstructs them initially and after reset; no patch steps or activation are needed. Here color doubles as the obstacle state. In a richer model, give obstacle state its own name and derive color from it, rather than making decoration change the movement rules.

Supply the sensing turtle

import {NLTurtle} from "@nwk/api/nlturtle.js";

class PatrolTurtle extends NLTurtle {
  constructor(id, main) {
    super(id, main);
    this.blocked = false;
  }

  init() {
    this.colorUpdater = () => this.blocked ? 1 : 0;
  }

  setup() {
    this.currentSpeed = this.main.SPD;
  }

  turtleStep() {
    const speed = this.main.SPD;
    const ahead = this.patch_ahead(speed);
    this.blocked = ahead === null || ahead.color === 1;
    this.nextSpeed = this.blocked ? 0 : speed;
    if (this.blocked) this.nextTheta = this.right(90);
    super.turtleStep();
  }

  reset() {
    super.reset();
    this.blocked = false;
  }
}

export default PatrolTurtle;

blocked is an ordinary field used only by this turtle's decision and color rule. It changes immediately, so the color updater reads the decision just made. Other turtles do not use it as synchronized state.

Both nextSpeed and any turn are assigned before the base call. A blocked decision therefore schedules zero displacement, and shift commits the turn and orange color together. On a clear step, the current slider speed replaces the previous zero speed, so the turtle can move again. Without that assignment, it could remain stopped after the first obstacle.

We install no heading updater: an updater evaluated by the base step could otherwise replace the explicit turn. Color is registered once through colorUpdater; the base step evaluates it after scheduling movement. No private Store access or manual commitment is needed.

World reset constructs a replacement turtle with blocked = false and its configured blue color. Its setup applies the current SPD input. The local reset override separately clears the custom flag while the base restores built-in state; local reset does not replay setup or reapply the current SPD input, and the next step reads SPD again.

Check the exact route

With SPD 1, use Step and compare:

Time x y Heading Color
0 1.5 1.5 East Blue
1 1.5 1.5 South Orange
2 1.5 2.5 South Blue
3 1.5 2.5 West Orange
4 0.5 2.5 West Blue
5 5.5 2.5 West Blue

The first turn avoids patch (2, 1). The second avoids patch (1, 3). The fifth step wraps across the left edge. Reset should restore the first row and both obstacles. Set SPD to 0 and step: the turtle stays in place, sampling its current clear patch, and remains blue. This is not a pause: time and other model rules still advance.

Scope of this rule: it tests one destination point, not the entire path or the turtle's drawing radius. The small speed range prevents skipping intermediate square patches here. This is not general collision avoidance: turtles may overlap, and fractional speeds can approach an obstacle boundary before the destination enters its patch. Do not extend it to long jumps, hex movement, or physical wall collisions without a different checked rule.

Implementation: NLTurtle, NLVector, and NLSimMain population helpers.