Physics Lab

Game of Life

Interactive Conway's Game of Life and cellular automata simulator with preset patterns and multiple rule sets.

Click to toggle cells · Drag to paint · Right-drag to erase

Simulation

Statistics

0
Generation
0
Live Cells
0
Births/Gen
0
Deaths/Gen
Idle

Rules

B3/S23 — Classic Conway's Game of Life

STILL LIFES
OSCILLATORS
SPACESHIPS
GUNS & METHUSELAHS

B36/S23 — Has replicators and exciting patterns

B3678/S34678 — Complex emergent behavior

B2/S — Explosive growth from every pattern

B3/S12345 — Generates maze-like structures

Display

1.0×
4px

Fill

30%
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// Game of Life Engine
    class GameOfLife {
      constructor(width, height, birthRule = [3], survivalRule = [2, 3]) {
        this.width = width;
        this.height = height;
        this.birthRule = birthRule;
        this.survivalRule = survivalRule;
        this.grid = Array(height).fill(null).map(() => Array(width).fill(0));
        this.ageGrid = Array(height).fill(null).map(() => Array(width).fill(0));
        this.generation = 0;
        this.toroidal = true;
        this.lastBirths = 0;
        this.lastDeaths = 0;
      }

      clear() {
        this.grid = Array(this.height).fill(null).map(() => Array(this.width).fill(0));
        this.ageGrid = Array(this.height).fill(null).map(() => Array(this.width).fill(0));
        this.generation = 0;
      }

      randomize(density = 30) {
        this.clear();
        for (let y = 0; y < this.height; y++) {
          for (let x = 0; x < this.width; x++) {
            if (Math.random() * 100 < density) {
              this.grid[y][x] = 1;
              this.ageGrid[y][x] = 1;
            }
          }
        }
      }

      setCell(x, y, alive) {
        if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
          this.grid[y][x] = alive ? 1 : 0;
          this.ageGrid[y][x] = alive ? 1 : 0;
        }
      }

      getCell(x, y) {
        if (!this.toroidal) {
          if (x < 0 || x >= this.width || y < 0 || y >= this.height) return 0;
        }
        x = ((x % this.width) + this.width) % this.width;
        y = ((y % this.height) + this.height) % this.height;
        return this.grid[y][x];
      }

      countNeighbors(x, y) {
        let count = 0;
        for (let dy = -1; dy <= 1; dy++) {
          for (let dx = -1; dx <= 1; dx++) {
            if (dx === 0 && dy === 0) continue;
            count += this.getCell(x + dx, y + dy);
          }
        }
        return count;
      }

      step() {
        const newGrid = Array(this.height).fill(null).map(() => Array(this.width).fill(0));
        const newAgeGrid = Array(this.height).fill(null).map(() => Array(this.width).fill(0));
        let births = 0, deaths = 0;

        for (let y = 0; y < this.height; y++) {
          for (let x = 0; x < this.width; x++) {
            const neighbors = this.countNeighbors(x, y);
            const isAlive = this.grid[y][x];

            let survives = false;
            if (isAlive && this.survivalRule.includes(neighbors)) {
              survives = true;
              newGrid[y][x] = 1;
              newAgeGrid[y][x] = this.ageGrid[y][x] + 1;
            } else if (!isAlive && this.birthRule.includes(neighbors)) {
              survives = true;
              newGrid[y][x] = 1;
              newAgeGrid[y][x] = 1;
              births++;
            }

            if (isAlive && !survives) {
              deaths++;
            }
          }
        }

        this.grid = newGrid;
        this.ageGrid = newAgeGrid;
        this.generation++;
        this.lastBirths = births;
        this.lastDeaths = deaths;
      }

      getLiveCount() {
        return this.grid.flat().filter(cell => cell === 1).length;
      }

      setRule(birthRule, survivalRule) {
        this.birthRule = birthRule;
        this.survivalRule = survivalRule;
      }
    }

    // Cellular Automata Patterns
    const PATTERNS = {
      // Still Lifes
      block: [[1, 1], [1, 1]],
      beehive: [[0, 1, 1, 0], [1, 0, 0, 1], [0, 1, 1, 0]],
      loaf: [[0, 1, 1], [1, 0, 1], [0, 1, 0]],
      boat: [[1, 1, 0], [1, 0, 1], [0, 1, 0]],

      // Oscillators
      blinker: [[1, 1, 1]],
      toad: [[0, 1, 1, 1], [1, 1, 1, 0]],
      beacon: [[1, 1, 0, 0], [1, 1, 0, 0], [0, 0, 1, 1], [0, 0, 1, 1]],
      pulsar: [
        [0,0,1,1,1,0,0,0,1,1,1,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [0,0,1,1,1,0,0,0,1,1,1,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,1,1,1,0,0,0,1,1,1,0,0],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [1,0,0,0,0,1,0,1,0,0,0,0,1],
        [0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,1,1,1,0,0,0,1,1,1,0,0]
      ],

      // Spaceships
      glider: [[0, 1, 0], [0, 0, 1], [1, 1, 1]],
      lwss: [[1, 0, 0, 1, 0], [0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 1, 0, 0, 1]],
      mwss: [[0, 1, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 1], [1, 1, 0, 1, 1]],
      hwss: [[1, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 1]],

      // Gosper Glider Gun
      gosper: [
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
        [0,0,0,0,0,0,0,1,0,0,0,0,1,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],
        [1,1,0,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [1,1,0,0,0,0,1,0,0,0,1,0,1,0,1,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
      ],

      // Methuselahs
      rpent: [[0, 1, 0], [1, 1, 0], [0, 1, 1]],
      diehard: [[0, 0, 0, 0, 1, 0, 0], [1, 1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 1, 1]],
      acorn: [[0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [1, 1, 0, 0, 1, 1, 1]],

      // HighLife
      replicator: [[1, 0, 1], [0, 1, 0], [1, 0, 1]]
    };

    // UI Controller
    class GameController {
      constructor() {
        this.canvas = document.getElementById('gameCanvas');
        this.ctx = this.canvas.getContext('2d');
        this.width = 150;
        this.height = 150;
        this.zoom = 4;
        this.game = new GameOfLife(this.width, this.height);
        this.running = false;
        this.speed = 1;
        this.coloring = 'age';
        this.wrap = 'toroidal';
        this.frameCount = 0;
        this.lastStats = { births: 0, deaths: 0, liveCells: 0 };
        this.populationHistory = [];

        this.setupCanvas();
        this.bindEvents();
        this.gameLoop();
      }

      setupCanvas() {
        this.canvas.width = this.width * this.zoom;
        this.canvas.height = this.height * this.zoom;
        this.draw();
      }

      bindEvents() {
        // Simulation buttons
        document.getElementById('playBtn').addEventListener('click', () => this.play());
        document.getElementById('pauseBtn').addEventListener('click', () => this.pause());
        document.getElementById('stepBtn').addEventListener('click', () => this.stepOnce());
        document.getElementById('clearBtn').addEventListener('click', () => this.clear());
        document.getElementById('randomBtn').addEventListener('click', () => this.randomize());
        document.getElementById('fillBtn').addEventListener('click', () => this.fillRandom());

        // Sliders
        document.getElementById('speedSlider').addEventListener('input', (e) => {
          this.speed = parseFloat(e.target.value);
          document.getElementById('speedValue').textContent = this.speed.toFixed(1);
        });

        document.getElementById('zoomSlider').addEventListener('input', (e) => {
          this.zoom = parseInt(e.target.value);
          this.setupCanvas();
          document.getElementById('zoomValue').textContent = this.zoom;
        });

        document.getElementById('fillSlider').addEventListener('input', (e) => {
          document.getElementById('fillValue').textContent = e.target.value;
        });

        // Grid size toggles
        document.querySelectorAll('[data-gridsize]').forEach(btn => {
          btn.addEventListener('click', (e) => {
            document.querySelectorAll('[data-gridsize]').forEach(b => b.classList.remove('active'));
            e.target.classList.add('active');
            const size = parseInt(e.target.dataset.gridsize);
            this.resize(size, size);
          });
        });

        // Coloring toggles
        document.querySelectorAll('[data-coloring]').forEach(btn => {
          btn.addEventListener('click', (e) => {
            document.querySelectorAll('[data-coloring]').forEach(b => b.classList.remove('active'));
            e.target.classList.add('active');
            this.coloring = e.target.dataset.coloring;
            this.draw();
          });
        });

        // Wrap mode toggles
        document.querySelectorAll('[data-wrap]').forEach(btn => {
          btn.addEventListener('click', (e) => {
            document.querySelectorAll('[data-wrap]').forEach(b => b.classList.remove('active'));
            e.target.classList.add('active');
            this.wrap = e.target.dataset.wrap;
            this.game.toroidal = (this.wrap === 'toroidal');
          });
        });

        // Rule tabs
        document.querySelectorAll('.tab').forEach(tab => {
          tab.addEventListener('click', (e) => {
            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
            document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
            e.target.classList.add('active');
            document.getElementById(e.target.dataset.tab).classList.add('active');
          });
        });

        // Pattern buttons
        document.querySelectorAll('.pattern-btn').forEach(btn => {
          btn.addEventListener('click', (e) => {
            const pattern = e.target.dataset.pattern;
            this.loadPattern(pattern);
          });
        });

        // Custom rule
        document.getElementById('applyRuleBtn').addEventListener('click', () => {
          const birth = document.getElementById('birthInput').value.split(',').map(x => parseInt(x.trim())).filter(x => !isNaN(x));
          const survival = document.getElementById('survivalInput').value.split(',').map(x => parseInt(x.trim())).filter(x => !isNaN(x));
          this.game.setRule(birth, survival);
          this.pause();
        });

        // Canvas interaction
        this.canvas.addEventListener('click', (e) => this.handleCanvasClick(e));
        this.canvas.addEventListener('mousemove', (e) => this.handleCanvasMove(e));
        this.canvas.addEventListener('contextmenu', (e) => this.handleCanvasRight(e));

        let isDrawing = false;
        let isErasing = false;

        this.canvas.addEventListener('mousedown', (e) => {
          if (e.button === 0) isDrawing = true;
          if (e.button === 2) isErasing = true;
        });

        this.canvas.addEventListener('mouseup', () => {
          isDrawing = false;
          isErasing = false;
        });

        this.canvas.addEventListener('mousemove', (e) => {
          if (!isDrawing && !isErasing) return;
          const rect = this.canvas.getBoundingClientRect();
          const x = Math.floor((e.clientX - rect.left) / this.zoom);
          const y = Math.floor((e.clientY - rect.top) / this.zoom);
          if (isDrawing) this.game.setCell(x, y, 1);
          if (isErasing) this.game.setCell(x, y, 0);
          this.draw();
        });
      }

      handleCanvasClick(e) {
        if (this.running) return;
        const rect = this.canvas.getBoundingClientRect();
        const x = Math.floor((e.clientX - rect.left) / this.zoom);
        const y = Math.floor((e.clientY - rect.top) / this.zoom);
        this.game.setCell(x, y, 1 - this.game.grid[y]?.[x]);
        this.draw();
      }

      handleCanvasMove(e) {}

      handleCanvasRight(e) {
        e.preventDefault();
      }

      play() {
        this.running = true;
      }

      pause() {
        this.running = false;
      }

      stepOnce() {
        this.game.step();
        this.updateStats();
        this.draw();
      }

      clear() {
        this.game.clear();
        this.updateStats();
        this.draw();
      }

      randomize() {
        const density = parseInt(document.getElementById('fillSlider').value);
        this.game.randomize(density);
        this.updateStats();
        this.draw();
      }

      fillRandom() {
        const density = parseInt(document.getElementById('fillSlider').value);
        this.game.randomize(density);
        this.updateStats();
        this.draw();
      }

      resize(width, height) {
        this.width = width;
        this.height = height;
        this.game = new GameOfLife(width, height, this.game.birthRule, this.game.survivalRule);
        this.game.toroidal = (this.wrap === 'toroidal');
        this.setupCanvas();
      }

      loadPattern(patternName) {
        if (!PATTERNS[patternName]) {
          console.warn('Unknown pattern:', patternName);
          return;
        }

        const pattern = PATTERNS[patternName];
        const startX = Math.floor((this.width - pattern[0].length) / 2);
        const startY = Math.floor((this.height - pattern.length) / 2);

        for (let y = 0; y < pattern.length; y++) {
          for (let x = 0; x < pattern[y].length; x++) {
            if (startY + y >= 0 && startY + y < this.height && startX + x >= 0 && startX + x < this.width) {
              this.game.setCell(startX + x, startY + y, pattern[y][x]);
            }
          }
        }

        this.pause();
        this.updateStats();
        this.draw();
      }

      updateStats() {
        const live = this.game.getLiveCount();
        this.lastStats = {
          births: this.game.lastBirths,
          deaths: this.game.lastDeaths,
          liveCells: live
        };
        this.populationHistory.push(live);
        if (this.populationHistory.length > 100) this.populationHistory.shift();

        document.getElementById('genCount').textContent = this.game.generation;
        document.getElementById('liveCells').textContent = live;
        document.getElementById('birthsCount').textContent = this.game.lastBirths;
        document.getElementById('deathsCount').textContent = this.game.lastDeaths;

        // Status indicator
        const statusInd = document.getElementById('statusInd');
        const statusText = document.getElementById('statusText');

        if (this.running) {
          if (live === 0) {
            statusInd.className = 'status-indicator dying';
            statusText.textContent = 'Extinct';
          } else if (this.game.lastBirths > this.game.lastDeaths) {
            statusInd.className = 'status-indicator growing';
            statusText.textContent = 'Growing';
          } else if (this.game.lastBirths < this.game.lastDeaths) {
            statusInd.className = 'status-indicator dying';
            statusText.textContent = 'Declining';
          } else {
            statusInd.className = 'status-indicator stable';
            statusText.textContent = 'Stable';
          }
        } else {
          statusInd.className = 'status-indicator';
          statusText.textContent = this.running ? 'Running' : 'Paused';
        }
      }

      draw() {
        const imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
        const data = imageData.data;

        for (let y = 0; y < this.height; y++) {
          for (let x = 0; x < this.width; x++) {
            const cell = this.game.grid[y][x];
            const age = this.game.ageGrid[y][x];

            let r = 10, g = 10, b = 10, a = 255;

            if (cell === 1) {
              if (this.coloring === 'age') {
                if (age === 1) {
                  r = 255; g = 255; b = 255; // New: white
                } else if (age <= 5) {
                  r = 255; g = 34; b = 0; // Hot: red
                } else if (age <= 10) {
                  r = 255; g = 136; b = 0; // Warm: orange
                } else {
                  r = 0; g = 200; b = 150; // Old: green
                }
              } else if (this.coloring === 'flat') {
                r = 255; g = 34; b = 0; // Red
              }
            }

            const pixelIdx = (y * this.zoom * this.canvas.width + x) * this.zoom * 4;
            for (let py = 0; py < this.zoom; py++) {
              for (let px = 0; px < this.zoom; px++) {
                const idx = pixelIdx + py * this.canvas.width * 4 + px * 4;
                data[idx] = r;
                data[idx + 1] = g;
                data[idx + 2] = b;
                data[idx + 3] = a;
              }
            }
          }
        }

        this.ctx.putImageData(imageData, 0, 0);
      }

      gameLoop() {
        if (this.running) {
          if (this.speed >= 1) {
            // Fast: run multiple steps per frame
            const steps = Math.round(this.speed);
            for (let i = 0; i < steps; i++) {
              this.game.step();
            }
            this.updateStats();
          } else {
            // Slow: skip frames (step every 1/speed frames)
            this.frameCount++;
            const skipFrames = Math.round(1 / this.speed);
            if (this.frameCount >= skipFrames) {
              this.game.step();
              this.frameCount = 0;
              this.updateStats();
            }
          }
        }
        this.draw();
        requestAnimationFrame(() => this.gameLoop());
      }
    }

    // Initialize on page load
    window.addEventListener('load', () => {
      new GameController();
    });