This commit is contained in:
louiscklaw
2025-01-31 19:30:10 +08:00
parent abff74fd77
commit d8e1289123
168 changed files with 91704 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
A JavaScript implementation of [Conway's Game of Life](http://web.stanford.edu/%7Ecdebs/GameOfLife/).
- represents the board as 2D array of cells
- creates a second representation of board as 2D array of number of live neighbors
- uses map, filter, and reduce for calculation/iteration
`index.html` displays a canvas version of the program, with play/pause and reload buttons. (Play/pause can be triggered with the `Space` or `Enter` keys. Reloading can be triggered with the `r` key.)
Also, can run on the terminal or in the browser console.

View File

@@ -0,0 +1,135 @@
console.log('Loading Game of Life');
const {
newBoard,
neighborCoordinatesForBoard,
boardAsNumberOfNeighbors,
isLive,
isUnderPopulated,
isOverPopulated,
willContinue,
canReproduce,
SIZE
} = life;
const playPause = document.querySelector('[data-play-pause]');
const reload = document.querySelector('[data-reload]');
const startClass = 'fa-play';
const pauseClass = 'fa-pause';
const canvas = document.querySelector('#life');
const ctx = canvas.getContext('2d');
const {height, width} = canvas;
const UPDATE_FREQUENCY = 30;
const GRID_COUNT = SIZE;
const CELL_SIZE = width/GRID_COUNT;
const COLORS = {
// live: 'rgba(200, 0, 0, 1)',
live: 'rgba(40, 169, 255, 0.75)',
dead: 'rgba(255, 255, 255, 0.5)'
};
// Uses function declaration for binding `this`
// to the canvas context.
function _renderBox (isLive, xGrid, yGrid) {
this.lineWidth = 0.8;
this.strokeStyle = isLive ? COLORS.live : COLORS.dead;
this.strokeRect(xGrid*CELL_SIZE,
yGrid*CELL_SIZE,
CELL_SIZE,
CELL_SIZE);
}
//
const liveAt = _renderBox.bind(ctx, true);
const deadAt = _renderBox.bind(ctx, false);
const numberToIsLive = (number, cell) => {
if (isLive(cell)) {
if (isUnderPopulated(number)) {
// return DEAD;
return false;
} else if (isOverPopulated(number)) {
// return DEAD;
return false
} else if (willContinue(number)) {
// return LIVE;
return true;
}
} else if (canReproduce(number)){
// return LIVE;
return true;
} else {
// return DEAD;
return false;
}
};
// Given rows or boards of cells and neighbor counts, calculate next states.
const numberRowAsLiveDeadCells = (rowOfNumbers, rowOfCells) => rowOfNumbers.map((n, i) => numberToIsLive(n, rowOfCells[i]));
const numberBoardAsLiveDeadCells = (boardOfNumbers, boardOfCells) => boardOfNumbers.map((r, i) => numberRowAsLiveDeadCells(r, boardOfCells[i]));
const renderRow = (r, y) => r.map((c, i) => (c ? liveAt(i, y) : deadAt(i, y)) && c);
const renderBoard = b => b.map(renderRow);
let rafID;
let board = newBoard(true);
const coords = neighborCoordinatesForBoard(board);
let neighbors; // The game board as number of live neighbors per cell.
let isRunning = false;
const main = () => {
if (isRunning) {
// Given a board, calculate all the valid neighbor coordinates.
neighbors = boardAsNumberOfNeighbors(board, coords); // Calculate live neighbor counts.
board = numberBoardAsLiveDeadCells(neighbors, board); // Calculate next state of board.
renderBoard(board);
setTimeout(() => {
rafID = requestAnimationFrame(main);
}, UPDATE_FREQUENCY);
}
}
const togglePlaying = () => {
if (isRunning) {
cancelAnimationFrame(rafID);
playPause.querySelector('i').classList.add(startClass);
playPause.querySelector('i').classList.remove(pauseClass);
} else {
rafID = requestAnimationFrame(main);
playPause.querySelector('i').classList.remove(startClass);
playPause.querySelector('i').classList.add(pauseClass);
}
isRunning = !isRunning;
};
const reloadBoard = () => board = newBoard(true);
playPause.addEventListener('click', togglePlaying);
reload.addEventListener('click', reloadBoard);
document.addEventListener('keydown', (e) => {
console.log(e.keyCode);
if (e.getModifierState('Control')) {
return;
}
switch (e.keyCode) {
case 32:
case 13:
e.preventDefault();
togglePlaying()
break;
case 82:
e.preventDefault();
reloadBoard();
break;
}
})

View File

@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Document</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<style>
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: column;
background-color: rgba(40, 169, 255, 0.25);
}
canvas {
border: 1px solid rgba(0, 0, 0, 0.7);
background-color: #fff;
}
button {
width: 80px;
}
</style>
</head>
<body>
<canvas id="life"
width="420"
height="420">
Loading...
</canvas>
<div>
<button data-play-pause>
<i class="fa fa-play"></i>
</button>
<button data-reload>
<i class="fa fa-refresh"></i>
</button>
</div>
<script src="life.js"></script>
<script src="canvas.js"></script>
</body>
</html>

View File

@@ -0,0 +1,2 @@
const life = require('./life');
life();

View File

@@ -0,0 +1,172 @@
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
}
}
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"name": "js-game-of-life",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"nodemon": "^1.17.1"
}
}