1
2
3
4
5
6
7
8
9(function (global) {
10 'use strict';
11
12
13
14
15
16 function createGrid(rows, cols) {
17 const grid = new Array(rows);
18 for (let r = 0; r < rows; r++) {
19 grid[r] = new Array(cols).fill(0);
20 }
21 return grid;
22 }
23
24 function cloneGrid(grid) {
25 return grid.map(function (row) { return row.slice(); });
26 }
27
28 function getRows(grid) {
29 return grid.length;
30 }
31
32 function getCols(grid) {
33 return grid.length ? grid[0].length : 0;
34 }
35
36 function getCell(grid, row, col, wrap) {
37 const rows = grid.length;
38 const cols = rows ? grid[0].length : 0;
39 if (!rows || !cols) return 0;
40 if (wrap) {
41 row = ((row % rows) + rows) % rows;
42 col = ((col % cols) + cols) % cols;
43 } else if (row < 0 || row >= rows || col < 0 || col >= cols) {
44 return 0;
45 }
46 return grid[row][col];
47 }
48
49 function setCell(grid, row, col, value) {
50 if (row < 0 || row >= grid.length) return;
51 if (!grid.length || col < 0 || col >= grid[0].length) return;
52 grid[row][col] = value ? 1 : 0;
53 }
54
55 function countLiveNeighbors(grid, row, col, wrap) {
56 let count = 0;
57 for (let dr = -1; dr <= 1; dr++) {
58 for (let dc = -1; dc <= 1; dc++) {
59 if (dr === 0 && dc === 0) continue;
60 count += getCell(grid, row + dr, col + dc, wrap);
61 }
62 }
63 return count;
64 }
65
66
67
68
69
70
71
72 function nextGeneration(grid, wrap) {
73 const rows = grid.length;
74 const cols = rows ? grid[0].length : 0;
75 const next = createGrid(rows, cols);
76 for (let r = 0; r < rows; r++) {
77 for (let c = 0; c < cols; c++) {
78 const alive = grid[r][c] === 1;
79 const neighbors = countLiveNeighbors(grid, r, c, wrap);
80 if (alive) {
81 next[r][c] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
82 } else {
83 next[r][c] = (neighbors === 3) ? 1 : 0;
84 }
85 }
86 }
87 return next;
88 }
89
90 function countLiveCells(grid) {
91 let total = 0;
92 for (let r = 0; r < grid.length; r++) {
93 const row = grid[r];
94 for (let c = 0; c < row.length; c++) total += row[c];
95 }
96 return total;
97 }
98
99
100
101
102
103
104 function placePattern(grid, pattern, originRow, originCol) {
105 const next = cloneGrid(grid);
106 pattern.cells.forEach(function (offset) {
107 setCell(next, originRow + offset[0], originCol + offset[1], 1);
108 });
109 return next;
110 }
111
112
113
114
115
116
117 const PATTERNS = {
118 block: {
119 name: 'Block',
120 cells: [[0, 0], [0, 1], [1, 0], [1, 1]]
121 },
122 blinker: {
123 name: 'Blinker',
124 cells: [[0, 0], [0, 1], [0, 2]]
125 },
126 toad: {
127 name: 'Toad',
128 cells: [[0, 1], [0, 2], [0, 3], [1, 0], [1, 1], [1, 2]]
129 },
130 beacon: {
131 name: 'Beacon',
132 cells: [[0, 0], [0, 1], [1, 0], [1, 1], [2, 2], [2, 3], [3, 2], [3, 3]]
133 },
134 glider: {
135 name: 'Glider',
136 cells: [[0, 1], [1, 2], [2, 0], [2, 1], [2, 2]]
137 },
138 pulsar: {
139
140 name: 'Pulsar',
− cells: (function () {
− const quad = [
− [0, 2], [0, 3], [0, 4],
− [2, 0], [3, 0], [4, 0],
− [2, 5], [3, 5], [4, 5],
− [5, 2], [5, 3], [5, 4]
− ];
− const cells = [];
−
−
− [0, 6].forEach(function (rowOffset) {
− [0, 6].forEach(function (colOffset) {
− quad.forEach(function (cell) {
− cells.push([cell[0] + rowOffset, cell[1] + colOffset]);
− });
− });
− });
− return cells;
− })()
141 cells: [
142 [0, 2], [0, 3], [0, 4], [0, 8], [0, 9], [0, 10],
143 [5, 2], [5, 3], [5, 4], [5, 8], [5, 9], [5, 10],
144 [7, 2], [7, 3], [7, 4], [7, 8], [7, 9], [7, 10],
145 [12, 2], [12, 3], [12, 4], [12, 8], [12, 9], [12, 10],
146 [2, 0], [2, 5], [2, 7], [2, 12],
147 [3, 0], [3, 5], [3, 7], [3, 12],
148 [4, 0], [4, 5], [4, 7], [4, 12],
149 [8, 0], [8, 5], [8, 7], [8, 12],
150 [9, 0], [9, 5], [9, 7], [9, 12],
151 [10, 0], [10, 5], [10, 7], [10, 12]
152 ]
153 },
154 gliderGun: {
155 name: 'Gosper Glider Gun',
156 cells: [
157 [0, 24],
158 [1, 22], [1, 24],
159 [2, 12], [2, 13], [2, 20], [2, 21], [2, 34], [2, 35],
160 [3, 11], [3, 15], [3, 20], [3, 21], [3, 34], [3, 35],
161 [4, 0], [4, 1], [4, 10], [4, 16], [4, 20], [4, 21],
162 [5, 0], [5, 1], [5, 10], [5, 14], [5, 16], [5, 17], [5, 22], [5, 24],
163 [6, 10], [6, 16], [6, 24],
164 [7, 11], [7, 15],
165 [8, 12], [8, 13]
166 ]
167 }
168 };
169
170 const Life = {
171 createGrid: createGrid,
172 cloneGrid: cloneGrid,
173 getRows: getRows,
174 getCols: getCols,
175 getCell: getCell,
176 setCell: setCell,
177 countLiveNeighbors: countLiveNeighbors,
178 nextGeneration: nextGeneration,
179 countLiveCells: countLiveCells,
180 placePattern: placePattern,
181 PATTERNS: PATTERNS
182 };
183
184 if (typeof module !== 'undefined' && module.exports) {
185 module.exports = Life;
186 } else {
187 global.Life = Life;
188 }
189
190
191
192
193
194 if (typeof document !== 'undefined') {
195 document.addEventListener('DOMContentLoaded', initUI);
196 }
197
198 function initUI() {
199 const ROWS = 40;
200 const COLS = 60;
201 const CELL = 14;
202
203 const COLORS = {
204 background: '#12141c',
205 cell: '#5ee6a0',
206 gridLine: 'rgba(255, 255, 255, 0.06)'
207 };
208
209 const canvas = document.getElementById('lifeCanvas');
210 const ctx = canvas.getContext('2d');
211 canvas.width = COLS * CELL;
212 canvas.height = ROWS * CELL;
213
214 const playPauseBtn = document.getElementById('playPauseBtn');
215 const stepBtn = document.getElementById('stepBtn');
216 const clearBtn = document.getElementById('clearBtn');
217 const randomizeBtn = document.getElementById('randomizeBtn');
218 const speedSlider = document.getElementById('speedSlider');
219 const speedValue = document.getElementById('speedValue');
220 const wrapToggle = document.getElementById('wrapToggle');
221 const generationDisplay = document.getElementById('generation');
222 const patternList = document.getElementById('patternList');
223
224 let grid = Life.createGrid(ROWS, COLS);
225 let generation = 0;
226 let running = false;
227 let wrap = wrapToggle.checked;
228 let speed = parseInt(speedSlider.value, 10);
229 let timer = null;
230 let isDrawing = false;
231 let drawValue = 1;
232 let activePatternKey = null;
233
234 function inBounds(row, col) {
235 return row >= 0 && row < ROWS && col >= 0 && col < COLS;
236 }
237
238 function render() {
239 ctx.fillStyle = COLORS.background;
240 ctx.fillRect(0, 0, canvas.width, canvas.height);
241
242 ctx.fillStyle = COLORS.cell;
243 for (let r = 0; r < ROWS; r++) {
244 for (let c = 0; c < COLS; c++) {
245 if (grid[r][c]) {
246 ctx.fillRect(c * CELL, r * CELL, CELL - 1, CELL - 1);
247 }
248 }
249 }
250
251 ctx.strokeStyle = COLORS.gridLine;
252 ctx.lineWidth = 1;
253 ctx.beginPath();
254 for (let c = 0; c <= COLS; c++) {
255 ctx.moveTo(c * CELL + 0.5, 0);
256 ctx.lineTo(c * CELL + 0.5, canvas.height);
257 }
258 for (let r = 0; r <= ROWS; r++) {
259 ctx.moveTo(0, r * CELL + 0.5);
260 ctx.lineTo(canvas.width, r * CELL + 0.5);
261 }
262 ctx.stroke();
263 }
264
265 function updateGenerationDisplay() {
266 generationDisplay.textContent = String(generation);
267 }
268
269 function updatePlayPauseButton() {
270 playPauseBtn.textContent = running ? 'Pause' : 'Play';
271 playPauseBtn.classList.toggle('btn-active', running);
272 }
273
274 function tick() {
275 grid = Life.nextGeneration(grid, wrap);
276 generation++;
277 updateGenerationDisplay();
278 render();
279 }
280
281 function scheduleTick() {
282 if (timer) clearInterval(timer);
283 timer = setInterval(tick, 1000 / speed);
284 }
285
286 function play() {
287 if (running) return;
288 running = true;
289 updatePlayPauseButton();
290 scheduleTick();
291 }
292
293 function pause() {
294 running = false;
295 updatePlayPauseButton();
296 if (timer) {
297 clearInterval(timer);
298 timer = null;
299 }
300 }
301
302 function step() {
303 pause();
304 tick();
305 }
306
307 function clearGrid() {
308 pause();
309 grid = Life.createGrid(ROWS, COLS);
310 generation = 0;
311 updateGenerationDisplay();
312 render();
313 }
314
315 function randomize() {
316 pause();
317 for (let r = 0; r < ROWS; r++) {
318 for (let c = 0; c < COLS; c++) {
319 grid[r][c] = Math.random() < 0.25 ? 1 : 0;
320 }
321 }
322 generation = 0;
323 updateGenerationDisplay();
324 render();
325 }
326
327 function getCellFromEvent(evt) {
328 const rect = canvas.getBoundingClientRect();
329 const scaleX = canvas.width / rect.width;
330 const scaleY = canvas.height / rect.height;
331 const x = (evt.clientX - rect.left) * scaleX;
332 const y = (evt.clientY - rect.top) * scaleY;
333 return {
334 row: Math.floor(y / CELL),
335 col: Math.floor(x / CELL)
336 };
337 }
338
339 function placeActivePattern(row, col) {
340 const pattern = Life.PATTERNS[activePatternKey];
341 if (!pattern) return;
342 const maxDr = Math.max.apply(null, pattern.cells.map(function (c) { return c[0]; }));
343 const maxDc = Math.max.apply(null, pattern.cells.map(function (c) { return c[1]; }));
344 const originRow = row - Math.floor(maxDr / 2);
345 const originCol = col - Math.floor(maxDc / 2);
346 grid = Life.placePattern(grid, pattern, originRow, originCol);
347 render();
348 }
349
350 function buildPatternButtons() {
351 Object.keys(Life.PATTERNS).forEach(function (key) {
352 const pattern = Life.PATTERNS[key];
353 const btn = document.createElement('button');
354 btn.type = 'button';
355 btn.className = 'btn pattern-btn';
356 btn.textContent = pattern.name;
357 btn.dataset.pattern = key;
358 btn.addEventListener('click', function () {
359 if (activePatternKey === key) {
360 activePatternKey = null;
361 btn.classList.remove('active');
362 return;
363 }
364 activePatternKey = key;
365 Array.prototype.forEach.call(
366 patternList.querySelectorAll('.pattern-btn'),
367 function (b) { b.classList.remove('active'); }
368 );
369 btn.classList.add('active');
370 });
371 patternList.appendChild(btn);
372 });
373 }
374
375 canvas.addEventListener('mousedown', function (e) {
376 const cell = getCellFromEvent(e);
377 if (!inBounds(cell.row, cell.col)) return;
378 if (activePatternKey) {
379 placeActivePattern(cell.row, cell.col);
380 return;
381 }
382 isDrawing = true;
383 drawValue = grid[cell.row][cell.col] ? 0 : 1;
384 Life.setCell(grid, cell.row, cell.col, drawValue);
385 render();
386 });
387
388 canvas.addEventListener('mousemove', function (e) {
389 if (!isDrawing) return;
390 const cell = getCellFromEvent(e);
391 if (!inBounds(cell.row, cell.col)) return;
392 Life.setCell(grid, cell.row, cell.col, drawValue);
393 render();
394 });
395
396 window.addEventListener('mouseup', function () {
397 isDrawing = false;
398 });
399
400 playPauseBtn.addEventListener('click', function () {
401 if (running) pause(); else play();
402 });
403
404 stepBtn.addEventListener('click', step);
405 clearBtn.addEventListener('click', clearGrid);
406 randomizeBtn.addEventListener('click', randomize);
407
408 speedSlider.addEventListener('input', function () {
409 speed = parseInt(speedSlider.value, 10);
410 speedValue.textContent = String(speed);
411 if (running) scheduleTick();
412 });
413
414 wrapToggle.addEventListener('change', function () {
415 wrap = wrapToggle.checked;
416 });
417
418 document.addEventListener('keydown', function (e) {
419 if (e.code === 'Space' && e.target === document.body) {
420 e.preventDefault();
421 if (running) pause(); else play();
422 }
423 });
424
425 buildPatternButtons();
426 updateGenerationDisplay();
427 updatePlayPauseButton();
428 render();
429 }
430})(typeof window !== 'undefined' ? window : globalThis);
431
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.