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 name: 'Pulsar',
140 cells: (function () {
141 const quad = [
142 [0, 2], [0, 3], [0, 4],
143 [2, 0], [3, 0], [4, 0],
144 [2, 5], [3, 5], [4, 5],
145 [5, 2], [5, 3], [5, 4]
146 ];
147 const cells = [];
148
149
150 [0, 6].forEach(function (rowOffset) {
151 [0, 6].forEach(function (colOffset) {
152 quad.forEach(function (cell) {
153 cells.push([cell[0] + rowOffset, cell[1] + colOffset]);
154 });
155 });
156 });
157 return cells;
158 })()
159 },
160 gliderGun: {
161 name: 'Gosper Glider Gun',
162 cells: [
163 [0, 24],
164 [1, 22], [1, 24],
165 [2, 12], [2, 13], [2, 20], [2, 21], [2, 34], [2, 35],
166 [3, 11], [3, 15], [3, 20], [3, 21], [3, 34], [3, 35],
167 [4, 0], [4, 1], [4, 10], [4, 16], [4, 20], [4, 21],
168 [5, 0], [5, 1], [5, 10], [5, 14], [5, 16], [5, 17], [5, 22], [5, 24],
169 [6, 10], [6, 16], [6, 24],
170 [7, 11], [7, 15],
171 [8, 12], [8, 13]
172 ]
173 }
174 };
175
176 const Life = {
177 createGrid: createGrid,
178 cloneGrid: cloneGrid,
179 getRows: getRows,
180 getCols: getCols,
181 getCell: getCell,
182 setCell: setCell,
183 countLiveNeighbors: countLiveNeighbors,
184 nextGeneration: nextGeneration,
185 countLiveCells: countLiveCells,
186 placePattern: placePattern,
187 PATTERNS: PATTERNS
188 };
189
190 if (typeof module !== 'undefined' && module.exports) {
191 module.exports = Life;
192 } else {
193 global.Life = Life;
194 }
195
196
197
198
199
200 if (typeof document !== 'undefined') {
201 document.addEventListener('DOMContentLoaded', initUI);
202 }
203
204 function initUI() {
205 const ROWS = 40;
206 const COLS = 60;
207 const CELL = 14;
208
209 const COLORS = {
210 background: '#12141c',
211 cell: '#5ee6a0',
212 gridLine: 'rgba(255, 255, 255, 0.06)'
213 };
214
215 const canvas = document.getElementById('lifeCanvas');
216 const ctx = canvas.getContext('2d');
217 canvas.width = COLS * CELL;
218 canvas.height = ROWS * CELL;
219
220 const playPauseBtn = document.getElementById('playPauseBtn');
221 const stepBtn = document.getElementById('stepBtn');
222 const clearBtn = document.getElementById('clearBtn');
223 const randomizeBtn = document.getElementById('randomizeBtn');
224 const speedSlider = document.getElementById('speedSlider');
225 const speedValue = document.getElementById('speedValue');
226 const wrapToggle = document.getElementById('wrapToggle');
227 const generationDisplay = document.getElementById('generation');
228 const patternList = document.getElementById('patternList');
229
230 let grid = Life.createGrid(ROWS, COLS);
231 let generation = 0;
232 let running = false;
233 let wrap = wrapToggle.checked;
234 let speed = parseInt(speedSlider.value, 10);
235 let timer = null;
236 let isDrawing = false;
237 let drawValue = 1;
238 let activePatternKey = null;
239
240 function inBounds(row, col) {
241 return row >= 0 && row < ROWS && col >= 0 && col < COLS;
242 }
243
244 function render() {
245 ctx.fillStyle = COLORS.background;
246 ctx.fillRect(0, 0, canvas.width, canvas.height);
247
248 ctx.fillStyle = COLORS.cell;
249 for (let r = 0; r < ROWS; r++) {
250 for (let c = 0; c < COLS; c++) {
251 if (grid[r][c]) {
252 ctx.fillRect(c * CELL, r * CELL, CELL - 1, CELL - 1);
253 }
254 }
255 }
256
257 ctx.strokeStyle = COLORS.gridLine;
258 ctx.lineWidth = 1;
259 ctx.beginPath();
260 for (let c = 0; c <= COLS; c++) {
261 ctx.moveTo(c * CELL + 0.5, 0);
262 ctx.lineTo(c * CELL + 0.5, canvas.height);
263 }
264 for (let r = 0; r <= ROWS; r++) {
265 ctx.moveTo(0, r * CELL + 0.5);
266 ctx.lineTo(canvas.width, r * CELL + 0.5);
267 }
268 ctx.stroke();
269 }
270
271 function updateGenerationDisplay() {
272 generationDisplay.textContent = String(generation);
273 }
274
275 function updatePlayPauseButton() {
276 playPauseBtn.textContent = running ? 'Pause' : 'Play';
277 playPauseBtn.classList.toggle('btn-active', running);
278 }
279
280 function tick() {
281 grid = Life.nextGeneration(grid, wrap);
282 generation++;
283 updateGenerationDisplay();
284 render();
285 }
286
287 function scheduleTick() {
288 if (timer) clearInterval(timer);
289 timer = setInterval(tick, 1000 / speed);
290 }
291
292 function play() {
293 if (running) return;
294 running = true;
295 updatePlayPauseButton();
296 scheduleTick();
297 }
298
299 function pause() {
300 running = false;
301 updatePlayPauseButton();
302 if (timer) {
303 clearInterval(timer);
304 timer = null;
305 }
306 }
307
308 function step() {
309 pause();
310 tick();
311 }
312
313 function clearGrid() {
314 pause();
315 grid = Life.createGrid(ROWS, COLS);
316 generation = 0;
317 updateGenerationDisplay();
318 render();
319 }
320
321 function randomize() {
322 pause();
323 for (let r = 0; r < ROWS; r++) {
324 for (let c = 0; c < COLS; c++) {
325 grid[r][c] = Math.random() < 0.25 ? 1 : 0;
326 }
327 }
328 generation = 0;
329 updateGenerationDisplay();
330 render();
331 }
332
333 function getCellFromEvent(evt) {
334 const rect = canvas.getBoundingClientRect();
335 const scaleX = canvas.width / rect.width;
336 const scaleY = canvas.height / rect.height;
337 const x = (evt.clientX - rect.left) * scaleX;
338 const y = (evt.clientY - rect.top) * scaleY;
339 return {
340 row: Math.floor(y / CELL),
341 col: Math.floor(x / CELL)
342 };
343 }
344
345 function placeActivePattern(row, col) {
346 const pattern = Life.PATTERNS[activePatternKey];
347 if (!pattern) return;
348 const maxDr = Math.max.apply(null, pattern.cells.map(function (c) { return c[0]; }));
349 const maxDc = Math.max.apply(null, pattern.cells.map(function (c) { return c[1]; }));
350 const originRow = row - Math.floor(maxDr / 2);
351 const originCol = col - Math.floor(maxDc / 2);
352 grid = Life.placePattern(grid, pattern, originRow, originCol);
353 render();
354 }
355
356 function buildPatternButtons() {
357 Object.keys(Life.PATTERNS).forEach(function (key) {
358 const pattern = Life.PATTERNS[key];
359 const btn = document.createElement('button');
360 btn.type = 'button';
361 btn.className = 'btn pattern-btn';
362 btn.textContent = pattern.name;
363 btn.dataset.pattern = key;
364 btn.addEventListener('click', function () {
365 if (activePatternKey === key) {
366 activePatternKey = null;
367 btn.classList.remove('active');
368 return;
369 }
370 activePatternKey = key;
371 Array.prototype.forEach.call(
372 patternList.querySelectorAll('.pattern-btn'),
373 function (b) { b.classList.remove('active'); }
374 );
375 btn.classList.add('active');
376 });
377 patternList.appendChild(btn);
378 });
379 }
380
381 canvas.addEventListener('mousedown', function (e) {
382 const cell = getCellFromEvent(e);
383 if (!inBounds(cell.row, cell.col)) return;
384 if (activePatternKey) {
385 placeActivePattern(cell.row, cell.col);
386 return;
387 }
388 isDrawing = true;
389 drawValue = grid[cell.row][cell.col] ? 0 : 1;
390 Life.setCell(grid, cell.row, cell.col, drawValue);
391 render();
392 });
393
394 canvas.addEventListener('mousemove', function (e) {
395 if (!isDrawing) return;
396 const cell = getCellFromEvent(e);
397 if (!inBounds(cell.row, cell.col)) return;
398 Life.setCell(grid, cell.row, cell.col, drawValue);
399 render();
400 });
401
402 window.addEventListener('mouseup', function () {
403 isDrawing = false;
404 });
405
406 playPauseBtn.addEventListener('click', function () {
407 if (running) pause(); else play();
408 });
409
410 stepBtn.addEventListener('click', step);
411 clearBtn.addEventListener('click', clearGrid);
412 randomizeBtn.addEventListener('click', randomize);
413
414 speedSlider.addEventListener('input', function () {
415 speed = parseInt(speedSlider.value, 10);
416 speedValue.textContent = String(speed);
417 if (running) scheduleTick();
418 });
419
420 wrapToggle.addEventListener('change', function () {
421 wrap = wrapToggle.checked;
422 });
423
424 document.addEventListener('keydown', function (e) {
425 if (e.code === 'Space' && e.target === document.body) {
426 e.preventDefault();
427 if (running) pause(); else play();
428 }
429 });
430
431 buildPatternButtons();
432 updateGenerationDisplay();
433 updatePlayPauseButton();
434 render();
435 }
436})(typeof window !== 'undefined' ? window : globalThis);
437
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.