const presetGrids = { "Glider": [ [8, 10], [9, 8], [9, 10], [10, 9], [10, 10] ], "Penta-Decathlon": [ [9, 7], [9, 12], [10, 5], [10, 6], [10, 8], [10, 9], [10, 10], [10, 11], [10, 13], [10, 14], [11, 7], [11, 12] ], "Pulsar": [ [4, 6], [4, 7], [4, 8], [4, 12], [4, 13], [4, 14], [6, 4], [6, 9], [6, 11], [6, 16], [7, 4], [7, 9], [7, 11], [7, 16], [8, 4], [8, 9], [8, 11], [8, 16], [9, 6], [9, 7], [9, 8], [9, 12], [9, 13], [9, 14], [11, 6], [11, 7], [11, 8], [11, 12], [11, 13], [11, 14], [12, 4], [12, 9], [12, 11], [12, 16], [13, 4], [13, 9], [13, 11], [13, 16], [14, 4], [14, 9], [14, 11], [14, 16], [16, 6], [16, 7], [16, 8], [16, 12], [16, 13], [16, 14] ], "Heavy Ship": [ [7, 9], [7, 10], [8, 7], [8, 12], [9, 6], [10, 6], [10, 12], [11, 6], [11, 7], [11, 8], [11, 9], [11, 10], [11, 11] ], "Explosion": [ [8, 7], [8, 8], [8, 9], [8, 11], [8, 12], [8, 13], [9, 7], [9, 9], [9, 11], [9, 13], [10, 7], [10, 8], [10, 9], [10, 11], [10, 12], [10, 13] ] }; class Cell { static isAlive(cells, row, col, tickCount) { if (row < 0) row = cells.length - 1; else if (row >= cells.length) row = 0; if (col < 0) col = cells.length - 1; else if (col >= cells.length) col = 0; if (tickCount % 2 == 0) { return cells[row][col].alivePing; } return cells[row][col].alivePong; } constructor(domTarget, row, col, alive) { this.domTarget = domTarget; this.row = row; this.col = col; this.alivePing = alive; this.alivePong = false; this.domTarget.onclick = () => { this.alivePing = !this.alivePing; this.alivePong = !this.alivePong; this.domTarget.classList.toggle('alive'); } } rerender(alive) { if (alive) this.domTarget.classList.add('alive'); else this.domTarget.classList.remove('alive'); } tick(cells, tickCount) { let aliveNeighbours = 0; for (let row = this.row - 1; row <= this.row + 1; row++) { for (let col = this.col - 1; col <= this.col + 1; col++) { if (row == this.row && col == this.col) continue; aliveNeighbours += (Cell.isAlive(cells, row, col, tickCount) ? 1 : 0) } } let newState = false; const aliveTarget = (tickCount % 2 == 0 ? this.alivePing : this.alivePong); // Follow rules of https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life if (aliveTarget) { if (aliveNeighbours === 2 || aliveNeighbours === 3) { newState = true; } } else if (aliveNeighbours == 3) { newState = true; } if (tickCount % 2 == 0) { this.alivePong = newState; } else { this.alivePing = newState; } this.rerender(newState); } } /** * Generates the dom elements to be placed inside the visible grid of dimensions * size * size */ const generateDomGrid = (size) => { const grid = document.getElementById('game-of-life-grid'); for (let i = 0; i < size * size; i++) { const cell = document.createElement('div'); grid.appendChild(cell); } return grid; } /** * Generates a new 2D cell array given the grid containing dom cells and the * size of each dimension of said grid. */ const generateCellArray = (domGrid, size) => { const array = []; let i = 0; for (const domCell of domGrid.children) { const row = parseInt(i / size); const col = i % size; // Generate second dimension of array on the fly if (col === 0) { array[row] = []; } array[row][col] = new Cell(domCell, row, col, false); i++; } return array; } let tickCount = 0; // Function to be called on every tick to reach the next cell state. const globalTick = () => { size = sizeSelect.value; for (let i = 0; i < size; i++) { for (let j = 0; j < size; j++) { cells[i][j].tick(cells, tickCount); } } tickCount++; } // Sets up the reset button to reset the game to the initial state const setupResetButton = (resetButton, playButton) => { resetButton.onclick = () => { for (const row of cells) { for (const cell of row) { cell.alivePing = false; cell.alivePong = false; cell.domTarget.classList.remove('alive'); } } if (playing) { playButton.click(); } } }; // Sets up the tick button to trigger a global tick event. const setupTickButton = (tickButton) => { tickButton.onclick = globalTick; }; // Various globally referenced variables let playing = false; let interval; // Sets up the play button to trigger a global tick event on an interval. const setupPlayButton = (playButton) => { playButton.onclick = () => { if (playing) { playButton.textContent = 'Play'; clearInterval(interval); } else { playButton.textContent = 'Pause'; interval = setInterval(globalTick, intervalSelect.value); } playing = !playing; } }; // Sets up the interval select attribute to update the grid if the play // button is currently active. const setupIntervalSelect = (intervalSelect) => { intervalSelect.onchange = () => { if (playing) { clearInterval(interval) interval = setInterval(globalTick, intervalSelect.value); } } }; // Sets up selection of the size of the grid. const setupSizeSelect = (sizeSelect) => { // Resets the grid to be used with the updated const resetGrid = () => { const size = sizeSelect.value; // Remove previous grid on reset if (domGrid) { while (domGrid.firstChild) { domGrid.removeChild(domGrid.firstChild); } } domGrid = generateDomGrid(size); cells = generateCellArray(domGrid, size); domGrid.style.gridTemplateRows = `repeat(${size}, 1fr)`; domGrid.style.gridTemplateColumns = `repeat(${size}, 1fr)`; }; resetGrid(); sizeSelect.onchange = () => { resetGrid(); } }; // Sets up the input copy button to get all pairs in the users clipboard. const setupCopyInputButon = (copyInputButton) => { copyInputButton.onclick = () => { let pairs = ''; for (const row of cells) { for (const cell of row) { if (cell.domTarget.classList.contains('alive')) { pairs += `${cell.row} ${cell.col}\n`; } } } navigator.clipboard.writeText(pairs); copyInputButton.textContent = 'Copied!'; window.setTimeout(() => { copyInputButton.textContent = 'Copy alive pairs to clipboard' }, 2000); } }; // Sets up all the preset buttons to create a new grid containing presets // when pressed const setupPresetButtons = () => { const presetsContainer = document.getElementById('gol-presets-container'); for (const presetButton of presetsContainer.getElementsByClassName('preset-button')) { presetButton.onclick = () => { resetButton.click(); const key = presetButton.textContent; for (const cellPos of presetGrids[key]) { cells[cellPos[0]][cellPos[1]].domTarget.click(); } } } } let domGrid = document.getElementById('game-of-life-grid'); domGrid.style.height = `${domGrid.clientWidth}px`; let cells = []; const resetButton = document.getElementById('gol-reset-button'); const playButton = document.getElementById('gol-play-button'); const tickButton = document.getElementById('gol-tick-button'); const intervalSelect = document.getElementById('gol-interval-select'); const sizeSelect = document.getElementById('gol-size-select'); const copyInputButton = document.getElementById('gol-copy-input-button'); setupResetButton(resetButton, playButton); setupTickButton(tickButton); setupPlayButton(playButton); setupIntervalSelect(intervalSelect); setupSizeSelect(sizeSelect); setupCopyInputButon(copyInputButton); setupPresetButtons();