Skip to content

Instantly share code, notes, and snippets.

@stemcstudio
Last active February 12, 2021 02:18
Show Gist options
  • Save stemcstudio/9e4228c678b4cf96912e9ad01a15e375 to your computer and use it in GitHub Desktop.
Save stemcstudio/9e4228c678b4cf96912e9ad01a15e375 to your computer and use it in GitHub Desktop.
Conway's Game of Life

Conway's Game of Life

Overview

The Game of Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.

For more information, (right click and open in a new tab) Wikipedia

This simulation is best experienced by pressing Hide Workspace or Hide Documentation in the toolbar

Instructions

  1. Use the Start/Stop button to start or pause the simulation.
  2. Use the Clear button to clear the view and enter your own shapes using the mouse (click on a square to toggle the state).
  3. Use the Reset button to create a random distribution of cells.

Tasks

  1. Program the Game of Life from scratch.
  2. Determine and prove which shapes are stable.
  3. Discover new stable shapes.

Copyright (c) 2015-2020 David Geo Holmes.

<!DOCTYPE html>
<html>
<head>
<base href='/'>
<style>body {
background-color: #cccccc;
font-family: Roboto, Arial, sans-serif;
color: #333333;
}
canvas {
background-color: #000000;
margin-top: 6px;
}
</style>
<script src='https://www.stemcstudio.com:/vendor/[email protected]/domready.js'></script>
<script src='https://unpkg.com/[email protected]/dist/system.js'></script>
</head>
<body>
<script>
try {
if (window['System']) {
System.config({
"warnings": false,
"map": {}
});
}
} catch(e) { console.error(e) }
</script>
<script>
try {
System.register("./index.js", ["./main.js"], function (exports_1, context_1) {
"use strict";
var main_1;
var __moduleName = context_1 && context_1.id;
return {
setters: [
function (main_1_1) {
main_1 = main_1_1;
}
],
execute: function () {
DomReady.ready(main_1.main).catch(function (e) { console.error(e); });
}
};
});
System.register("./main.js", ["./model.js", "./view.js"], function (exports_1, context_1) {
"use strict";
var model_1, view_1;
var __moduleName = context_1 && context_1.id;
function main() {
const model = new model_1.Model(40, 40, { "threshold": 0.80 });
const view = new view_1.View('game-canvas', model, { margin: 1 });
let running = false;
const stopButton = document.getElementById('stop');
setRunning(false);
stopButton.addEventListener('click', () => {
console.log("Start/Stop button was clicked.");
setRunning(!running);
});
const stepButton = document.getElementById('step');
stepButton.addEventListener('click', () => {
console.log("Step button was clicked.");
setRunning(false);
model.step();
});
const clearButton = document.getElementById('clear');
clearButton.addEventListener('click', () => {
console.log("Clear button was clicked.");
setRunning(false);
model.clear();
});
const resetButton = document.getElementById('reset');
resetButton.addEventListener('click', () => {
console.log("Reset button was clicked.");
setRunning(false);
model.reset();
});
view.canvas.addEventListener('mousedown', function (mouse) {
setRunning(false);
const { row, column } = view.toPosition(mouse.offsetX, mouse.offsetY);
model.setAlive(row, column, !model.isAlive(row, column));
});
function setRunning(isRunning) {
running = isRunning;
stopButton.value = running ? 'Stop' : 'Start';
}
let frame = 0;
const animate = function () {
if (running) {
if (frame === 30) {
model.step();
frame = 0;
}
frame += 1;
}
view.draw();
window.requestAnimationFrame(animate);
};
window.requestAnimationFrame(animate);
}
exports_1("main", main);
return {
setters: [
function (model_1_1) {
model_1 = model_1_1;
},
function (view_1_1) {
view_1 = view_1_1;
}
],
execute: function () {
}
};
});
System.register("./model.js", [], function (exports_1, context_1) {
"use strict";
var THRESHOLD_DEFAULT, Model;
var __moduleName = context_1 && context_1.id;
return {
setters: [],
execute: function () {
THRESHOLD_DEFAULT = 0.5;
Model = class Model {
constructor(rows, columns, options = {}) {
this.data = [];
this.next = [];
this.threshold = typeof options.threshold === 'number' ? options.threshold : THRESHOLD_DEFAULT;
this.rows = rows;
this.columns = columns;
this.reset();
}
indexFromPosition(row, column) {
row = row + this.rows;
column = column + this.columns;
return (row % this.rows) * this.columns + (column % this.columns);
}
isAlive(row, column) {
return this.data[this.indexFromPosition(row, column)];
}
setAlive(row, column, alive) {
this.data[this.indexFromPosition(row, column)] = alive;
}
clear() {
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
this.setAlive(row, column, false);
}
}
}
reset() {
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
this.setAlive(row, column, Math.random() > this.threshold);
}
}
}
neighbors(row, column) {
let count = 0;
if (this.isAlive(row - 1, column - 1)) {
count += 1;
}
if (this.isAlive(row - 1, column)) {
count += 1;
}
if (this.isAlive(row - 1, column + 1)) {
count += 1;
}
if (this.isAlive(row, column - 1)) {
count += 1;
}
if (this.isAlive(row, column + 1)) {
count += 1;
}
if (this.isAlive(row + 1, column - 1)) {
count += 1;
}
if (this.isAlive(row + 1, column)) {
count += 1;
}
if (this.isAlive(row + 1, column + 1)) {
count += 1;
}
return count;
}
step() {
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
const index = this.indexFromPosition(row, column);
const N = this.neighbors(row, column);
if (this.isAlive(row, column)) {
if (N < 2) {
this.next[index] = false;
}
else if (N > 3) {
this.next[index] = false;
}
else {
this.next[index] = this.data[index];
}
}
else {
if (N === 3) {
this.next[index] = true;
}
else {
this.next[index] = this.data[index];
}
}
}
}
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
const index = this.indexFromPosition(row, column);
this.data[index] = this.next[index];
}
}
}
};
exports_1("Model", Model);
}
};
});
System.register("./view.js", [], function (exports_1, context_1) {
"use strict";
var View;
var __moduleName = context_1 && context_1.id;
return {
setters: [],
execute: function () {
View = class View {
constructor(canvasId, model, options = {}) {
this.margin = typeof options.margin === 'number' ? options.margin : 1;
this.model = model;
this.canvas = document.getElementById(canvasId);
if (this.canvas) {
this.ctxt2d = this.canvas.getContext('2d');
this.stepX = this.canvas.width / model.columns;
this.stepY = this.canvas.height / model.rows;
this.tileX = (this.canvas.width / model.columns) - 2 * this.margin;
this.tileY = (this.canvas.height / model.rows) - 2 * this.margin;
}
else {
throw new Error(`${canvasId} is not an element identifier in the document.`);
}
}
draw() {
this.ctxt2d.fillStyle = '#444444';
this.ctxt2d.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (let row = 0; row < this.model.rows; row++) {
for (let column = 0; column < this.model.columns; column++) {
this.drawCell(row, column);
}
}
}
drawCell(row, column) {
this.ctxt2d.fillStyle = this.model.isAlive(row, column) ? '#555577' : '#AAAACC';
const x = this.margin + column * this.stepX;
const y = this.margin + row * this.stepY;
this.ctxt2d.fillRect(x, y, this.tileX, this.tileY);
}
toPosition(x, y) {
const column = (x - x % this.stepX) / this.stepX;
const row = (y - y % this.stepY) / this.stepY;
return { row, column };
}
};
exports_1("View", View);
}
};
});
} catch(e) { console.error(e) }
</script>
<input type='button' id='stop' value='Stop' />
<input type='button' id='step' value='Step' />
<input type='button' id='clear' value='Clear' />
<input type='button' id='reset' value='Reset' />
<br/>
<canvas id='game-canvas' width='500' height='500'></canvas>
<script>
System.defaultJSExtensions = true
System.import('./index.js').catch(function(e) { console.error(e) })
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<base href='/'>
<link rel="stylesheet" href="style.css">
</head>
<body>
<input type='button' id='stop' value='Stop' />
<input type='button' id='step' value='Step' />
<input type='button' id='clear' value='Clear' />
<input type='button' id='reset' value='Reset' />
<br/>
<canvas id='game-canvas' width='500' height='500'></canvas>
</body>
</html>
import { main } from './main'
DomReady.ready(main).catch(function(e) { console.error(e) })
import { Model } from './model'
import { View } from './view'
export function main() {
const model = new Model(40, 40, { "threshold": 0.80 })
const view = new View('game-canvas', model, { margin: 1 })
let running = false
const stopButton = document.getElementById('stop') as HTMLButtonElement
setRunning(false)
stopButton.addEventListener('click', () => {
console.log("Start/Stop button was clicked.")
setRunning(!running)
})
const stepButton = document.getElementById('step') as HTMLButtonElement
stepButton.addEventListener('click', () => {
console.log("Step button was clicked.")
setRunning(false)
model.step()
})
const clearButton = document.getElementById('clear') as HTMLButtonElement
clearButton.addEventListener('click', () => {
console.log("Clear button was clicked.")
setRunning(false)
model.clear()
})
const resetButton = document.getElementById('reset') as HTMLButtonElement
resetButton.addEventListener('click', () => {
console.log("Reset button was clicked.")
setRunning(false)
model.reset()
})
view.canvas.addEventListener('mousedown', function(mouse) {
setRunning(false)
const { row, column } = view.toPosition(mouse.offsetX, mouse.offsetY)
model.setAlive(row, column, !model.isAlive(row, column))
})
function setRunning(isRunning: boolean): void {
running = isRunning
stopButton.value = running ? 'Stop' : 'Start'
}
let frame = 0
const animate = function() {
if (running) {
if (frame === 30) {
model.step()
frame = 0
}
frame += 1
}
view.draw()
window.requestAnimationFrame(animate)
}
window.requestAnimationFrame(animate)
}
const THRESHOLD_DEFAULT = 0.5
/**
* The options used for constructing the Model.
*/
export interface ModelOptions {
/**
* The threshold for a cell to be considered active.
* This is a number in the range [0,1].
* During initialization, a random number in [0,1] is generated for each cell,
* and compared to the threshold, in order to determine whether the cell is active.
*/
threshold?: number
}
/**
* The model for Game of Life simulation.
*/
export class Model {
/**
* The number of cells in the vertical direction.
*/
public rows: number
/**
* The number of cells in the horizontal direction.
*/
public columns: number
/**
* The cells that are active in the current generation.
*/
public data: boolean[] = []
/**
* The cells that are active in the next generation.
*/
private next: boolean[] = []
/**
* The threshold above which a cell is deemed to be active.
* A number in the range [0, 1].
*
*/
private threshold: number
/**
* Constructs a Model with the specified number of rows and columns.
* The options define the threshold for a cell to be considered active.
*/
constructor(rows: number, columns: number, options: ModelOptions = {}) {
this.threshold = typeof options.threshold === 'number' ? options.threshold : THRESHOLD_DEFAULT
this.rows = rows
this.columns = columns
this.reset()
}
/**
* Computes the index into the data and next arrays from a zero-based row and column.
*/
private indexFromPosition(row: number, column: number): number {
row = row + this.rows
column = column + this.columns
return (row % this.rows) * this.columns + (column % this.columns)
}
/**
* Determines whether the cell at the specified row and column is alive (active).
*/
isAlive(row: number, column: number): boolean {
return this.data[this.indexFromPosition(row, column)]
}
/**
* Sets the status of a cell at a specified zero-based row and column.
*/
setAlive(row: number, column: number, alive: boolean): void {
this.data[this.indexFromPosition(row, column)] = alive
}
/**
* Sets the status of all cells to dead (inactive).
*/
clear(): void {
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
this.setAlive(row, column, false)
}
}
}
/**
* Sets the status of all cells individually to a random alive status
* which depends upon the threshold.
*/
reset(): void {
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
this.setAlive(row, column, Math.random() > this.threshold)
}
}
}
/**
* Computes the number of immediate neighbors of the specified cell that are alive (active).
*/
private neighbors(row: number, column: number): number {
let count = 0
if (this.isAlive(row - 1, column - 1)) {
count += 1
}
if (this.isAlive(row - 1, column)) {
count += 1
}
if (this.isAlive(row - 1, column + 1)) {
count += 1
}
if (this.isAlive(row, column - 1)) {
count += 1
}
if (this.isAlive(row, column + 1)) {
count += 1
}
if (this.isAlive(row + 1, column - 1)) {
count += 1
}
if (this.isAlive(row + 1, column)) {
count += 1
}
if (this.isAlive(row + 1, column + 1)) {
count += 1
}
return count
}
/**
* Advances the simulation by computing the next generation according to the
* rules of the Game of Life. the next generation is then applied simultaneously
* to the current generation.
*/
step(): void {
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
const index = this.indexFromPosition(row, column)
const N = this.neighbors(row, column)
if (this.isAlive(row, column)) {
if (N < 2) {
this.next[index] = false
}
else if (N > 3) {
this.next[index] = false
}
else {
// Do nothing (stay alive).
this.next[index] = this.data[index]
}
}
else {
if (N === 3) {
this.next[index] = true
}
else {
// Do nothing (stay dead).
this.next[index] = this.data[index]
}
}
}
}
// Copy the next generation to the current generation.
for (let row = 0; row < this.rows; row++) {
for (let column = 0; column < this.columns; column++) {
const index = this.indexFromPosition(row, column)
this.data[index] = this.next[index]
}
}
}
}
{
"description": "Conway's Game of Life",
"dependencies": {
"DomReady": "^1.0.0"
},
"linting": true,
"name": "conway's-game-of-life",
"version": "1.0.0",
"author": "David Geo Holmes",
"keywords": [
"STEMCstudio",
"Conway",
"Game",
"Life"
],
"noLoopCheck": true,
"hideConfigFiles": true
}
body {
background-color: #cccccc;
font-family: Roboto, Arial, sans-serif;
color: #333333;
}
canvas {
background-color: #000000;
margin-top: 6px;
}
{
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"jsx": "react",
"module": "system",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": true,
"skipLibCheck": true,
"sourceMap": false,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es2015",
"traceResolution": true
}
{
"rules": {
"array-type": [
true,
"array"
],
"curly": false,
"comment-format": [
true,
"check-space"
],
"eofline": true,
"forin": true,
"jsdoc-format": true,
"new-parens": true,
"no-conditional-assignment": false,
"no-consecutive-blank-lines": true,
"no-construct": true,
"no-for-in-array": true,
"no-inferrable-types": [
true
],
"no-magic-numbers": false,
"no-shadowed-variable": true,
"no-string-throw": true,
"no-trailing-whitespace": [
true,
"ignore-jsdoc"
],
"no-var-keyword": true,
"one-variable-per-declaration": [
true,
"ignore-for-loop"
],
"prefer-const": true,
"prefer-for-of": true,
"prefer-function-over-method": false,
"prefer-method-signature": true,
"radix": true,
"semicolon": [
true,
"never"
],
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"triple-equals": true,
"use-isnan": true
}
}
import { Model } from './model'
export interface ViewOptions {
margin?: number
}
export class View {
private model: Model
public canvas: HTMLCanvasElement
private ctxt2d: CanvasRenderingContext2D
private margin: number
private tileX: number
private tileY: number
private stepX: number
private stepY: number
/**
* Constructs a View using a specified canvas identifier.
* The model provides the data used to render the view.
*/
constructor(canvasId: string, model: Model, options: ViewOptions = {}) {
this.margin = typeof options.margin === 'number' ? options.margin : 1
this.model = model
this.canvas = document.getElementById(canvasId) as HTMLCanvasElement
if (this.canvas) {
this.ctxt2d = this.canvas.getContext('2d') as CanvasRenderingContext2D
this.stepX = this.canvas.width / model.columns
this.stepY = this.canvas.height / model.rows
this.tileX = (this.canvas.width / model.columns) - 2 * this.margin
this.tileY = (this.canvas.height / model.rows) - 2 * this.margin
}
else {
throw new Error(`${canvasId} is not an element identifier in the document.`)
}
}
/**
* Render the model to the canvas.
*/
draw() {
// Redraw the view
this.ctxt2d.fillStyle = '#444444'
this.ctxt2d.fillRect(0, 0, this.canvas.width, this.canvas.height)
for (let row = 0; row < this.model.rows; row++) {
for (let column = 0; column < this.model.columns; column++) {
this.drawCell(row, column)
}
}
}
/**
* Draws the specified cell with a color that depends on its alive/dead (active/inactive) status.
*/
private drawCell(row: number, column: number): void {
this.ctxt2d.fillStyle = this.model.isAlive(row, column) ? '#555577' : '#AAAACC'
const x = this.margin + column * this.stepX
const y = this.margin + row * this.stepY
this.ctxt2d.fillRect(x, y, this.tileX, this.tileY)
}
/**
* Computes the (row, column) position of a cell from pixel coordinates.
* The pixel coordinates may come from a mouse click.
*/
toPosition(x: number, y: number): { row: number; column: number } {
const column = (x - x % this.stepX) / this.stepX
const row = (y - y % this.stepY) / this.stepY
return { row, column }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment