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

BIN
chris20202020/_ref/1.docx Normal file

Binary file not shown.

BIN
chris20202020/_ref/1.pdf Normal file

Binary file not shown.

BIN
chris20202020/_ref/2.docx Normal file

Binary file not shown.

BIN
chris20202020/_ref/2.pdf Normal file

Binary file not shown.

BIN
chris20202020/_ref/3.pdf Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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"
}
}