AI Cellular Automaton Lab - Conway's Game of Life Classic cellular automaton simulation! Click or d

This sketch implements Conway's Game of Life, a classic cellular automaton where cells live or die based on neighbor counts. Click or drag to toggle cells, press spacebar to start/pause the simulation, and watch patterns evolve on a neon green grid against a dark background.

๐ŸŽ“ Concepts You'll Learn

2D arraysCellular automatonConway's rulesGrid-based simulationMouse interactionKeyboard inputWindow resizingState management

๐Ÿ”„ Code Flow

Code flow showing setup, initgrid, make2darray, draw, stepgameoflife, drawcells, drawgridlines, mousepressed, mousedragged, mousereleased, togglecellat, keypressed, windowresized

๐Ÿ’ก Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> initgrid[initgrid] initgrid --> make2darray[make2darray] make2darray --> outerloop[outer-loop] outerloop --> innerloop[inner-loop] innerloop --> make2darray setup --> draw[draw loop] draw --> runningcheck[running-check] runningcheck -->|true| stepgameoflife[stepgameoflife] stepgameoflife --> gridloop[grid-loop] gridloop --> neighborcount[neighbor-count] neighborcount --> boundarycheck[boundary-check] boundarycheck -->|valid| deathrule[death-rule] boundarycheck -->|valid| birthrule[birth-rule] deathrule --> gridswap[grid-swap] birthrule --> gridswap gridswap --> gridloop gridloop --> drawcells[drawcells] drawcells --> cellloop[cell-loop] cellloop --> alivecheck[alive-check] alivecheck -->|alive| drawcells drawcells --> drawgridlines[drawgridlines] drawgridlines --> verticallines[vertical-lines] verticallines --> horizontalLines[horizontal-lines] horizontalLines --> draw draw --> mousepressed[mousepressed] mousepressed --> canvasboundary[canvas-boundary] canvasboundary -->|inside| pixeltoGrid[pixel-to-grid] pixeltoGrid --> dragprevention[drag-prevention] dragprevention -->|not toggled| togglecellat[togglecellat] togglecellat --> draw draw --> mousedragged[mousedragged] mousedragged --> canvasboundary mousedragged --> pixeltoGrid mousedragged --> dragprevention draw --> mousereleased[mousereleased] mousereleased --> draw draw --> keypressed[keypressed] keypressed --> spacebarcheck[spacebar-check] spacebarcheck -->|spacebar| runningcheck draw --> windowresized[windowresized] windowresized --> initgrid click setup href "#fn-setup" click initgrid href "#fn-initgrid" click make2darray href "#fn-make2darray" click draw href "#fn-draw" click stepgameoflife href "#fn-stepgameoflife" click drawcells href "#fn-drawcells" click drawgridlines href "#fn-drawgridlines" click mousepressed href "#fn-mousepressed" click mousedragged href "#fn-mousedragged" click mousereleased href "#fn-mousereleased" click togglecellat href "#fn-togglecellat" click keypressed href "#fn-keypressed" click windowresized href "#fn-windowresized" click outerloop href "#sub-outer-loop" click innerloop href "#sub-inner-loop" click runningcheck href "#sub-running-check" click gridloop href "#sub-grid-loop" click neighborcount href "#sub-neighbor-count" click boundarycheck href "#sub-boundary-check" click deathrule href "#sub-death-rule" click birthrule href "#sub-birth-rule" click gridswap href "#sub-grid-swap" click cellloop href "#sub-cell-loop" click alivecheck href "#sub-alive-check" click verticallines href "#sub-vertical-lines" click horizontalLines href "#sub-horizontal-lines" click canvasboundary href "#sub-canvas-boundary" click pixeltoGrid href "#sub-pixel-to-grid" click dragprevention href "#sub-drag-prevention" click togglecellat href "#sub-toggle-logic" click spacebarcheck href "#sub-spacebar-check"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It's the perfect place to initialize your canvas and set up data structures like grids and arrays.

function setup() {
  createCanvas(windowWidth, windowHeight);
  initGrid();
}

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window, allowing the Game of Life to use all available space
initGrid()
Calls the initialization function to create and populate the 2D grid arrays with starting values

initGrid()

This function sets up two grids: one for the current state and one for calculating the next state. This double-buffering approach is essential in cellular automata to ensure all cells update simultaneously.

function initGrid() {
  cols = floor(width / cellSize);
  rows = floor(height / cellSize);

  grid = make2DArray(cols, rows, 0);
  nextGrid = make2DArray(cols, rows, 0);
}

Line by Line:

cols = floor(width / cellSize)
Calculates how many columns fit in the canvas width by dividing total width by cell size and rounding down
rows = floor(height / cellSize)
Calculates how many rows fit in the canvas height by dividing total height by cell size and rounding down
grid = make2DArray(cols, rows, 0)
Creates the main grid array filled with 0s (dead cells) using the helper function
nextGrid = make2DArray(cols, rows, 0)
Creates a second grid to store the next generation's state, preventing conflicts when updating cells

make2DArray(cols, rows, initialValue)

This helper function creates a 2D array (array of arrays) which represents a grid. Each arr[x][y] represents a cell at column x and row y. The nested loops ensure every position is initialized.

function make2DArray(cols, rows, initialValue) {
  const arr = new Array(cols);
  for (let x = 0; x < cols; x++) {
    arr[x] = new Array(rows);
    for (let y = 0; y < rows; y++) {
      arr[x][y] = initialValue;
    }
  }
  return arr;
}

๐Ÿ”ง Subcomponents:

for-loop Column Creation Loop for (let x = 0; x < cols; x++)

Iterates through each column and creates a new array for that column

for-loop Row Initialization Loop for (let y = 0; y < rows; y++)

Fills each cell in the column with the initial value

Line by Line:

const arr = new Array(cols)
Creates a new array with length equal to the number of columns
for (let x = 0; x < cols; x++)
Loops through each column index from 0 to cols-1
arr[x] = new Array(rows)
Creates a new array at each column position to hold the row values
for (let y = 0; y < rows; y++)
Loops through each row index from 0 to rows-1
arr[x][y] = initialValue
Sets each cell in the 2D array to the initial value (0 for dead, 1 for alive)
return arr
Returns the completed 2D array ready to be used as a grid

draw()

draw() runs 60 times per second. It's the main animation loop where you clear the background, update your simulation, and redraw everything. The order matters: background first, then updates, then drawing.

function draw() {
  background(5, 5, 15);

  if (running) {
    stepGameOfLife();
  }

  drawCells();
  drawGridLines();
}

๐Ÿ”ง Subcomponents:

conditional Simulation Running Check if (running) { stepGameOfLife(); }

Only advances the simulation one generation when the spacebar has been pressed to start it

Line by Line:

background(5, 5, 15)
Clears the canvas with a very dark blue-black color, creating the dark background effect
if (running) { stepGameOfLife(); }
Checks if the simulation is running; if true, calculates the next generation using Conway's rules
drawCells()
Draws all live cells (value 1) as neon green rectangles on the grid
drawGridLines()
Draws the grid lines to show cell boundaries

stepGameOfLife()

This is the heart of Conway's Game of Life. For each cell, it counts live neighbors and applies three rules: (1) Live cells with 2-3 neighbors survive, (2) Dead cells with exactly 3 neighbors are born, (3) All other cells die or stay dead. The grid swap ensures all cells update simultaneously based on the previous generation.

function stepGameOfLife() {
  for (let x = 0; x < cols; x++) {
    for (let y = 0; y < rows; y++) {
      const state = grid[x][y];
      let neighbors = 0;

      for (let i = -1; i <= 1; i++) {
        for (let j = -1; j <= 1; j++) {
          if (i === 0 && j === 0) continue;
          const nx = x + i;
          const ny = y + j;
          if (nx >= 0 && nx < cols && ny >= 0 && ny < rows) {
            neighbors += grid[nx][ny];
          }
        }
      }

      if (state === 1 && (neighbors < 2 || neighbors > 3)) {
        nextGrid[x][y] = 0;
      } else if (state === 0 && neighbors === 3) {
        nextGrid[x][y] = 1;
      } else {
        nextGrid[x][y] = state;
      }
    }
  }

  const temp = grid;
  grid = nextGrid;
  nextGrid = temp;
}

๐Ÿ”ง Subcomponents:

for-loop Grid Iteration for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++)

Loops through every cell in the grid

for-loop Neighbor Counting for (let i = -1; i <= 1; i++) { for (let j = -1; j <= 1; j++)

Checks all 8 surrounding cells and counts how many are alive

conditional Boundary Check if (nx >= 0 && nx < cols && ny >= 0 && ny < rows)

Ensures we don't count cells outside the grid boundaries

conditional Death Rule if (state === 1 && (neighbors < 2 || neighbors > 3))

Live cell dies from underpopulation (< 2 neighbors) or overpopulation (> 3 neighbors)

conditional Birth Rule else if (state === 0 && neighbors === 3)

Dead cell becomes alive if it has exactly 3 live neighbors

calculation Grid Swap const temp = grid; grid = nextGrid; nextGrid = temp;

Exchanges the current grid with the next grid to prepare for the next generation

Line by Line:

for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++)
Nested loops that iterate through every cell in the grid, checking each one
const state = grid[x][y]
Stores the current state of the cell (0 = dead, 1 = alive)
let neighbors = 0
Initialize a counter to count how many live neighbors this cell has
for (let i = -1; i <= 1; i++) { for (let j = -1; j <= 1; j++)
Nested loops that check all 8 surrounding cells (offsets from -1 to 1 in both directions)
if (i === 0 && j === 0) continue
Skips the center cell itself, only counting the 8 neighbors
const nx = x + i; const ny = y + j
Calculates the coordinates of the neighbor cell
if (nx >= 0 && nx < cols && ny >= 0 && ny < rows)
Checks that the neighbor coordinates are within grid boundaries (no wrapping at edges)
neighbors += grid[nx][ny]
Adds the neighbor's state (0 or 1) to the neighbor count
if (state === 1 && (neighbors < 2 || neighbors > 3)) { nextGrid[x][y] = 0; }
Conway's rule: live cell dies if it has fewer than 2 or more than 3 neighbors
else if (state === 0 && neighbors === 3) { nextGrid[x][y] = 1; }
Conway's rule: dead cell becomes alive if it has exactly 3 neighbors
else { nextGrid[x][y] = state; }
Otherwise, the cell stays in its current state
const temp = grid; grid = nextGrid; nextGrid = temp;
Swaps the grids so nextGrid becomes the current grid for the next frame

drawCells()

This function only draws the live cells. It loops through the entire grid but only creates rectangles for cells with value 1. The multiplication by cellSize converts grid indices to pixel positions on the canvas.

function drawCells() {
  noStroke();
  fill(0, 255, 140);

  for (let x = 0; x < cols; x++) {
    for (let y = 0; y < rows; y++) {
      if (grid[x][y] === 1) {
        rect(x * cellSize, y * cellSize, cellSize, cellSize);
      }
    }
  }
}

๐Ÿ”ง Subcomponents:

for-loop Cell Drawing Loop for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++)

Iterates through every cell in the grid

conditional Alive Cell Check if (grid[x][y] === 1)

Only draws rectangles for cells that are alive (value = 1)

Line by Line:

noStroke()
Disables drawing the outline of rectangles, so cells appear as solid filled squares
fill(0, 255, 140)
Sets the fill color to neon green (RGB: 0 red, 255 green, 140 blue)
for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++)
Nested loops that check every cell in the grid
if (grid[x][y] === 1)
Checks if the cell is alive (value equals 1)
rect(x * cellSize, y * cellSize, cellSize, cellSize)
Draws a square at the cell's grid position, multiplying by cellSize to convert grid coordinates to pixel coordinates

drawGridLines()

This function draws the grid lines that separate cells. The +0.5 offset ensures crisp, anti-aliased lines. The loops go to cols+1 and rows+1 to draw the borders of the grid, not just between cells.

function drawGridLines() {
  stroke(25, 40, 40);
  strokeWeight(1);

  const w = cols * cellSize;
  const h = rows * cellSize;

  for (let x = 0; x <= cols; x++) {
    line(x * cellSize + 0.5, 0, x * cellSize + 0.5, h);
  }

  for (let y = 0; y <= rows; y++) {
    line(0, y * cellSize + 0.5, w, y * cellSize + 0.5);
  }
}

๐Ÿ”ง Subcomponents:

for-loop Vertical Grid Lines for (let x = 0; x <= cols; x++)

Draws vertical lines to separate columns

for-loop Horizontal Grid Lines for (let y = 0; y <= rows; y++)

Draws horizontal lines to separate rows

Line by Line:

stroke(25, 40, 40)
Sets the line color to a very dark blue-gray, making grid lines subtle against the dark background
strokeWeight(1)
Sets the line thickness to 1 pixel
const w = cols * cellSize; const h = rows * cellSize
Calculates the total width and height of the grid in pixels
for (let x = 0; x <= cols; x++)
Loops from 0 to cols (inclusive), creating cols+1 vertical lines
line(x * cellSize + 0.5, 0, x * cellSize + 0.5, h)
Draws a vertical line at x position, from top to bottom of the grid; +0.5 offsets for crisp rendering
for (let y = 0; y <= rows; y++)
Loops from 0 to rows (inclusive), creating rows+1 horizontal lines
line(0, y * cellSize + 0.5, w, y * cellSize + 0.5)
Draws a horizontal line at y position, from left to right of the grid

mousePressed()

mousePressed() is called once when the mouse button is pressed. It's used here to allow single-click toggling of cells.

function mousePressed() {
  toggleCellAt(mouseX, mouseY);
}

Line by Line:

toggleCellAt(mouseX, mouseY)
Calls the toggle function with the current mouse position to flip the cell at that location

mouseDragged()

mouseDragged() is called repeatedly while the mouse button is held down and moved. This enables drag-to-draw functionality for creating patterns.

function mouseDragged() {
  toggleCellAt(mouseX, mouseY);
}

Line by Line:

toggleCellAt(mouseX, mouseY)
Calls the toggle function continuously while the mouse is dragged, allowing the user to draw patterns by dragging

mouseReleased()

mouseReleased() is called when the mouse button is released. It resets the lastCol and lastRow variables so the next drag operation starts fresh.

function mouseReleased() {
  lastCol = -1;
  lastRow = -1;
}

Line by Line:

lastCol = -1; lastRow = -1
Resets the tracking variables to -1, clearing the memory of the last toggled cell

toggleCellAt(mx, my)

This function handles the core interaction logic. It converts pixel coordinates to grid indices, prevents duplicate toggles during dragging, and flips cell states. The ternary operator (? :) is a compact way to toggle between 0 and 1.

function toggleCellAt(mx, my) {
  if (mx < 0 || mx >= width || my < 0 || my >= height) return;

  const col = floor(mx / cellSize);
  const row = floor(my / cellSize);

  if (col === lastCol && row === lastRow) return;

  if (col >= 0 && col < cols && row >= 0 && row < rows) {
    grid[col][row] = grid[col][row] ? 0 : 1;
    lastCol = col;
    lastRow = row;
  }
}

๐Ÿ”ง Subcomponents:

conditional Canvas Boundary Check if (mx < 0 || mx >= width || my < 0 || my >= height) return;

Exits early if the click is outside the canvas

calculation Pixel to Grid Conversion const col = floor(mx / cellSize); const row = floor(my / cellSize);

Converts pixel coordinates to grid cell indices

conditional Drag Prevention if (col === lastCol && row === lastRow) return;

Prevents toggling the same cell multiple times during a single drag motion

conditional Grid Boundary Check if (col >= 0 && col < cols && row >= 0 && row < rows)

Ensures the cell is within valid grid bounds

calculation Toggle Cell State grid[col][row] = grid[col][row] ? 0 : 1;

Flips the cell between alive (1) and dead (0)

Line by Line:

if (mx < 0 || mx >= width || my < 0 || my >= height) return;
Checks if the mouse position is outside the canvas; if so, exits the function early to avoid errors
const col = floor(mx / cellSize)
Converts the mouse's pixel x-coordinate to a grid column index by dividing by cell size and rounding down
const row = floor(my / cellSize)
Converts the mouse's pixel y-coordinate to a grid row index by dividing by cell size and rounding down
if (col === lastCol && row === lastRow) return;
Checks if this is the same cell as the last toggle; if so, exits to prevent toggling the same cell repeatedly during a drag
if (col >= 0 && col < cols && row >= 0 && row < rows)
Verifies the cell is within the valid grid boundaries
grid[col][row] = grid[col][row] ? 0 : 1;
Toggles the cell: if it's 1 (alive), set to 0 (dead); if it's 0 (dead), set to 1 (alive)
lastCol = col; lastRow = row;
Stores the current cell's coordinates to prevent re-toggling during the same drag motion

keyPressed()

keyPressed() is called whenever a key is pressed. The spacebar toggles the running state to pause/resume the simulation. Returning false prevents the browser's default spacebar action (scrolling the page).

function keyPressed() {
  if (key === " ") {
    running = !running;
    return false;
  }
}

๐Ÿ”ง Subcomponents:

conditional Spacebar Detection if (key === " ")

Checks if the pressed key is the spacebar

Line by Line:

if (key === " ")
Checks if the key pressed is a spacebar (space character)
running = !running
Toggles the running state: if true becomes false, if false becomes true, pausing or resuming the simulation
return false
Returns false to prevent the default browser behavior (page scrolling) when spacebar is pressed

windowResized()

windowResized() is called automatically whenever the browser window is resized. It updates the canvas size and recreates the grid to match, ensuring the sketch adapts to different screen sizes. The grid is reset to empty (all 0s).

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  initGrid();
}

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Resizes the p5.js canvas to match the new window dimensions
initGrid()
Reinitializes the grid with new dimensions based on the resized canvas

๐Ÿ“ฆ Key Variables

cellSize number

Stores the width and height of each cell in pixels. Controls the granularity of the grid.

let cellSize = 14;
cols number

Stores the number of columns in the grid, calculated by dividing canvas width by cellSize

cols = floor(width / cellSize);
rows number

Stores the number of rows in the grid, calculated by dividing canvas height by cellSize

rows = floor(height / cellSize);
grid array

2D array storing the current generation state. grid[x][y] = 1 means alive, 0 means dead

grid = make2DArray(cols, rows, 0);
nextGrid array

2D array storing the next generation state while the current generation is being evaluated. Prevents simultaneous updates.

nextGrid = make2DArray(cols, rows, 0);
running boolean

Tracks whether the simulation is playing (true) or paused (false). Toggled by pressing spacebar.

let running = false;
lastCol number

Stores the column index of the last toggled cell during dragging to prevent re-toggling the same cell

let lastCol = -1;
lastRow number

Stores the row index of the last toggled cell during dragging to prevent re-toggling the same cell

let lastRow = -1;

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change cellSize from 14 to 20 to make cells bigger and the grid coarser. Notice how this changes the number of columns and rows.
  2. Modify the fill color in drawCells() from fill(0, 255, 140) to fill(255, 0, 255) to make cells magenta instead of neon green.
  3. Change the background color in draw() from background(5, 5, 15) to background(20, 20, 40) to make the background slightly lighter.
  4. Try changing the neighbor counting rules in stepGameOfLife(). For example, change neighbors < 2 to neighbors < 1 to make cells survive with only 1 neighbor.
  5. Add a button to clear the grid by setting all cells to 0. You could add a function like: function clearGrid() { for (let x = 0; x < cols; x++) { for (let y = 0; y < rows; y++) { grid[x][y] = 0; } } } and call it with a key press.
  6. Create a pattern manually by clicking and dragging to draw a 'glider' (a small pattern that moves across the grid), then press spacebar to watch it move.
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

BUG toggleCellAt function

The boundary check at the start only checks canvas boundaries, but then checks grid boundaries again. If cellSize doesn't divide evenly into width/height, there could be unused pixel space.

๐Ÿ’ก The current implementation is actually fine, but you could add a comment explaining why both checks exist. The first check prevents errors, the second ensures grid validity.

PERFORMANCE stepGameOfLife function

The function loops through every cell every frame, even if the simulation is paused. While not a major issue, the loop runs but the results aren't used.

๐Ÿ’ก The current code already prevents this by checking 'if (running)' in draw() before calling stepGameOfLife(), so this is actually well-optimized.

STYLE make2DArray function

The function uses 'const arr' but could be more descriptive about what it's creating

๐Ÿ’ก Consider renaming to 'const grid' or 'const newArray' to make the purpose clearer, or add a comment explaining it creates a 2D array structure.

FEATURE sketch.js global scope

There's no way to adjust cellSize or speed without editing the code. Users can't customize the simulation parameters.

๐Ÿ’ก Add keyboard shortcuts like pressing '+' to increase cellSize or '-' to decrease it. You could also add a speed variable to control how often stepGameOfLife() is called.

FEATURE sketch.js

No way to save or load patterns. Users must manually draw patterns each time.

๐Ÿ’ก Add functions to export the current grid as a string and import it back, or add preset patterns like 'glider', 'blinker', or 'block' that can be placed with a key press.

STYLE drawGridLines function

The +0.5 offset for crisp rendering is not explained in comments, which might confuse beginners

๐Ÿ’ก Add a comment explaining: '// +0.5 offset prevents blurry lines by aligning to pixel centers' to help learners understand this rendering technique.

Preview

AI Cellular Automaton Lab - Conway's Game of Life Classic cellular automaton simulation! Click or d - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Cellular Automaton Lab - Conway's Game of Life Classic cellular automaton simulation! Click or d - Code flow showing setup, initgrid, make2darray, draw, stepgameoflife, drawcells, drawgridlines, mousepressed, mousedragged, mousereleased, togglecellat, keypressed, windowresized
Code Flow Diagram