Files
004_comission/chris20202020/_ref/game-of-life-js/life.js
louiscklaw d8e1289123 update,
2025-01-31 19:30:10 +08:00

173 lines
6.4 KiB
JavaScript

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
}
}
})();