“To play life you must have a fairly large checkerboard and a plentiful supply of flat counters of two colors. It is possible to work with pencil and graph paper but it is much easier, particularly for beginners, to use counters and a board.” —Martin Gardner, Scientific American (October 1970)
-
In Chapter 5, I defined a complex system as a network of elements with short-range relationships, operating in parallel, that exhibit emergent behavior. I illustrated this definition by creating a flocking simulation and demonstrated how a complex system ads up to more than the sum of its parts. In this chapter, I’m going to turn to developing other complex systems known as cellular automata.
-
In some respects, this shift may seem like a step backward. No longer will the individual elements of my systems be members of a physics world, driven by forces and vectors to move around the canvas. Instead, I’ll build systems out of the simplest digital element possible: a single bit. This bit is called a cell, and its value (0 or 1) is called its state. Working with such simple elements will help illustrate the details behind how complex systems work, and it will offer an opportunity to elaborate on some programming techniques that apply to code-based projects. Building cellular automata will also set the stage for the rest of the book, where I’ll increasingly focus on systems an algorithms rather than vectors an motion—albeit systems an algorithms that I can and will apply to moving bodies.
+
In Chapter 5, I defined a complex system as a network of elements with short-range relationships, operating in parallel, that exhibit emergent behavior. I illustrated this definition by creating a flocking simulation and demonstrated how a complex system adds up to more than the sum of its parts. In this chapter, I’m going to turn to developing other complex systems known as cellular automata.
+
In some respects, this shift may seem like a step backward. No longer will the individual elements of my systems be members of a physics world, driven by forces and vectors to move around the canvas. Instead, I’ll build systems out of the simplest digital element possible: a single bit. This bit is called a cell, and its value (0 or 1) is called its state. Working with such simple elements will help illustrate the details behind how complex systems operate, and it will offer an opportunity to elaborate on some programming techniques that apply to code-based projects. Building cellular automata will also set the stage for the rest of the book, where I’ll increasingly focus on systems and algorithms rather than vectors and motion—albeit systems and algorithms that I can and will apply to moving bodies.
What Is a Cellular Automaton?
A cellular automaton (cellular automata plural, or CA for short) is a model of a system of “cell” objects with the following characteristics:
@@ -58,7 +58,7 @@
Elementary Cellular Automata
Figure 7.7: Counting with 3 bits in binary, or the eight possible configurations of a three-cell neighborhood
-
Once all the possible neighborhood configurations are defined, an outcome (new state value: 0 or 1) is specified for each configuration. In Wolfram's original notation and other common references, these configurations are written in descending order, Figure 7.8 follows this convention, starting with 111 and counting down to 000.
+
Once all the possible neighborhood configurations are defined, an outcome (new state value: 0 or 1) is specified for each configuration. In Wolfram's original notation and other common references, these configurations are written in descending order. Figure 7.8 follows this convention, starting with 111 and counting down to 000.
-
Figure 7.16 is identified by Wolfram as “Rule 90.” Where does 90 come from? Each ruleset is essentially an 8-bit number. How many combinations of eight 0s and 1s are there? Exactly 2^8, or 256. You might remember this from when you first learned about RGB color in p5.js. When you write background(r, g, b), each color component (red, green, and blue) is represented by an 8-bit number ranging from 0 to 255 in decimal, or 00000000 to 11111111 in binary.
-
To make ruleset naming even more concise, Wolfram uses decimal (or “base 10”) representations. To name a rule, you convert its 8-bit binary number to its decimal counterpart. The binary number “01011010” translates to the decimal number 90, and therefore it’s named “Rule 90.”
-
Since there are 256 possible combinations of eight 0s and 1s, there are also 256 unique rulesets. Let’s check out another one. How about rule 11011110, or more commonly, rule 222. Figure 7.18 shows how it looks.
+
I’ve said that each ruleset can essentially be boiled down to an 8-bit number, and how many combinations of eight 0s and 1s are there? Exactly 2^8, or 256. You might remember this from when you first learned about RGB color in p5.js. When you write background(r, g, b), each color component (red, green, and blue) is represented by an 8-bit number ranging from 0 to 255 in decimal, or 00000000 to 11111111 in binary.
+
The ruleset in Figure 7.16 could be called “Rule 01011010,” but Wolfram instead refers to it as “Rule 90.” Where does 90 come from? To make ruleset naming even more concise, Wolfram uses decimal (or “base 10”) representations rather than binary. To name a rule, you convert its 8-bit binary number to its decimal counterpart. The binary number 01011010 translates to the decimal number 90, and therefore it’s named “Rule 90.”
+
Since there are 256 possible combinations of eight 0s and 1s, there are also 256 unique rulesets. Let’s check out another one. How about rule 11011110, or more commonly, rule 222. Figure 7.17 shows how it looks.
-
The result is a recognizable shape, though it certainly isn’t as exciting as the Sierpiński triangle. As I saidd earlier, most of the 256 elementary rulesets dodn’t produce compelling outcomes. However, it’s still quite incredible that even just a few of these rulesets—simple systems of cells with only two possible states—can produce fascinating patterns seen every day in nature (see Figure 7.18, a snail shell resembling Wolfram’s rule 30). This demonstrates how valuable CAs can be in simulation and pattern generation.
+
The result is a recognizable shape, though it certainly isn’t as exciting as the Sierpiński triangle. As I said earlier, most of the 256 elementary rulesets don’t produce compelling outcomes. However, it’s still quite incredible that even just a few of these rulesets—simple systems of cells with only two possible states—can produce fascinating patterns seen every day in nature. For example, see Figure 7.18, a snail shell resembling Wolfram’s rule 30. This demonstrates how valuable CAs can be in simulation and pattern generation.
Before I go too far down the road of characterizing the results of different rulesets, though, let’s look at how to build a p5.js sketch that generates and visualizes a Wolfram elementary CA.
Programming an Elementary CA
You may be thinking: “OK, I’ve got this cell thing. And the cell thing has some properties, like a state, what generation it’s on, who its neighbors are, and where it lives pixel-wise on the screen. And maybe it has some functions, like to display itself and determine its new state.” This line of thinking is an excellent one and would likely lead you to write some code like this:
@@ -167,7 +167,7 @@
Programming an Elementary CA
cells[i] = newstate;
}
I’m fairly close to getting this right, but there are a few issues to resolve. For one, I’m farming out the calculation of a new state value to some function called rules(). Obviously, I’m going to have to write this function, so my work isn’t done, but what I’m aiming for here is modularity. I want a for loop that provides a basic framework for managing any CA, regardless of the specific ruleset. If I want to try different rulesets, I shouldn’t have to touch that framework at all; I can just rewrite the rules() function to compute the new states differently.
-
So I still have the rules() function to write, but more important, I’ve made one minor blunder and one major blunder in the for loop itself. Let’s examine the code more closely.
+
So I still have the rules() function to write, but more important, I’ve made one minor blunder and one major blunder in the for loop itself. Let’s examine the code more closely.
First, notice how easy it is to look at a cell’s neighbors. Because an array is an ordered list of data, I can use the fact that the indices are numbered to know which cells are next to which cells. I know that cell number 15, for example, has cell 14 to its left and 16 to its right. More generally, I can say that for any cell i, its neighbors are i - 1 and i + 1.
In fact, it’s not quite that easy. What have I done wrong? Think about how the code will execute. The first time through the loop, cell index i equals 0. The code wants to look at cell 0’s neighbors. Left is i - 1 or -1. Oops! An array by definition doesn’t have an element with an index of -1. It starts with 0.
I alluded to this problem of edge cases earlier in the chapter and said I could worry about it later. Well, later is now. How should I handle the cell on the edge that doesn’t have a neighbor to both its left and its right? Here are three possible solutions to this problem:
@@ -221,7 +221,7 @@
Programming an Elementary CA
I can store this ruleset in an array.
let ruleset = [0, 1, 0, 1, 1, 0, 1, 0];
-
And then say, for example:
+
And then I can say, for example:
if (a == 1 && b == 1 && c == 1) return ruleset[0];
If left, middle, and right all have the state 1, that matches the configuration 111, so the new state should be equal to the first value in the ruleset array. Duplicating this strategy for all eight possibilities looks like this:
function rules (a, b, c) {
@@ -234,7 +234,7 @@
Programming an Elementary CA
else if (a === 0 && b === 0 && c === 1) return ruleset[6];
else if (a === 0 && b === 0 && c === 0) return ruleset[7];
}
-
I like writing the rules() function this way because it describes line by line exactly what’s happening for each neighborhood configuration. However, it’s not a great solution. After all, what if a CA has four possible states (0 through 3) instead of two? Suddenly there are 64 possible neighborhood configurations. And with 10 possible states, 1,000 configurations. And just imagine programming von Neumann’s 29 possible states. I’d be stuck typing out thousands of else if statements!
+
I like writing the rules() function this way because it describes line by line exactly what’s happening for each neighborhood configuration. However, it’s not a great solution. After all, what if a CA has four possible states (0 through 3) instead of two? Suddenly there are 64 possible neighborhood configurations. And with 10 possible states, 1,000 configurations. And just imagine programming von Neumann’s 29 possible states. I’d be stuck typing out thousands upon thousands of else if statements!
Another solution, though not quite as transparent, is to convert the neighborhood configuration (a 3-bit number) into a regular integer and use that value as the index into the ruleset array. This can be done as follows, using JavaScript’s built-in parseInt() function:
function rules (a, b, c) {
// A quick way to concatenate three numbers into a string
@@ -369,7 +369,7 @@
Example 7.1: Wolfram El
let index = parseInt(s, 2);
return ruleset[7 - index];
}
-
You may have noticed an optimization I made in this example by simplifying the drawing. I included a white background and rendered only the black squares, which saves the work of drawing many squares. This solution isn’t suitable for all cases (what if I want multi-colored cells!), but it provides a performance boost in this simple case. I’ll also note that if the size of each cell were 1 pixel I would not want to use p5.js’s square() function, but rather access the pixel array directly.
+
You may have noticed an optimization I made in this example by simplifying the drawing: I included a white background and rendered only the black squares, which saves the work of drawing many squares. This solution isn’t suitable for all cases (what if I want multicolored cells!), but it provides a performance boost in this simple case. I’ll also note that if the size of each cell were 1 pixel, I wouldn’t want to use p5.js’s square() function, but rather access the pixel array directly.
Exercise 7.1
Expand Example 7.1 to have the following feature: when the CA reaches the bottom of the canvas, the CA starts over with a new, random ruleset.
@@ -428,7 +428,7 @@
The Rules of the Game
Figure 7.26: A two-dimensional CA showing the neighborhood of 9 cells.
-
With three cells, a 3-bit number had eight possible configurations. With nine cells, there are 9 bits, or 512 possible neighborhoods. In most cases, it would be impractical to define an outcome for every single possibility. The Game of Life gets around this problem by defining a set of rules according to general characteristics of the neighborhood: is the neighborhood overpopulated with life, surrounded by death, or just right? Here are the rules of life.
+
With three cells, a 3-bit number had eight possible configurations. With nine cells, there are 9 bits, or 512 possible neighborhoods. In most cases, it would be impractical to define an outcome for every single possibility. The Game of Life gets around this problem by defining a set of rules according to general characteristics of the neighborhood: is the neighborhood overpopulated with life, surrounded by death, or just right? Here are the rules of life:
Death. If a cell is alive (state = 1), it will die (state becomes 0) under the following circumstances:
@@ -450,7 +450,7 @@
The Rules of the Game
Figure 7.27: Example scenarios for “death” and “birth” in the Game of Life
With the elementary CA, I visualized many generations at once, stacked as rows in a 2D grid. With the Game of Life, however, the CA itself is in two dimensions. I could try to create an elaborate 3D visualization of the results and stack all the generations in a cube structure (and in fact, you might want to try this as an exercise), but a more typical way to visualize the Game of Life is to treat each generation as a single frame in an animation. This way, instead of viewing all the generations at once, you see them one at a time, and the result resembles rapidly developing bacteria in a Petri dish.
-
One of the exciting aspects of the Game of Life is that there are known initial patterns that yield intriguing results. For example, the patterns shown in Figure 7.28 some remain static and never change.
+
One of the exciting aspects of the Game of Life is that there are known initial patterns that yield intriguing results. For example, the patterns shown in Figure 7.28 remain static and never change.