-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathlife.js
172 lines (146 loc) · 6.38 KB
/
life.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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 = '@';
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
}
}
})();