Game of Life
Conway’s Game of Life simulation using React
Game of Life
Conway’s Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970. It is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input. One interacts with the Game of Life by creating an initial configuration and observing how it evolves.
Rules
The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, alive or dead. Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:
- Any live cell with fewer than two live neighbours dies, as if by underpopulation.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overpopulation.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
These rules, which compare the behavior of the automaton to real life, can be condensed into the following:
- Any live cell with two or three live neighbours survives.
- Any dead cell with three live neighbours becomes a live cell.
- All other live cells die in the next generation. Similarly, all other dead cells stay dead.
Implementation
Let's implement the Game of Life simulation using React. We will create a grid of cells that evolve according to the rules mentioned above. Step by step explanation of the implementation and code:
- We start by importing the
useState
anduseEffect
hooks from React.
import { useState, useEffect } from 'react';
- We define the
GameOfLife
component that will contain the logic for the simulation.
const GameOfLife = () => {
// Component logic goes here
};
- We set the
cellSize
to 5 pixels and calculate the number of columns and rows based on the window size.
const cellSize = 5;
const columns = Math.floor(window.innerWidth / cellSize);
const rows = Math.floor(window.innerHeight / cellSize);
- We define a helper function
generateEmptyGrid
that creates a 2D array filled withfalse
values.
const generateEmptyGrid = (rows, columns) => {
return Array.from({ length: rows }, () => Array(columns).fill(false));
};
- We define a helper function
initializeRandomCells
that randomly sets some cells to be alive (true
) based on a probability threshold.
const initializeRandomCells = (grid) => {
grid.forEach((row, x) => {
grid[x] = row.map(() => Math.random() > 0.4);
});
};
- We define a helper function
countNeighbors
that counts the number of live neighbors for a given cell.
const countNeighbors = (grid, x, y) => {
let count = 0;
const directions = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
for (const [dx, dy] of directions) {
const newX = x + dx;
const newY = y + dy;
if (newX >= 0 && newX < grid.length && newY >= 0 && newY < grid[0].length) {
count += grid[newX][newY] ? 1 : 0;
}
}
return count;
};
- We use the
useState
hook to store the grid size and initialize it with the calculated number of rows and columns.
const [gridSize, setGridSize] = useState({ rows, columns });
- We use the
useEffect
hook to update the grid size when the window is resized.
useEffect(() => {
const handleResize = () => {
const newColumns = Math.floor(window.innerWidth / cellSize);
const newRows = Math.floor(window.innerHeight / cellSize);
setGridSize({ rows: newRows, columns: newColumns });
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [cellSize]);
- We use the
useState
hook to store the grid state and initialize it with an empty grid filled with random cells.
const [grid, setGrid] = useState(() => {
const initialGrid = generateEmptyGrid(gridSize.rows, gridSize.columns);
initializeRandomCells(initialGrid);
return initialGrid;
});
- We use the
useEffect
hook to update the grid state based on the rules of the Game of Life.
useEffect(() => {
const updateGrid = () => {
setGrid((prevGrid) => {
return prevGrid.map((row, x) =>
row.map((cell, y) => {
const neighbors = countNeighbors(prevGrid, x, y);
if (cell && (neighbors < 2 || neighbors > 3)) {
return false;
} else if (!cell && neighbors === 3) {
return true;
} else {
return cell;
}
})
);
});
};
const intervalId = setInterval(() => {
updateGrid();
}, 100);
return () => clearInterval(intervalId);
}, []);
- We define a helper function
toggleCell
that toggles the state of a cell when clicked.
const toggleCell = (x, y) => {
setGrid((prevGrid) => {
const newGrid = [...prevGrid];
newGrid[x] = [...prevGrid[x]];
newGrid[x][y] = !newGrid[x][y];
return newGrid;
});
};
- We render the grid of cells and attach the
toggleCell
function to theonClick
event of each cell.
return (
<div className="game-container">
{grid.map((row, x) => row.map((cell, y) => (<div key={`${x}-${y}`} onClick={() => toggleCell(x, y)} className={`cell ${cell ? 'alive' : ''}`} style={{ width: cellSize, height: cellSize }} />)))}
</div>
);
- We apply the
alive
class to cells that are alive to style them differently.
className={`cell ${cell ? 'alive' : ''}`}
- We export the
GameOfLife
component as the default export.
export default GameOfLife;
Styling
To style the Game of Life simulation, you can add the following CSS code to your project:
.alive {
background-color: #808080;
}
.game-container {
display: grid;
grid-template-columns: repeat(auto-fill, 5px);
gap: 0px;
background-color: #121212;
height: 100vh;
}
body {
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
display: none;
}
The whole code for the Game of Life simulation component looks like this:
import { useState, useEffect } from 'react';
const GameOfLife = () => {
const cellSize = 5;
const columns = Math.floor(window.innerWidth / cellSize);
const rows = Math.floor(window.innerHeight / cellSize);
const generateEmptyGrid = (rows, columns) => {
return Array.from({ length: rows }, () => Array(columns).fill(false));
};
const initializeRandomCells = (grid) => {
grid.forEach((row, x) => {
grid[x] = row.map(() => Math.random() > 0.4);
});
};
const countNeighbors = (grid, x, y) => {
let count = 0;
const directions = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
for (const [dx, dy] of directions) {
const newX = x + dx;
const newY = y + dy;
if (newX >= 0 && newX < grid.length && newY >= 0 && newY < grid[0].length) {
count += grid[newX][newY] ? 1 : 0;
}
}
return count;
};
const [gridSize, setGridSize] = useState({ rows, columns });
useEffect(() => {
const handleResize = () => {
const newColumns = Math.floor(window.innerWidth / cellSize);
const newRows = Math.floor(window.innerHeight / cellSize);
setGridSize({ rows: newRows, columns: newColumns });
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [cellSize]);
const [grid, setGrid] = useState(() => {
const initialGrid = generateEmptyGrid(gridSize.rows, gridSize.columns);
initializeRandomCells(initialGrid);
return initialGrid;
});
useEffect(() => {
const updateGrid = () => {
setGrid((prevGrid) => {
return prevGrid.map((row, x) =>
row.map((cell, y) => {
const neighbors = countNeighbors(prevGrid, x, y);
if (cell && (neighbors < 2 || neighbors > 3)) {
return false;
} else if (!cell && neighbors === 3) {
return true;
} else {
return cell;
}
})
);
});
};
const intervalId = setInterval(() => {
updateGrid();
}, 100);
return () => clearInterval(intervalId);
}, []);
const toggleCell = (x, y) => {
setGrid((prevGrid) => {
const newGrid = [...prevGrid];
newGrid[x] = [...prevGrid[x]];
newGrid[x][y] = !newGrid[x][y];
return newGrid;
});
};
return (
<div className="game-container">
{grid.map((row, x) => row.map((cell, y) => (<div key={`${x}-${y}`} onClick={() => toggleCell(x, y)} className={`cell ${cell ? 'alive' : ''}`} style={{ width: cellSize, height: cellSize }} />)))}
</div>
);
};
export default GameOfLife;
You can see the live demo of the Game of Life simulation here.
That's it! We have successfully implemented the Game of Life simulation using React. You can now experiment with different patterns and see how they evolve over time.
Happy coding! 🚀