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:
forward()currently reports heading; an argument does not turn it into NetLogofd. Duplicate JavaScript declarations replaced the older form.fd(distance)has legacy implementation defects; use trajectory movement,fdNext(distance)for an explicit pending move, ormoveNextinstead.realLocation, layer comparisons,towardsvariants, and trail sampling have specialized existing behavior; they are not a general unwrapped odometer.sniffer/sniffConeare advanced legacy field samplers, not aliases for the repaired neighborhood/radius queries. Wrapped row sampling has an unresolved dimension reference; they also have callback/error-handling behavior unsuitable as an introductory sensing recipe.
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:
- Sample the patch at the proposed destination.
- If it is blocked, stay in place and turn right by 90 degrees.
- Otherwise, move forward.
- 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:
- Set
cell.colDimandcell.rowDimto6. - Set
cell.paletteto["#f0f4e8", "#444444"](clear, blocked). - Set
agent.paletteto["#1f77b4", "#e67e22"](moving, blocked). - Set the SPD slider's
valueto1; keep its range0to1. - Remove the TURN input: this turtle uses a fixed right-angle rule, not the Walkers random-heading updater. The unused Main TURN getter may be removed.
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.