Skip to content

Instantly share code, notes, and snippets.

@jonurry
Last active March 11, 2019 15:37
Show Gist options
  • Save jonurry/5c4ebb8b9a57d69c4bd1b6baa1a0696b to your computer and use it in GitHub Desktop.
Save jonurry/5c4ebb8b9a57d69c4bd1b6baa1a0696b to your computer and use it in GitHub Desktop.
18.3 Conway’s Game of Life (Eloquent JavaScript Solutions)
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Life</title>
<style type="text/css">
.cell {
border-radius: 50%;
height: 20px;
margin: 0px;
width: 20px;
}
.alive {
background-color: #f5b623;
height: 18px;
margin: 1px;
width: 18px;
}
.dead {
background-color: darkgray;
height: 6px;
margin: 7px;
width: 6px;
}
.alive,
.dead {
transition: width 1s, height 1s, margin 1s, background-color 1s;
}
</style>
</head>
<body>
<label for="rows">Rows:</label>
<input id="rows" type="number" name="rows" value="20">
<label for="columns">Columns:</label>
<input id="columns" type="number" name="columns" value="60">
<button id="life">Create life</button>
<button id="next">Next generation</button>
<button id="auto">Switch autopilot on</button>
<div id="grid"></div>
<!-- JavaScript -->
<script>
class Life {
constructor(width = 50, height = 50, density = 5) {
this.initialiseLife(width, height, density);
}
initialiseLife(width = 50, height = 50, density = 5) {
// density is a weighting from 0 to 10
// 0 = no life
// 5 = 50/50 chance of life
// 10 = life everywhere
this.rows = [];
this.height = height;
this.width = width;
for (let row = 0; row < width; row++) {
let columns = [];
for (let col = 0; col < height; col++) {
columns[col] = !!Math.floor(Math.random() + 0.1 * density);
}
this.rows[row] = columns;
}
}
nextGeneration() {
// Each generation (turn), the following rules are applied:
// 1. Any live cell with fewer than two or more than three
// live neighbours dies.
// 2. Any live cell with two or three live neighbours
// lives on to the next generation.
// 3. Any dead cell with exactly three live neighbours
// becomes a live cell.
let nextGen = [];
for (let row = 0; row < this.width; row++) {
let columns = [];
for (let col = 0; col < this.height; col++) {
let value = this.rows[row][col];
columns[col] = value;
let neighbours = this.numberOfNeighbours(row, col);
if (value) {
// cell is alive
if (neighbours < 2 || neighbours > 3) {
// cell dies
columns[col] = 0;
}
} else {
// cell is dead
if (neighbours === 3) {
// cell lives
columns[col] = 1;
}
}
}
nextGen[row] = columns;
}
this.rows = nextGen;
}
numberOfNeighbours(row, col) {
let count = 0;
for (let x = row - 1; x <= row + 1; x++) {
for (let y = col - 1; y <= col + 1; y++) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
count += this.rows[x][y] ? 1 : 0;
}
}
}
count -= this.rows[row][col] ? 1 : 0;
return count;
}
toggleLife(row, col) {
this.rows[row][col] = !this.rows[row][col];
}
}
// iterate over Life using a generator
Life.prototype[Symbol.iterator] = function* () {
for (let row = 0; row < this.width; row++) {
let columns = [];
for (let col = 0; col < this.height; col++) {
yield {
row,
col,
value: this.rows[row][col],
numberOfNeighbours: this.numberOfNeighbours.bind(this, row, col)
};
}
}
}
let gridElement = document.getElementById('grid');
let nextGenButton = document.getElementById('next');
let autoPilotButton = document.getElementById('auto');
let createLifeButton = document.getElementById('life');
let autoPilot = false;
let autoPilotID = 0;
let life;
const renderLife = (resetGrid = false) => {
if (resetGrid) {
// grid is empty so populate with base elements
// use html table as basis for grid
let currentRow = -1;
let divCell;
let table = document.createElement('table');
let tableRow;
let tableCell;
if (gridElement.firstChild) {
gridElement.removeChild(gridElement.firstChild);
}
for (let cell of life) {
if (currentRow !== cell.row) {
tableRow = document.createElement('tr');
table.appendChild(tableRow);
currentRow = cell.row;
}
tableCell = document.createElement('td');
tableCell.dataset.row = cell.row;
tableCell.dataset.col = cell.col;
tableCell.addEventListener('click', (e) => {
let target;
let data;
if (e.target.tagName == 'DIV') {
target = e.target;
data = e.target.parentNode;
} else {
target = e.target.firstChild;
data = e.target;
}
life.toggleLife(cell.row, cell.col);
if (life.rows[data.dataset.row][data.dataset.col]) {
target.className = 'cell alive';
} else {
target.className = 'cell dead';
}
});
divCell = document.createElement('div');
divCell.className = 'cell';
tableCell.appendChild(divCell);
tableRow.appendChild(tableCell);
}
gridElement.appendChild(table);
}
// iterate through life and update content
for (let cell of life) {
// article[data-col='3']
let divCell = document.querySelector(`td[data-row='${cell.row}'][data-col='${cell.col}']`);
let targetClass = cell.value ? 'cell alive' : 'cell dead';
if (divCell.firstChild.className !== targetClass) {
divCell.firstChild.className = targetClass;
}
}
if (autoPilot && autoPilotID === 0) {
autoPilotID = setInterval(() => {
life.nextGeneration();
renderLife();
}, 1000);
} else if (!autoPilot && autoPilotID !== 0) {
clearInterval(autoPilotID);
autoPilotID = 0;
}
}
const initialiseLife = (width, height, density = 5) => {
life = new Life(width, height, density);
renderLife(true);
}
const createLife = () => {
let width = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
let height = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
let columns = Math.floor(width / 25);
let rows = Math.floor(height / 25);
document.getElementById('columns').value = columns;
document.getElementById('rows').value = rows;
initialiseLife(rows, columns, 3);
}
autoPilotButton.addEventListener('click', () => {
autoPilot = !autoPilot;
autoPilotButton.textContent = `Switch autopilot ${autoPilot ? 'off' : 'on'}`;
life.nextGeneration();
renderLife();
});
createLifeButton.addEventListener('click', () => {
createLife();
});
nextGenButton.addEventListener('click', () => {
life.nextGeneration();
renderLife();
});
createLife();
</script>
</body>
</html>
@jonurry
Copy link
Author

jonurry commented Apr 24, 2018

18.3 Conway’s Game of Life

Conway’s Game of Life is a simple simulation that creates artificial “life” on a grid, each cell of which is either live or not. Each generation (turn), the following rules are applied:

Any live cell with fewer than two or more than three live neighbours dies.

Any live cell with two or three live neighbours lives on to the next generation.

Any dead cell with exactly three live neighbours becomes a live cell.

A neighbour is defined as an adjacent cell, including diagonally adjacent ones.

Note that these rules are applied to the whole grid at once, not one square at a time. That means the counting of neighbours is based on the situation at the start of the generation, and changes happening to neighbour cells during this generation should not influence the new state of a given cell.

Implement this game using whichever data structure you find appropriate. Use Math.random to populate the grid with a random pattern initially. Display it as a grid of checkbox fields, with a button next to it to advance to the next generation. When the user checks or unchecks the checkboxes, their changes should be included when computing the next generation.

@jonurry
Copy link
Author

jonurry commented Apr 24, 2018

Hints

To solve the problem of having the changes conceptually happen at the same time, try to see the computation of a generation as a pure function, which takes one grid and produces a new grid that represents the next turn.

Representing the matrix can be done in the way shown in Chapter 6. You can count live neighbours with two nested loops, looping over adjacent coordinates in both dimensions. Take care not to count cells outside of the field and to ignore the cell in the centre, whose neighbours we are counting.

Making changes to checkboxes take effect on the next generation can be done in two ways. An event handler could notice these changes and update the current grid to reflect them, or you could generate a fresh grid from the values in the checkboxes before computing the next turn.

If you choose to go with event handlers, you might want to attach attributes that identify the position that each checkbox corresponds to so that it is easy to find out which cell to change.

To draw the grid of checkboxes, you can either use a <table> element (see Chapter 14) or simply put them all in the same element and put <br> (line break) elements between the rows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment