
I feel like the most unexpected of paths is Langton’s Ant!.
Okay, it’s fairly expected. And I’ve even done it before. Been a while though.
Anyways, here we go!
In a nutshell, you have a grid with N possible values (the length of the rule string). For each pattern, when the ‘ant’ walks on that cell, the value in incremented by one and you will turn according to these rules:
Rturn right (90° or 60° in hex)Lturn left (same)Sturn right 120° in hex mode (nothing in square)Mturn left 120° in hex modeUturn 180° in hex mode- anything else (
Nabove), do nothing/go straight
This ends up with some really interesting behavior for such a short ruleset. Langton’s Ant (LR, the default) is definitely an interesting one. For 10,000 ticks, you get chaotic behavior… and then suddenly it stabilizes!
Here are some interesting patterns:
- RLR - chaotic growth
- LLRR - symmetric growth
- LRRRRRLLR - square filling
- LLRRRLRLRLLR - convoluted highway
- RRLLLRLLLRRR - fills a triangle
- MNNLML - hex circular growth
- LMNUMLS - hex spiral growth
You can also do some interesting things with multiple ants (they’ll spawn in a circle):
The modes are:
pauseModecontrols when the simulation will stop (note: will always pause at width * height * 100 tiles)n-tileswill pause when the tile count iswidth * height(no matter where they are)one-at-edgewill stop when each cell is 1 pixel and any ant reaches the edge of the imageall-at-edgewill only stop when all ants reach the edgeno-pausewill never pause (this can get really laggy eventually)
centerModeis how the displayed part of the simulation will be centeredoriginwill stay centered at 0,0boundswill center on the middle of the current overall boundsantswill average the x,y of all antstileswill average the x,y of all tilesmousewill allow some mouse control; left click and drag to move, right click (in theory) to reset
let gui;
let params = {
rule: "RL",
antCount: 1, antCountMin: 1, antCountMax: 100,
spawnRadius: 10, spawnRadiusMin: 0, spawnRadiusMax: 100,
dieOfOldAge: false,
maxAge: "100",
updatesPerTick: 1, updatesPerTickMin: 1, updatesPerTickMax: 1000,
hexGrid: false,
pauseMode: ["n-tiles", "one-at-edge", "all-at-edge", "no-pause"],
centerMode: ["origin", "bounds", "ants", "tiles", "mouse"],
colorScheme: ["rainbow", "fire", "ocean", "forest", "pastel", "monochrome"],
};
const HEX_DIRS = [
{ x: 1, y: 0 },
{ x: 0, y: 1 },
{ x: -1, y: 1 },
{ x: -1, y: 0 },
{ x: 0, y: -1 },
{ x: 1, y: -1 },
];
const SQUARE_DIRS = [
{ x: 0, y: -1 },
{ x: 1, y: 0 },
{ x: 0, y: 1 },
{ x: -1, y: 0 },
];
let lastParams;
let paused = false;
let ants;
let tiles;
let bounds;
let mouseCenterX = 0;
let mouseCenterY = 0;
const randomD = () => {
const r = Math.floor(Math.random() * 4);
return [
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 },
][r];
};
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
gui = createGuiPanel("params");
gui.addObject(params);
gui.setPosition(420, 0);
}
function draw() {
if (
lastParams == undefined ||
Object.keys(params).some((k) => params[k] !== lastParams[k])
) {
ants = [];
if (params.antCount == 1) {
ants.push({
position: { x: 0, y: 0 },
direction: 0,
age: 0,
});
} else {
for (let i = 0; i < params.antCount; i++) {
let x = floor(cos((i * TWO_PI) / params.antCount) * params.spawnRadius);
let y = floor(sin((i * TWO_PI) / params.antCount) * params.spawnRadius);
ants.push({
position: { x, y },
direction: 0,
age: 0,
});
}
}
tiles = new Map();
bounds = {
minX: ants[0].position.x,
maxX: ants[0].position.x,
minY: ants[0].position.y,
maxY: ants[0].position.y,
};
for (let ant of ants) {
bounds.minX = min(bounds.minX, ant.position.x);
bounds.maxX = max(bounds.maxX, ant.position.x);
bounds.minY = min(bounds.minY, ant.position.y);
bounds.maxY = max(bounds.maxY, ant.position.y);
}
paused = false;
lastParams = { ...params };
}
if (paused) {
return;
}
for (let tick = 0; tick < params.updatesPerTick; tick++) {
for (let ant of ants) {
ant.age += 1;
let positionString = `${ant.position.x},${ant.position.y}`;
let ruleIndex = tiles.get(positionString) || 0;
tiles.set(positionString, (ruleIndex + 1) % params.rule.length);
let ruleValue = params.rule[ruleIndex];
if (params.hexGrid) {
if (ruleValue === "L") ant.direction = (ant.direction + 5) % 6; // -60°
else if (ruleValue === "R")
ant.direction = (ant.direction + 1) % 6; // +60°
else if (ruleValue === "M")
ant.direction = (ant.direction + 4) % 6; // -120°
else if (ruleValue === "S")
ant.direction = (ant.direction + 2) % 6; // +120°
else if (ruleValue === "U") ant.direction = (ant.direction + 3) % 6; // +180°
} else {
if (ruleValue === "L") ant.direction = (ant.direction + 3) % 4;
else if (ruleValue === "R") ant.direction = (ant.direction + 1) % 4;
else if (ruleValue === "U") ant.direction = (ant.direction + 2) % 4;
}
let d = (params.hexGrid ? HEX_DIRS : SQUARE_DIRS)[ant.direction];
ant.position.x += d.x;
ant.position.y += d.y;
bounds.minX = min(bounds.minX, ant.position.x);
bounds.maxX = max(bounds.maxX, ant.position.x);
bounds.minY = min(bounds.minY, ant.position.y);
bounds.maxY = max(bounds.maxY, ant.position.y);
}
if (params.dieOfOldAge) {
let maxAge = parseInt(params.maxAge);
if (!isNaN(maxAge)) {
for (let j = ants.length - 1; j >= 0; j--) {
if (ants[j].age > maxAge) {
ants.splice(j, 1);
}
}
}
}
}
background("black");
let tilesWide = bounds.maxX - bounds.minX + 1;
let tilesTall = bounds.maxY - bounds.minY + 1;
let centerX = 0;
let centerY = 0;
if (params.centerMode === "bounds") {
centerX = (bounds.minX + bounds.maxX) / 2;
centerY = (bounds.minY + bounds.maxY) / 2;
} else if (params.centerMode === "ants") {
for (let ant of ants) {
centerX += ant.position.x;
centerY += ant.position.y;
}
centerX /= ants.length;
centerY /= ants.length;
} else if (params.centerMode === "tiles") {
for (let [key, value] of tiles.entries()) {
let [x, y] = key.split(",").map(Number);
centerX += x;
centerY += y;
}
centerX /= tiles.size;
centerY /= tiles.size;
} else if (params.centerMode === "mouse") {
// Move towards the direction the mouse is from the center of then screen
// Only when the mouse button is pressed
if (mouseIsPressed) {
let targetX = map(mouseX, 0, width, -tilesWide / 2, tilesWide / 2);
let targetY = map(mouseY, 0, height, -tilesTall / 2, tilesTall / 2);
mouseCenterX += (targetX - mouseCenterX) * 0.1;
mouseCenterY += (targetY - mouseCenterY) * 0.1;
}
// Right mouse button resets to center
if (mouseIsPressed && mouseButton === RIGHT) {
mouseCenterX = 0;
mouseCenterY = 0;
}
centerX = mouseCenterX;
centerY = mouseCenterY;
}
let cellSize = min(width / tilesWide, height / tilesTall);
if (cellSize < 1) {
cellSize = 1;
if (params.pauseMode == "one-at-edge") {
// If any ant is more than width / 2 from centerX or height / 2 from centerY, pause
for (let ant of ants) {
if (
abs(ant.position.x - centerX) > width / (2 * cellSize) ||
abs(ant.position.y - centerY) > height / (2 * cellSize)
) {
paused = true;
break;
}
}
} else if (params.pauseMode == "one-at-edge") {
// If all ants are more than width / 2 from centerX or height / 2 from centerY, pause
let allAtEdge = true;
for (let ant of ants) {
if (
abs(ant.position.x - centerX) <= width / (2 * cellSize) &&
abs(ant.position.y - centerY) <= height / (2 * cellSize)
) {
allAtEdge = false;
break;
}
}
if (allAtEdge) {
paused = true;
}
}
}
if (ants.length == 0) {
paused = true;
}
if (params.pauseMode == "n-tiles" && tiles.size > width * height) {
paused = true;
}
push();
translate(centerX * -cellSize + width / 2, centerY * -cellSize + height / 2);
noStroke();
for (let [key, value] of tiles.entries()) {
let [x, y] = key.split(",").map(Number);
if (params.colorScheme === "monochrome") {
fill(0, 0, map(value, 0, params.rule.length, 100, 50));
} else if (params.colorScheme === "pastel") {
let hue = map(value, 0, params.rule.length, 0, 360);
fill(hue, 30, 100);
} else if (params.colorScheme === "fire") {
let hue = map(value, 0, params.rule.length, 30, 60);
fill(hue, 100, 100);
} else if (params.colorScheme === "ocean") {
let hue = map(value, 0, params.rule.length, 180, 240);
fill(hue, 100, 100);
} else if (params.colorScheme === "forest") {
let hue = map(value, 0, params.rule.length, 90, 150);
fill(hue, 100, 50);
} else if (params.colorScheme === "rainbow") {
let hue = map(value, 0, params.rule.length, 0, 360);
fill(hue, 100, 100);
}
rect(x * cellSize, y * cellSize, cellSize, cellSize);
}
fill(0);
for (let ant of ants) {
rect(
ant.position.x * cellSize,
ant.position.y * cellSize,
cellSize,
cellSize
);
}
pop();
}