Conway's Game of Life
Conway's Game of Life is a mathematical game invented by English Mathematician John Horton Conway. The game is based on the theory of cellular automatons. It is a so-called zero-player game because once the "player" set the initial state of the automaton, the game is just about to watch the deterministic outcome of the game.
The rules are as follows:
- Living cells with 2 or 3 neighbors stay alive.
- Living cells with less than two or more than 3 neighbors die.
- Dead cells with exactly 3 neighbors are "re-born"
For more information, please see the great article on wikipedia.
My implementation
I've used pure JavaScript and a bit of JQuery to implement the Game of Life.
The code
let WIDTH = 800;
let HEIGHT = 500;
const RES = 10;
const DEAD_CELL = 0;
const LIVING_CELL = 1;
const BORDER = 2;
let grid;
let isRunning = false;
let interval;
let generation = 0;
window.addEventListener('load', function() {
if(window.innerWidth < 840) {
WIDTH = window.innerWidth - 100;
WIDTH = WIDTH - WIDTH % 100;
HEIGHT = window.innerHeight - 40;
HEIGHT = HEIGHT - HEIGHT % 100;
}
document.getElementById('canvas').height = HEIGHT;
document.getElementById('canvas').width = WIDTH;
generation = 0;
updateGeneration();
initialDraw();
});
window.addEventListener('resize', function() {
if(window.innerWidth < 840) {
WIDTH = window.innerWidth - 100;
WIDTH = WIDTH - WIDTH % 100;
HEIGHT = window.innerHeight - 40;
HEIGHT = HEIGHT - HEIGHT % 100;
}
else {
HEIGHT = 500;
WIDTH = 800;
}
document.getElementById('canvas').height = HEIGHT;
document.getElementById('canvas').width = WIDTH;
generation = 0;
updateGeneration();
initialDraw();
if(isRunning) {
clearInterval(interval);
isRunning = false;
document.getElementById('startStop').textContent = 'Start';
switchButtons();
}
});
/**
* Allows the user to draw own pattern on the canvas
*/
document.getElementById('canvas').addEventListener('click', function(e) {
// Which box did user click on?
const row = Math.floor(e.offsetX / RES);
const col = Math.floor(e.offsetY / RES);
// Don't allow changes while simulation is running
if (isRunning) {
return;
}
// Case user clicked on a dead cell
if (grid[row][col] === DEAD_CELL) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
grid[row][col] = LIVING_CELL;
drawCell('#2B823A', 'LightGray', 1, row * RES, col * RES, RES, RES);
} else if (grid[row][col] === LIVING_CELL) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
grid[row][col] = DEAD_CELL;
drawCell('white', 'LightGray', 1, row * RES, col * RES, RES, RES);
}
});
/**
* Starts / Stops the simulation and enables / disables the other buttons
*/
document.getElementById('startStop').addEventListener('click', function() {
// Case user wants to start simulation
if (!isRunning) {
interval = setInterval(function() {
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
draw(cols, rows);
// Count generation and show user
updateGeneration();
generation++;
}, (1 / document.getElementById('speed').value) * 1000);
isRunning = true;
document.getElementById('startStop').textContent = 'Stop';
switchButtons();
} else {
clearInterval(interval);
isRunning = false;
document.getElementById('startStop').textContent = 'Start';
switchButtons();
}
});
/**
* Deletes pattern from canvas
*/
document.getElementById('clear').addEventListener('click', function() {
if (!isRunning) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
createGrid(WIDTH / RES, HEIGHT / RES);
draw(WIDTH / RES, HEIGHT / RES);
}
});
/**
* Changes the speed of the simulation: The higher the value,
* the faster the simulation
*/
document.getElementById('speed').addEventListener('change', function() {
if (isRunning) {
clearInterval(interval);
interval = setInterval(function() {
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
draw(cols, rows);
// Count generation and show user
updateGeneration();
generation++;
}, (1 / document.getElementById('speed').value) * 1000);
}
});
/**
* Random pattern on the canvas
*/
document.getElementById('random').addEventListener('click', function() {
// Case user wants to start simulation
if (!isRunning) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
grid = createArray(cols, rows);
createRandom(cols, rows);
draw(cols, rows);
}
});
/**
* Creates a Lightweight spaceship
* https://www.conwaylife.com/wiki/Lightweight_spaceship
*/
document.getElementById('lwss').addEventListener('click', function() {
// Case user wants to start simulation
if (!isRunning) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
createGrid(cols, rows);
const x = Math.floor(cols / 2);
const y = Math.floor(rows / 2);
createLwss(x, y);
draw(cols, rows);
}
});
/**
* Creates a r-pentomino
* https://www.conwaylife.com/wiki/R-pentomino
*/
document.getElementById('r-pentomino').addEventListener('click', function() {
// Case user wants to start simulation
if (!isRunning) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
createGrid(cols, rows);
const x = Math.floor(cols / 2);
const y = Math.floor(rows / 2);
createRPentomino(x, y);
draw(cols, rows);
}
});
/**
* Create an acorn
* https://www.conwaylife.com/wiki/Acorn
*/
document.getElementById('acorn').addEventListener('click', function() {
// Case user wants to start simulation
if (!isRunning) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
createGrid(cols, rows);
const x = Math.floor(cols / 2);
const y = Math.floor(rows / 2);
createAcorn(x, y);
draw(cols, rows);
}
});
/**
* Creates a gosper glider gun
* https://www.conwaylife.com/wiki/Gosper_glider_gun
*/
document.getElementById('gosper').addEventListener('click', function() {
// Case user wants to start simulation
if (!isRunning) {
// Generation needs to be resetted
generation = 0;
updateGeneration();
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
createGrid(cols, rows);
const x = Math.floor(cols / 2);
const y = Math.floor(rows / 2);
createGosperGliderGun(x, y);
draw(cols, rows);
}
});
/**
* Draws the initial state of the canvas
*/
function initialDraw() {
const cols = WIDTH / RES;
const rows = HEIGHT / RES;
grid = createArray(cols, rows);
// Field edge is getting filled up with "2"
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
grid[i][j] = BORDER;
}
}
// Field gets filled up with "0" = dead cells
for (let i = 1; i < cols - 1; i++) {
for (let j = 1; j < rows - 1; j++) {
grid[i][j] = DEAD_CELL;
}
}
draw(cols, rows);
}
/**
* Draws the canvas
*
* @param {int} cols Number of columns
* @param {int} rows Number of rows
*/
function draw(cols, rows) {
// Draw
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const x = i * RES;
const y = j * RES;
if (grid[i][j] === DEAD_CELL) {
// Dead cells are white
drawCell('white', 'LightGray', 1, x, y, RES, RES);
} else if (grid[i][j] === LIVING_CELL) {
// Living cells are #2B823A (green)
drawCell('#2B823A', 'LightGray', 1, x, y, RES, RES);
} else {
// Border cells are #00410B (red)
drawCell('#00410B', 'LightGray', 1, x, y, RES, RES);
}
}
}
// Updates grid with values of next generation
applyRules(cols, rows);
}
/**
* Draws one cell of the canvas
*
* @param {string} fillStyle Color of the cell
* @param {string} strokeStyle Color of the border of the cell
* @param {int} lineWidth linewidth of the border
* @param {int} x x-coordinate of upper-left corner
* @param {int} y y-coordinate
* @param {int} width width of cell
* @param {int} height height of cell
*/
function drawCell(fillStyle, strokeStyle, lineWidth, x, y, width, height) {
const ctx = document.getElementById('canvas').getContext('2d');
ctx.fillStyle = fillStyle;
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
ctx.beginPath();
// Draw border of color strokeStyle
ctx.rect(x, y, width, height);
// Fill cell with color fillStyle
ctx.fill();
ctx.stroke();
}
/**
* Updates the grid with the next generation:
* Living cells with 2 or 3 neighbors stay alive.
* Living cells with less than two or more than 3 neighbors die.
* Dead cells with exactly 3 neighbors are "re-born"
*
* @param {int} cols Number of columns
* @param {int} rows Number of rows
*/
function applyRules(cols, rows) {
// Temporary array to save new values (next generation)
const newArray = createArray(cols, rows);
for (let i = 1; i < cols - 1; i++) {
for (let j = 1; j < rows - 1; j++) {
// how many living cells does the current cell (grid[i][j]) have
const neighbors = countNeighbors(i, j);
if (grid[i][j] == LIVING_CELL) {
// Living cells with less than two or more than 3 neighbors die
if ((neighbors < 2) || (neighbors > 3)) {
newArray[i][j] = DEAD_CELL;
}
// Living cells with 2 or 3 neighbors stay alive
if ((neighbors == 2) || (neighbors == 3)) {
newArray[i][j] = LIVING_CELL;
}
} else if (grid[i][j] == DEAD_CELL) {
// Dead cells with exactly 3 neighbors are "re-born"
if (neighbors == 3) {
newArray[i][j] = LIVING_CELL;
} else {
newArray[i][j] = DEAD_CELL;
}
}
}
}
// Save temporary variables in grid so that grid now represents
// the next generation
for (let i = 1; i < cols - 1; i++) {
for (let j = 1; j < rows - 1; j++) {
grid[i][j] = newArray[i][j];
}
}
}
/**
* Counts the neighbors of cell in grid at
* position (x,y)
*
* @param {int} x x-coordinate of cell
* @param {int} y y-coordinate of cell
*
* @return {int} neighbors Number of neighbors
*/
function countNeighbors(x, y) {
let neighbors = 0;
/*
001
0X1
101
-> X has four neighbors
*/
for (let i = x - 1; i <= x + 1; i++) {
for (let j = y - 1; j <= y + 1; j++) {
if (grid[i][j] == LIVING_CELL) {
neighbors = neighbors + 1;
}
}
}
// Cell is not neighbor of itself
if (grid[x][y] == LIVING_CELL) {
neighbors = neighbors - 1;
}
return neighbors;
}
/**
* Creates an empty 2d-array
*
* @param {int} cols Number of columns
* @param {int} rows Number of rows
*
* @return {Obj} array newly created array
*/
function createArray(cols, rows) {
const array = new Array(cols);
for (let i = 0; i < cols; i++) {
array[i] = new Array(rows);
}
return array;
}
/**
* Creates a grid with initial state
*
* @param {int} cols Number of columns
* @param {int} rows Number of rows
*/
function createGrid(cols, rows) {
grid = createArray(cols, rows);
// Field edge is getting filled up with "2"
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
grid[i][j] = BORDER;
}
}
// Field gets filled up with "0" = dead cells
for (let i = 1; i < cols - 1; i++) {
for (let j = 1; j < rows - 1; j++) {
grid[i][j] = DEAD_CELL;
}
}
}
/**
* Updates the generation-counter
*/
function updateGeneration() {
$(document).ready(function() {
$('#generation').text(generation);
});
}
/**
* Filling grid with random numbers (0 or 1)
*
* @param {int} cols Number of columns
* @param {int} rows Number of rows
*/
function createRandom(cols, rows) {
for (let i = 1; i < cols - 1; i++) {
for (let j = 1; j < rows - 1; j++) {
grid[i][j] = (Math.random() * 2 | 0);
}
}
}
/**
* Enables buttons when user stops simulation,
* disables buttons when user starts simulation
*/
function switchButtons() {
document.getElementById('clear').disabled = isRunning;
document.getElementById('random').disabled = isRunning;
document.getElementById('lwss').disabled = isRunning;
document.getElementById('r-pentomino').disabled = isRunning;
document.getElementById('acorn').disabled = isRunning;
document.getElementById('gosper').disabled = isRunning;
}
/**
* Helper function to create a
* lightweight spaceship
*
* @param {int} x x-coordinate of cell in the middle of grid
* @param {int} y y-coordinate of cell in the middle of grid
*/
function createLwss(x, y) {
const arr = [[0, 1], [-1, 1], [1, 1], [-2, 1], [-2, 0],
[2, 0], [-2, -1], [-1, -2], [2, -2]];
createPattern(arr, x, y);
}
/**
* Helper function to create a
* r-pentomino
*
* @param {int} x x-coordinate of cell in the middle of grid
* @param {int} y y-coordinate of cell in the middle of grid
*/
function createRPentomino(x, y) {
const arr = [[0, 0], [0, -1], [0, 1], [-1, 0], [1, -1]];
createPattern(arr, x, y);
}
/**
* Helper function to create a
* acorn
*
* @param {int} x x-coordinate of cell in the middle of grid
* @param {int} y y-coordinate of cell in the middle of grid
*/
function createAcorn(x, y) {
const arr = [[1, 0], [2, 0], [3, 0], [-2, 0], [-3, 0], [0, -1], [-2, -2]];
createPattern(arr, x, y);
}
/**
* Helper function to create a
* gosper glider gun
*
* @param {int} x x-coordinate of cell in the middle of grid
* @param {int} y y-coordinate of cell in the middle of grid
*/
function createGosperGliderGun(x, y) {
const arr = [[-13, 0], [-12, 0], [-3, 0], [1, 0], [3, 0], [4, 0],
[9, 0], [11, 0], [-3, 1], [3, 1], [11, 1], [-2, 2],
[2, 2], [-1, 3], [0, 3], [-13, -1], [-12, -1], [-3, -1],
[3, -1], [7, -1], [8, -1], [-2, -2], [2, -2], [7, -2],
[8, -2], [-1, -3], [0, -3], [7, -3], [8, -3], [21, -2],
[22, -2], [9, -4], [11, -4], [21, -3], [22, -3],
[11, -5]];
createPattern(arr, x, y);
}
/**
* Helper function to write pattern saved in
* arr to grid
*
* @param {Obj} arr array of coordinates of cells to fill with 1
* @param {int} x x-coordinate of cell in the middle of grid
* @param {int} y y-coordinate of cell in the middle of grid
*/
function createPattern(arr, x, y) {
for (let i = 0; i < arr.length; i++) {
grid[x + arr[i][0]][y + arr[i][1]] = 1;
}
}
/*
=========================================================================
Main style
=========================================================================
*/
/* color palette */
:root {
--active: #00410b;
--light: #2b823a;
--lighter: #82c38d;
}
html,
body {
padding: 0;
font-family: "Open Sans", sans-serif;
}
/* Content in middle of page */
.inner {
width: 840px;
margin: 0 auto;
padding: 0;
position: relative;
}
/* main content not as wide as inner-div */
.content {
padding-left: 20px;
padding-right: 20px;
padding-bottom: 100px;
}
/*
=========================================================================
footer
=========================================================================
*/
.footer {
text-align: center;
width: 840px;
}
hr {
color: var(--active);
}
/*
=========================================================================
number input
=========================================================================
*/
label {
display: block;
}
input[type="number"] {
width: 18%;
padding: 10px 10px;
display: block;
border: 1px solid black;
border-radius: 5px;
box-sizing: border-box;
margin-bottom: 10px;
margin-right: 12px;
}
.tooltip {
display: inline-block;
position: relative;
margin-bottom: 10px;
}
.tooltip:hover::before {
content: attr(tooltip-title);
position: absolute;
width: 250px;
background: var(--active);
color: white;
padding: 10px;
border-radius: 5px;
box-sizing: border-box;
bottom: 20px;
left: 20px;
opacity: 0.9;
}
/*
=========================================================================
Other
=========================================================================
*/
canvas {
border: 1px solid LightGray;
border-radius: 5px;
box-sizing: border-box;
}
button {
width: 18%;
padding: 10px 10px;
display: inline;
border: 1px solid black;
border-radius: 5px;
box-sizing: border-box;
margin-bottom: 10px;
margin-right: 12px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--active);
font-family: "Baloo 2", sans-serif;
}
a.link {
display: inline-block;
margin: 0;
color: var(--light);
text-decoration: none;
}
a.link::after {
display: block;
content: "";
width: 0;
border-bottom: 2px solid var(--light);
transition: width 200ms;
}
a.link:hover::after {
width: 100%;
}
/*
=========================================================================
responsive
=========================================================================
*/
@media (max-width: 840px) {
.inner {
min-height: 100%;
width: 100%;
margin: 0 auto;
padding: 0;
position: relative;
}
.footer {
text-align: center;
width: 100%;
}
.pattern {
width: 100%;
padding: 10px 10px;
display: inline;
border: 1px solid black;
border-radius: 5px;
box-sizing: border-box;
float: left;
margin-bottom: 10px;
}
button {
width: 50%;
}
canvas {
height: calc(((100% - 100px) / 100) * 100);
width: calc(((100% - 40px) / 100) * 100);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="inner">
<div class="content">
<h1>Conway's Game Of Life</h1>
<canvas id="canvas" width="800" height="500"></canvas>
<h3>Information</h3>
<p>Generation: <span id="generation"></span></p>
<h3>Control</h3>
<button id="startStop">Start</button>
<button id="clear">Clear</button>
<label>
<span class="tooltip" tooltip-title="The higher the number, the faster the simulation" style="border-bottom: 1px dotted black">Speed:</span>
<input id="speed" type="number" min=1 max=20 value="5" onkeydown="return false;">
</label>
<h3>Pattern</h3>
<button id="random">Random</button>
<button id="lwss">LWSS</button>
<button id="r-pentomino">R-pentomino</button>
<button id="acorn">Acorn</button>
<button id="gosper">Gosper glider gun</button>
<h3>What is this?</h3>
<p>Conway's Game of Life is a mathematical game invented by English Mathematician John Horton Conway. The game is based on the theory of cellular automatons. It is a so-called zero-player game because once the "player" set the initial state of the automaton, the game is just about to watch the deterministic outcome of the game.</p>
<p>The rules are as follows:</p>
<ol>
<li>Living cells with 2 or 3 neighbors stay alive.</li>
<li>Living cells with less than two or more than 3 neighbors die.</li>
<li>Dead cells with exactly 3 neighbors are "re-born"</li>
</ol>
<p>In this implementation, the living cells are green and the dead cells are white.</p>
<p>If you want to learn more about the Game of Life, reading the following articles will help:</p>
<ul>
<li><a class="link" href="https://en.wikipedia.org/wiki/Conway's_Game_of_Life">Conway's Game of Life</a></li>
<li><a class="link" href="https://en.wikipedia.org/wiki/Cellular_automaton">Cellular automaton</a></li>
<li><a class="link" href="https://en.wikipedia.org/wiki/John_Horton_Conway">John Conway</a></li>
</ul>
</div>
<footer>
<hr>
<div class="footer">
footer-text
</div>
</footer>
</div>
Question
All suggestions are welcome.