/** * 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 world.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();