Cellular Automata - Instructions
A visual state simulation for practicing generated behaviour testing, JavaScript console exploration, and browser-based game hacking.
About Page
Cellular Automata is useful for exploring visual state, rules, and generated behaviour over time.
Use it to practice:
- modelling visual state transitions
- changing rule inputs
- observing repeatability
- inspecting JavaScript state
- writing small console bots
- controlling simulation setup and teardown
- deciding what can be asserted in a visual simulation
Overview And Videos
These instructions are based on the Hacking JavaScript Games - Cellular Automata overview, which includes supporting notes and videos.
Supporting videos:
- Change Colour of Cells
- Change Colour Automatically
- Create a Glider Invasion
- Change Size of the World
- Create a Randomly Generated World
- History Of The Game of Life
- Bonus: Making Text With Cellular Automata
Exploratory Testing
Change one variable at a time and record how the visual behaviour changes. If a seed or rule can be repeated, compare repeated runs.
Console Hacking Exercises
The page exposes the simulation objects globally, so the browser console is the main testing tool.
Start by finding these objects and functions:
worldworldSizesaddLifeFormsstartGamestopGamestartInvasionstopInvasionclearCanvasdrawCurrentWorldLifeFormgetRandomInt
Explore the World
The world object has a lot of config and functions, you can just type world into the JavaScript console and you’ll see it all listed.
Since they are there you can call them or change them.
e.g.
world.wrapWorld=false;set the world such that there is an edge, rather than wrapping
You’ll see other config variables and methods used below but, explore what you see and experiment to get the most out of this particular playground.
Change Colour
Change the living-cell colour:
world.entityColour = "#111111";
Set it back to default colour:
world.entityColour = "#FF0000";
Create a colour bot:
var colourChangeBot = setInterval(function() {
world.entityColour = "#" + (("000000" +
getRandomInt(0, 0xFFFFFF).toString(16)).substr(-6));
}, 1000);
Stop it with:
clearInterval(colourChangeBot);
You can make the bot faster or slower by changing the interval. Be careful with very fast colour changes because they can create flashing visuals.
Control The Invasion
Stop the current invasion:
stopInvasion();
Start a new invasion every 100 milliseconds:
startInvasion(100);
Add a single glider:
addLifeForms.glider(world, 10, 0);
Create a random glider invasion:
var gliderInvasion = setInterval(function() {
addLifeForms.glider(world,
getRandomInt(0, world.xSize - 50),
getRandomInt(0, world.ySize - 50));
}, 100);
Stop it with:
clearInterval(gliderInvasion);
Change World Size
The rendered size and the simulation size are related but separate. Changing only worldSizes changes scale calculations, but the world also needs to know its new dimensions.
Try a small black-and-white world:
worldSizes.myWorldWidth = 64;
worldSizes.myWorldHeight = 48;
world.xSize = worldSizes.myWorldWidth;
world.ySize = worldSizes.myWorldHeight;
world.entityColour = "#000000";
worldSizes.calculateScales();
stopInvasion();
stopGame();
startGame(1000);
startInvasion(3000);
Then try a larger world and watch the performance impact:
worldSizes.myWorldWidth = 1000;
worldSizes.myWorldHeight = 1000;
world.xSize = 1000;
world.ySize = 1000;
worldSizes.calculateScales();
Change Edge Wrapping
The world wraps at the edges by default, so life forms that interact across one edge can continue from the opposite edge.
Turn wrapping off:
world.wrapWorld = false;
Turn wrapping back on:
world.wrapWorld = true;
Try placing a pattern near an edge with wrapping on and off, then compare how the population changes.
Create A Random World
Take control of the running simulation:
stopGame();
stopInvasion();
world.population = [];
clearCanvas();
Seed random life forms:
for (var itemLoop = 0; itemLoop < 500; itemLoop++) {
world.addToPopulation(new LifeForm().move(0,
getRandomInt(0, world.xSize),
getRandomInt(0, world.ySize)));
}
See the starting position:
drawCurrentWorld();
Start the simulation:
startGame(5);
Create Text As Life
One way to create custom patterns is to draw text on the canvas, read back the pixels, and turn those pixels into life forms.
stopInvasion();
stopGame();
world.population = [];
clearCanvas();
context.font = "14px verdana";
context.strokeStyle = "#FF0000";
var padding = String.fromCharCode(8202) + String.fromCharCode(8202);
context.strokeText("TEST".split("").join(padding), 0, 15);
context.strokeText("PAGES".split("").join(padding), 0, 30);
Convert the visible pixels to blocks:
var imageData = context.getImageData(0, 0, 200, 60);
var scale = 4;
var imageX = 0;
var imageY = 0;
for (var i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i] !== 0 ||
imageData.data[i + 1] !== 0 ||
imageData.data[i + 2] !== 0) {
addLifeForms.block(world, imageX * scale, imageY * scale);
}
imageX++;
if (imageX >= 200) {
imageX = 0;
imageY++;
}
}
startGame(5);
Hey, did you notice you can add #interactive on the url? What does that do? Does that change your test approach now?
Other Activities
The Cellular Automata is a blank canvas, literally clearCanvas() so what you do with it is really only limited by your imagination.
AI can easily help you write the code to build patterns and explore cellular automata, or look around the web for algorithms to apply.
Life-like Rules
The default simulation behaves like Conway’s Game of Life. If world.rulestring is empty, the page uses the hard-coded Conway rules in the JavaScript:
- a live cell survives with 2 or 3 neighbours
- a dead cell is born with exactly 3 neighbours
Control the Automata Rules
The page can also use Life-like rulestring notation, commonly used by tools such as Golly and described by the LifeWiki Life-like cellular automata guide.
The supported format is:
B<birth neighbour counts>/S<survival neighbour counts>
For example the default Conway Life rules are represented as follows:
B3/S23
This means:
B3- a dead cell becomes alive when it has 3 live neighboursS23- a live cell survives when it has 2 or 3 live neighbours
Try these examples:
| Name | Rulestring | Notes |
|---|---|---|
| Conway Life | B3/S23 |
The default behaviour |
| HighLife | B36/S23 |
Similar to Life, with extra births on 6 neighbours |
| Seeds | B2/S |
Cells are born with 2 neighbours and never survive |
| Life Without Death | B3/S012345678 |
Live cells never die |
| Day & Night | B3678/S34678 |
Dense, symmetric-looking behaviour |
| Maze | B3/S12345 |
Maze-like growth |
| Coral | B3/S45678 |
Coral-like edges |
| Replicator | B1357/S1357 |
Good for exploring repeating structures |
| 34 Life | B34/S34 |
A small variation from the default |
Change the rule set from the console:
world.rulestring = "B36/S23";
Reset to the hard-coded default rules:
world.rulestring = "";
To compare rules, stop the running simulation, seed the same starting position, draw it, then start the simulation again.
stopGame();
stopInvasion();
world.population = [];
clearCanvas();
addLifeForms.glider(world, 10, 10);
drawCurrentWorld();
world.rulestring = "B36/S23";
startGame(100);
Colour Based on Age
If world.entityColour is an array, the world uses the cell age to choose a colour. Age 0 uses the first colour, age 1 uses the second colour, and older cells keep using the final colour in the list.
world.entityColour = [
"#fff7bc",
"#fec44f",
"#fe9929",
"#d95f0e",
"#993404"
];
Colour Cycle Bot
You can rotate that palette over time to make the age bands move.
Rotate the colours every 250 milliseconds:
var colourCycleBot = setInterval(function() {
var firstColour = world.entityColour.shift();
world.entityColour.push(firstColour);
}, 250);
Stop the bot:
clearInterval(colourCycleBot);
Rotate in the opposite direction:
var reverseColourCycleBot = setInterval(function() {
var lastColour = world.entityColour.pop();
world.entityColour.unshift(lastColour);
}, 250);
Stop the reverse bot:
clearInterval(reverseColourCycleBot);
Seeding The World With Fractals
We can see the world Algorithmically and fractals are a good source of algorithmically generated images.
Sierpinski Triangle
A Simple Algorithm
This clears the world and draws a simple Sierpinski triangle from the 0,0 point.
Stop the simulation and invasion, empty the population, and clear the canvas.
stopGame();
stopInvasion();
world.population = [];
clearCanvas();
Choose the largest power-of-two square that fits the world.
var triangleSize = 1;
while (triangleSize * 2 <= Math.min(world.xSize, world.ySize)) {
triangleSize *= 2;
}
Seed a Sierpiński triangle, then see what it looks like.
for (var y = 0; y < triangleSize; y++) {
for (var x = 0; x < triangleSize; x++) {
if ((x & y) === 0) {
world.addToPopulation(new LifeForm().move(0, x, y));
}
}
}
drawCurrentWorld();
Now start the generation to refresh every 5 milliseconds.
startGame(5);
A 'normal' vertical Sierpiński triangle
stopGame();
stopInvasion();
world.population = [];
clearCanvas();
const borderX = Math.ceil(world.xSize * 0.1);
const borderY = Math.ceil(world.ySize * 0.1);
const usableWidth = world.xSize - borderX * 2;
const usableHeight = world.ySize - borderY * 2;
const rows = Math.min(
usableHeight,
Math.floor((usableWidth + 1) / 2)
);
if (rows < 2) {
throw new Error("The world is too small.");
}
const triangleWidth = rows * 2 - 1;
const startX = Math.floor((world.xSize - triangleWidth) / 2);
const startY = Math.floor((world.ySize - rows) / 2);
for (let row = 0; row < rows; row++) {
for (let column = 0; column <= row; column++) {
if ((column & (row - column)) === 0) {
const x = startX + rows - 1 - row + column * 2;
const y = startY + row;
world.addToPopulation(
new LifeForm().move(0, x, y)
);
}
}
}
drawCurrentWorld();
startGame(5);
A centred Sierpiński triangle
If the screen is not a power of two size then we really want to force the algorithm to do that for us.
stopGame();
stopInvasion();
world.population = [];
clearCanvas();
const borderX = Math.ceil(world.xSize * 0.1);
const borderY = Math.ceil(world.ySize * 0.1);
const usableWidth = world.xSize - borderX * 2;
const usableHeight = world.ySize - borderY * 2;
const maxRows = Math.min(
usableHeight,
Math.floor((usableWidth + 1) / 2)
);
if (maxRows < 2) {
throw new Error("The world is too small.");
}
const rows = 2 ** Math.floor(Math.log2(maxRows));
const triangleWidth = rows * 2 - 1;
const startX = Math.floor((world.xSize - triangleWidth) / 2);
const startY = Math.floor((world.ySize - rows) / 2);
for (let row = 0; row < rows; row++) {
for (let column = 0; column <= row; column++) {
if ((column & (row - column)) === 0) {
const x = startX + rows - 1 - row + column * 2;
const y = startY + row;
world.addToPopulation(new LifeForm().move(0, x, y));
}
}
}
drawCurrentWorld();
startGame(5);
I often paste the startGame(5) separately because I want to see the start state revealed by drawCurrentWorld() first.