How I Programmed the Game of Life in a Google Sheet with Google Apps Script
A Google Sheet, being essentially a two-dimensional array, is perfect for this task!
What Is the Game of Life?
According to Wikipedia, the Game of Life “is a cellular automaton devised by the British mathematician John Horton Conway in 1970.”
It begins on a two-dimensional grid of square cells. Each cell can be either alive or dead. Every cell interacts with its eight immediate neighbors. A live cell only remains alive if it has two or three living neighbors. If it has fewer than two living neighbors, it dies as if by underpopulation. Conversely, if it has more than three, it dies as if by overpopulation. A dead cell remains dead unless it has exactly three living neighbors; otherwise, it becomes a live cell, as if by reproduction.
By simultaneously recalculating the state of the cells, we can create a new generation, and another one, and so on. In some cases, the cells all ultimately die, whereas in other cases, they do not. Along the way, they might even form some interesting patterns (more on them later).
With this simple set of rules, we are looking at a simulation that plays out like this:
There is no immediate practical use for the Game of Life in a spreadsheet; however, it is a fun algorithmic challenge. Moreover, Google Sheets natively provides us with the perfect data structure: a two-dimensional array. This is all the more reason to work on those array skills!
We will be using the OOP style and ES6 syntax extensively. Although I did try FP, it seemed to lend itself a little less well than OOP in this specific challenge. I am also a fan of using
Our classes diagram looks like this:
You will notice that our main class
GameOfLife contains a
Grid that is composed of
We will also be using a 25x25 grid that we will need to set up manually. Although you can automate this on your own if you prefer, it is not the objective of this article. We will use the
setBackgrounds() method to color the alive and dead cells.
Now, let’s jump into the code.
We will look into the classes in the order in which they appear: first
Cell. Ultimately, we will specifically look into our
render() function that displays the results of our app’s calculations.
The constructor of
GameOfLife accepts two parameters:
The former is, as the name suggests, a callback that will be used to render a generation of cells on the grid. Having it passed as a callback rather than building it in has the advantage of making this library extensible, meaning that it respects the open/closed principle. This allows any other developers to use their own rendering logic (e.g., inserting values rather than changing cell backgrounds).
options parameter is an object that contains the following optional properties:
width: number, the width of the grid, defaults to 25
height: number, the height of the grid, defaults to 25
patternName: string, in case we want to initiate the grid with a specific pattern
frameMs: number, how long to pause in milliseconds before rendering a new generation, defaults to 100
numIterations: number, how many generations to calculate and display on the grid (you probably want to stay within the six-minute or thirty-minute execution threshold, depending on the type of your account)
_grid property is an instance of the
Grid class that we will examine next. Some of the options, namely
patternName, which we pass to
GameOfLife, are passed on to the
The game starts when the
play() method is called. It’s a recursive method that pauses the game a predefined number of milliseconds. It then asks the
Grid to create a new generation of cells via the
Grid.nextGen() method and renders them. It stops after a predefined number of times. Additionally,
Grid.nextGen() can return
null in case there are no changes between generations. In this case, the game stops as there is no longer a need to run any more calculations. It also calls the
render() method that calls the
The full source code for the class is provided below.
Grid class’ constructor accepts the
options object as a parameter that contains
patternName entries and stores them as read-only properties. It also has the
grid property that is a two-dimensional array of instances of the
Cell class. Granted, I recognize that
Grid.grid is not the best name, so if you have a better idea of what this property can be renamed to, let me know in the comments. I’m all ears!
The class has a few methods:
setCell() sets the value of a specific cell in the
grid property to a given
Cell instance. It is used when initiating or calculating a new generation of cells and setting new values to the grid.
init() creates a new
grid that is a two-dimensional array with the private
_make2DArray() method with random alive and dead cells. Later, we will modify this method to allow setting specific patterns. From the start, we’re building a two-dimensional array and will be storing our cell data there. This will allow us to render the grid easily.
nextGen() calculates a new generation of cells. It loops through the whole grid, calls the
getNewIsAlive() method of the cell in question, and sets that value to a new
Grid instance. Once all cells have been covered, that new
Grid instance is returned.
Here is the full source code of the
Cell’s constructor is called by the
Grid class when a grid is built. The constructor accepts the following parameters:
isAlive. It is the
Grid class that sets the value of
false; however, it also passes its own reference as well as the row and column index to the cell. This is necessary for the cell to count its alive neighbors and return its new state. Then, the grid can create a new
Cell based on the current cell’s future state.
neighborsAlive property and
getNewIsAlive() method are what implement the actual rules of the Game of Life.
neighborsAlive returns the number of alive neighbor cells for the current cell. If a cell is on the edge of the grid, it wraps around to check the cells on the opposite side of the grid. This property is then used in
getNewIsAlive() to calculate the future state of the cell depending on whether it is currently alive or dead. If you remember, this method is used in
Grid.nextGen() iteratively to create a whole new generation.
The full source code for the
Cell class is provided below:
render callback accepts the
Grid.grid property, which, if you remember, is a two-dimensional array of
Cells and replaces them with hex color codes depending on whether
false. Then, it applies the new two-dimensional arrays to the spreadsheet as background colors with
Notice that I am storing the
Range reference in a Singleton to avoid calling
SpreadsheetApp.getActive().getRange() on every iteration. This improves the function’s performance.
The Different Patterns
As previously mentioned, there are several types of patterns depending on how they behave. Let’s review a few different types and then implement them in our code.
Spaceships move across the grid and never die. Below is a glider, which is a one example of a spaceship.
Oscillators return to their initial state after a finite number of generations. Below is an example of an oscillator called Penta-decathlon (period 15)
Die-hards eventually disappear rather than stabilizing.
Still lifes do not change from one generation to the next.
First, let’s define a
pattern object and create a few patterns with zeros and ones in a two-dimensional array as follows:
Of course, ones represent alive cells and zeros represent dead ones. Now that we have a simple way to describe our patterns, let’s add a new private method to our
Grid that will insert them into the defined grid size. This method should read the width and height of the grid and insert the pattern in the middle. This is what I came up with:
Now, we just need to add one line to our
Grid.init() method to return the pattern if there is a
patternName present in the
options object instead of randomizing, as follows:
Running the Game of Life
Running the Game of Life in our spreadsheet is now simply a matter of creating an instance of
GameOfLife with the right parameters and calling its
If you have read until here, thank you very much. I certainly hope that you found the information useful! This is just another example of how versatile spreadsheets are and the multitude of things you can do with with a two-dimensional array data structure and the OOP approach.
If you enjoyed this article, feel free to buy me a coffee — it’s what fuels my code. :)
About the Author
Dmitry Kostyuk is a full-time GAS developer and the Founder of Wurkspaces.dev.