On this page

Second model: wandering turtles

Manual contents | Previous: A spreading fire | Next: Programming Main | Resource tutorial

In the first tutorial, patches changed state but stayed in place. Now we will make forty turtles that move and turn. Two sliders control their speed and random turning. A chart counts how many are in the right half of the world.

This is a random walk, not a flocking model: turtles do not follow, avoid, or infect one another. We will add interactions in a later tutorial.

1. Create Walkers in Studio

  1. Download your current app first if you want to keep it.
  2. Choose New App, name it Walkers, choose Agent-Environment Feedback, and choose OK. This pattern enables both Patch and Turtle editing. We will use the patches only as a background for now.
  3. Replace the Project, Patch, Turtle, and Main tab contents with the four definitions below. The class names match the app name; retain the generated app shell and imports that connect those sources.
  4. Choose Save All, then Launch. Allow the app window to open if asked.

As before, these are aligned definitions. Do not retain old template constructor calls to init() or setup().

Piece Responsibility in this model
Project World, initial population, sliders, chart, movement options
Main Create populations, read inputs, count turtles, publish results
Patch Supply the background grid; no local rule
Turtle Move and choose a new heading

2. Configure the world and controls

Put this in Project:

const Project = {
  version: 0.2,
  name: "Walkers",
  title: "Wandering turtles",
  lifecycle: "aligned",
  frameWidth: 1170,
  frameHeight: 1000,
  connect: true,
  visitMode: "A",
  dt: 1,
  cell: {
    cellSize: 16,
    rowDim: 20,
    colDim: 30,
    boundary: "Wrap",
    palette: ["#f0f4e8"]
  },
  agent: {
    palette: ["#1f77b4"],
    defaults: {
      color: 0,
      size: 0.6,
      shape: "circle",
      speed: 0.5,
      theta: null
    },
    count: 40,
    edge: "Wrap",
    movement: "Trajectory",
    collisions: "None",
    motion: "Continuous",
    agentGrid: true,
    start: "random"
  },
  inputs: [
    {
      code: "SPD", type: "slider", legend: "Speed",
      min: 0, max: 1, step: 0.1, value: 0.5, vtype: "Double"
    },
    {
      code: "TURN", type: "slider", legend: "Maximum turn (degrees)",
      min: 0, max: 90, step: 1, value: 15, vtype: "Integer"
    }
  ],
  displays: [
    {
      code: "RIGHT",
      site: "display_01",
      type: "linechart",
      dom: [0, 100],
      ran: [0, 40],
      width: 460,
      height: 275,
      legend: ["Turtles in right half"],
      palette: ["#1f77b4"]
    }
  ]
};

What changed from the fire model?

visitMode: "A" executes turtle rules. The patches still exist, but their patchStep() rules are not automatically run. No patch activation is needed.

agent.count creates forty turtles. "random" gives them random positions; theta: null gives them random initial headings. color: 0 selects the blue entry in the agent palette, separate from the background's cell palette.

"Trajectory" means the base turtle step schedules movement using speed and heading. "Continuous" permits fractional positions. "None" means no collision response: two turtles can pass through each other.

Both boundaries say "Wrap" deliberately: turtles leaving one side reappear on the opposite side, and later patch-neighborhood queries will wrap too. Cell boundaries and agent edge behavior are separate settings.

Speed is in world-coordinate units per tick in this dt: 1 model, not pixels. With cellSize: 16, speed 0.5 is roughly eight display pixels per tick. The speed slider changes simulated distance, not the browser's run-speed control, which changes how quickly ticks are displayed.

"SPD" and "TURN" are case-sensitive input codes. Merely declaring a slider does not make it control arbitrary model properties. Our Turtle code below explicitly reads the speed input each tick.

3. Supply the background Patch

Put this in Patch:

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

class WalkersPatch extends NLPatch {}

export default WalkersPatch;

An empty class is enough. Its base supplies coordinates, color, and reset. We do not need a constructor, custom Store, init, or patch rule.

4. Give turtles movement and turning

Put this in Turtle:

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

class WalkersTurtle extends NLTurtle {
  init() {
    this.thetaUpdater = () => {
      const degrees = (2 * NLRoot.random() - 1) * this.main.TURN;
      return NLRoot.normalize(this.theta + NLRoot.degToRad(degrees));
    };
  }

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

  turtleStep() {
    this.nextSpeed = this.main.SPD;
    super.turtleStep();
  }
}

export default WalkersTurtle;

Heading is a value; the updater is a rule

this.theta is the current heading in radians. Our slider uses degrees, so NLRoot.degToRad(degrees) converts the proposed turn. NLRoot.normalize(angle) expresses the result in the usual -pi to pi range.

NLRoot.random() gives a number from 0 inclusive to 1 exclusive. Multiplying by 2 and subtracting 1 gives a number from -1 to 1. Multiplying by TURN gives a turn between minus and plus the slider's value. With TURN 15, each turtle chooses a turn between -15 and +15 degrees each tick. With TURN 0, it continues straight.

The expression () => { ... } is a function saved for later use. thetaUpdater asks the base turtle step to evaluate it each time the turtle steps. Assign the function, not the result of calling it. We register it once in aligned init(), after the turtle's built-in defaults are available. We do not access a private heading Store or evaluate the updater manually.

First move, then choose the next heading

Here is the order in this model:

  1. this.nextSpeed = this.main.SPD schedules the current slider speed.
  2. super.turtleStep() schedules movement using that pending speed and the heading already in effect.
  3. The same base call evaluates thetaUpdater, proposing the next heading.
  4. At shift, the runtime commits position, speed, and heading together.

The new turn affects the following move, not the move already scheduled. This is intentional, and is why a change to TURN is not an immediate sideways jump. Do not add a separate fd, forward, or manual Store-shift call: trajectory movement and commitment are already provided.

For orientation in this world:

Direction Heading in radians Change in position
East 0 x increases
South Math.PI / 2 y increases
West Math.PI or -Math.PI x decreases
North -Math.PI / 2 y decreases

This differs from NetLogo's zero-north heading convention. Positive turns rotate clockwise on the display because y increases downward.

Why is speed also assigned in setup?

Setup puts the current slider speed in effect before the first tick, including after Reset. currentSpeed changes speed immediately without changing the saved start baseline. nextSpeed handles subsequent tick proposals. They serve different moments; neither requires a private Store.

This also makes zero speed work explicitly. With SPD 0 the turtles stay in place, although their heading updater still runs when you step. Setting speed to zero is not the same as pausing the clock.

5. Create populations and publish the count

Put this in Main:

import {NLSimMain} from "@nwk/api/nlsimmain.js";

class Walkers extends NLSimMain {
  constructor(Patch, Turtle) {
    super();
    this.Patch = Patch;
    this.Turtle = Turtle;
  }

  initCells() {
    this.initializePatches(this.Patch);
  }

  initAgents() {
    this.initializeTurtles(this.Turtle);
  }

  setup() {
    this.publishCount();
  }

  update() {
    this.publishCount();
  }

  publishCount() {
    const turtles = [...this.turtles.values()];
    const count = turtles.filter(turtle => turtle.x >= this.cols / 2).length;
    this.show("RIGHT", count);
  }

  get SPD() { return this.getParam("SPD"); }
  get TURN() { return this.getParam("TURN"); }
}

export default Walkers;

The runtime supplies our Patch and Turtle classes. Main saves them, then creates their populations through the two initialization hooks. No manual calls to a turtle's init or setup are needed.

get SPD() and get TURN() define convenient named reads of Project's inputs. Inside a turtle, this.main.SPD reads the current speed setting. Changing an input does not require reconstructing Main.

this.turtles.values() iterates the population. [...values] makes an ordinary array so we can use filter. The right half begins at column coordinate 15 because our world is 30 columns wide. turtle.x is the committed fractional position, not the column rounded down to a patch.

show("RIGHT", count) sends a value to the chart with that code. Main setup publishes the initial count after both populations are ready. Main update publishes after movement has committed and the clock has advanced. This is a count of where turtles are, not an accumulated number of crossings.

6. Run a small experiment

Wait for connection (or use Connect if automatic connection is disabled).

  1. Step a few times. Blue turtles move; there should still be forty. Overlapping turtles may look like fewer dots. The chart stays between 0 and 40, and changes only when turtles cross the middle or wrap across an edge.
  2. Set TURN to 0. Step several more times. Paths are now straight, using each turtle's current heading; they need not all point the same way.
  3. Set SPD to 0. Steps still advance time, but positions and the right-half count stay fixed. With nonzero TURN, headings can still change.
  4. Restore SPD to 0.5 and TURN to 15. Run, then pause. Motion resumes.
  5. Reset twice, stepping between resets. Each reset returns time to zero and restores the original turtle positions/headings. The initial right-half count returns too. The sliders retain their current settings.

Reset recreates turtles from the saved initial population; it does not rewind the random-number stream. With TURN above zero, later paths need not repeat. Reloading/reconnecting starts a fresh random population. Do not treat either operation as a seeded experiment.

The fixed forty-turtle population is deliberate: this lesson does not promise live population resizing from a slider. To try eighty turtles, change agent.count to 80 and the chart's ran to [0, 80], then save and relaunch. Births and deaths are a separate modeling operation.

7. An exact one-turtle check

Random models are easier to understand after checking one predictable case. In Project, temporarily replace agent.start: "random" with this value:

const singleStart = [[0, 14.5, 10, 0, 0.6, 0, 0.5, 0]];

Use the array value, not the const declaration, as agent.start. The entries in each turtle's start row are:

[id, x, y, colorIndex, size, shapeIndex, speed, headingRadians]

Here shape index 0 is Circle and heading 0 is east. A nonempty start list determines the initial population size, so this creates one turtle. Also set the TURN input's starting value to 0 and keep SPD's value at 0.5. Save and relaunch.

Time x y Right-half count
0 14.5 10 0
1 15 10 1
2 15.5 10 1

Changing SPD to 0 and stepping must leave it at (15.5, 10). Change SPD to 1 and Reset: it returns to (14.5, 10) at time zero with the current slider speed of 1. Its next step reaches (15.5, 10). The table applies to the zero-turn, half-unit-speed experiment only.

Restore "random" and the original inputs to return to the full lesson.

8. Troubleshooting and next steps

Symptom First things to check
Background only Project has agent; Main defines initAgents(); Turtle class is exported
Turtles appear but never move visitMode: "A", "Trajectory", nonzero SPD, and super.turtleStep()
An input has no effect Its exact code matches the Main getter; Step is still being called
Turns seem one move late That is this model's movement-before-heading-update contract
Heading change is enormous Convert degrees to radians once, not zero or two times
Chart is empty "RIGHT" matches the display code and display_01 exists
Reset seems random Starts restore; the random turning stream does not rewind

We have used no custom Store and no underscore-prefixed fields. Keep that simple public interface until your model needs independent state. See public state/updater properties and shared random utilities. Continue with Programming Main and Programming turtles, then connect the populations in Grazers and food.