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();
});