John Horton Conway invented the Game of Life in 1970. It was first featured in Martin Gardner’s Mathematical Games column in Scientific American. Searching for it even results in a Google easter egg.
The game of life is a cellular automaton, which is a grid of cells. Each cell can be one of a finite number of states. In this case, the cells are either alive (1
) or dead (0
). The game progresses in generations, where the state of each cell is determined by the state of its neighbors according to a set of pre-defined rules:
- A living cell with
< 2
living neighbors dies. - A living cell with
2 || 3
living neighbors survives. - A living cell with
> 3
living neighbors dies. - A dead cell with
3
living neighbors becomes alive.
A neighbor is any cell that is adjacent to the current cell, including diagonals. For example:
0 0 0
0 1 0
0 0 0
The cell in the middle has 0
living neighbors.
My Attempt
The first thing I wanted to do was generate the grid:
type Cell = 0 | 1
type Grid = Cell[][]
/**
* Initializes a new grid of a given size.
* Just a 2D array of 0s and 1s.
*/
function initGrid(size: number): Grid {
const grid = []
for (let i = 0; i < size; i++) {
const row: Cell[] = []
for (let j = 0; j < size; j++) {
// random 0 or 1
const cell = Math.round(Math.random()) as Cell
row.push(cell)
}
grid.push(row)
}
return grid
}
Next was counting living neighbors of a cell:
type Point = [number, number]
/**
* Calculates the number of living neighbors for a given cell.
* Neighbors are the 8 cells surrounding the given cell.
*/
const countLivingNeighbors = (grid: Grid, point: Point) => {
const size = grid.length
const [x, y] = point
let count = 0
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (!(i === 0 && j === 0)) {
const newX = x + i
const newY = y + j
if (newX >= 0 && newX < size && newY >= 0 && newY < size) {
const row = grid[newY] as Cell[]
count += row[newX] as Cell
}
}
}
}
return count
}
And updating the grid based on the rules:
const [grid, setGrid] = useState(initGrid(size))
const [generation, setGeneration] = useState(0)
const [running, setRunning] = useState(false)
/** Compares two grids. */
const areGridsEqual = (grid1: Grid, grid2: Grid) => {
const size = grid1.length
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
// if any corresponding cells are different, the grids are not equal
if (grid1[i][j] !== grid2[i][j]) return false
}
}
return true
}
/** Updates the grid based on the rules. */
const updateGrid = () => {
// use functional form to access previous state
setGrid((oldGrid) => {
// create a new grid derived from the old one
const newGrid = oldGrid.map((row, y) =>
row.map((cell, x) => {
const neighbors = countLivingNeighbors(oldGrid, [x, y])
if (cell === 1 && (neighbors < 2 || neighbors > 3)) return 0
if (cell === 0 && neighbors === 3) return 1
return cell
})
)
// if the new grid is the same as the old grid, stop the game
if (areGridsEqual(oldGrid, newGrid)) {
setRunning(false)
return oldGrid
}
// otherwise, increment the generation
setGeneration((oldGeneration) => oldGeneration + 1)
return newGrid
})
}
At this point, we have everything we need to implement the game. The grid itself is just a basic HTML table where each cell has a fixed height, width, and background color. The animation loop uses setInterval
:
useEffect(() => {
// delay between renders for aesthetics
const DELAY = 150
// intervals are type `Timeout` in Node and numbers in the browser
let intervalId: number | null = null
if (running) {
intervalId = setInterval(updateGrid, DELAY) as unknown as number
}
// clear the interval on unmount
return () => {
if (intervalId) clearInterval(intervalId)
}
}, [running, updateGrid])
Optimizations
The above implementation works. However, given the nested loops the complexity grows
The LLMs tell me that one solution is to create a separate data structure to store only active cells. An active cell is one that is either alive or has at least 1
living neighbor. The data structure to use is Set<string>
, where each string is a "x,y"
coordinate. Sets offer
Conclusion
If you’re interested in things like cellular automata, then you should follow me on GitHub and Hugging Face to see what else I’m working on.
Enjoy! 🕹️