<div id="root"></div>
/*

Rules
1. Born: If three neighbours are alive.
2. Dies: If cell has less than 2 neighbours or more than 3 neighbours

*/
body {
  font-family:"Inter", system-ui, sans-serif;
  position:relative;
  margin:0;
  padding:0;
}
.flex {
  display:flex;
  place-items:center;
}
.cell {
  border:1px solid #aaa;
  aspect-ratio: 1 / 1;
  width:1.25rem;
  height:1.25rem;
  display:inline-block;
  line-height:1rem;
}
.cell.editable {
  cursor:pointer;
}
.cell.editable:hover {
  background:lightblue;
}
.cell.alive, .cell.alive:hover {
  background:blue;
}

header{
  position:fixed;
  top:10px; 
  left:10px;
  background:white;
  padding:1rem;
  border:1px solid #ccc;
  border-radius:4px;
  box-shadow: 0 0 4px 4px #ddd;
}
header + div {
  margin-top:7.25rem;
}
.opts {
  gap:1rem;
  font-size:14px;
  width:max-content;
}
p {
  margin-top:0;
  width:max-content;
  font-weight:600;
}
.opts > div {
  border-right:1px solid black;
  padding-right:1rem;
  width:max-content;
}
input[type="number"] {
  width:60px;
}
input {
  padding-block:0.15rem;
  padding-left:0.5rem;
}
button {
  border:none;
  background: #2563eb;
  color: white;
  border-radius:4px;
  padding:0.35rem 0.75rem;
  cursor:pointer;
  font-weight:500;
  font-family:"Inter", system-ui, sans-serif;
}
button:hover{
  background:#1d4ed8;
}
/*
 * Authored by: Gauravjot Garaya
 * Date: 2023-12-13
*/

// You may add your patterns to below object

const patterns = {
  glider: new Set([
    JSON.stringify([11,10]),
    JSON.stringify([12,11]),
    JSON.stringify([10,12]),
    JSON.stringify([11,12]),
    JSON.stringify([12,12]),
  ]),
  toad: new Set([
    JSON.stringify([11, 11]),
    JSON.stringify([12, 11]),
    JSON.stringify([13, 11]),
    JSON.stringify([10, 12]),
    JSON.stringify([11, 12]),
    JSON.stringify([12, 12]),
  ]),
  beacon: new Set([
    JSON.stringify([10, 10]),
    JSON.stringify([11, 10]),
    JSON.stringify([10, 11]),
    JSON.stringify([13, 12]),
    JSON.stringify([12, 13]),
    JSON.stringify([13, 13]),
  ]),
  gun: new Set([
    JSON.stringify([1, 5]),
    JSON.stringify([1, 6]),
    JSON.stringify([2, 5]),
    JSON.stringify([2, 6]),

    JSON.stringify([11, 5]),
    JSON.stringify([11, 6]),
    JSON.stringify([11, 7]),
    JSON.stringify([12, 4]),
    JSON.stringify([12, 8]),
    JSON.stringify([13, 3]),
    JSON.stringify([14, 3]),
    JSON.stringify([13, 9]),
    JSON.stringify([14, 9]),
    JSON.stringify([15, 6]),

    JSON.stringify([16, 4]),
    JSON.stringify([16, 8]),
    JSON.stringify([17, 5]),
    JSON.stringify([17, 6]),
    JSON.stringify([17, 7]),
    JSON.stringify([18, 6]),

    JSON.stringify([21, 3]),
    JSON.stringify([21, 4]),
    JSON.stringify([21, 5]),
    JSON.stringify([22, 3]),
    JSON.stringify([22, 4]),
    JSON.stringify([22, 5]),
    JSON.stringify([23, 2]),
    JSON.stringify([23, 6]),
    JSON.stringify([25, 1]),
    JSON.stringify([25, 2]),
    JSON.stringify([25, 6]),
    JSON.stringify([25, 7]),

    JSON.stringify([35, 3]),
    JSON.stringify([35, 4]),
    JSON.stringify([36, 3]),
    JSON.stringify([36, 4]),
  ]),
}

// Probably no need to change code below as settings are configurable in UI
const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <React.StrictMode>
    <Board />
  </React.StrictMode>
);

function Board() {
  const [width, setWidth] = React.useState(80);
  const [height, setHeight] = React.useState(80);
  const [speed, setSpeed] = React.useState(101);
  const [activeSquares, setActiveSquares] = React.useState(patterns.glider);
  const [isDrawMode, setIsDrawMode] = React.useState(false);
  
  let timedLoop;
  
  React.useEffect(()=> {
    // Calculate each cell
    timedLoop = setTimeout(()=> {
      if (!isDrawMode) {
        let nextgen = new Set();
        for (let i = 0; i < height; i++) {
          for (let j = 0; j < width; j++) {
            // Simply check the number of neighbours to this cell
            let count = 0;
            if (isSquareActive(j-1,i-1)) count++;
            if (isSquareActive(j,i-1)) count++;
            if (isSquareActive(j+1,i-1)) count++;
            if (isSquareActive(j-1,i)) count++;
            if (isSquareActive(j+1,i)) count++;
            if (isSquareActive(j-1,i+1)) count++;
            if (isSquareActive(j,i+1)) count++;
            if (isSquareActive(j+1,i+1)) count++;
            // Check if cell is alive or not
            if (isSquareActive(j,i)) {
              // Check if it be dead in next generation
              if (count < 2 || count > 3) {
                // dies
              } else {
                // it stays alive
                nextgen.add(JSON.stringify([j,i]))
              }
            } else {
              // Check if this square can be brought to life
              if (count === 3) {
                // born
                nextgen.add(JSON.stringify([j,i]))
              }
            }
          }
        }
        setActiveSquares(nextgen);
      }
    }, speed);
    
    return () => {
      clearTimeout(timedLoop);
    }
  });
  
  function isSquareActive(x,y) {
    return activeSquares.has(JSON.stringify([x,y]))
  }
  
  const onOptionChangeHandler = (event) => {
    clearTimeout(timedLoop); // Clears any previous pattern calculations in progress
    setActiveSquares(patterns[event.target.value]);
  };
  
  const onSpeedChangeHandler = (event) => {
    // Clears any previous pattern calculations in progress
    // since they are set at old speed
    clearTimeout(timedLoop);
    setSpeed(event.target.value);
  }
  
  const setDrawMode = () => {
    if (isDrawMode) {
      // Play the animation
      setIsDrawMode(false);
    } else {
      // Clear all the cells for drawing
      clearTimeout(timedLoop);
      setActiveSquares(new Set());
      setIsDrawMode(true);
    }
  }
  
  const cellDraw = (j,i) => {
    if (isDrawMode) {
      let newBoard = new Set(activeSquares);
      if (isSquareActive(j,i)) {
        newBoard.delete(JSON.stringify([j,i]));
      } else {
        newBoard.add(JSON.stringify([j,i]));
      }
      setActiveSquares(newBoard);
    }
  };
  
  return <>
    <header>
      <p>Conway's Game of Life</p>
      <div class="opts flex">
        <div>Grid : <input type="number" min="30" max="1000" value={width} onChange={(e) => setWidth(e.target.value)} /> X <input type="number" min="30" max="1000" value={height} onChange={(e) => setHeight(e.target.value)} /></div>
        <div>Pattern: <select onChange={onOptionChangeHandler}>{Object.keys(patterns).map((k)=><option key={k}>{k}</option>)}</select></div>
        <div>Speed: <input type="number" onChange={onSpeedChangeHandler} value={speed} min="1" max="2000" step="10"/> ms (lower is faster)</div>
        <button onClick={setDrawMode}>{isDrawMode ? "Play" : "Draw Custom"}</button>
      </div>
    </header>
    {Array.from({ length: height }, (_, i) => 
      {
        return <div className="flex">
          {
            Array.from({ length: width }, (_, j) => 
                       <Cell 
                         alive={activeSquares.has(JSON.stringify([j,i]))} 
                         clickFn={cellDraw} 
                         x={j} 
                         y={i} 
                         isEditable={isDrawMode}
                         />)
          }
        </div>
      }
    )}</>
}

function Cell({alive, clickFn, x, y, isEditable}) {
  return (
    <div onClick={()=>clickFn(x,y)} className={(alive ? "alive" : "") + (isEditable ? " editable" : "") + " cell"}></div>
  )
}
View Compiled
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/inter-ui/3.19.3/inter.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js