mirror of
https://github.com/nature-of-code/noc-book-2
synced 2024-11-17 07:49:05 +01:00
716 lines
No EOL
64 KiB
HTML
716 lines
No EOL
64 KiB
HTML
<section data-type="chapter">
|
||
<h1 id="chapter-7-cellular-automata">Chapter 7. Cellular Automata</h1>
|
||
<blockquote data-type="epigraph">
|
||
<p>“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.”</p>
|
||
</blockquote>
|
||
<p>In this chapter, I’m going to take a break from talking about vectors and motion. In fact, the rest of the book will mostly focus on systems and algorithms (albeit ones that I can and will apply to moving bodies). In the chapter 5, the first p5.js example of a complex system was introduced: flocking. I briefly stated the core principles behind complex systems: more than the sum of its parts, a complex system is a system of elements, operating in parallel, with short-range relationships that as a whole exhibit emergent behavior. This entire chapter is going to be dedicated to building another complex system simulation. I am, however, going to take some steps backward and simplify the elements of the system. No longer will the individual elements be members of a physics world; instead I will build a system out of the simplest digital element possible, a single bit. This bit is called a cell and its value (0 or 1) will be called its state. Working with such simple elements will help you to understand more of the details behind how complex systems work, and will offer an opportunity to elaborate on some programming techniques that apply to code-based projects.</p>
|
||
<h2 id="71-what-is-a-cellular-automaton">7.1 What Is a Cellular Automaton?</h2>
|
||
<p>First, let’s get one thing straight. The term <strong><em>cellular automata</em></strong> is plural. The code examples here will simulate just one—a <strong><em>cellular automaton</em></strong>, singular. To keep things short and sweet, I’ll refer to cellular automata as “CA.”</p>
|
||
<p>In Chapters 1 through 6, the objects (mover, particle, vehicle, boid) generally existed in only one “state.” They might have moved around with advanced behaviors and physics, but ultimately they remained the same type of object over the course of their digital lifetime. I’ve alluded to the possibility that these entities can change over time (for example, the weights of steering “desires” can vary), but I haven’t fully put this into practice. In this context, cellular automata make a great first step in building a system of many objects that have varying states over time.</p>
|
||
<p>A cellular automaton is a model of a system of “cell” objects with the following characteristics.</p>
|
||
<ul>
|
||
<li>The cells live on a <strong><em>grid</em></strong>. (I’ll include examples in both one and two dimensions in this chapter, though a cellular automaton can exist in any finite number of dimensions.)</li>
|
||
<li>Each cell has a <strong><em>state</em></strong>. The number of state possibilities is typically finite. The simplest example has the two possibilities of 1 and 0 (otherwise referred to as “on” and “off” or “alive” and “dead”).</li>
|
||
<li>Each cell has a <strong><em>neighborhood</em></strong>. This can be defined in any number of ways, but it is typically a list of adjacent cells.</li>
|
||
</ul>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_1.png" alt="Figure 7.1: A 2D grid of cells each with a state of “on” and “off”. A neighborhood is a subsection of the large grid. These examples will focus on how the algorithm will act on the circled cell in combination with its neighbors.">
|
||
<figcaption>Figure 7.1: A 2D grid of cells each with a state of “on” and “off”. A neighborhood is a subsection of the large grid. These examples will focus on how the algorithm will act on the circled cell in combination with its neighbors.</figcaption>
|
||
</figure>
|
||
<p>The development of cellular automata systems is typically attributed to Stanisław Ulam and John von Neumann, who were both researchers at the Los Alamos National Laboratory in New Mexico in the 1940s. Ulam was studying the growth of crystals and von Neumann was imagining a world of self-replicating robots. That’s right, robots that build copies of themselves. Once you see some examples of CA visualized, it’ll be clear how one might imagine modeling crystal growth; the robots idea is perhaps less obvious. Consider the design of a robot as a pattern on a grid of cells (think of filling in some squares on a piece of graph paper). Now consider a set of simple rules that would allow that pattern to create copies of itself on that grid. This is essentially the process of a CA that exhibits behavior similar to biological reproduction and evolution. (Incidentally, von Neumann’s cells had twenty-nine possible states.) Von Neumann’s work in self-replication and CA is conceptually similar to what is probably the most famous cellular automaton: the “Game of Life,” which I will discuss in detail in section 7.6.</p>
|
||
<p>Perhaps the most significant scientific (and lengthy) work studying cellular automata arrived in 2002: Stephen Wolfram’s 1,280-page <em>A New Kind of Science</em>. Available in its entirety for free online, Wolfram’s book discusses how CA are not simply neat tricks, but are relevant to the study of biology, chemistry, physics, and all branches of science. This chapter will barely scratch the surface of the theories Wolfram outlines (we will focus on the code implementation) so if the examples provided spark your curiosity, you’ll find plenty more to read about in his book.</p>
|
||
<h2 id="72-elementary-cellular-automata">7.2 Elementary Cellular Automata</h2>
|
||
<p>The examples in this chapter will begin with a simulation of Wolfram’s work. To understand Wolfram’s elementary CA, it’s best to begin with the question: “What is the simplest cellular automaton you can imagine?” What’s exciting about this question and its answer is that even with the simplest CA imaginable, you will see the properties of complex systems at work.</p>
|
||
<p>Let’s build Wolfram’s elementary CA from scratch. Concepts first, then code. I’ll begin with the three key elements of a CA.</p>
|
||
<p>1) <strong><em>Grid</em></strong>. The simplest grid would be one-dimensional: a line of cells.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_2.png" alt="Figure 7.2: A one-dimensional line of cells">
|
||
<figcaption>Figure 7.2: A one-dimensional line of cells</figcaption>
|
||
</figure>
|
||
<p>2) <strong><em>States</em></strong>. The simplest set of states (beyond having only one state) would be two states: 0 or 1.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_3.png" alt="Figure 7.3: A one-dimensional line of cells marked with states 0 or 1.">
|
||
<figcaption>Figure 7.3: A one-dimensional line of cells marked with states 0 or 1.</figcaption>
|
||
</figure>
|
||
<p>3) <strong><em>Neighborhood</em></strong>. The simplest neighborhood in one dimension for any given cell would be the cell itself and its two adjacent neighbors: one to the left and one to the right.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_4.png" alt="Figure 7.4: A neighborhood in one dimension is three cells. ">
|
||
<figcaption>Figure 7.4: A neighborhood in one dimension is three cells. </figcaption>
|
||
</figure>
|
||
<p>Let’s begin with a line of cells, each with an initial state (let’s say set at random), and each with two neighbors. I’ll have to decide out what I want to do with the cells on the edges (since those have only one neighbor each), but this is something to be sorted out later.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_5.png" alt="Figure 7.5: The edge cell only has a neighborhood of two. ">
|
||
<figcaption>Figure 7.5: The edge cell only has a neighborhood of two. </figcaption>
|
||
</figure>
|
||
<p>I haven’t yet discussed, however, what is perhaps the most important detail of how cellular automata work—<em>time</em>. I’m not really talking about real-world time here, but rather about the CA living over a period of <em>time</em>, which could also be called a <strong><em>generation</em></strong> and, in this case, will likely refer to the <strong><em>frame count</em></strong> of an animation. The figures above show the CA at time equals 0 (or generation 0). The questions to ask are: <em>How do you compute the states for all cells at generation 1? And generation 2?</em> And so on and so forth.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_6.png" alt="Figure 7.6: The states for generation 1 are computed based on generation 0.">
|
||
<figcaption>Figure 7.6: The states for generation 1 are computed based on generation 0.</figcaption>
|
||
</figure>
|
||
<p>Let’s say there is an individual cell in the CA called <span data-type="equation">\text{CELL}</span>. The formula for calculating the <span data-type="equation">\text{CELL state}</span> at any given time <span data-type="equation">t</span> is as follows:</p>
|
||
<div data-type="equation">\text{CELL state } \text{at~time } t = f(\text{CELL~neighborhood~at~time~} t-1)</div>
|
||
<p>In other words, a cell’s new state is a function of all the states in the cell’s neighborhood at the previous generation. A new state value is calculated by looking at the previous generation’s neighbor states.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_7.png" alt="Figure 7.7 The state of a cell at generation 1 is a function of the previous generation’s neighborhood.">
|
||
<figcaption>Figure 7.7 The state of a cell at generation 1 is a function of the previous generation’s neighborhood.</figcaption>
|
||
</figure>
|
||
<p>Now, in the world of cellular automata, there are many ways to compute a cell’s state from a neighborhood of cells. Consider blurring an image. (Guess what? Image processing works with CA-like rules!) A pixel’s new state (i.e. its color) is the average of all of its neighbors’ colors. A cell’s new state could also be the sum of all of its neighbors’ states. With Wolfram’s elementary CA, however, the process is something a bit simpler and seemingly absurd: the algorithm looks at all the possible configurations of a cell and its neighbors and define the state outcome for every possible configuration. It seems ridiculous—wouldn’t there be way too many possibilities for this to be practical? Let’s give it a try.</p>
|
||
<p>There are three cells, each with a state of 0 or 1. How many possible ways can the states be configured? If you love binary, you’ll notice that three cells define a 3-bit number, and how high can you count with 3 bits? Up to 8. Let’s have a look.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_8.png" alt="Figure 7.8: Counting up to 8 with 3 bits in binary.">
|
||
<figcaption>Figure 7.8: Counting up to 8 with 3 bits in binary.</figcaption>
|
||
</figure>
|
||
<p>Once all the possible neighborhood configurations are defined, an outcome (new state value: 0 or 1) is specified for each configuration.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_9.png" alt="Figure 7.9: The outcome for each 3 bit configuration of cells.">
|
||
<figcaption>Figure 7.9: The outcome for each 3 bit configuration of cells.</figcaption>
|
||
</figure>
|
||
<p>The standard Wolfram model is to start generation 0 with all cells having a state of 0 except for the middle cell, which should have a state of 1.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_10.png" alt="Figure 7.10: Generation 0 in a Wolfram CA">
|
||
<figcaption>Figure 7.10: Generation 0 in a Wolfram CA</figcaption>
|
||
</figure>
|
||
<p>Referring to the ruleset above, let’s see how a given cell (I’ll pick the center one) would change from generation 0 to generation 1.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_11.png" alt="Figure 7.11: Demonstration how the state for generation 1 is set by the CA rule set.">
|
||
<figcaption>Figure 7.11: Demonstration how the state for generation 1 is set by the CA rule set.</figcaption>
|
||
</figure>
|
||
<p>Try applying the same logic to all of the cells above and fill in the empty cells.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_12.png" alt="Figure 7.x: 0 translates to a white cell, 1 to a black cell.">
|
||
<figcaption>Figure 7.x: 0 translates to a white cell, 1 to a black cell.</figcaption>
|
||
</figure>
|
||
<p><strong><em>New paragraph to explain how math becomes visual, how the states translate to color.</em></strong></p>
|
||
<p>Now, let’s go past just one generation and color the cells —0 means white, 1 means black—and stack the generations, with each new generation appearing below the previous one.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_13.png" alt="Figure 7.12: Wolfram Elementary CA: Rule 90 ">
|
||
<figcaption>Figure 7.12: Wolfram Elementary CA: Rule 90 </figcaption>
|
||
</figure>
|
||
<p>The low-resolution shape depicted above is the “Sierpiński triangle.” Named after the Polish mathematician Wacław Sierpiński, it’s a fractal pattern that I’ll examine more closely in the next chapter. That’s right: this incredibly simple system of 0s and 1s, with little neighborhoods of three cells, can generate a shape as sophisticated and detailed as the Sierpiński triangle. Let’s look at it again, only with each cell a single pixel wide so that the resolution is much higher.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_14.png" alt="Figure 7.12: Wolfram Elementary CA: Rule 90 at higher resolution">
|
||
<figcaption>Figure 7.12: Wolfram Elementary CA: Rule 90 at higher resolution</figcaption>
|
||
</figure>
|
||
<p>This particular result didn’t happen by accident. I picked this set of rules because of the pattern it generates. Take a look at Figure 7.8 one more time. Notice how there are eight possible neighborhood configurations. A “ruleset” is defined a list of 8 bits.</p>
|
||
<p><strong><em>[”This is how we can visually display these from now on” — New illustration showing from 7.11 010→0 as numbers turned into squares]</em></strong></p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_15.png" alt="">
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
<p>Eight 0s and 1s means an 8-bit number. How many combinations of eight 0s and 1s are there? 256. This is the same way components of an RGB color are defined. There are 8 bits for red, green, and blue, meaning colors values range from 0 to 255 (256 possibilities).</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_16.png" alt="Figure 7.14: The Wofram CA rule set “90”">
|
||
<figcaption>Figure 7.14: The Wofram CA rule set “90”</figcaption>
|
||
</figure>
|
||
<p>For a Wolfram elementary CA, there are 256 possible rulesets. The above ruleset is commonly referred to as “Rule 90” because if you convert the binary sequence—01011010—to a decimal number, you’ll get the integer 90. Let’s try looking at the results of another ruleset.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_17.png" alt="Figure 7.15: Wolfram CA: Rule 222 ">
|
||
<figcaption>Figure 7.15: Wolfram CA: Rule 222 </figcaption>
|
||
</figure>
|
||
<figure class="half-width-right">
|
||
<img src="images/07_ca/07_ca_18.jpg" alt="Figure 7.16: A Textile Cone Snail (Conus textile), Cod Hole, Great Barrier Reef, Australia, 7 August 2005. Photographer: Richard Ling richard@research.canon.com.au ">
|
||
<figcaption>Figure 7.16: A Textile Cone Snail (Conus textile), Cod Hole, Great Barrier Reef, Australia, 7 August 2005. Photographer: Richard Ling richard@research.canon.com.au </figcaption>
|
||
</figure>
|
||
<p>As you can now see, the simple act of creating a CA and defining a ruleset does not guarantee visually interesting results. Out of all 256 rulesets, only a handful produce compelling outcomes. However, it’s quite incredible that even one of these rulesets for a one-dimensional CA with only two possible states can produce the patterns seen every day in nature (see Figure 7.16), and it demonstrates how valuable these systems can be in simulation and pattern generation.</p>
|
||
<p>Before I go too far down the road of how Wolfram classifies the results of varying rulesets, let’s look at how to build a p5.js sketch that generates the Wolfram CA and visualizes it onscreen.</p>
|
||
<h2 id="73-how-to-program-an-elementary-ca">7.3 How to Program an Elementary CA</h2>
|
||
<p>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, where it lives pixel-wise on the screen. And maybe it has some functions: it can display itself, it can generate its new state, etc.” This line of thinking is an excellent one and would likely lead you to write some code like this:</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Cell {
|
||
|
||
}</pre>
|
||
<p>This line of thinking, however, is not the road I will first travel. Later in this chapter, I will discuss why an object-oriented approach could prove valuable in developing a CA simulation, but to begin, it’s easier to work with a more elementary data structure. After all, what is an elementary CA but a list of 0s and 1s? Certainly, a generation of a one-dimensional CA could be described using an array:</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_19.png" alt="Figure 7.17: One generation of a 1D cellular automata">
|
||
<figcaption>Figure 7.17: One generation of a 1D cellular automata</figcaption>
|
||
</figure>
|
||
<pre class="codesplit" data-code-language="javascript">let cells = [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0];</pre>
|
||
<p>To draw that array, I check the element is 0 or a 1 and create a fill color accordingly.</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{!1} Loop through every cell.
|
||
for (let i = 0; i < cells.length; i++) {
|
||
//{!5} Create a fill based on its state (0 or 1).
|
||
if (cells[i] == 0) {
|
||
fill(255);
|
||
} else {
|
||
fill(0);
|
||
}
|
||
stroke(0);
|
||
rect(i * 50, 0, 50, 50);
|
||
}</pre>
|
||
<p>Now that the array describes the cell states of a given generation (which will ultimately be considered the “current” generation), a mechanism to compute the next generation is needed. Let’s think about the pseudocode of what I am hoping to do here.</p>
|
||
<p><strong>For every cell in the array:</strong></p>
|
||
<ul>
|
||
<li><strong><em>Take a look at the neighborhood states: left, middle, right.</em></strong></li>
|
||
<li><strong><em>Look up the new value for the cell state according to some ruleset.</em></strong></li>
|
||
<li><strong><em>Set the cell’s state to that new value.</em></strong></li>
|
||
</ul>
|
||
<p>This may lead you to write some code like this:</p>
|
||
<pre class="codesplit" data-code-language="javascript">// For every cell in the array...
|
||
for (let i = 0; i < cells.length; i++) {
|
||
|
||
//{!3} ...take a look at the neighborhood.
|
||
let left = cells[i - 1];
|
||
let middle = cells[i];
|
||
let right = cells[i + 1];
|
||
|
||
//{!1} Look up the new value according to the rules.
|
||
let newstate = rules(left, middle, right);
|
||
|
||
//{!1} Set the cell’s state to the new value.
|
||
cells[i] = newstate;
|
||
}</pre>
|
||
<p>I’m fairly close to getting this right, but I’ve made one minor blunder and one major blunder in the above code. Let’s examine this code more closely.</p>
|
||
<p>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 <code>i</code>, its neighbors are <code>i - 1</code> and <code>i + 1</code>.</p>
|
||
<p>I’m also farming out the calculation of a new state value to some function called <code>rules()</code>. Obviously, I’m going to have to write this function, but the point I’m making here is modularity. I have a basic framework for the CA in this function, and if I later want to change how the rules operate, I don’t have to touch that framework; I can just rewrite the <code>rules()</code> function to compute the new states differently.</p>
|
||
<p>So what have I done wrong? Let’s talk through how the code will execute. First, look at cell index <code>i</code> when it equals 0. Now let’s look at 0’s neighbors. Left is <code>i - 1</code> or <code>-1</code>. Middle is <code>i</code> or <code>0</code>. And right is <code>i + 1</code> or <code>1</code>. However, the array by definition does not have an element with the index <code>-1</code>. It starts with <code>0!</code> This is a problem I alluded to before: the edge cases.</p>
|
||
<p>How do we deal with the cell on the edge who doesn’t have a neighbor to both its left and its right? Here are three possible solutions to this problem:</p>
|
||
<ol>
|
||
<li><strong><em>Edges remain constant.</em></strong> This is perhaps the simplest solution. Don’t bother to evaluate the edges and always leave their state value constant (0 or 1).</li>
|
||
<li><strong><em>Edges wrap around.</em></strong> Think of the CA as a strip of paper and turn that strip of paper into a ring. The cell on the left edge is a neighbor of the cell on the right and vice versa. This can create the appearance of an infinite grid and is probably the most used solution.</li>
|
||
<li><strong><em>Edges have different neighborhoods and rules.</em></strong> If I wanted to, I could treat the edge cells differently and create rules for cells that have a neighborhood of two instead of three. You may want to do this in some circumstances, but in this case, it’s going to be a lot of extra lines of code for little benefit.</li>
|
||
</ol>
|
||
<p>To make the code easiest to read and understand right now, I’ll go with option #1 and skip the edge cases, leaving the values constant. This can be accomplished by starting the loop one cell later and ending one cell earlier:</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{.bold} A loop that ignores the first and last cell
|
||
for (let i = 1; i < cells.length - 1; i++) {
|
||
let left = cells[i - 1];
|
||
let middle = cells[i];
|
||
let right = cells[i + 1];
|
||
let newstate = rules(left, middle, right);
|
||
cells[i] = newstate;
|
||
}</pre>
|
||
<p>There’s one more problem to fix before this is done. It’s subtle and you won’t get an error; the CA just won’t perform correctly. However, identifying this problem is absolutely fundamental to the techniques behind programming CA simulations. It all lies in this line of code:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> cells[i] = newstate;</pre>
|
||
<p>This seems like a perfectly innocent line. After all, I’ve computed the new state value and I’m assigning the cell its new state. But in the next iteration, there will be a massive bug. Let’s say the new state for cell #5 was just computed. What happens next? The new state value for cell #6 is calculated.</p>
|
||
<p><em>Cell #6, generation 0 = some state, 0 or 1</em></p>
|
||
<p><em>Cell #6, generation 1 = a function of states for </em><strong><em>cell #5</em></strong><em>, cell #6, and cell #7 at *</em><strong><em>generation 0</em></strong><em>*</em></p>
|
||
<p>Notice how the value of cell #5 at generation 0 is needed in order to calculate cell #6’s new state at generation 1? A cell’s new state is a function of the previous neighbor states. Have I savedcell #5’s value at generation 0? Remember, this line of code was just executed for <em>i = 5</em>.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> cells[i] = newstate;</pre>
|
||
<p>Once this happens, cell #5’s state at generation 0 has been erased; cell index 5 is now storing the value for generation 1. I cannot overwrite the values in the array while I am processing the array, because I need those values to calculate the new values! A solution to this problem is to have two arrays, one to store the current generation states and one for the next generation states. To save myself the step of re-initializing an array, I’ll use the JavaScript array function <code>slice()</code> which makes a copy of an array.</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{!1 .bold} Another array to store the states for the next generation.
|
||
let newcells = cells.slice();
|
||
|
||
for (let i = 1; i < cells.length - 1; i++) {
|
||
//{!3} Look at the states from the current array.
|
||
const left = cells[i - 1];
|
||
const middle = cells[i];
|
||
const right = cells[i + 1];
|
||
const newstate = rules(left, middle, right);
|
||
//{!1 .bold} Saving the new state in the new array
|
||
newcells[i] = newstate;
|
||
}</pre>
|
||
<p>Once the entire array of values is processed, the <code>cells</code> variable assigned the new array of states effectively throwing away the previous generation’s values.</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{.bold} The new generation becomes the current generation.
|
||
cells = newcells;</pre>
|
||
<p>I’m almost done. The above code is complete except for the <code>rules()</code> function that computes the new state value based on the neighborhood (left, middle, and right cells). I know that function needs to return an integer (0 or 1) as well as receive three arguments (for the three neighbors).</p>
|
||
<pre class="codesplit" data-code-language="javascript"> //{!1} Function signature: receives 3 ints and returns 1.
|
||
function rules (a, b, c) { return _______ }</pre>
|
||
<p>Now, there are many ways to write this function, but I’d like to start with a long-winded one that will hopefully provide a clear illustration of what is happening.</p>
|
||
<p>Let’s first establish how I will store the ruleset. The ruleset, if you remember from the previous section, is a series of 8 bits (0 or 1) that defines that outcome for every possible neighborhood configuration. Let’s look bring back Figure 7.x to look at more closely adding back the numbers since that’s how I will encode the ruleset into an array.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_20.png" alt="Figure 7.14 A 1D CA ruleset (repeated) ">
|
||
<figcaption>Figure 7.14 A 1D CA ruleset (repeated) </figcaption>
|
||
</figure>
|
||
<p>I can store this ruleset in an array.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let ruleset = [0, 1, 0, 1, 1, 0, 1, 0];</pre>
|
||
<p>And then say:</p>
|
||
<pre class="codesplit" data-code-language="javascript">if (a == 1 && b == 1 && c == 1) return ruleset[0];</pre>
|
||
<p>If left, middle, and right all have the state 1, that matches the configuration 111 and the new state should be equal to the first value in the ruleset array. Duplicating this strategy for all eight possibilities looks like:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> function rules (a, b, c) {
|
||
if (a === 1 && b === 1 && c === 1) return ruleset[0];
|
||
else if (a === 1 && b === 1 && c === 0) return ruleset[1];
|
||
else if (a === 1 && b === 0 && c === 1) return ruleset[2];
|
||
else if (a === 1 && b === 0 && c === 0) return ruleset[3];
|
||
else if (a === 0 && b === 1 && c === 1) return ruleset[4];
|
||
else if (a === 0 && b === 1 && c === 0) return ruleset[5];
|
||
else if (a === 0 && b === 0 && c === 1) return ruleset[6];
|
||
else if (a === 0 && b === 0 && c === 0) return ruleset[7];
|
||
}</pre>
|
||
<p>I like the example written as above because it describes line by line exactly what is happening for each neighborhood configuration. However, it’s not a great solution. After all, what if a CA has 4 possible states (0-3)? Suddenly there are 64 possible neighborhood configurations. With 10 possible states, 1,000 configurations. Certainly I don’t want to type in 1,000 <code>else if</code> statements!</p>
|
||
<p>Another solution, though perhaps more tricky to follow, 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 the built-in JavaScript function <code>parseInt()</code>:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> function rules (a, b, c) {
|
||
// A quick way to concatenate three numbers into a string
|
||
let s = "" + a + b + c;
|
||
|
||
// The second argument ‘2’ indicates that the number should be parsed as binary (base 2).
|
||
let index = parseInt(s, 2);
|
||
|
||
return ruleset[index];
|
||
}</pre>
|
||
<p>There’s one tiny problem with this solution, however. Let’s consider rule 222:</p>
|
||
<pre class="codesplit" data-code-language="javascript">// Rule 222
|
||
let ruleset = [1, 1, 0, 1, 1, 1, 1, 0];</pre>
|
||
<p>And the neighborhood being tested is “111”. The resulting state is equal to ruleset index 0, based on how I first wrote the <code>rules()</code> function .</p>
|
||
<pre class="codesplit" data-code-language="javascript"> if (a === 1 && b === 1 && c === 1) return ruleset[0];</pre>
|
||
<p>The binary number “111” converted to a decimal number is 7. But I don’t want <code>ruleset[7]</code>; I want <code>ruleset[0]</code>. For this to work, ruleset needs to be written with the bits in reverse order:</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{!1} Rule 222 in “reverse” order
|
||
let ruleset = [0, 1, 1, 1, 1, 0, 1, 1];</pre>
|
||
<p>I now have everything needed to compute the generations for a Wolfram elementary CA. Let’s take a moment to organize the code all together.</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{!1} Array for the cells
|
||
let cells = [];
|
||
//{!1} Arbitrarily starting with rule 90
|
||
let ruleset = [0, 1, 0, 1, 1, 0, 1, 0];
|
||
|
||
function setup() {
|
||
//{!3} All cells start with state 0
|
||
for (let i = 0; i < width; i++) {
|
||
cells[i] = 0;
|
||
}
|
||
// Except the center cell is set to state 1.
|
||
cells[floor(cells.length / 2)] = 1;
|
||
}
|
||
|
||
function draw() {
|
||
//{!7} Compute the next generation.
|
||
let nextgen = cells.slice();
|
||
for (let i = 1; i < cells.length - 1; i++) {
|
||
let left = cells[i - 1];
|
||
let me = cells[i];
|
||
let right = cells[i + 1];
|
||
nextgen[i] = rules(left, me, right);
|
||
}
|
||
cells = nextgen;
|
||
}
|
||
|
||
//{!4} Look up a new state from the ruleset.
|
||
function rules(a, b, c) {
|
||
let s = "" + a + b + c;
|
||
let index = parseInt(s, 2);
|
||
return ruleset[index];
|
||
}</pre>
|
||
<h2 id="74-drawing-an-elementary-ca">7.4 Drawing an Elementary CA</h2>
|
||
<p>What’s missing? Presumably, the point here is to draw the cells. As you saw earlier, the standard technique for doing this is to stack the generations one on top of each other and draw a rectangle that is black (for state 1) or white (for state 0).</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_21.png" alt="Figure 7.12 Ruleset 90 visualized as a stack of generations (repeated) ">
|
||
<figcaption>Figure 7.12 Ruleset 90 visualized as a stack of generations (repeated) </figcaption>
|
||
</figure>
|
||
<p>Before implementing this particular visualization, I’d like to point out two things.</p>
|
||
<p>One, this visual interpretation of the data is completely literal. It’s useful for demonstrating the algorithms and results of Wolfram’s elementary CA, but it shouldn’t necessarily drive your own personal work. It’s rather unlikely that you are building a project that needs precisely this algorithm with this visual style. So while learning to draw the CA in this way will help you understand and implement CA systems, this skill should exist only as a foundation.</p>
|
||
<p>Second, the fact that a one-dimensional CA is visualized with a two-dimensional image can be confusing. It’s very important to remember that this is not a 2D CA. I am simply choosing to show a history of all the generations stacked vertically. This technique creates a two-dimensional image out of many instances of one-dimensional data. But the system itself is one-dimensional. Later, you will see an actual 2D CA (the Game of Life) and I’ll cover how to visualized such a system.</p>
|
||
<p>The good news is that drawing the CA is not particularly difficult. Let’s begin by looking at how to render a single generation. Let’s assume a canvas 640 pixels wide with cell being be a 10x10 square. We therefore have a CA with 64 cells. Of course, we can calculate this value dynamically.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let w = 10;
|
||
//{!1} How many cells fit across given a certain width
|
||
let cells = new Array(floor(width / w));</pre>
|
||
<p>Assuming the cell initialization in <code>setup()</code> drawing the cells involves iterating over the array and drawing a white square when the state equals 1 and a black when the state equals 0.</p>
|
||
<pre class="codesplit" data-code-language="javascript">for (let i = 0; i < cells.length; i++) {
|
||
//{!2} By multiplying the cell state the result is 0 or 255
|
||
fill(cells[i] * 255);
|
||
//{!1} Notice how the x-position is the cell index times the cell width.
|
||
// 0, 10, 20, 30, all the way to 640.
|
||
square(i * w, 0, w);
|
||
}</pre>
|
||
<p>In truth, I could simplify the code by having a black background and only drawing when there is a white cell (saving the work of drawing many squares), but in most cases this solution is good enough (and necessary for other more sophisticated designs with varying colors.) I’ll also note that if the size of each cell were 1 pixel I would not want to use p5.js’s <code>square()</code> function, but rather access the pixel array directly.</p>
|
||
<p>You might also notice that the y-position for each square is 0. If I want the generations to be drawn next to each other, with each row of cells marking a new generation, I’ll also need to calculate a y-position based on number of generation iterations. I could accomplish this by adding a variable and incrementing it each time through <code>draw()</code>. With these additions, I can now look at the entire sketch.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-71-wolfram-elementary-cellular-automata">Example 7.1: Wolfram elementary cellular automata</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/SaLy-OnPZ" data-example-path="examples/07_ca/example_7_1_elementary_wolfram_ca"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">// Array of cells
|
||
let cells;
|
||
// Starting at generation 0
|
||
let generation = 0;
|
||
// Cell size
|
||
let w = 10;
|
||
|
||
//{!1} Rule 90
|
||
let ruleset = [0, 1, 0, 1, 1, 0, 1, 0];
|
||
|
||
function setup() {
|
||
createCanvas(640, 240);
|
||
background(255);
|
||
//{!5} An array of 0s and 1s
|
||
cells = new Array(floor(width / w));
|
||
for (let i = 0; i < cells.length; i++) {
|
||
cells[i] = 0;
|
||
}
|
||
cells[floor(cells.length / 2)] = 1;
|
||
}
|
||
|
||
function draw() {
|
||
for (let i = 1; i < cells.length - 1; i++) {
|
||
//{!1} Only drawing the cell's with a state of 1
|
||
if (cells[i] == 1) {
|
||
fill(0);
|
||
//{!1} Set the y-position according to the generation.
|
||
square(i * w, generation * w, w);
|
||
}
|
||
}
|
||
|
||
//{!7} Compute the next generation.
|
||
let nextgen = cells.slice();
|
||
for (let i = 1; i < cells.length - 1; i++) {
|
||
let left = cells[i - 1];
|
||
let me = cells[i];
|
||
let right = cells[i + 1];
|
||
nextgen[i] = rules(left, me, right);
|
||
}
|
||
cells = nextgen;
|
||
|
||
//{!1} The next generation
|
||
generation++;
|
||
}
|
||
|
||
//{!4} Look up a new state from the ruleset.
|
||
function rules(a, b, c) {
|
||
let s = "" + a + b + c;
|
||
let index = parseInt(s, 2);
|
||
return ruleset[index];
|
||
}</pre>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-71">Exercise 7.1</h3>
|
||
<p>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.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-72">Exercise 7.2</h3>
|
||
<p>Examine what patterns occur if you initialize the first generation with each cell having a random state.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-73">Exercise 7.3</h3>
|
||
<p>Visualize the CA in a non-traditional way. Break all the rules you can; don’t feel tied to using squares on a perfect grid with black and white.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-74">Exercise 7.4</h3>
|
||
<p>Create a visualization of the CA that scrolls upwards as the generations increase so that you can view the generations to “infinity.” Hint: instead of keeping track of one generation at a time, you’ll need to store a history of generations, always adding a new one and deleting the oldest one in each frame.</p>
|
||
</div>
|
||
<h2 id="75-wolfram-classification">7.5 Wolfram Classification</h2>
|
||
<p>Before moving on to looking at CA in two dimensions, it’s worth taking a brief look at Wolfram’s classification for cellular automata. As we noted earlier, the vast majority of elementary CA rulesets produce uninspiring results, while some result in wondrously complex patterns like those found in nature. Wolfram has divided up the range of outcomes into four classes:</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_22.png" alt="Figure 7.18: Rule 222 ">
|
||
<figcaption>Figure 7.18: Rule 222 </figcaption>
|
||
</figure>
|
||
<p><strong><em>Class 1: Uniformity.</em></strong> Class 1 CAs end up, after some number of generations, with every cell constant. This is not terribly exciting to watch. Rule 222 (above) is a class 1 CA; if you run it for enough generations, every cell will eventually become and remain black.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_23.png" alt="Figure 7.19: Rule 190 ">
|
||
<figcaption>Figure 7.19: Rule 190 </figcaption>
|
||
</figure>
|
||
<p><strong><em>Class 2: Repetition.</em></strong> Like class 1 CAs, class 2 CAs remain stable, but the cell states are not constant. Rather, they oscillate in some repeating pattern of 0s and 1s. In rule 190 (above), each cell follows the sequence <code>11101110111011101110</code>.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_24.png" alt="Figure 7.20: Rule 30 ">
|
||
<figcaption>Figure 7.20: Rule 30 </figcaption>
|
||
</figure>
|
||
<p><strong><em>Class 3: Random.</em></strong> Class 3 CAs appear random and have no easily discernible pattern. In fact, rule 30 (above) is used as a random number generator in Wolfram’s Mathematica software. Again, this is a moment where we can feel amazed that such a simple system with simple rules can descend into a chaotic and random pattern.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_25.png" alt="Figure 7.21: Rule 110 ">
|
||
<figcaption>Figure 7.21: Rule 110 </figcaption>
|
||
</figure>
|
||
<p><strong><em>Class 4: Complexity.</em></strong> Class 4 CAs can be thought of as a mix between class 2 and class 3. One can find repetitive, oscillating patterns inside the CA, but where and when these patterns appear is unpredictable and seemingly random. Class 4 CAs exhibit the properties of complex systems described earlier in this chapter and in Chapter 5. If a class 3 CA wowed you, then a class 4 like Rule 110 should really blow your mind.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-75">Exercise 7.5</h3>
|
||
<p>Create a sketch that draws for every possible ruleset. Can you classify them?</p>
|
||
</div>
|
||
<h2 id="76-the-game-of-life">7.6 The Game of Life</h2>
|
||
<p>The next step to take is to move from a one-dimensional CA to a two-dimensional one. This will introduce some additional complexity; each cell will have a bigger neighborhood, but that will open up the door to a range of possible applications. After all, most of what happens in computer graphics lives in two dimensions, and this chapter will demonstrate how to apply CA thinking to a 2D p5.js canvas.</p>
|
||
<p>In 1970, Martin Gardner wrote an article in <em>Scientific American</em> that documented mathematician John Conway’s new “Game of Life,” describing it as “recreational” mathematics and suggesting that the reader get out a chessboard and some checkers and “play.” While the Game of Life has become something of a computational cliché (make note of the myriad projects that display the Game of Life on LEDs, screens, projection surfaces, etc.), it is still valuable to practice building the system with code. For one, it provides a good opportunity to practice skills with two-dimensional arrays, nested loops, and more. But perhaps more importantly, its core principles are tied directly to the core goals of this book—simulating the natural world with code. Though you may want to avoid duplicating it without a great deal of thought or care, the algorithm and its technical implementation will provide you with the inspiration and foundation to build simulations that exhibit the characteristics and behaviors of biological systems of reproduction.</p>
|
||
<p>Unlike von Neumann, who created an extraordinarily complex system of states and rules, Conway wanted to achieve a similar “lifelike” result with the simplest set of rules possible. Martin Gardner outlined Conway’s goals as follows:</p>
|
||
<blockquote data-type="epigraph">
|
||
<p>“1. There should be no initial pattern for which there is a simple proof that the population can grow without limit. 2. There should be initial patterns that apparently do grow without limit. 3. There should be simple initial patterns that grow and change for a considerable period of time before coming to an end in three possible ways: fading away completely (from overcrowding or becoming too sparse), settling into a stable configuration that remains unchanged thereafter, or entering an oscillating phase in which they repeat an endless cycle of two or more periods.”</p>
|
||
</blockquote>
|
||
<p>The above might sound cryptic, but it essentially describes a Wolfram class 4 CA. The CA should be patterned but unpredictable over time, eventually settling into a uniform or oscillating state. In other words, though Conway didn’t use this terminology, it should have all those properties of a <em>complex system</em>.</p>
|
||
<p>Let’s look at how the Game of Life works. It won’t take up too much time or space, since I ca build on everything from the Wolfram Elementary CA.</p>
|
||
<figure class="half-width-right">
|
||
<img src="images/07_ca/07_ca_26.png" alt="Figure 7.22: A two-">
|
||
<figcaption>Figure 7.22: A two-</figcaption>
|
||
</figure>
|
||
<p>First, instead of a line of cells, there is now a two-dimensional matrix of cells. As with the elementary CA, the possible states are 0 or 1. Only in this case, since the system is about “life," 0 means dead and 1 means alive.</p>
|
||
<p>The cell’s neighborhood has also expanded. If a neighbor is an adjacent cell, a neighborhood is now nine cells instead of three.</p>
|
||
<p>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. In other words, is the neighborhood overpopulated with life? Surrounded by death? Or just right? Here are the rules of life.</p>
|
||
<ol>
|
||
<li><strong><em>Death.</em></strong> If a cell is alive (state = 1) it will die (state becomes 0) under the following circumstances.
|
||
<ul>
|
||
<li><strong><em>Overpopulation:</em></strong> If the cell has four or more alive neighbors, it dies.</li>
|
||
<li><strong><em>Loneliness:</em></strong> If the cell has one or fewer alive neighbors, it dies.</li>
|
||
</ul>
|
||
</li>
|
||
<li><strong><em>Birth.</em></strong> If a cell is dead (state = 0) it will come to life (state becomes 1) if it has exactly three alive neighbors (no more, no less).</li>
|
||
<li><strong><em>Stasis.</em></strong> In all other cases, the cell state does not change. To be thorough, I’ll describe those scenarios.
|
||
<ul>
|
||
<li><strong><em>Staying Alive:</em></strong> If a cell is alive and has exactly two or three live neighbors, it stays alive.</li>
|
||
<li><strong><em>Staying Dead:</em></strong> If a cell is dead and has anything other than three live neighbors, it stays dead.</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
<p>Let’s look at a few examples, focussing on the center cell.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_27.png" alt="Figure 7.23: Example scenarios for “death” and “birth” in the Game of Life.">
|
||
<figcaption>Figure 7.23: Example scenarios for “death” and “birth” in the Game of Life.</figcaption>
|
||
</figure>
|
||
<p>With the elementary CA, I visualized the generations next to each other, 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). Nevertheless, the typical way the Game of Life is displayed is to treat each generation as a single frame in an animation. So instead of viewing all the generations at once, you see them one at a time, and the result resembles rapidly growing bacteria in a Petri dish.</p>
|
||
<p>One of the exciting aspects of the Game of Life is that there are known initial patterns that yield intriguing results. For example, some remain static and never change.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_28.png" alt="Figure 7.24: Initial configurations of cells that remain stable.">
|
||
<figcaption>Figure 7.24: Initial configurations of cells that remain stable.</figcaption>
|
||
</figure>
|
||
<p>There are patterns that oscillate back and forth between two states.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_29.png" alt="Figure 7.25: Initial configurations of cells that oscillate between two states.">
|
||
<figcaption>Figure 7.25: Initial configurations of cells that oscillate between two states.</figcaption>
|
||
</figure>
|
||
<p>And there are also patterns that from generation to generation appear to move about the grid. (It’s important to note that the cells themselves aren’t actually moving, rather you see the illusion of motion in the result as the cells turn on and off.)</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_30.png" alt="Figure 7.26: Initial configurations of cells that appear to move.">
|
||
<figcaption>Figure 7.26: Initial configurations of cells that appear to move.</figcaption>
|
||
</figure>
|
||
<p>If you are interested in these patterns, there are several good “out of the box” Game of Life demonstrations online that allow you to configure the CA’s initial state and watch it run at varying speeds. Two examples such examples are:</p>
|
||
<ul>
|
||
<li><a href="http://www.playfulinvention.com/emergence/">Exploring Emergence by Mitchel Resnick and Brian Silverman, Lifelong Kindergarten Group, MIT Media Laboratory</a></li>
|
||
<li><a href="https://sklise.github.io/conways-game-of-life/">Conway’s Game of Life by Steven Klise</a></li>
|
||
</ul>
|
||
<p>For the example I’ll build in the next section, I’ll focus on randomly initializing the states for each cell.</p>
|
||
<h2 id="77-programming-the-game-of-life">7.7 Programming the Game of Life</h2>
|
||
<p>Now I just need to extend the code from the Wolfram CA to two dimensions. I previously used a one-dimensional array to store the list of cell states before, and for the Game of Life, I’ll use a two-dimensional array.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let w = 8;
|
||
let columns = width / w;
|
||
let rows = height / w;
|
||
let board = new Array(columns);
|
||
for (let i = 0; i < columns; i++) {
|
||
board[i] = new Array(rows);
|
||
}</pre>
|
||
<p>I’ll begin by initializing each cell of the board with a random state: 0 or 1.</p>
|
||
<pre class="codesplit" data-code-language="javascript">for (let i = 0; i < columns; i++) {
|
||
for (let j = 0; j < rows; j++) {
|
||
//{!1} Initialize each cell with a 0 or 1.
|
||
board[i][j] = floor(random(2));
|
||
}
|
||
}</pre>
|
||
<p>And to compute the next generation, just as before, I need to make sure I don’t overwrite the previous generation states. Rather than write all the steps to create a 2D array in both <code>setup()</code> and <code>draw()</code>it’s worth writing a function that returns a 2D array based on the number of columns and rows. I’ll also initialize each element of the array to <code>0</code> so that it isn’t filled with <code>undefined.</code></p>
|
||
<pre class="codesplit" data-code-language="javascript">function create2DArray(columns, rows) {
|
||
let arr = new Array(columns);
|
||
for (let i = 0; i < columns; i++) {
|
||
arr[i] = new Array(rows);
|
||
for (let j = 0; j < rows; j++) {
|
||
arr[i][j] = 0;
|
||
}
|
||
}
|
||
return arr;
|
||
}</pre>
|
||
<p>Then I can just call that function whenever a new 2D array is required.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let next = create2DArray(columns, rows);
|
||
|
||
for (let i = 0; i < columns; i++) {
|
||
for (let j = 0; j < rows; j++) {
|
||
//{!1} Calculate the state for each cell.
|
||
next[x][y] = _______________?;
|
||
}
|
||
}</pre>
|
||
<figure class="half-width-right">
|
||
<img src="images/07_ca/07_ca_31.png" alt="Figure 7.27: The index values for the neighborhood of cells.">
|
||
<figcaption>Figure 7.27: The index values for the neighborhood of cells.</figcaption>
|
||
</figure>
|
||
<p>OK. Before I can sort out how to actually calculate the new state, I need to determine how to reference the cell’s neighbors. In the case of a 1D CA, this was simple: if a cell index was <code>i</code>, its neighbors were <code>i-1</code> and <code>i+1</code>. Here each cell doesn’t have a single index, but rather a column and row index: <code>i,j</code>. As shown in Figure 7.27, the neighbors are <code>i-1,j-1</code> , <code>i,j-1</code>, <code>i+1,j-1</code>, <code>i-1,j</code>, <code>i+1,j</code>, <code>i-1,j+1</code>, <code>i,j+1</code>, and <code>i+1,j+1</code>.</p>
|
||
<p>The Game of Life rules operate by knowing how many neighbors are alive. So if I create a counter variable and increment it for each neighbor with a state of 1, I’ll have the total of live neighbors.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let sum = 0;
|
||
|
||
// Top row of neighbors
|
||
if (board[i - 1][j - 1] == 1) sum++;
|
||
if (board[i ][j - 1] == 1) sum++;
|
||
if (board[i + 1][j - 1] == 1) sum++;
|
||
|
||
// Middle row of neighbors (note i,j is skipped)
|
||
if (board[i - 1][j ] == 1) sum++;
|
||
if (board[i + 1][j ] == 1) sum++;
|
||
|
||
// Bottom row of neighbors
|
||
if (board[i - 1][j + 1] == 1) sum++;
|
||
if (board[i ][j + 1] == 1) sum++;
|
||
if (board[i + 1][j + 1] == 1) sum++;</pre>
|
||
<p>And again, just as with the Wolfram CA, I find myself in a situation where the above is a useful and clear way to write the code for teaching purposes, explicitly stating every step (each time a neighbor has a state of one, the counter increases). Nevertheless, it’s a bit silly to say, “If the cell state equals one, add one to a counter” when I could just as easily say, “Add the cell state to a counter.” After all, if the state can only be 0 or 1, the sum of all the neighbors’ states will yield the total number of live cells. Since the neighbors are arranged in a mini 3x3 grid, I can introduce another nested loop to computer the sum.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let sum = 0;
|
||
|
||
//{!2} Using k and l as the counters since i and j are already used!
|
||
for (let k = -1; k <= 1; k++) {
|
||
for (let l = -1; l <= 1; l++) {
|
||
//{!1} Add up all the neighbors’ states.
|
||
sum += board[i + k][j + l];
|
||
}
|
||
}</pre>
|
||
<p>Of course, I’ve made a significant mistake. In the Game of Life, the cell itself does not count as one of the neighbors. I could include a conditional to skip adding the state when both <code>k</code> and <code>l</code> equal 0, but another option is be to subtract the cell state once the loop is completed.</p>
|
||
<pre class="codesplit" data-code-language="javascript">// Whoops! Subtract the cell’s state!
|
||
neighbors -= board[i][j];</pre>
|
||
<p>Finally, once we know the total number of live neighbors, we can decide what the cell’s new state should be according to the rules: birth, death, or stasis.</p>
|
||
<pre class="codesplit" data-code-language="javascript">// {.code-wide} If it is alive and has less than 2 live neighbors, it dies from loneliness.
|
||
if (board[i][j] == 1 && sum < 2) {
|
||
next[i][j] = 0;
|
||
// {.code-wide} If it is alive and has more than 3 live neighbors, it dies from overpopulation.
|
||
} else if (board[x][y] == 1 && sum > 3) {
|
||
next[i][j] = 0;
|
||
// {.code-wide} If it is dead and has exactly 3 live neighbors, it is born!
|
||
} else if (board[x][y] == 0 && sum == 3) {
|
||
next[i][j] = 1;
|
||
// {.code-wide} In all other cases, its state remains the same.
|
||
} else {
|
||
next[i][j] = board[i][j];
|
||
}</pre>
|
||
<p>Putting this all together:</p>
|
||
<pre class="codesplit" data-code-language="javascript">// The next board
|
||
let next = create2DArray(columns, rows);
|
||
|
||
//{!2} Looping but skipping the edge cells
|
||
for (let i = 1; i < columns - 1; i++) {
|
||
for (let j = 1; j < rows - 1; j++) {
|
||
// Add up all the neighbor states to
|
||
// calculate the number of live neighbors.
|
||
let neighbors = 0;
|
||
for (let k = -1; k <= 1; k++) {
|
||
for (let l = -1; l <= 1; l++) {
|
||
neighbors += board[i + k][j + l];
|
||
}
|
||
}
|
||
// Correct by subtracting the cell state itself.
|
||
neighbors -= board[i][j];
|
||
|
||
//{!4} The rules of life!
|
||
if (board[i][j] == 1 && neighbors < 2) next[i][j] = 0;
|
||
else if (board[i][j] == 1 && neighbors > 3) next[i][j] = 0;
|
||
else if (board[i][j] == 0 && neighbors == 3) next[i][j] = 1;
|
||
else next[i][j] = board[i][j];
|
||
}
|
||
}
|
||
|
||
board = next;</pre>
|
||
<p>Finally, once the next generation is calculated, I can employ the same method used to draw the Wolfram CA—a square for each spot, white for off, black for on. The only new code here is drawing the board!</p>
|
||
<div data-type="example">
|
||
<h3 id="example-72-game-of-life">Example 7.2: Game of Life</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/Hy8uT3Qux"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript"> for (let i = 0; i < columns; i++) {
|
||
for (let j = 0; j < rows; j++) {
|
||
//{!1} evaluates to 255 when state is 0 and 0 when state is 1
|
||
fill(255 - board[i][j] * 255);
|
||
stroke(0);
|
||
square(i * w, j * w, w);
|
||
}
|
||
}</pre>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-76">Exercise 7.6</h3>
|
||
<p>Create a Game of Life simulation that allows you to manually configure the grid by drawing or with specific known patterns.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-77">Exercise 7.7</h3>
|
||
<p>Implement “wrap-around” for the Game of Life so that cells on the edges have neighbors on the opposite side of the grid.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-78">Exercise 7.8</h3>
|
||
<p>While the above solution (Example 7.2) is convenient, it is not particularly memory-efficient. It creates a new 2D array for every frame of animation! This matters very little for a p5.js application, but if you were implementing the Game of Life on a microcontroller or mobile device, you’d want to be more careful. One solution is to have only two arrays and constantly swap them, writing the next set of states into whichever one isn’t the current array. Implement this particular solution.</p>
|
||
</div>
|
||
<h2 id="78-object-oriented-cells">7.8 Object-Oriented Cells</h2>
|
||
<p>Over the course of the previous six chapters, we’ve slowly built examples of systems of <em>objects</em> with properties that move about the screen. And in this chapter, although we’ve been talking about a “cell” as if it were an object, we actually haven’t been using any object orientation in our code (other than a class to describe the CA system as a whole). This has worked because a cell is such an enormously simple object (a single bit). However, in a moment, we are going to discuss some ideas for further developing CA systems, many of which involve keeping track of multiple properties for each cell. For example, what if a cell needed to remember its last ten states? Or what if we wanted to apply some of our motion and physics thinking to a CA and have the cells move about the window, dynamically changing their neighbors from frame to frame?</p>
|
||
<p>To accomplish any of these ideas (and more), it would be helpful to see how we might treat a cell as an object with multiple properties, rather than as a single 0 or 1. To show this, let’s just recreate the Game of Life simulation. Only instead of:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let board;</pre>
|
||
<p>Let’s have:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let board;</pre>
|
||
<p>where <code>Cell</code> is a class we will write. What are the properties of a <code>Cell</code> object? In our Game of Life example, each cell has a position and size, as well as a state.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Cell {
|
||
constructor(x, y, w){
|
||
// position and size
|
||
this.x = x;
|
||
this.y = y;
|
||
this.w = w;
|
||
|
||
// What is the cell’s state?
|
||
this.state = ????;
|
||
|
||
}
|
||
</pre>
|
||
<p>In the non-OOP version, we used a separate 2D array to keep track of the states for the current and next generation. By making a cell an object, however, each cell could keep track of both states. In this case, we’ll think of the cell as remembering its previous state (for when new states need to be computed).</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // What was its previous state?
|
||
this.previous = this.state;</pre>
|
||
<p>This allows us to visualize more information about what the state is doing. For example, we could choose to color a cell differently if its state has changed. For example:</p>
|
||
<div data-type="example">
|
||
<h3 id="example-73-game-of-life-oop">Example 7.3: Game of Life OOP</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/SyO0p3mux"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_32.png" alt="">
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">display() {
|
||
//{!1} If the cell is born, color it blue!
|
||
if (previous == 0 && this.state == 1) fill(0, 0, 255);
|
||
else if (this.state == 1) fill(0);
|
||
|
||
//{!1} If the cell dies, color it red!
|
||
else if (this.previous == 1 && this.state == 0) fill(255, 0, 0);
|
||
else fill(255);
|
||
|
||
rect(this.x, this.y, this.w, this.w);
|
||
}</pre>
|
||
<p>Not much else about the code (at least for our purposes here) has to change. The neighbors can still be counted the same way; the difference is that we now need to refer to the object’s state variables as we loop through the 2D array.</p>
|
||
<pre class="codesplit" data-code-language="javascript">for (let this.x = 1; x < this.columns-1; x++) {
|
||
for (let this.y = 1; y < this.rows-1; y++) {
|
||
|
||
let neighbors = 0;
|
||
for (let i = -1; i <= 1; i++) {
|
||
for (let j = -1; j <= 1; j++) {
|
||
//{!1 .bold} Use the previous state when tracking neighbors.
|
||
neighbors += this.board[this.x+i][this.y+j].previous;
|
||
}
|
||
}
|
||
neighbors -= this.board[x][y].previous;
|
||
|
||
//{!3} We are calling a function newState() to assign a new state to each cell.
|
||
if ((this.board[x][y].state == 1) && (neighbors < 2)) this.board[x][y].newState(0);
|
||
else if ((this.board[x][y].state == 1) && (neighbors > 3)) this.board[x][y].newState(0);
|
||
else if ((this.board[x][y].state == 0) && (neighbors == 3)) this.board[x][y].newState(1);
|
||
// else do nothing!
|
||
}
|
||
}</pre>
|
||
<h2 id="79-variations-of-traditional-ca">7.9 Variations of Traditional CA</h2><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="variations of"></a>
|
||
<p>Now that we have covered the basic concepts, algorithms, and programming strategies behind the most famous 1D and 2D cellular automata, it’s time to think about how you might take this foundation of code and build on it, developing creative applications of CAs in your own work. In this section, we’ll talk through some ideas for expanding the features of the CA examples. Example answers to each of these exercises can be found on the book website.</p><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="non-rectangular grids and"></a><a data-type="indexterm" data-primary="non-rectangular grids (cellular automata)"></a>
|
||
<p><strong><em>1) Non-rectangular Grids</em></strong>. There’s no particular reason why you should limit yourself to having your cells on a rectangular grid. What happens if you design a CA with another type of shape?</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-79">Exercise 7.9</h3>
|
||
<p>Create a CA using a grid of hexagons (as below), each with six neighbors.</p>
|
||
<figure>
|
||
<img src="images/07_ca/07_ca_33.png" alt="">
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="probabilistic"></a><a data-type="indexterm" data-primary="probabilistic (cellular automata)"></a><a data-type="indexterm" data-primary="probability" data-secondary="cellular automata based on"></a>
|
||
<p><strong><em>2) Probabilistic</em></strong>. The rules of a CA don’t necessarily have to define an exact outcome.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-710">Exercise 7.10</h3>
|
||
<p>Rewrite the Game of Life rules as follows:</p>
|
||
<p>Overpopulation: If the cell has four or more alive neighbors, it has a 80% chance of dying.Loneliness: If the cell has one or fewer alive neighbors, it has a 60% chance of dying.Etc.</p>
|
||
</div><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="continuous"></a><a data-type="indexterm" data-primary="continuous (cellular automata)"></a>
|
||
<p><strong><em>3) Continuous</em></strong>. We’ve looked at examples where the cell’s state can only be a 1 or a 0. But what if the cell’s state was a floating point number between 0 and 1?</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-711">Exercise 7.11</h3>
|
||
<p>Adapt Wolfram elementary CA to have the state be a float. You could define rules such as, “If the state is greater than 0.5” or “…less than 0.2.”</p>
|
||
</div><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="image processing"></a><a data-type="indexterm" data-primary="image processing (cellular automata)"></a>
|
||
<p><strong><em>4) Image Processing</em></strong>. We briefly touched on this earlier, but many image-processing algorithms operate on CA-like rules. Blurring an image is creating a new pixel out of the average of a neighborhood of pixels. Simulations of ink dispersing on paper or water rippling over an image can be achieved with CA rules.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-712">Exercise 7.12</h3>
|
||
<p>Create a CA in which a pixel is a cell and a color is its state.</p>
|
||
</div><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="historical"></a><a data-type="indexterm" data-primary="historical (cellular automata)"></a>
|
||
<p><strong><em>5) Historical</em></strong>. In the Game of Life object-oriented example, we used two variables to keep track of its state: current and previous. What if you use an array to keep track of a cell’s state history? This relates to the idea of a “complex adaptive system,” one that has the ability to adapt and change its rules over time by learning from its history. We’ll see an example of this in Chapter 10: Neural Networks.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-713">Exercise 7.13</h3>
|
||
<p>Visualize the Game of Life by coloring each cell according to how long it’s been alive or dead. Can you also use the cell’s history to inform the rules?</p>
|
||
</div><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="moving cells"></a><a data-type="indexterm" data-primary="moving cells (cellular automata)"></a>
|
||
<p><strong><em>6) Moving cells</em></strong>. In these basic examples, cells have a fixed position on a grid, but you could build a CA with cells that have no fixed position and instead move about the screen.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-714">Exercise 7.14</h3>
|
||
<p>Use CA rules in a flocking system. What if each boid had a state (that perhaps informs its steering behaviors) and its neighborhood changed from frame to frame as it moved closer to or further from other boids?</p>
|
||
</div><a data-type="indexterm" data-primary="cellular automaton (automata)" data-secondary="nesting"></a><a data-type="indexterm" data-primary="nesting (cellular automata)"></a>
|
||
<p><strong><em>7) Nesting</em></strong>. Another feature of complex systems is that they can be nested. Our world tends to work this way: a city is a complex system of people, a person is a complex system of organs, an organ is a complex system of cells, and so on and so forth.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-715">Exercise 7.15</h3>
|
||
<p>Design a CA in which each cell itself is a smaller CA or a system of boids.</p>
|
||
</div>
|
||
<div data-type="project">
|
||
<h3 id="the-ecosystem-project-6">The Ecosystem Project</h3>
|
||
<p>Step 7 Exercise:</p>
|
||
<p>Incorporate cellular automata into your ecosystem. Some possibilities:</p>
|
||
<ul>
|
||
<li>Give each creature a state. How can that state drive their behavior? Taking inspiration from CA, how can that state change over time according to its neighbors’ states?</li>
|
||
<li>Consider the ecosystem’s world to be a CA. The creatures move from tile to tile. Each tile has a state—is it land? water? food?</li>
|
||
<li>Use a CA to generate a pattern for the design of a creature in your ecosystem.</li>
|
||
</ul>
|
||
</div>
|
||
</section> |