Skip to content

Instantly share code, notes, and snippets.

@MarcPer
Created August 7, 2015 22:23
Show Gist options
  • Save MarcPer/275709a6ff7aa54cd64e to your computer and use it in GitHub Desktop.
Save MarcPer/275709a6ff7aa54cd64e to your computer and use it in GitHub Desktop.
Code from Play by Play: HTML, CSS, and JavaScript with Lea Verou (with minor corrections)
<!DOCTYPE html>
<html lang="eng">
<head>
<meta charset="utf-8">
<title>Conway's Game of Life</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Conway's Game of Life</h1>
<button class="next">Next</button>
<input type="checkbox" id="autoplay">
<label for="autoplay" class="button">Autoplay</label>
<table id="grid"></table>
<footer></footer>
<script src="life.js"></script>
</body>
</html>

Code from Play by Play: HTML, CSS, and JavaScript with Lea Verou (with minor corrections)

This is the code transcript from the video above, where Lea Verou walks through the process of implementing Conway's Game of Life from scratch using only HTML, CSS and Javascript.

IDEAS / TODO

  • Detect when board reached a repeating pattern
  • Red animation for dying squares
  • Autoplay speed dial
  • Save previous state in local storage
  • Clear button to reset board
  • 3D Board wrapping around (with 3D transitions)
  • Count number of generations
function $(selector, container) {
return (container || document).querySelector(selector);
}
(function() {
var _ = self.Life = function(seed) {
this.seed = seed;
this.height = seed.length;
this.width = seed[0].length;
this.prevBoard = [];
this.board = cloneArray(seed);
};
_.prototype = {
next: function() {
this.prevBoard = cloneArray(this.board);
for (var y=0; y<this.height; y++) {
for (var x=0; x<this.width; x++) {
var neighbors = this.aliveNeighbors(this.prevBoard, x, y);
var alive = !!this.board[y][x];
if (alive) {
if (neighbors < 2 || neighbors > 3) {
this.board[y][x] = 0;
}
}
else {
if (neighbors == 3) {
this.board[y][x] = 1;
}
}
}
}
},
aliveNeighbors: function (array, x, y) {
var prevRow = array[y-1] || [];
var nextRow = array[y+1] || [];
return [
prevRow[x-1], prevRow[x], prevRow[x+1],
array[y][x-1], array[y][x+1],
nextRow[x-1], nextRow[x], nextRow[x+1]
].reduce(function (prev, cur) {
return prev + +!!cur; // converts undefined or 0 to 0
}, 0);
},
toString: function() {
return this.board.map(function (row) { return row.join(' '); });
}
};
// Helpers
// Warning: Only clones 2D arrays
function cloneArray(array) {
return array.slice().map(function(row) { return row.slice(); });
}
})();
// var game = new life([
// [0, 0, 0, 0, 0],
// [0, 0, 1, 0, 0],
// [0, 0, 1, 0, 0],
// [0, 0, 1, 0, 0],
// [0, 0, 0, 0, 0]
// ]);
// console.log(game + '');
// games.next();
// console.log(game + '');
(function(){
var _ = self.LifeView = function(table, size) {
this.grid = table;
this.size = size;
this.started = false;
this.autoplay = false;
this.createGrid();
};
_.prototype = {
createGrid: function() {
var me = this;
var fragment = document.createDocumentFragment();
this.grid.innerHTML = '';
this.checkboxes = [];
for (var y=0; y<this.size; y++) {
var row = document.createElement('tr');
this.checkboxes[y] = [];
for (var x=0; x<this.size; x++) {
var cell = document.createElement('td');
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
this.checkboxes[y][x] = checkbox;
checkbox.coords = [y, x];
cell.appendChild(checkbox);
row.appendChild(cell);
}
fragment.appendChild(row);
}
this.grid.addEventListener('change', function(evt) {
if (evt.target.nodeName.toLowerCase() === 'input') {
me.started = false;
}
});
this.grid.addEventListener('keyup', function(evt) {
var checkbox = evt.target;
if (checkbox.nodeName.toLowerCase() === 'input') {
var coords = checkbox.coords;
var y = coords[0];
var x = coords[1];
switch (evt.keyCode) {
case 37: // left
if (x > 0) {
me.checkboxes[y][x-1].focus();
}
break;
case 38: // up
if (y > 0) {
me.checkboxes[y-1][x].focus();
}
break;
case 39: // right
if (x < me.size - 1) {
me.checkboxes[y][x+1].focus();
}
break;
case 40: // bottom
if (y < me.size - 1) {
me.checkboxes[y+1][x].focus();
}
break;
}
}
});
this.grid.appendChild(fragment);
},
get boardArray() {
return this.checkboxes.map(function (row) {
return row.map(function (checkbox) {
return +checkbox.checked;
});
});
},
play: function() {
this.game = new Life(this.boardArray);
this.started = true;
},
next: function() {
var me = this;
if (!this.started || !this.game) {
this.play();
}
this.game.next();
var board = this.game.board;
for (var y=0; y<this.size; y++) {
for (var x=0; x<this.size; x++) {
this.checkboxes[y][x].checked = !!board[y][x];
}
}
if (this.autoplay) {
this.timer = setTimeout(function () {
me.next();
}, 1000);
}
}
};
})();
var lifeView = new LifeView(document.getElementById('grid'), 12);
(function() {
var buttons = {
next: $('button.next')
};
$('button.next:enabled').addEventListener('click', function(event) {
lifeView.next();
});
$('#autoplay').addEventListener('change', function() {
// buttons.next.textContent = this.checked? 'Start' : 'Next';
buttons.next.disabled = this.checked;
lifeView.autoplay = this.checked;
if (this.checked) {
lifeView.next();
}
else {
clearTimeout(lifeView.timer);
}
});
})();
* {
padding: 0;
margin: 0;
}
body {
text-align: center;
font-family: sans-serif;
}
h1 {
margin: .5em 0 1em;
}
#grid {
margin: 1em auto 0;
border-spacing: 0;
border-collapse: collapse; /*prevents two neighboring borders from looking like one thick border*/
}
#grid td {
border: 1px solid gray;
}
@-webkit-keyframes birth {
50% {
box-shadow: 0 0 0 99px green inset;
}
}
@keyframes birth {
50% {
box-shadow: 0 0 0 99px green inset;
}
}
#grid input[type="checkbox"] {
/*dev.w3.org/csswg/css-ui/*/
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: block;
width: 20px;
height: 20px;
border: 0; /* Firefox */
background: white;
transition: 500ms;
}
#grid input[type="checkbox"]:checked {
background: black;
box-shadow: 0 0 0 99px black inset; /* Hack for Firefox */
-webkit-animation: birth 500ms ;
animation: birth 500ms ;
}
button, .button {
padding: .4em .6em;
border: 1px solid rgba(0,0,0,.2);
border-radius: .3em;
box-shadow: 0 1px 0 hsla(0,0%,100%,.7) inset, 0 .15em .1em -.1em rgba(0,0,0,.4);
background: linear-gradient(hsla(0,0%,100%,.3), hsla(0,0%,100%,0)) hsl(80,80%,35%);
color: white;
text-shadow: 0 -.05em .05em rgba(0,0,0,.3);
font: inherit; /* Fonts don't inherit from parents by default */
font-weight: bold;
outline: none;
cursor: pointer;
}
button:enabled:active,
.button:active,
input[type="checkbox"]:checked + label.button {
background-image: none;
box-shadow: 0 .1em .3em rgba(0,0,0,.5) inset;
}
#autoplay {
position: absolute;
clip: rect(0,0,0,0);
}
button:disabled {
background-color: hsl(80,20%,40%);
cursor: not-allowed;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment