-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.html
304 lines (269 loc) · 10.9 KB
/
index.html
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>Game of Life</title>
<script src="https://d3js.org/d3.v4.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
<link rel="stylesheet" href="css/style.css">
<script src="js/grid.js"></script>
<script src="js/game.js"></script>
</head>
<body>
<div class="container">
<h1>John Conway's Game of Life</h1>
<p>See <a href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life" target="_blank" rel="noopener noreferrer">Wikipedia</a> to learn about this fascinating game. The rules are:</p>
<ul>
<li>A "cell" (blue square) will die on the next iteration of the game if it has strictly less than 2 (out of loneliness) or more than 3 (out of overpopulation) neighboring cells (including diagonals)</li>
<li>A new cell will come alive on an empty space if it has exactly 3 live neighbors</li>
</ul>
<p>Use the controls below to start/stop the simulation, tweak speed, load and save some starting layouts, change speed etc...</p>
<p>Check out the <a href="https://github.com/Yann-J/game-of-life" target="_blank" rel="noopener noreferrer">source code</a>.</p>
<form class="m-2">
<div class="form-row align-items-center">
<div class="col-auto" style="width: 80px;">
<i class="fa fa-tachometer"></i> <span class="small" id="speed-indicator">10 fps</span>
</div>
<div class="col-auto">
<input type="range" class="custom-range mr-3 pt-2" id="speed" value="10" min="1" max="50">
</div>
<div class="col-auto">
<div class="btn-group" role="group">
<button type="button" id="start" class="btn btn-warning btn-sm" title="Run simulation"><i class="fa fa-play"></i></button>
<button type="button" id="stop" class="btn btn-warning btn-sm" title="Stop" disabled="disabled"><i class="fa fa-stop"></i></button>
<button type="button" id="step" class="btn btn-warning btn-sm" title="Run one iteration"><i class="fa fa-step-forward"></i></button>
<button type="button" id="clear" class="btn btn-danger btn-sm" title="Clear layout"><i class="fa fa-times-circle"></i></button>
<button type="button" id="random" class="btn btn-danger btn-sm" title="Generate random layout"><i class="fa fa-random"></i></button>
</div>
</div>
<div class="col-auto">
<div class="input-group">
<input type="text" class="form-control form-control-sm" placeholder="Save this layout..." id="save-name" onFocus="this.select();">
<div class="input-group-append">
<button class="btn btn-outline-success btn-sm" type="button" id="save" title="Save this layout in browser cache"><i class="fa fa-save"></i></button>
<button class="btn btn-outline-danger btn-sm" type="button" id="delete" title="Delete layout with this name from browser cache"><i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="col-auto">
<select class="custom-select custom-select-sm" placeholder="Load a layout" id="load">
</select>
</div>
<div class="col-auto">
<span class="badge badge-secondary" id="iterations">0</span>
</div>
</div>
</form>
<div id="grid" class="m-2 crosshair"></div>
</div>
<script>
const ROWS = 100;
const COLUMNS = 100;
const WIDTH = 900; //px
let interval = 100; //ms
refreshStoredLayouts();
function emptyGrid() {
let data = new Array();
// iterate for rows
for (var row = 0; row < ROWS; row++) {
data.push( new Array() );
// iterate for cells/columns inside rows
for (var column = 0; column < COLUMNS; column++) {
data[row].push({
alive: false,
x: column,
y: row
});
}
}
return data;
};
let data = emptyGrid();
initGrid('#grid', ROWS, COLUMNS, WIDTH, data);
// Life Logic
// This mutates the grid...
function iterate(grid) {
// Store all new statuses in a new grid to avoid affecting other calculations
let newGrid = new Array(grid.length);
// workaround for JS modulus returning negative values...
function mod(n, m) {
return ((n % m) + m) % m;
}
for(let row = 0; row<grid.length; row++) {
newGrid[row] = new Array(grid[row].length);
for(let column = 0; column<grid[row].length; column++) {
// compute number of neighbors
let neighbors = 0;
[row-1, row, row+1].forEach(r => {
[column-1, column, column+1].forEach(c => {
// We use modulus the grid lengths to avoid border effects - this will make the shapes loop around the borders
if(grid[mod(r,grid.length)][mod(c,grid[row].length)].alive && !(r==row && c==column)) {
neighbors++;
}
});
});
// For a space that is 'populated':
// Each cell with one or no neighbors dies, as if by solitude.
// Each cell with four or more neighbors dies, as if by overpopulation.
// Each cell with two or three neighbors survives.
// For a space that is 'empty' or 'unpopulated'
// Each cell with three neighbors becomes populated.
if(grid[row][column].alive) {
newGrid[row][column] = (neighbors == 2 || neighbors == 3);
}
else {
newGrid[row][column] = (neighbors == 3);
}
}
}
// Mutate the initial grid all at once
for(let row = 0; row<grid.length; row++) {
for(let column = 0; column<grid[row].length; column++) {
grid[row][column].alive = newGrid[row][column];
}
}
};
// Timer
let iterations = 0;
let timer = null;
function reset() {
iterations = 0;
}
function tick() {
// Compute next frame
iterate(data);
iterations++;
// Redraw
redraw();
};
function redraw() {
// Update chart
// This is REALLY ugly ... there HAS to be a better way to tell D3 to redraw...
let e = new Event('draw');
let elements = document.querySelectorAll(".square");
Array.prototype.forEach.call(elements, function(el, i){
el.dispatchEvent(e);
});
// Update iterations badge
document.getElementById('iterations').textContent = String(iterations);
}
function start() {
console.log('Starting!');
if(timer) {clearInterval(timer);}
timer = setInterval(tick, interval);
document.getElementById('start').setAttribute('disabled','disabled');
document.getElementById('stop').removeAttribute('disabled');
};
function stop() {
console.log('Stopping!');
if(timer) {clearInterval(timer);}
timer = null;
document.getElementById('stop').setAttribute('disabled','disabled');
document.getElementById('start').removeAttribute('disabled');
};
document.getElementById('start').addEventListener('click', function() {
start();
});
document.getElementById('stop').addEventListener('click', function() {
stop();
});
document.getElementById('step').addEventListener('click', function() {
tick();
});
document.getElementById('speed').addEventListener('change', function() {
// range is 1-100 fps
let fps = Math.min(Math.max(this.value, 1), 100);
interval = 1000.0 / fps; //ms
console.log(interval);
// Update indicator
document.getElementById('speed-indicator').textContent = `${fps} fps`;
// Restart timer if running
if(timer) {
clearInterval(timer);
timer = setInterval(tick, interval);
}
});
document.getElementById('clear').addEventListener('click', function() {
stop();
reset();
data.forEach(row => {
row.forEach(cell => cell.alive = false);
})
redraw();
});
document.getElementById('random').addEventListener('click', function() {
stop();
reset();
data.forEach(row => {
row.forEach(cell => cell.alive = (Math.random() > 0.5));
})
redraw();
});
// Load / save
function refreshStoredLayouts() {
let list = document.getElementById('load');
list.innerHTML = "<option>Select layout to load</option>";
// From default list
for(let name in DEFAULT_LAYOUTS) {
let option = document.createElement('option');
option.text = name;
list.appendChild(option);
}
// Separator
if(localStorage.length) {
let option = document.createElement('option');
option.text = "──────────";
option.disabled = "disabled";
list.appendChild(option);
}
// From local storage
for(let i = 0; i<localStorage.length; i++) {
let name = localStorage.key(i);
let option = document.createElement('option');
option.text = name;
list.appendChild(option);
}
};
document.getElementById('save').addEventListener('click', function() {
// Produce a simplified model
let copy = data.map(row => {
// Use 0/1 instead of boolean to save space (it is stored as JSON string)
return row.map(cell => cell.alive ? 1 : 0);
});
let name = document.getElementById('save-name').value;
if(name && !DEFAULT_LAYOUTS[name]) {
console.log(`Saving layout ${name}`);
console.log(copy);
localStorage.setItem(name,JSON.stringify(copy));
refreshStoredLayouts();
}
});
document.getElementById('delete').addEventListener('click', function() {
let name = document.getElementById('save-name').value;
if(name) {
console.log(`Deleting layout ${name}`);
localStorage.removeItem(name);
refreshStoredLayouts();
}
});
document.getElementById('load').addEventListener('change', function() {
let name = this.value;
if(name) {
console.log(`Loading layout ${name}`);
let loaded = DEFAULT_LAYOUTS[name] || JSON.parse(localStorage.getItem(name));
if(loaded) {
console.log(loaded);
for(let i = 0; i<data.length; i++) {
for(let j = 0; j<data[i].length; j++) {
data[i][j].alive = !!loaded[i][j];
}
}
document.getElementById('save-name').value = name;
stop();
reset();
redraw();
}
}
});
</script>
</body>