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
*
* 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 boardScheme = '......\n' +
* '***...\n' +
* '......\n' +
* '......\n' +
* '......\n' +
* '......\n';
* const world = createWorld(board);
* world.evolve();
* print(world);
*
* ---------------------------
* .*....
* .*....
* .*....
* ......
* ......
* .*....
*/
const CELL_CHAR = {
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 the same 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(rowIndex, row));
}
evolveRow(rowIndex, row) {
return row.map((cell, colIndex) => this.evolveCell(new Location(rowIndex, colIndex), cell));
}
evolveCell(cellLocation, cell) {
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)) {
let neighbourCell = this.getCell(neighbourLocation);
if (neighbourCell.isAlive) {
count++;
}
return count;
}
}, 0);
}
/**
* Checks if a location is within the World bounds.
*/
inBounds(location) {
if (location.row > (this.rows - 1) || location.row < 0) {
return false;
}
if (location.col > (this.cols - 1) || location.col < 0) {
return false;
}
return true;
}
/**
* Prints the current state of the World board.
*
* @public
*/
print() {
let str = '';
for (const row of this.board) {
for (const cell of row) {
str += cell.isAlive ? CELL_CHAR.ALIVE : CELL_CHAR.DEAD;
}
str += '\n';
}
console.log(str);
}
}
/**
* Helper for creating a new world from a string.
*
* @param {string} boardScheme
* @return {World}
*/
function createWorld(boardScheme) {
const board = createBoard(boardScheme);
return new World(board);
}
function createBoard(boardScheme) {
const board = getBoardRows(boardScheme);
return board.map((row) => createRowCells(row));
}
function getBoardRows(boardScheme) {
return boardScheme.split('\n').slice(0, -1);
}
function createRowCells(row) {
return row.split('').map((char) => convertCharToCell(char));
}
function convertCharToCell(char) {
return new Cell(char === CELL_CHAR.ALIVE);
}
/**
* Helper for printing the state of the game world.
*
* @param {World} world
*/
function print(world) {
let str = '';
for (const row of this.board) {
for (const cell of row) {
str += cell.isAlive ? CELL_CHAR.ALIVE : CELL_CHAR.DEAD;
}
str += '\n';
}
console.log(str);
}
function test() {
const boardScheme =
'........\n' +
'........\n' +
'...***..\n' +
'........\n' +
'........\n' +
'........\n';
const world = createWorld(boardScheme);
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