5 min min read • Loading views • Jul 05, 2024

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.

GoL

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:

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. 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:

  1. Any live cell with two or three live neighbours survives.
  2. Any dead cell with three live neighbours becomes a live cell.
  3. 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:

  1. We start by importing the useState and useEffect hooks from React.
import { useState, useEffect } from 'react';
  1. We define the GameOfLife component that will contain the logic for the simulation.
const GameOfLife = () => {
    // Component logic goes here
};
  1. 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);
  1. We define a helper function generateEmptyGrid that creates a 2D array filled with false values.
const generateEmptyGrid = (rows, columns) => {
    return Array.from({ length: rows }, () => Array(columns).fill(false));
};
  1. 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);
    });
};
  1. 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;
};
  1. 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 });
  1. 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]);
  1. 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;
});
  1. 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);
}, []);
  1. 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;
    });
};
  1. We render the grid of cells and attach the toggleCell function to the onClick 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>
);
  1. We apply the alive class to cells that are alive to style them differently.
className={`cell ${cell ? 'alive' : ''}`}
  1. 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! 🚀