Skip to content

Instantly share code, notes, and snippets.

@holmberd
Last active May 18, 2022 14:52
Show Gist options
  • Save holmberd/d1daa56176caf9b95953ffbd3e6ddf21 to your computer and use it in GitHub Desktop.
Save holmberd/d1daa56176caf9b95953ffbd3e6ddf21 to your computer and use it in GitHub Desktop.
Game of life Javascript [1]
/**
* Conway's Game of Life
*
* The world of the Game of Life is an infinite two-dimensional orthogonal grid of square
* "cells", each of which is in one of two possible states, alive or dead.
*
* Rules:
* 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
* 2. Any live cell with two or three live neighbours lives on to the next generation.
* 3. Any live cell with more than three live neighbours dies, as if by overpopulation.
* 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
*
* Example usage:
* const boardStr = '......\n' +
* '***...\n' +
* '......\n' +
* '......\n' +
* '......\n' +
* '......\n';
* const world = createWorld(boardStr);
* world.evolve();
* print(world);
*
* ---------------------------
* .*....
* .*....
* .*....
* ......
* ......
* .*....
*/
const CellChar = {
ALIVE: '*',
DEAD: '.',
};
/**
* Represents a World rule determining if a Cell lives or dies.
*
* @constructor
* @param {string} ruleStr
* @return {Rule}
*/
class Rule {
constructor(ruleStr) {
const [born, survival] = ruleStr.split('/');
this.born = born.split('').map(Number);
this.survival = survival.split('').map(Number);
}
}
/**
* Represents a single Cell on the Board.
*
* @constructor
* @param {boolean} state - State of a Cell (Alive | Dead)
* @return {Cell}
*/
class Cell {
constructor(state) {
this.state = state;
}
get isAlive() {
return this.state;
}
}
/**
* Represents a Location on the Board.
*
* @constructor
* @param {number} rowIndex
* @param {number} colIndex
* @return {Location}
*/
class Location {
constructor(rowIndex, colIndex) {
this.row = rowIndex;
this.col = colIndex;
}
}
/**
* Represents the game World.
*
* @constructor
* @param {Array} board - Representation of the Board on which the Game of Life unfolds.
* @return {World}
*/
class World {
constructor(board = []) {
this.validateBoard(board);
this.rows = board.length;
this.cols = board[0].length;
this.board = board;
this.generation = 0;
this.rule = new Rule('3/23'); // B3/S23 (Conway's Life)
}
validateBoard(board) {
if (!board.length) {
throw Error('Board is empty');
}
const set = new Set(board.map(row => row.length));
if (set.size > 1) {
throw Error('Rows must be of equal length');
}
}
getCell(location) {
return this.board[location.row][location.col];
}
/**
* Evolves the current World state one generation using the rules of the game.
*
* @public
*/
evolve() {
this.board = this.evolveBoard(this.board);
this.generation++;
return this;
}
evolveBoard(board) {
return board.map((row, rowIndex) => this.evolveRow(row, rowIndex));
}
evolveRow(row, rowIndex) {
return row.map((cell, colIndex) => this.evolveCell(cell, new Location(rowIndex, colIndex)));
}
evolveCell(cell, cellLocation) {
const aliveNeighbours = this.countAliveNeighbours(cellLocation);
return cell.isAlive
? new Cell(this.rule.survival.some(surviveCount => surviveCount === aliveNeighbours))
: new Cell(this.rule.born.some(bornCount => bornCount === aliveNeighbours));
}
countAliveNeighbours(location) {
const cellNeighbourPositions = [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]];
return cellNeighbourPositions.reduce((count, [rowPos, colPos]) => {
let neighbourLocation = new Location(location.row + rowPos, location.col + colPos);
if (this.inBounds(neighbourLocation) && this.getCell(neighbourLocation).isAlive) {
count++;
}
return count;
}, 0);
}
/**
* Checks if a location is within the bounds of the board.
*/
inBounds({ row, col }) {
return (row >= 0 && row < this.rows && col >= 0 && col < this.cols);
}
}
/**
* Helper for creating a new world from a string.
*
* @param {string} boardStr
* @return {World}
*/
function createWorld(boardStr) {
const board = createBoard(boardStr);
return new World(board);
}
function createBoard(boardStr) {
const board = getBoardRows(boardStr);
return board.map(createRowCells);
}
function getBoardRows(boardStr) {
return boardStr.split('\n').slice(0, -1);
}
function createRowCells(row) {
return row.split('').map(convertCharToCell);
}
function convertCharToCell(char) {
return new Cell(char === CellChar.ALIVE);
}
/**
* Helper for printing the current state of the game world.
*
* @param {World} world
*/
function print(world) {
let str = '';
for (const row of world.board) {
for (const cell of row) {
str += cell.isAlive ? CellChar.ALIVE : CellChar.DEAD;
}
str += '\n';
}
console.log(str);
}
function test() {
const boardStr =
'........\n' +
'........\n' +
'...***..\n' +
'........\n' +
'........\n' +
'........\n';
const world = createWorld(boardStr);
print(world);
world.evolve();
print(world);
}
test();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment