← Cellular Automata From First Principles

Build a Predator-Prey Ecosystem

Gray-Scott, like the forest fire, transforms each cell in place.

Rule 184 moves occupancy, but in one constrained direction.

An ecosystem adds a harder problem:

organisms move
organisms reproduce
predators consume prey
several organisms may want one destination

Now local intentions can conflict.

That means update semantics become part of the model.


Separate visible kind from internal state

Start with:

0 = empty
1 = prey
2 = predator
import numpy as np

EMPTY = 0
PREY = 1
PREDATOR = 2

kind = np.zeros(
    (100, 100),
    dtype=np.uint8,
)

A richer model may need predator energy, age or reproduction state.

Do not overload one integer with every concept.

Use additional fields:

energy = np.zeros(
    kind.shape,
    dtype=np.float32,
)

Now a cell’s state is layered:

visible occupancy
+
internal organism state

This continues Chapter 8’s lesson about state as local memory — now the memory records per-organism history (energy, age) rather than just a refractory timer.


Why naive in-place movement is dangerous

Suppose two predators both target the same prey.

If we mutate the grid immediately, whichever predator happens to be processed first wins.

Then Python loop order has become part of the ecology.

Sometimes sequential updates are a deliberate model choice.

But if they are not deliberate, they are a hidden source of causality. The classic cautionary case is Huberman and Glance’s 1993 spatial-games result: outcomes that looked like discoveries about cooperation turned out to depend on synchronous updating, and introducing asynchrony changed the dynamics. Update order is never neutral scenery; it is always either a stated assumption or an uncontrolled one.

A cleaner synchronous architecture is:

current world
      ↓
propose actions
      ↓
resolve conflicting targets
      ↓
apply accepted actions
      ↓
next world

That is very close to transaction processing.


Make conflict resolution explicit

A prey movement proposal might look like:

(source, target, PREY)

Group proposals by target:

from collections import defaultdict


def resolve_targets(
    proposals,
    rng,
):
    by_target = defaultdict(list)

    for proposal in proposals:
        by_target[
            proposal[1]
        ].append(proposal)

    accepted = []

    for target, choices in by_target.items():
        choice = choices[
            rng.integers(len(choices))
        ]

        accepted.append(choice)

    return accepted

(Verified: with two proposals targeting one cell and one targeting another, exactly one winner per target is accepted.)

Now the collision policy is part of the experiment.

We could instead choose:

PolicyWho wins a contested cellModeling implication
first proposalloop orderfast but order-dependent — usually a bug
highest-energy organismstrongest claimantmerit-based, needs energy state
random proposaluniform drawneutral, seed-controlled
no proposalnobody moves incautious, may deadlock
priority by speciesfixed rankingbuilt-in asymmetry, state it openly

Those are different models.


Give predators persistent state

A predator can lose energy every step and gain energy when it eats.

MOVE_COST = 1.0
FOOD_ENERGY = 4.0

Conceptually:

choose local action
      ↓
pay movement cost
      ↓
gain food energy if predation succeeds
      ↓
die if energy <= 0

Now the future depends on local history.

State has become memory.


Track the world and the populations separately

A spatial snapshot tells us where interactions occur.

A population curve tells us what happens globally.

Those are complementary observables.

Predator-prey spatial evolution

Predator and prey populations through time

The figure generator uses a deliberately compact local predator-prey CA to visualize the population-level phenomenon.

The chapter’s propose/resolve architecture is the richer implementation pattern to use when explicit movement and target conflicts matter.

That distinction is useful:

figure model:
demonstrate spatial population dynamics

engineering model:
make agent intentions and conflicts inspectable

Stating which model produced a figure is part of honest reporting: the curves illustrate possible dynamics, not predictions of the propose/resolve system.


Oscillation is not guaranteed

It is tempting to draw this loop:

prey increase
      ↓
predators increase
      ↓
prey decrease
      ↓
predators starve
      ↓
prey recover

That mechanism can produce oscillatory population dynamics.

But not every parameter choice will.

Possible outcomes include:

prey extinction
predator extinction
both extinction
persistent coexistence
oscillation
spatial patchiness

So the existence and character of oscillation should be measured rather than assumed — with the guardrail that an observed oscillation is not yet a demonstrated stable limit cycle, and neither is ecological realism. Cycles in a toy spatial system suggest mechanisms worth testing, not predictions about real populations.


Treat hidden choices as parameters

Important choices include:

neighborhood shape
movement policy
collision resolution
predation probability
reproduction probability
energy gain
energy cost
boundary conditions
update synchrony

If those remain buried inside code, two ecosystem runs are difficult to compare meaningfully.

Make them explicit configuration.


Keep causality inspectable

When an organism disappears, we should know why.

A lightweight event record can help:

from dataclasses import dataclass


@dataclass
class Event:
    kind: str
    source: tuple[int, int] | None
    target: tuple[int, int] | None

Possible event kinds:

move
eat
birth
starve
collision_lost

Now debugging does not require reconstructing every causal decision from snapshots after the fact.

This becomes increasingly important as cellular systems start to resemble local agents.


One idea to keep

The difficult part of multi-agent cellular worlds is not merely writing more transition rules.

It is defining what simultaneous local action means.

Once several cells compete for shared resources or destinations, conflict resolution becomes part of the model.

In the next chapter we will use local updates for a different purpose: not to simulate an ongoing world, but to construct an organic cave map and then validate whether the result is actually usable.


Research

  • Huberman, B. A. & Glance, N. S. — Evolutionary Games and Computer Simulations (1993). The cautionary case behind this chapter’s update-semantics warning: spatial-game outcomes that looked like discoveries about cooperation depended on the synchronous update scheme, and asynchrony changed the dynamics. Read before treating any multi-agent result as scheme-independent. https://www.researchgate.net/publication/14843377_Evolutionary_Games_and_Computer_Simulations

  • Zenil, H. & Martinez, G. J. — Cellular Automata (Scholarpedia). Distinguishes the update-scheme relaxations this chapter navigates: non-uniform automata (rule varies by position) versus asynchronous updating (timing varies), plus stochastic local updates. Use it to name which relaxation a design actually uses. http://www.scholarpedia.org/article/Cellular_automata

  • Berto, F. & Tagliabue, J. — Cellular Automata (Stanford Encyclopedia of Philosophy). Covers asynchronous updating as a legitimate modeling choice (Ingerson & Buvel 1984) and the broader simulator lineage — the framing that keeps this chapter’s toy ecology a mechanism study rather than a biological claim. https://plato.stanford.edu/entries/cellular-automata/