const life = (() => { const SIZE = 42; // Size of (square) board const INTERVAL = 300; // Frequency of screen updates const THRESHOLD = 33; // % chance a cell will be seeded with life // Printable representations of cells let LIVE = "L"; let DEAD = " "; // --------------------------------------------------------------------- // The rules /* - Any live cell with fewer than two live neighbours dies, as if caused by underpopulation. - Any live cell with two or three live neighbours lives on to the next generation. - Any live cell with more than three live neighbours dies, as if by overpopulation. - Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. */ const FEW = 2; const MANY = 3; const PLENTY = 3; const isLive = c => c === LIVE; const isUnderPopulated = n => n < FEW; const isOverPopulated = n => n > MANY; const canReproduce = n => n === PLENTY; const willContinue = n => !(isUnderPopulated(n)) && !(isOverPopulated(n)); // --------------------------------------------------------------------- const getRandomInt = (max, min=0) => { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive } // New boards are rows of DEAD cells, optionally seeded with LIVE cells. const newRow = () => Array(SIZE).fill(DEAD); const newBoard = shouldSeed => { let board = newRow().map(newRow); if (shouldSeed) { board = board.map(row => row.map(_ => getRandomInt(100) < THRESHOLD ? LIVE : DEAD)) } return board; } // When counting live neighbors, make sure to stay within array bounds. const isWithinBounds = v => v >= 0 && v < SIZE; const areWithinBounds = (x, y) => isWithinBounds(x) && isWithinBounds(y); // Given a coordinate pair, return an array of valid neighbor coordinate pairs. const neighborCoordinates = (x, y) => [ [x-1, y-1], [x, y-1], [x+1, y-1], [x-1, y], [x+1, y], [x-1, y+1], [x, y+1], [x+1, y+1], ].filter(xyArr => areWithinBounds(...xyArr)); // Functions to produce board containing coordinate pairs for each cell. const coordsForRow = (r, x=0) => r.map((_, y) => [x, y]); const coordsForBoard = b => b.map(coordsForRow); // Functions to produce board containing array of valid neighbor coordinate pairs. const neighborCoordinatesForRow = (r, x=0) => coordsForRow(r, x).map(xyArr => neighborCoordinates(...xyArr)); const neighborCoordinatesForBoard = b => b.map(neighborCoordinatesForRow); // Retrieve value of cell at specified coordinates. const cellAtCoorinate = (board, x, y) => board[x][y]; // Given a board and an array of neighbor coordinates, retrieve the neighbor cells. const neighborCellsForCoordinateArray = (board, arrayOfNeighborCoors) => { return arrayOfNeighborCoors.map(neighborCoordsForCell => { return neighborCoordsForCell.map(coordsArray => { return coordsArray.map(xyArray => cellAtCoorinate(board, ...xyArray)) }) }) }; // Given a board and an array of neighbor coordinates, return a board with the live neighbor count // for each cell. const boardAsNumberOfNeighbors = (board, arrayOfNeighborCoords) => { return neighborCellsForCoordinateArray(board, arrayOfNeighborCoords).map(neighborCellsForRow => { return neighborCellsForRow.map(neighborCellsForCell => { return neighborCellsForCell .filter(isLive) .reduce((total, _) => total + 1, 0) }); }); }; // Given a live neighbor count and a cell, calculate the cell's next state. const numberToLiveDead = (number, cell) => { if (isLive(cell)) { if (isUnderPopulated(number)) { return DEAD; } else if (isOverPopulated(number)) { return DEAD; } else if (willContinue(number)) { return LIVE; } } else if (canReproduce(number)){ return LIVE; } else { return DEAD; } }; // Given rows or boards of cells and neighbor counts, calculate next states. const numberRowAsLiveDeadCells = (rowOfNumbers, rowOfCells) => rowOfNumbers.map((n, i) => numberToLiveDead(n, rowOfCells[i])); const numberBoardAsLiveDeadCells = (boardOfNumbers, boardOfCells) => boardOfNumbers.map((r, i) => numberRowAsLiveDeadCells(r, boardOfCells[i])); // Functions for printing to the console. const printRow = r => r.join(' '); // A little horizontal space looks better const printBoard = b => { const boardAsString = b.map(printRow).join('\n'); console.log(boardAsString); return boardAsString; }; // The game loop! const main = board => { // Given a board, calculate all the valid neighbor coordinates. const coords = neighborCoordinatesForBoard(board); let neighbors; // The game board as number of live neighbors per cell. let generation = 0; // What generation we're on. let curr = ''; // Current generation as a string. let prev = ''; // For checking if this generation is same as current - 1. let prevMinusOne = ''; // For checking if this generation is same as current - 2. let tick = setInterval(() => { neighbors = boardAsNumberOfNeighbors(board, coords); // Calculate live neighbor counts. board = numberBoardAsLiveDeadCells(neighbors, board); // Calculate next state of board. console.clear(); prevMinusOne = prev.slice(); // Copy string representation of current - 2. prev = curr.slice(); // Copy string representation of current - 1. curr = printBoard(board); // Print board, saving string representation. console.log(`Generation ${generation}`); generation++; // If the current generation is identical to one of the previous two, // then we've reached the end of the simulation. if (curr === prev || curr === prevMinusOne) { clearInterval(tick); } }, INTERVAL); }; if (typeof module !== "undefined" && typeof module.exports !== "undefined") { // This must be node! module.exports = () => { main(newBoard(true)); // Start game with new board, seeded with some live cells. } } else { LIVE = true; DEAD = false; return { newBoard, neighborCoordinatesForBoard, boardAsNumberOfNeighbors, isLive, isUnderPopulated, isOverPopulated, willContinue, canReproduce, SIZE } } })();