-
Notifications
You must be signed in to change notification settings - Fork 0
/
ConwaysRules.js
312 lines (263 loc) · 12.5 KB
/
ConwaysRules.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
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
305
306
307
308
309
310
311
312
/*
The 4 Rules of Conways Game:
1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
2. Any live cell with two or three live neighbours lives on to the next generation.
3. Any live cell with more than three live neighbours dies, as if by overpopulation.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
*/
"use strict"; // I program in TypeScript where I work, so quite liked the errors thrown, kind of like a compiler, as it caught some bugs early.
// I removed the oncontextmenu because in the site, it's very unaesthetic and ugly.
// It only obscured the grid, and provided poor UX.
document.oncontextmenu = function () {
return false;
}
/*<Summary> In an attempt to logically group the methods, we place all grid manipulation on the Grid object.
*/
const Grid = {
// The number of columns - same as rows, as we render a square
columns : 0,
/*<Summary> Clears the grid, and removes all the classes applied, also clears the info, and some other bits to start over fresh.
*/
ClearGrid: function () {
if (Conway.simulationRunning) {
return;
}
const grid = document.querySelectorAll('.boxSmall, .boxMedium, .boxLarge, .boxXLarge');
const info = document.getElementById("information");
for (let i = 0; i < grid.length; i++) {
grid[i].classList.remove("population");
}
info.innerHTML = "";
document.getElementById("information").innerHTML = ""; // We dont need to keep warnings on screen for the user, if they're rendering a grid.
},
/*<Summary> Actually disposes and deletes the grid; not just clearing the grid.
*/
DeleteGrid: function () {
if (Conway.simulationRunning) {
return;
}
const grid = document.querySelectorAll('.boxSmall, .boxMedium, .boxLarge, .boxXLarge, br');
for (let i = 0; i < grid.length; i++) {
let elem = grid[i];
elem.parentNode.removeChild(elem);
}
},
/*<Summary> Responsible for randomly generating obstacles in the grid, ~25% chance that the square will be an obstacle.
*/
RandomObstacleGeneration: function () {
if (Conway.simulationRunning) {
return;
}
const grid = document.querySelectorAll('.boxSmall, .boxMedium, .boxLarge, .boxXLarge');
if (grid.length) { // Just check the grid has been rendered. Or else we will warn the user they need to render a grid first
for (let i = 0; i < grid.length; i++) {
let highlight = (Math.round((Math.random()) * 100) / 100) // Generare a random number
if (highlight > 0.75) {
grid[i].classList.add("population")
} else {
// This class removal is just to ensure the user can click randomly generate button multiple times, and "try again" if they're not happy with the generation
grid[i].classList.remove("population");
}
}
} else {
document.getElementById("information").innerHTML = "You must first render a grid before you randomly generate obstacles.";
}
},
/*<Summary> Renders a small, medium or large grid.
*/
RenderGrid: function () {
if (Conway.simulationRunning) {
return;
}
let boxSizeClass = "", // CSS class to apply to the cells of the grid (changes if they render a small grid, medium grid or large grid).
checkedRadioButton;
const radioButtons = document.getElementsByName('gridSize');
Grid.DeleteGrid(); // If there's already a grid, dispose of anything before we draw a new grid.
for (let i = 0; i < radioButtons.length; i++) {
if (radioButtons[i].checked) {
checkedRadioButton = radioButtons[i];
}
}
// The number variable is the number of rows and columns we render, and the boxSizeClass is the css styling we apply to each of the cells in the grid.
switch (checkedRadioButton.value) {
case 'small':
this.columns = 15;
boxSizeClass = "boxSmall";
break;
case 'medium':
this.columns = 25;
boxSizeClass = "boxMedium";
break;
case 'large':
this.columns = 35;
boxSizeClass = "boxLarge"
break;
case 'xlarge':
this.columns = 100;
boxSizeClass = "boxXLarge"
break;
}
for (let x = 1; x <= this.columns; x++) { // This deals with total numbers of rows
for (let i = 1; i <= this.columns; i++) { // This deals with drawing one row
const div = document.createElement("div");
div.classList.add(boxSizeClass);
if (i === 1) {
let linebreak = document.createElement("br"); // Push the next row onto a new line
document.getElementById("main").appendChild(linebreak);
}
document.getElementById("main").appendChild(div);
}
}
Grid.AttachEventListeners(); // Attaches listeners to the parent container of my grid.
},
/*<Summary> Attaches event listeners to the main container. Avoids attaching event listeners to every cell, so we can bubble to parent.
*/
AttachEventListeners: function () {
const main = document.getElementById('main');
let drag = "";
// This was used to have "create drags" and "delete drags"
// If your first click of the drag, begins on an obstacle grid, your drag will only ever remove obstacles
// If your first click of the drag, begins on a free cell, you will only ever create obstacles.
// Or you can individually click a cell to toggle obstacle on/off
main.onmousedown = function (ev) {
if (ev.target.classList.contains("population")) {
drag = "remove";
} else if (ev.target.className.startsWith('box')) {
drag = "create";
} else {
drag = "";
}
}
main.onmouseup = function (ev) {
drag = "";
}
/*<Summary> Handles drag events,
*/
function dragHandler(ev) {
if (drag === "create" && ev.target.className.startsWith('box')) {
ev.target.classList.add("population");
} else if (drag === "remove" && ev.target.className.startsWith('box')) {
ev.target.classList.remove("population");
}
}
/*<Summary> Handles mousemove event.
*/
function mouseMoveHandler(ev) {
if (drag === "create" && ev.target.className.startsWith('box')) {
ev.target.classList.add("population");
} else if (drag === "remove" && ev.target.className.startsWith('box')) {
ev.target.classList.remove("population");
}
}
/*<Summary> Handles mouse down events. We add start and end buttons before we add obstacles.
*/
function mouseDownHandler(ev) { // handles setting start and end square
if (ev.target.className.startsWith('box')) {
ev.target.classList.toggle("population");
return;
}
}
// We add it to the parent, to avoid attaching eventListeners to every single child.
// Essentially event bubbling to the parent.
main.addEventListener("mousedown", mouseDownHandler);
main.addEventListener('mousemove', mouseMoveHandler);
main.addEventListener('drag', dragHandler);
}
}
/*<Summary> In an attempt to logically group methods, we place all simulation logic on the Conway object.
*/
const Conway = {
simulationRunning : false,
generationCount : 0,
// We can't just immeadiatly highlight all the ones that will come back to life/die, or else we'll throw off subsequent calculations later in the row etc.
// So push it an array and toggle the population class before next generation
totalNodes : [],
Begin : function(){
if(!this.simulationRunning) {
// Make the grid radio buttons unclickable whilst the simulation runs
document.getElementById("GridRender").classList.add("noClick");
document.getElementById("main").classList.add("noClick");
// We could just call Tick straightaway, but we need to reset generation etc. if the user renders Conways multiple times
this.simulationRunning = true;
this.generationCount = 0;
Conway.Tick();
}
},
End : function(){
if (!this.simulationRunning) { // Just check the grid has been rendered. Or else we will warn the user they need to render a grid first
document.getElementById("information").innerHTML = "You must first start the game before attempting to end it.";
this.simulationRunning = false;
return;
}
this.simulationRunning = false;
this.totalNodes.length = 0;
document.getElementById("GridRender").classList.remove("noClick");
document.getElementById("main").classList.remove("noClick");
},
Tick: function () {
// Has the user ended the simulation? Stop drawing
if (!this.simulationRunning) {
return;
} else {
const grid = document.querySelectorAll('.boxSmall, .boxMedium, .boxLarge, .boxXLarge');
if (!grid.length) { // Just check the grid has been rendered. Or else we will warn the user they need to render a grid first
document.getElementById("information").innerHTML = "You must first render a grid before you attempt to run the game.";
this.simulationRunning = false;
return;
}
if(this.totalNodes.length){
for(let i=0; i < this.totalNodes.length; i++){
this.totalNodes[i].classList.toggle("population");
}
}
this.totalNodes = [];
document.getElementById("information").innerText = "Generation " + this.generationCount;
this.generationCount++;
for (let i = 0; i < grid.length; i++) {
// The current population node we're going to explore
let current = grid[i],
up = grid[i - Grid.columns],
down = grid[i + Grid.columns],
left = grid[i - 1],
right = grid[i + 1],
diagTopLeft = grid[i - Grid.columns - 1],
diagTopRight = grid[i - Grid.columns + 1],
diagBotLeft = grid[i + Grid.columns - 1],
diagBotRight = grid[i + Grid.columns + 1];
const neighbours = [up, down, left, right, diagTopLeft, diagTopRight, diagBotLeft, diagBotRight];
const neighbourCount = Conway.CalculateNeighbours(current, neighbours);
if (current.classList.contains("population")) {
this.LiveCell(current, neighbourCount);
} else {
this.DeadCell(current, neighbourCount);
}
}
setTimeout(() => {
this.Tick();
}, 500)
}
},
CalculateNeighbours: function (current, neighbours) {
let neighbourCount = 0;
for (let i = 0; i < neighbours.length; i++) {
if (neighbours[i] && neighbours[i].classList.contains("population")) {
neighbourCount++;
}
}
return neighbourCount;
},
DeadCell: function (current, neighbourCount) {
if(neighbourCount === 3){
this.totalNodes.push(current);
}
},
LiveCell: function (current, neighbourCount) {
if(neighbourCount === 0 || neighbourCount === 1){
// Dies by underpopulation
this.totalNodes.push(current);
} else if(neighbourCount > 3){
// Dies by overpopulation
this.totalNodes.push(current)
}
}
}