Learn Creative Coding (#58) - Swarm Intelligence: Ants, Bees, and Slime Molds
Twelve episodes into the emergent systems arc. Grid automata (ep047-049), free-moving flocks (ep050-051), continuous chemistry (ep052-053), formal grammars (ep054-055), autonomous crawlers and slime molds (ep056), erosion and growth (ep057). Every system so far starts with simple agents or cells and produces complex structure from nothing. But most of those agents are... kind of dumb? They follow gradients, they react to neighbors, they deposit chemical. They don't solve problems.
Today's agents solve problems. Ant colonies find the shortest path to food without any ant knowing the full map. Bee swarms allocate foragers to the best flowers without a central scheduler. Slime molds build transport networks that rival human-engineered rail systems. This is swarm intelligence -- collective problem-solving where the group is smarter than any individual member. No leader, no blueprint, no global plan. Just simple agents following simple rules, and somehow the colony as a whole does something clever.
We already touched on some of this in episode 56 with the Physarum simulation and stigmergy. But that was focused on the visual patterns -- the glowing filament networks. Today we go deeper into the intelligence side. How do ants actually find shortest paths? How does pheromone evaporation prevent the colony from getting stuck? How do you build a working ant colony optimization in Canvas? And then we'll push the Physarum model further -- not just as art but as a network designer that connects food sources with efficient paths.
The creative coding angle: these simulations produce genuinely beautiful output. Pheromone trails glow and fade. Ant highways form and dissolve. Slime mold networks branch and merge like living circuitry. And the fact that they're SOLVING something -- not just making pretty noise -- gives the visuals a kind of purposeful energy that random systems don't have.
Ant colony optimization: the pheromone trick
The basic setup. You have a grid (our Canvas). Somewhere on it is a nest (where ants live) and one or more food sources. Ants leave the nest, wander around looking for food, and when they find it they grab some and walk back home. Simple enough so far. The magic is pheromone.
Every ant deposits a chemical trail as it walks. Other ants can sense this pheromone and prefer to follow paths that have more of it. When an ant finds food and returns home, it deposits a stronger pheromone trail on the return path. Other ants smell that strong trail and follow it toward the food. More ants on a path means more pheromone, which attracts even more ants. Positive feedback loop.
But here's the crucial part that makes it actually work: pheromone evaporates. Every frame, every cell's pheromone decays a little bit. Long paths lose pheromone faster than short paths (because the pheromone is spread over more cells and each cell decays independently). Short paths between nest and food get traversed more frequently per unit time, so they accumulate more pheromone relative to evaporation. The colony converges on the shortest route without any ant knowing geometry.
Let's build it.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width = 600;
const H = canvas.height = 600;
// pheromone grids -- two types
const toFoodPhero = new Float32Array(W * H); // "food is this way"
const toHomePhero = new Float32Array(W * H); // "home is this way"
const NEST = { x: 100, y: 300 };
const FOOD_SOURCES = [
{ x: 480, y: 120, radius: 20 },
{ x: 450, y: 450, radius: 20 }
];
function isInFood(x, y) {
for (const f of FOOD_SOURCES) {
const dx = x - f.x;
const dy = y - f.y;
if (dx * dx + dy * dy < f.radius * f.radius) return true;
}
return false;
}
function isNearNest(x, y) {
const dx = x - NEST.x;
const dy = y - NEST.y;
return dx * dx + dy * dy < 400; // radius 20
}
Two pheromone fields. That's the key insight most tutorials skip. If you only have one pheromone type, ants just wander randomly and deposit generic "I was here" chemical. With two types -- "to food" and "to home" -- ants searching for food follow the "to food" pheromone, and ants returning home follow the "to home" pheromone. Each direction has its own highway system.
Here's a helper to sample pheromone at any position (we'll reuse this in the ant class):
function sampleField(field, x, y) {
const ix = Math.floor(((x % W) + W) % W);
const iy = Math.floor(((y % H) + H) % H);
if (ix < 0 || ix >= W || iy < 0 || iy >= H) return 0;
return field[iy * W + ix];
}
The ant agent
Each ant has a position, direction, speed, and a state: either searching for food or carrying food home. When searching, it follows "to food" pheromone and deposits "to home" pheromone (so other ants can find their way back). When carrying food, it follows "to home" pheromone and deposits "to food" pheromone (marking the path to the food source for others).
class Ant {
constructor() {
this.x = NEST.x + (Math.random() - 0.5) * 20;
this.y = NEST.y + (Math.random() - 0.5) * 20;
this.dir = Math.random() * Math.PI * 2;
this.speed = 1.8;
this.hasFood = false;
this.lifetime = 0;
}
sense(field, angle, distance) {
const sx = this.x + Math.cos(this.dir + angle) * distance;
const sy = this.y + Math.sin(this.dir + angle) * distance;
const ix = Math.floor(((sx % W) + W) % W);
const iy = Math.floor(((sy % H) + H) % H);
return field[iy * W + ix];
}
step() {
this.lifetime++;
// which pheromone to follow?
const followField = this.hasFood ? toHomePhero : toFoodPhero;
// which pheromone to deposit?
const depositField = this.hasFood ? toFoodPhero : toHomePhero;
// three-sensor steering (same pattern as ep056 Physarum)
const sensorDist = 12;
const sensorAngle = 0.6;
const left = this.sense(followField, -sensorAngle, sensorDist);
const center = this.sense(followField, 0, sensorDist);
const right = this.sense(followField, sensorAngle, sensorDist);
if (center >= left && center >= right) {
this.dir += (Math.random() - 0.5) * 0.15;
} else if (left > right) {
this.dir -= 0.25;
} else if (right > left) {
this.dir += 0.25;
} else {
this.dir += (Math.random() < 0.5 ? -1 : 1) * 0.25;
}
// random wandering when no pheromone is sensed
if (left < 0.01 && center < 0.01 && right < 0.01) {
this.dir += (Math.random() - 0.5) * 0.8;
}
// move
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
// bounce off walls
if (this.x < 2) { this.x = 2; this.dir = Math.PI - this.dir; }
if (this.x > W - 2) { this.x = W - 2; this.dir = Math.PI - this.dir; }
if (this.y < 2) { this.y = 2; this.dir = -this.dir; }
if (this.y > H - 2) { this.y = H - 2; this.dir = -this.dir; }
// deposit pheromone
const ix = Math.floor(this.x);
const iy = Math.floor(this.y);
if (ix >= 0 && ix < W && iy >= 0 && iy < H) {
depositField[iy * W + ix] += this.hasFood ? 2.0 : 0.5;
}
// check for food / nest arrival
if (!this.hasFood && isInFood(this.x, this.y)) {
this.hasFood = true;
this.dir += Math.PI; // turn around
}
if (this.hasFood && isNearNest(this.x, this.y)) {
this.hasFood = false;
this.dir += Math.PI; // turn around, go find more
}
}
}
Notice the deposit amounts are asymmetric: ants carrying food deposit 2.0 (strong "to food" signal) while searching ants deposit only 0.5 (weaker "to home" signal). This is deliberate. The strong "to food" pheromone is the valuable information -- "I confirmed there's food this way." The weaker "to home" pheromone is more of a general trail. This asymmetry makes food trails dominate, which is what you want -- the colony prioritizes the important paths.
The dir += Math.PI when picking up food or arriving home is the simplest possible "turn around" behavior. In a more sophisticated model the ant would gradually turn, but the instant reversal works fine and keeps the code clean.
Pheromone dynamics: evaporation and diffusion
Without evaporation the whole grid would eventually saturate and every path would be equally strong. Evaporation is what gives the system its ability to adapt -- if a food source runs out, the pheromone trail to it fades and ants stop going there. Diffusion spreads the pheromone slightly so trails have width (not single-pixel threads) and ants don't need to be perfectly on the trail to sense it.
function evaporateAndDiffuse(field, decay, diffuseStrength) {
const next = new Float32Array(W * H);
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
const idx = y * W + x;
// 3x3 blur for diffusion
let sum = field[idx] * (1 - diffuseStrength);
const weight = diffuseStrength / 8;
sum += field[idx - 1] * weight;
sum += field[idx + 1] * weight;
sum += field[idx - W] * weight;
sum += field[idx + W] * weight;
sum += field[idx - W - 1] * weight;
sum += field[idx - W + 1] * weight;
sum += field[idx + W - 1] * weight;
sum += field[idx + W + 1] * weight;
next[idx] = sum * decay;
}
}
field.set(next);
}
The decay parameter (0.97-0.99 typically) controls how fast pheromone evaporates. Lower values = faster evaporation = more dynamic system that adapts quickly but has weaker trails. Higher values = slower evaporation = more stable highways but slower adaptation when conditions change. The diffuseStrength (0.1-0.3) controls how much pheromone spreads to neighboring cells. More diffusion = wider, softer trails that are easier for ants to find. Less diffusion = thin precise trails that are harder to discover.
Running the colony
const ANT_COUNT = 300;
const ants = [];
for (let i = 0; i < ANT_COUNT; i++) {
ants.push(new Ant());
}
function update() {
// step all ants
for (const ant of ants) {
ant.step();
}
// evaporate both pheromone fields
evaporateAndDiffuse(toFoodPhero, 0.985, 0.15);
evaporateAndDiffuse(toHomePhero, 0.985, 0.15);
}
function render() {
const imgData = ctx.createImageData(W, H);
for (let i = 0; i < W * H; i++) {
const food = Math.min(1, toFoodPhero[i] * 0.15);
const home = Math.min(1, toHomePhero[i] * 0.15);
// food pheromone = green, home pheromone = blue
imgData.data[i * 4 + 0] = Math.floor(food * 40);
imgData.data[i * 4 + 1] = Math.floor(food * 200 + home * 60);
imgData.data[i * 4 + 2] = Math.floor(home * 180);
imgData.data[i * 4 + 3] = 255;
}
ctx.putImageData(imgData, 0, 0);
// draw nest
ctx.fillStyle = '#ffcc44';
ctx.beginPath();
ctx.arc(NEST.x, NEST.y, 8, 0, Math.PI * 2);
ctx.fill();
// draw food sources
ctx.fillStyle = '#44ff88';
for (const f of FOOD_SOURCES) {
ctx.beginPath();
ctx.arc(f.x, f.y, f.radius, 0, Math.PI * 2);
ctx.fill();
}
// draw ants as tiny dots
for (const ant of ants) {
ctx.fillStyle = ant.hasFood ? '#ff8844' : '#cccccc';
ctx.fillRect(ant.x - 1, ant.y - 1, 2, 2);
}
}
function loop() {
for (let i = 0; i < 3; i++) update(); // 3 substeps per frame
render();
requestAnimationFrame(loop);
}
loop();
Three substeps per frame speed things up without making the display choppy. Run it and watch. For the first hundred frames or so, ants wander randomly -- the canvas is mostly dark with faint scattered trails. Then a few ants stumble onto a food source. They turn around, deposit strong "to food" pheromone on the way home. Other ants near that trail smell it and follow. More ants reach the food, more return with strong pheromone, the trail brightens, more ants join.
Within a few hundred frames you see clear green highways connecting the nest to each food source. The highways aren't straight lines -- they meander a bit because the first few ants that found the path weren't walking in a straight line. But over time, as more ants travel the route, the pheromone concentrates along the shorter path segments and the highway straightens. It's optimization by accumulation.
The color coding helps you read it: green channels are "to food" pheromone (the valuble information), blue channels are "to home" pheromone. Where both overlap you get a cyan-ish highway. Food-carrying ants (orange dots) walk along green trails toward the yellow nest. Searching ants (gray dots) walk along green trails toward the green food sources. Two traffic streams on the same highway, going opposite directions.
Why short paths win
This is the part that takes a minute to internalize. No ant measures distance. No ant compares paths. The shortest path wins purely through pheromone dynamics. Here's why:
Say there are two paths to a food source -- one is 200 pixels long, the other is 350 pixels. An ant on the short path completes a round trip faster. It deposits pheromone on the return leg. That pheromone starts evaporating immediately. But because the short-path ant arrives home sooner, its pheromone has less time to evaporate before the next ant walks over it and reinforces it. The long-path ant's pheromone has been evaporating longer by the time reinforcement arrives.
Over many round trips, the short path accumulates pheromone faster than it evaporates. The long path doesn't -- evaporation wins. More ants shift to the short path (because it smells stronger), which amplifies the effect. Eventually the long path fades entirely and all traffic is on the short route. The colony found the shortest path through nothing but local pheromone dynamics.
You can visualize this by tracking pheromone levels on two paths over time. Even without the full simulation, the math is clear:
// simplified path competition model
let shortPathPhero = 0;
let longPathPhero = 0;
const evapRate = 0.02;
for (let tick = 0; tick < 500; tick++) {
// short path: ant completes round trip every 40 ticks
if (tick % 40 === 0) shortPathPhero += 1.0;
// long path: ant completes round trip every 70 ticks
if (tick % 70 === 0) longPathPhero += 1.0;
// both evaporate equally
shortPathPhero *= (1 - evapRate);
longPathPhero *= (1 - evapRate);
}
// shortPathPhero >> longPathPhero
// the faster round-trip wins the evaporation race
This is ant colony optimization (ACO) and it's a real algorithm used in logistics, network routing, and combinatorial optimization. The simulation we just built is a spatial version of it.
Multi-colony competition
Things get really interesting with two colonies. Different nest positions, different pheromone types. Territory boundaries emerge from competition:
// colony A: red pheromone
const colonyA = {
nest: { x: 80, y: 300 },
foodPhero: new Float32Array(W * H),
homePhero: new Float32Array(W * H),
ants: [],
color: [220, 60, 40]
};
// colony B: blue pheromone
const colonyB = {
nest: { x: 520, y: 300 },
foodPhero: new Float32Array(W * H),
homePhero: new Float32Array(W * H),
ants: [],
color: [40, 80, 220]
};
// shared food source in the middle
const sharedFood = { x: 300, y: 300, radius: 25 };
function stepColony(colony) {
for (const ant of colony.ants) {
// each ant follows its OWN colony's pheromone
// and ignores the other colony's pheromone entirely
antStep(ant, colony.foodPhero, colony.homePhero);
}
evaporateAndDiffuse(colony.foodPhero, 0.985, 0.15);
evaporateAndDiffuse(colony.homePhero, 0.985, 0.15);
}
Each colony has its own pheromone fields. Colony A ants can't smell Colony B's pheromone and vice versa. Both colonies compete for the same central food source. The result is fascinating -- both colonies build highways toward the food, but from different directions. Where the highways overlap near the food source, you get a contested zone where red and blue trails mix. The colony with the shorter path (closer nest) tends to dominate because its ants complete more round trips per unit time, building stronger pheromone. The further colony's trail weakens and eventually diverts to find the food from a less contested angle.
The territory boundary between the two colonies isn't static. It shifts as one colony temporarily gets more ants to the food and strengthens its trails. Then the other colony adapts. The border undulates back and forth, finding an equilibrium that reflects the geographic advantage of each colony. Beautiful to watch -- two competing systems self-organizing spatial territory from nothing but local chemistry.
Bee foraging: the waggle dance
Ants use stigmergy -- indirect communication through the environment. Bees communicate differently. A scout bee finds a food source, returns to the hive, and performs a waggle dance that tells other bees the direction and distance to the food. Other bees watch the dance, fly to the location, assess the quality, and come back to either reinforce the dance (good food) or not dance (bad food). The hive allocates foragers proportionally to food source quality.
We can model this without the full dance mechanics -- just the information sharing:
class Bee {
constructor(hiveX, hiveY) {
this.x = hiveX;
this.y = hiveY;
this.hiveX = hiveX;
this.hiveY = hiveY;
this.dir = Math.random() * Math.PI * 2;
this.speed = 2.5;
this.state = 'scouting'; // scouting, harvesting, returning
this.targetFood = null;
this.knowledge = null; // remembered food source
}
step(foodSources, sharedMemory) {
if (this.state === 'scouting') {
// random exploration
this.dir += (Math.random() - 0.5) * 0.6;
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
// bounce off edges
if (this.x < 0 || this.x > W) this.dir = Math.PI - this.dir;
if (this.y < 0 || this.y > H) this.dir = -this.dir;
this.x = Math.max(0, Math.min(W, this.x));
this.y = Math.max(0, Math.min(H, this.y));
// check if found food
for (const food of foodSources) {
const dx = this.x - food.x;
const dy = this.y - food.y;
if (dx * dx + dy * dy < food.radius * food.radius) {
this.knowledge = { x: food.x, y: food.y, quality: food.quality };
this.state = 'returning';
break;
}
}
} else if (this.state === 'returning') {
// fly straight back to hive
const dx = this.hiveX - this.x;
const dy = this.hiveY - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 5) {
// arrived at hive -- share knowledge
if (this.knowledge) {
sharedMemory.push({
x: this.knowledge.x,
y: this.knowledge.y,
quality: this.knowledge.quality,
age: 0
});
}
this.state = 'harvesting';
} else {
this.x += (dx / dist) * this.speed * 1.5;
this.y += (dy / dist) * this.speed * 1.5;
}
} else if (this.state === 'harvesting') {
// pick best known food source from shared memory
if (!this.targetFood && sharedMemory.length > 0) {
// probability of following proportional to quality
const totalQ = sharedMemory.reduce((s, m) => s + m.quality, 0);
let pick = Math.random() * totalQ;
for (const mem of sharedMemory) {
pick -= mem.quality;
if (pick <= 0) {
this.targetFood = { x: mem.x, y: mem.y };
break;
}
}
}
if (this.targetFood) {
const dx = this.targetFood.x - this.x;
const dy = this.targetFood.y - this.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 10) {
// arrived at food, head home
this.state = 'returning';
this.targetFood = null;
} else {
this.x += (dx / dist) * this.speed;
this.y += (dy / dist) * this.speed;
}
}
}
}
}
The sharedMemory array is the hive's communal knowledge -- the equivalent of watching waggle dances. Each returning scout adds an entry with the food location and quality. Harvester bees pick from this shared memory with probability proportional to quality. High-quality food sources get more foragers. Low-quality ones get fewer. If a source depletes (quality drops to zero), no more bees dance for it and foragers naturally redistribute to remaining sources.
This is a different optimization strategy than ants. Ants use spatial pheromone trails -- the communication medium is the environment. Bees use direct information transfer -- the communication medium is the shared memory (the dance). Both achieve efficient resource allocation but through completely different mechanisms. The ant system is more robust to noise (pheromone is analog and spatial). The bee system is faster at responding to new information (direct communication, no waiting for pheromone to accumulate).
Physarum as network optimizer
Back to our old friend from episode 56. The Physarum slime mold simulation produces beautiful filament networks. But those networks aren't just pretty -- they're solving a real optimization problem. The network that forms between food sources is close to the minimum spanning tree (the shortest set of connections that links all food sources). Sometimes it even finds solutions that are better than the MST because it balances shortest-total-length against redundancy (having backup paths).
The setup: place food sources as permanent high-concentration zones. Distribute agents uniformly. Let the Physarum simulation run. Watch the network form between the food sources.
const W = 512;
const H = 512;
const trail = new Float32Array(W * H);
const AGENT_COUNT = 150000;
// food sources -- place them like cities on a map
const foods = [
{ x: 80, y: 80 },
{ x: 420, y: 60 },
{ x: 256, y: 240 },
{ x: 90, y: 420 },
{ x: 400, y: 400 },
{ x: 256, y: 480 },
{ x: 480, y: 256 }
];
const agents = [];
for (let i = 0; i < AGENT_COUNT; i++) {
agents.push({
x: Math.random() * W,
y: Math.random() * H,
dir: Math.random() * Math.PI * 2
});
}
// Physarum parameters (same model as ep056)
const sensorAngle = 0.65;
const sensorDist = 9;
const turnSpeed = 0.45;
const moveSpeed = 1.0;
const depositAmt = 0.8;
const decayRate = 0.96;
function emitFoodSignal() {
for (const food of foods) {
for (let dy = -8; dy <= 8; dy++) {
for (let dx = -8; dx <= 8; dx++) {
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 8) {
const ix = ((Math.floor(food.x) + dx) + W) % W;
const iy = ((Math.floor(food.y) + dy) + H) % H;
trail[iy * W + ix] += 5.0 * (1 - dist / 8);
}
}
}
}
}
function stepPhysarum() {
for (let i = 0; i < AGENT_COUNT; i++) {
const a = agents[i];
// sense (same three-sensor model)
const fL = sampleTrail(a.x + Math.cos(a.dir - sensorAngle) * sensorDist,
a.y + Math.sin(a.dir - sensorAngle) * sensorDist);
const fC = sampleTrail(a.x + Math.cos(a.dir) * sensorDist,
a.y + Math.sin(a.dir) * sensorDist);
const fR = sampleTrail(a.x + Math.cos(a.dir + sensorAngle) * sensorDist,
a.y + Math.sin(a.dir + sensorAngle) * sensorDist);
// turn
if (fC >= fL && fC >= fR) {
// straight
} else if (fC < fL && fC < fR) {
a.dir += (Math.random() < 0.5 ? -1 : 1) * turnSpeed;
} else if (fL > fR) {
a.dir -= turnSpeed;
} else {
a.dir += turnSpeed;
}
// move
a.x += Math.cos(a.dir) * moveSpeed;
a.y += Math.sin(a.dir) * moveSpeed;
a.x = ((a.x % W) + W) % W;
a.y = ((a.y % H) + H) % H;
// deposit
const ix = Math.floor(a.x);
const iy = Math.floor(a.y);
trail[iy * W + ix] += depositAmt;
}
}
function sampleTrail(x, y) {
const ix = Math.floor(((x % W) + W) % W);
const iy = Math.floor(((y % H) + H) % H);
return trail[iy * W + ix];
}
150,000 agents is a lot. But that's what gives the Physarum simulation its network-building power. With fewer agents the connections between food sources are thin and unstable. With 150k, the network forms thick, robust highways between clusters. The topology stabilizes within about 500-800 frames into something remarkably similar to what real Physarum polycephalum builds in lab experiments.
The famous Tokyo rail experiment (Tero et al., 2010): researchers placed oat flakes at positions corresponding to cities in the Tokyo metro area and let Physarum grow. The slime mold network converged on a topology very close to the actual Tokyo rail system. Our code does the same thing. Place food sources at "city" coordinates and the Physarum builds an efficient transport network between them. No optimization algorithm, no graph theory, no shortest-path computation. Just agents, chemical trails, and diffusion.
Rendering the network
The rendering from ep056 works here too, but let's add food source indicators and network thickness visualization:
function render() {
const imgData = ctx.createImageData(W, H);
for (let i = 0; i < W * H; i++) {
const v = Math.min(1, trail[i] * 0.2);
// cool blue-white palette for the network
const r = Math.floor(v * v * 180 + v * 40);
const g = Math.floor(v * v * v * 200 + v * 50);
const b = Math.floor(v * 220 + (1 - v) * 8);
imgData.data[i * 4 + 0] = r;
imgData.data[i * 4 + 1] = g;
imgData.data[i * 4 + 2] = b;
imgData.data[i * 4 + 3] = 255;
}
ctx.putImageData(imgData, 0, 0);
// food sources as bright circles
ctx.fillStyle = '#ffdd33';
for (const food of foods) {
ctx.beginPath();
ctx.arc(food.x, food.y, 6, 0, Math.PI * 2);
ctx.fill();
}
}
function mainLoop() {
emitFoodSignal();
stepPhysarum();
diffuseAndDecay(trail, decayRate);
render();
requestAnimationFrame(mainLoop);
}
function diffuseAndDecay(field, decay) {
const next = new Float32Array(W * H);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
let sum = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
sum += field[((y + dy + H) % H) * W + ((x + dx + W) % W)];
}
}
next[y * W + x] = (sum / 9) * decay;
}
}
field.set(next);
}
mainLoop();
The blue-white palette makes the network look like bioluminescent veins. Thick connections (high traffic) glow bright white. Thin exploratory filaments are dim blue. Dead areas are nearly black. The food sources are golden dots -- bright anchors that the network connects. You can watch the network forming in real time: first random noise as agents scatter, then dim threads appear between nearby food sources, threads merge and thicken, weaker connections thin out and vanish, and within a minute you have a stable transport graph.
Comparing the strategies
The three systems we built today represent fundamentally different approaches to the same problem (efficient resource allocation):
Ants: Indirect communication through the environment (stigmergy). Pheromone trails in physical space. Optimization through positive feedback + evaporation. Slow to find solutions, very robust to disruption. If you remove a trail, ants rebuild it. If a food source moves, the old trail fades and a new one forms. No central memory -- all information is in the environment.
Bees: Direct communication through shared memory (waggle dance). No environmental modification. Optimization through proportional recruitment -- better sources get more foragers. Fast to respond to new information, less spatially robust. The hive "knows" about food sources instantly when a scout returns, but has no spatial trail to follow.
Physarum: Continuous field model. No discrete agents making decisions -- instead, a chemical field that diffuses, decays, and gets reinforced by moving particles. Finds near-optimal network topologies. The most visually dramatic of the three because the entire field is the artwork.
All three produce efficient solutions to routing and allocation problems. All three use positive feedback (reinforcement of good solutions) balanced by negative feedback (decay/evaporation of bad ones). All three scale gracefully -- add more agents and the solutions get better, not worse. None of them need any global knowledge or central control.
The visual connection
Purely as art pieces, these simulations have a quality that random generative systems don't. Because the agents are SOLVING something, the patterns have directionality and purpose. Ant trails converge on food sources. Bee flight paths form focused beams between hive and flowers. Physarum networks are efficient and branching, not random. The viewer can sense that these shapes mean something even if they don't know the simulation rules. Compare that to plain noise or random walker art -- beautiful in its own way, but without the sense of intentional structure.
That tension between bottom-up emergence and apparent top-down design is one of the most interesting things in creative coding. The agents have no intention. The patterns look designed. The gap between those two facts is where the art lives.
Twelve episodes of emergent systems now. We've covered a LOT of ground -- grid automata, flocking, reaction-diffusion, L-systems, crawlers, erosion, and now swarm intelligence. Still a few more to go in this arc before we wrap up and move on to something completley different.
Looking ahead -- we've been building systems that create structure and systems that destroy it (erosion). We've seen agents that wander (random crawlers), agents that follow rules (boids, ants), and agents that communicate through shared fields (Physarum). What happens when we model physical processes like wave propagation? When you drop a stone in water, the ripples aren't agents and they aren't cells -- they're a continuous phenomenon governed by differential equations. Simulating that connects back to the reaction-diffusion work from episode 52 but with very different visual results. And what about building a full ecosystem where multiple species of agents interact -- predators, prey, plants, decomposers -- all in one simulation? That's where all these techniques come together.
Allez, wa weten we nu allemaal?
- Ant colony optimization uses two pheromone types: "to food" (deposited by returning ants) and "to home" (deposited by searching ants). Ants follow the pheromone type that matches their current goal. The two-pheromone system creates directional highways between nest and food sources
- Pheromone evaporation is what makes shortest paths win. Short paths get traversed more frequently per unit time, so their pheromone accumulates faster than it evaporates. Long paths lose the evaporation race. No ant measures distance -- the colony optimizes through chemistry alone
- Multi-colony competition with separate pheromone fields produces emergent territory boundaries. The colony with the shorter path to a shared resource dominates because its round-trip pheromone reinforcement rate is higher
- Bee foraging uses direct information sharing (shared memory / waggle dance) instead of environmental pheromone. Foragers are recruited proportionally to food source quality. Faster response to new information than ants, but less spatial robustness
- Physarum with 150,000 agents and fixed food emitters builds efficient transport networks between food sources. The network topology converges on solutions close to the minimum spanning tree -- the same result as the famous Tokyo rail experiment where real slime mold matched human-engineered infrastructure
- All three swarm systems use positive feedback (reinforcement of good solutions) balanced by negative feedback (evaporation/decay). This feedback balance is the universal mechanism that makes swarm intelligence work across ants, bees, slime molds, and many other collective systems
- The visual quality of these simulations differs from random generative art because the agents are solving problems. The patterns have directionality and purpose that viewers can sense even without knowing the rules
Sallukes! Thanks for reading.
X