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
< 2living neighbors dies. - A living cell with
2 || 3living neighbors survives. - A living cell with
> 3living neighbors dies. - A dead cell with
3living 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! 🕹️