Build a Predator-Prey Ecosystem
Cellular Automata From First Principles 13: Build a Predator-Prey Ecosystem
A forest fire mostly transforms cells in place.
Rule 184 moves occupancy 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
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.
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
Now the collision policy is part of the experiment.
We could instead choose:
first proposal
highest-energy organism
random proposal
no proposal
priority by species
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.


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
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.
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.