Cellular Automata
Pascal’s triangle
Here’s the example I showed at the projector, which is also the starting code you’ll find in the link above:
neighborhood: `
oo
x
`,
rule: (cell) => {
return cell.sumNeighbors() == 1
},
everyStep: (automaton) => {
automaton.setCell(0, 0, 1)
},
Ask: What does each part of this code do?
- What is the
neighborhood? What do thexandos mean? - What is the
rule? - Why
everyStep? (What happens if you remove it?)
Conduct some experiments! Tinker with other things in the code too, like width and height and cellSize and colorScheme.
There’s another way to write this. The % operator (“remainder” operator, sometimes also incorrectly called the “modulo” operator) makes positive numbers “wrap around” when they reach some limit. For example, n % 4 means “as n increases, when n reaches 4 then it goes back to 0:”
n |
n % 4 |
| 0 | 0 |
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 0 |
| 5 | 1 |
| 6 | 2 |
| 7 | 3 |
| 8 | 0 |
| 9 | 1 |
We thus get the same results if we replace this:
cell.sumNeighbors() == 1
..with this:
cell.sumNeighbors() % 2
Try it! That will set you up better for further experiments where the sum of the neighbors can be great than 2.
Sierpiński carpet and friends
Sticking with the same code as above, try this neighborhood instead:
oo
ox
Now try making the numbers wrap around at 3 instead of 3:
rule: (cell) => {
return cell.sumNeighbors() % 3
},
Try some even higher numbers!
In the starter code, you’ll see this line that sets the upper left cell’s value to 1:
automaton.setCell(0, 0, 1)
Change it to set the center cell instead:
automaton.setCenterCell(1)
…and then try this neighborhood:
o
oxo
o
…or this one:
ooo
oxo
ooo
Make up your own oddly shaped neighborhoods!
You can also get interesting results by changing everyStep to firstStep. (Warning: variations on this can create flashing / strobing patterns. If you are photosensitive, use caution.)
Area average
Here is an automaton that makes each cell take on the average value of its neighbors:
neighborhood: `
o
oxo
o
`,
rule: (cell) => {
return Math.round(
(cell.sumNeighbors() + cell.value()) / (cell.neighborhoodSize + 1)
)
},
firstStep: (automaton) => {
automaton.fillRandom(50)
},
Note the firstStep: we start by filling the whole gride with random numbers less than 3. (There is no everyStep now, just firstStep!)
Try larger arguments to fillRandom.
Try this neighborhood instead:
ooo
oxo
ooo
Try passing a number in the thousands or tens of thousands to fillRandom. Now try changing Math.round (which rounds to the nearest integer) into Math.floor (which always rounds down) and notice the difference!
Influencerville
Let’s make each cell take on the value of one of its neighbors, selected at random:
neighborhood: `
ooo
oxo
ooo
`,
rule: (cell) => {
return cell.randomNeighborValue()
},
firstStep: (automaton) => {
automaton.fillRandom(3)
},
What if we make the cell choose two neighbors, and make it agree with them if they both have the same value?
rule: (cell) => {
let choice0 = cell.randomNeighborValue()
let choice1 = cell.randomNeighborValue()
if (choice0 == choice1) {
return choice0
} else {
return cell.value()
}
},
Social scientists sometimes use more complex versions of this to simulate social pressure and the propagation of opinions.
Mineral growth
Here’s a terrible, wildly inaccurate simulation of mineral growth:
neighborhood: `
ooo
oxo
ooo
`,
rule: (cell) => {
if (cell.value() == 2) {
return 2;
}
let randNeighbor = cell.randomNeighborValue()
if (cell.value() == 1 && cell.sumNeighbors() < 5 && randNeighbor == 2) {
return 2;
}
if (randNeighbor != 2) {
return randNeighbor
}
return cell.value()
},
firstStep: (automaton) => {
automaton.fillRandom(2.1)
},
In this code, && means “and.” This code:
cell.value() == 1 && cell.sumNeighbors() < 5 && randNeighbor == 2
…means “cell value is 1 and the sum of the neighbors is less than 5 and the random neighbor’s value is 2.””
Try changing the neighborhood! And try changing 5 to other numbers.
Rock paper scissors
In the game of “rock, paper, scissors,” two players simultaneously choose one of those three choices in the name of the game. Each choice wins against one of the others and loses against one of the others: paper wraps rock, rock blunts scissors, scissors cut paper.
When humans play this game, they play with hands signs. In this cellular automaton version, there are three cell states / colors that correspond to “rock,” “paper,” and “scissors.” Each cell plays the game against all of its neighbors. If it loses more of those games than it wins, then its value changes. Try it:
neighborhood: `
o
oxo
o
`,
rule: (cell) => {
let wins = cell.countNeighborsWithValue(wrapAround(cell.value() + 1, 3))
let losses = cell.countNeighborsWithValue(wrapAround(cell.value() - 1, 3))
if (losses > wins) {
return wrapAround(cell.value() - 1, 3);
} else {
return cell.value()
}
},
firstStep: (automaton) => {
automaton.fillRandom(3)
},
You’ll see waves traveling across the grid: waves of scissors cutting paper, chased by waves of rock blunting scissors, chased by waves of paper wrapping rock.
Try changing the argument to fillRandom to a larger number. The rule code here expects numbers in the range 0…2, and allowing those larger numbers breaks it in an interesting way!
Conway’s Game of Life
This is probably the most famous cellular automaton of all time:
neighborhood: `
ooo
oxo
ooo
`,
rule: (cell) => {
let sum = cell.sumNeighbors()
if (sum == 3 || (sum == 2 && cell.value() == 1)) {
return 1
} else {
return 0
}
},
firstStep: (automaton) => {
automaton.fillRandom(1.5)
},
What does the rule mean? Can you figure it out?
That simple rule generates incredibly complex behaviors. You can go deep into this one! Here is more info.