On this page

Third model: turtles eat a regrowing resource

Manual contents | Previous: Programming turtles | State management

The fire model had changing patches. Walkers had moving turtles. This lesson connects them: patches grow food, and turtles eat it to maintain energy. Food is a number from 0 to 4, not merely a color. Energy is a number from 0 to 8. Color is just a picture of that state.

This is a deliberately small model, not an ecological prediction. There is no reproduction, death, search for better food, or fairness lottery. Hungry turtles keep moving. Those are sensible later experiments, not hidden features of this lesson.

1. Create Grazers

Save/download your current Studio app. Choose New App, name it Grazers, and choose Agent-Environment Feedback. Replace the four editor definitions below; retain the generated app shell/imports that connect them. Choose Save All, then Launch and allow the app window to open. Do not retain legacy template calls to init/setup.

The class names below match the app name. We keep lifecycle: "aligned". If you prefer, complete Walkers first; this lesson adds custom Stores to its basic structure.

What you will learn

By the end, you will have built a feedback loop:

patch food -> turtle intake -> turtle energy and appearance
     ^             |
     |             v
patch regrowth <- food removed by grazing

The environment is not just a background image. Turtles change a quantity owned by patches, and that quantity limits what turtles can eat later. Regrowth replenishes the shared resource; it does not depend on turtle energy. Movement determines which patch receives the next claim.

You need only the class/getter patterns from Walkers. A Store is a small container holding a current number and a proposed next number:

Expression Meaning in this lesson
new Store(4) Start with 4 and remember 4 as the reset baseline
store.get() Read the committed number
store.next = amount Propose the number for commitment
store.next Read the latest proposal, including earlier claims
main.synchronize(store) Register proposals for commitment by the runtime
store.reinitialize() Restore the saved baseline

These are custom food/energy Stores, not private built-in turtle Stores. Position and appearance still use their public turtle properties. For more detail, consult state management; you do not need to implement a Store or a scheduler.

Decide who owns each rule

Owner State Rule
Patch Food, initially 4, capped at 4 Grow; grant claims without spending the same food twice
Turtle Energy, initially 4, capped at 8 Pay a cost, request food, then schedule movement
Main No extra ecological state Create populations, expose inputs, report committed totals
Project Configuration Set populations, visit order, controls, palettes, and charts

The turtle asks myPatch().takeFood(...); it does not reach into the patch's Store and implement a second version of food accounting. Keeping the accounting on the owner gives growth and all eaters one consistent rule.

2. Project: controls and outputs

const Project = {
  version: 0.2,
  name: "Grazers",
  title: "Grazers and food",
  lifecycle: "aligned",
  frameWidth: 1170,
  frameHeight: 1000,
  connect: true,
  visitMode: "C_A",
  dt: 1,
  cell: {
    cellSize: 24,
    rowDim: 12,
    colDim: 20,
    boundary: "Wrap",
    palette: ["#e6d5b8", "#84b547"]
  },
  agent: {
    palette: ["#d95f02", "#1f77b4"],
    defaults: {
      color: 1, size: 0.6, shape: "circle", speed: 0.5, theta: null
    },
    count: 30,
    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: "REGROW", type: "slider", legend: "Food growth per tick",
      min: 0, max: 2, step: 0.25, value: 1, vtype: "Double"
    },
    {
      code: "BITE", type: "slider", legend: "Maximum bite",
      min: 0, max: 4, step: 0.25, value: 2, vtype: "Double"
    }
  ],
  displays: [
    {
      code: "FOOD", site: "display_01", type: "linechart",
      dom: [0, 100], ran: [0, 960], width: 460, height: 240,
      legend: ["Total food"], palette: ["#60942c"]
    },
    {
      code: "ENERGY", site: "display_02", type: "linechart",
      dom: [0, 100], ran: [0, 8], width: 460, height: 240,
      legend: ["Mean turtle energy"], palette: ["#1f77b4"]
    }
  ]
};

"C_A" means patches step before turtles, with a shared commitment afterward. We exploit that order explicitly: patches propose growth, then turtles claim from the pending food. Unlike a slash mode, there is no intervening shift. This is an intentional ordered allocation, not a claim that simultaneous independent eaters magically agree about a shared resource.

3. Patch: grow food and honor claims

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

class GrazersPatch extends NLPatch {
  constructor(col, row, main) {
    super(col, row, main);
    this.food = new Store(4);
  }

  init() {
    this.main.synchronize(this.food);
  }

  setup() {
    this.currentColor = this.food.get() > 0 ? 1 : 0;
    this.main.visitAlways(this);
  }

  reset() {
    super.reset();
    this.food.reinitialize();
  }

  patchStep() {
    super.patchStep();
    this.proposeFood(Math.min(4, this.food.get() + this.main.REGROW));
  }

  proposeFood(amount) {
    this.food.next = amount;
    this.nextColor = amount > 0 ? 1 : 0;
  }

  takeFood(requested) {
    const eaten = Math.min(requested, this.food.next);
    this.proposeFood(this.food.next - eaten);
    return eaten;
  }
}

export default GrazersPatch;

There are four different jobs here:

proposeFood keeps the proposed color consistent with the final proposed food. Green means some food, not full food; tan means none. This helper is used by both growth and consumption, so an exhausted patch changes color at the same shift as its food changes. We do not ask a pre-consumption color updater to guess the eventual result.

Suppose a patch has 3 food available and two turtles each want 2. If both read current food and independently propose 3 - 2, both could gain 2 while only 2 food was removed. That creates food from nothing.

Here the first claim gets 2 and leaves pending food 1. The second sees that pending 1, gets 1, and leaves 0. The total eaten is exactly 3. Reading pending state is intentional here: it is a reservation ledger shared by ordered visitors. For an independently synchronized local rule, keep reading current state as in the fire tutorial.

Earlier turtles in the population's traversal can get more food. We have not randomized that priority or proved an equitable allocation. A fair version would collect requests, choose an allocation policy in Main, then apply results.

4. Turtle: pay an energy cost, eat, and move

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

class GrazersTurtle extends NLTurtle {
  constructor(id, main) {
    super(id, main);
    this.energy = new Store(4);
  }

  init() {
    this.main.synchronize(this.energy);
    this.thetaUpdater = () => NLRoot.normalize(
      this.theta + NLRoot.degToRad((2 * NLRoot.random() - 1) * 15)
    );
    this.colorUpdater = () => this.energy.next > 0 ? 1 : 0;
  }

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

  reset() {
    super.reset();
    this.energy.reinitialize();
  }

  turtleStep() {
    const requested = Math.min(this.main.BITE, 8 - this.energy.get() + 1);
    const eaten = this.myPatch().takeFood(requested);
    this.energy.next = Math.max(0, Math.min(8, this.energy.get() - 1 + eaten));
    this.nextSpeed = this.main.SPD;
    super.turtleStep();
  }
}

export default GrazersTurtle;

Each tick costs one energy unit. A turtle eats at most BITE, limited by available food and by the energy space after paying that cost. This makes the cap explicit rather than discarding excess food after eating. Energy cannot go below zero; zero-energy turtles are orange, not dead.

Turtles eat on their current patch, before moving. Base movement then proposes the destination. The heading updater chooses a turn for the next move, just as in Walkers. The color updater reads pending energy because that is the value about to appear on the chart. These public built-in updaters need no manual evaluation. The custom energy Store is updated explicitly in turtleStep.

Turtle's local reset handles energy for completeness; whole-world Reset instead creates fresh turtles with fresh energy Stores. Patch food Stores survive and reinitialize. There are no external turtle-owned listeners to retire.

5. Main: report food and energy after commitment

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

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

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

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

  setup() {
    this.publish();
  }

  update() {
    this.publish();
  }

  publish() {
    const food = this.patches.toList()
      .reduce((sum, patch) => sum + patch.food.get(), 0);
    const turtles = [...this.turtles.values()];
    const energy = turtles.reduce((sum, turtle) => sum + turtle.energy.get(), 0);
    this.show("FOOD", food);
    this.show("ENERGY", turtles.length === 0 ? 0 : energy / turtles.length);
  }

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

export default Grazers;

At startup: total food is 12 * 20 * 4 = 960, mean energy is 4, and there are 30 turtles. With no turtles, this model deliberately plots mean energy as 0; that convention is not a statistical claim that the mean of an empty set is 0.

6. Run experiments

  1. Step and watch food, turtle colors, and energy. Charts show committed state.
  2. Set REGROW to 0. The total food cannot increase. Some patches turn tan.
  3. Keep BITE at 2; let the model run. Food runs out and energy declines. The exact trajectory depends on random positions/turns.
  4. Set BITE to 0. Every turtle loses up to one energy per tick, then stays at 0. Growth can still replenish patches.
  5. Set SPD to 0. Turtles keep paying costs and eating where they stand. Speed zero is not a pause.
  6. Reset twice. Food returns to 960, energy to 4, time to zero. Current slider settings remain; the next tick uses them.

No whole-population slider is provided. To change the initial population, edit Project and relaunch.

Predict one tick before pressing Step

Use the starting values REGROW 1, BITE 2, and energy 4. Imagine one turtle on a patch containing 3 food:

Phase Patch food Turtle energy What happens
Start Current 3 Current 4 Everyone sees the committed starting world
Patch rule Pending 4 Current 4 Growth adds 1, limited by capacity 4
Turtle rule Pending 2 Pending 5 Turtle takes 2; energy becomes 4 - 1 + 2
Movement proposal Pending 2 Pending 5 Destination is proposed, not yet occupied
Commitment Current 2 Current 5 Food, energy, position, and appearance become current
Main update Current 2 Current 5 Charts measure the completed tick

The turtle can eat the food proposed by growth during this tick because it deliberately reads the patch's pending claim ledger. It still eats at its starting patch, not at the destination. Changing either of those choices changes the model, not just its implementation.

Do not change "C_A" to "A_C" as a cosmetic experiment: patch growth would then overwrite the earlier consumption proposal. Similarly, changing reads of food.next to food.get() would allow competing turtles to spend the same food. The visit order and pending-state policy are part of this model's rules.

7. Check one stationary turtle exactly

Temporarily use cell.rowDim: 1, cell.colDim: 2, SPD's starting value: 0, and this start-array value instead of "random":

const grazingStart = [[0, 0.5, 0.5, 1, 0.6, 0, 0.5, 0]];

Leave REGROW at 1 and BITE at 2. The initial start speed is overridden by setup to the current SPD, so it remains stationary. The other patch grows no higher than its cap of 4.

Time Food on turtle's patch Total food Energy
0 4 8 4
1 2 6 5
2 1 5 6
3 0 4 7
4 0 4 7

After the third tick the occupied patch is tan. At the fourth tick it briefly proposes growing one food, which the turtle eats before commitment; the final color correctly stays tan. Reset restores both patches to food 4 and the turtle to energy 4.

For a competition check, provide two complete start rows on (0.5, 0.5), with first entries 0 and 1, and set REGROW to 0. Both first get 2 food and reach energy 5. On the next tick neither gets food; both fall to energy 4. With BITE 3 instead, the first gets 3 and the second gets only 1. Observe the priority bias rather than confusing it with a bug in synchronization.

Restore the full world and inputs after checking.

8. Watch movement spread grazing between patches

The stationary check isolates accounting. Now change just movement so we can see how visits distribute the pressure on food. Keep the two-patch, one-row world and the single start row from section 7. Set SPD's starting value to 1, REGROW to 1, and BITE to 2. In Turtle init, replace the random thetaUpdater assignment with:

this.thetaUpdater = () => this.theta;

Leave the energy registration and color updater in init unchanged. This replacement holds the heading steady; it is not an extra updater to add alongside the original. Relaunch the edited model. The turtle begins facing east, so it alternates between the two patch centers. Call the left patch A (column 0) and the right patch B (column 1).

Time Turtle x Food A Food B Energy
0 0.5 4 4 4
1 1.5 2 4 5
2 0.5 3 2 6
3 1.5 2 3 7
4 0.5 3 2 8
5 1.5 3 3 8

At time 1 the turtle is on B, but A lost the food: eating preceded movement. At time 2, A has recovered while the turtle ate on B. Compare this with the stationary table: visiting alternately gives each patch a tick without a claim.

At energy 8 the turtle still pays one unit each tick, but requests only enough food to replace that cost. It does not keep removing a full bite and throw the excess away. At time 5, A grew from 3 to 4, then supplied only 1 food. The ENERGY chart is the table's energy for this one-turtle check; the FOOD chart is Food A plus Food B.

Check the resource budget

For each patch, the amount actually grown is:

grown = min(REGROW, 4 - food at start)
food at end = food at start + grown - total granted to turtles

Unused growth above the capacity is not stored for later. With REGROW 0, food can only stay unchanged or decrease. Food never becomes negative because each claim is limited by the remaining pending food.

Energy has its own bounds:

energy at end = max(0, min(8, energy at start - 1 + food eaten))

Food granted is converted one-for-one before the energy floor is applied. This is not a conservation law for food plus energy: growth adds resources, the tick cost spends energy, and energy cannot fall below zero. An exhausted turtle that eats 1 ends at energy 0, not 1, because the cost is included in the same expression.

A small experiment sheet

Predict the result, change one setting, and then check:

Change Prediction to check
REGROW 0 Total food never increases; once it is gone, energy eventually reaches 0
BITE 0 Food recovers toward capacity; energy falls by 1 per tick until 0
SPD 0 Grazing remains local; energy costs and patch growth still run
Two turtles share a patch, BITE 3, REGROW 0 The first gets 3 of the initial 4 food; the second gets 1
Reset after changing sliders Food/energy return to baselines; current input values remain

Do not use a single random large-world run as proof of the budget. The small tables isolate the rules; larger runs explore their consequences. Restore the random heading updater and full world when finished.

9. Next experiments, one change at a time

Keep the exact two-patch experiment as a regression check when changing rules. If a result surprises you, inspect one patch/turtle before tuning a large run.

Continue with checking and troubleshooting, and keep the quick reference beside the editor while building your own model.