4
\$\begingroup\$

Conway's Game of Life

Conway's Game of Life is a mathematical game invented by English Mathematician John Horton Conway. The game is based on the theory of cellular automatons. It is a so-called zero-player game because once the "player" set the initial state of the automaton, the game is just about to watch the deterministic outcome of the game.

The rules are as follows:

  1. Living cells with 2 or 3 neighbors stay alive.
  2. Living cells with less than two or more than 3 neighbors die.
  3. Dead cells with exactly 3 neighbors are "re-born"

For more information, please see the great article on wikipedia.

My implementation

I've used pure JavaScript and a bit of JQuery to implement the Game of Life.

The code

let WIDTH = 800;
let HEIGHT = 500;
const RES = 10;

const DEAD_CELL = 0;
const LIVING_CELL = 1;
const BORDER = 2;

let grid;
let isRunning = false;
let interval;
let generation = 0;


window.addEventListener('load', function() {
  if(window.innerWidth < 840) {
    WIDTH = window.innerWidth - 100;
    WIDTH = WIDTH - WIDTH % 100;
    HEIGHT = window.innerHeight - 40;
    HEIGHT = HEIGHT - HEIGHT % 100;
  }
  document.getElementById('canvas').height = HEIGHT;
  document.getElementById('canvas').width = WIDTH;
  generation = 0;
  updateGeneration();
  initialDraw(); 
});

window.addEventListener('resize', function() {
  if(window.innerWidth < 840) {
    WIDTH = window.innerWidth - 100;
    WIDTH = WIDTH - WIDTH % 100;
    HEIGHT = window.innerHeight - 40;
    HEIGHT = HEIGHT - HEIGHT % 100;
  }
  else {
    HEIGHT = 500;
    WIDTH = 800;
  }
  document.getElementById('canvas').height = HEIGHT;
  document.getElementById('canvas').width = WIDTH;
  generation = 0;
  updateGeneration();
  initialDraw(); 
  if(isRunning) {
    clearInterval(interval);
    isRunning = false;
    document.getElementById('startStop').textContent = 'Start';
    switchButtons();
  }
});

/**
 * Allows the user to draw own pattern on the canvas
 */
document.getElementById('canvas').addEventListener('click', function(e) {
  // Which box did user click on?
  const row = Math.floor(e.offsetX / RES);
  const col = Math.floor(e.offsetY / RES);

  // Don't allow changes while simulation is running
  if (isRunning) {
    return;
  }

  // Case user clicked on a dead cell
  if (grid[row][col] === DEAD_CELL) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();

    grid[row][col] = LIVING_CELL;
    drawCell('#2B823A', 'LightGray', 1, row * RES, col * RES, RES, RES);
  } else if (grid[row][col] === LIVING_CELL) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();

    grid[row][col] = DEAD_CELL;
    drawCell('white', 'LightGray', 1, row * RES, col * RES, RES, RES);
  }
});

/**
 * Starts / Stops the simulation and enables / disables the other buttons
 */
document.getElementById('startStop').addEventListener('click', function() {
  // Case user wants to start simulation
  if (!isRunning) {
    interval = setInterval(function() {
      const cols = WIDTH / RES;
      const rows = HEIGHT / RES;
      draw(cols, rows);

      // Count generation and show user
      updateGeneration();
      generation++;
    }, (1 / document.getElementById('speed').value) * 1000);

    isRunning = true;
    document.getElementById('startStop').textContent = 'Stop';
    switchButtons();
  } else {
    clearInterval(interval);
    isRunning = false;
    document.getElementById('startStop').textContent = 'Start';
    switchButtons();
  }
});

/**
 * Deletes pattern from canvas
 */
document.getElementById('clear').addEventListener('click', function() {
  if (!isRunning) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();
    
    createGrid(WIDTH / RES, HEIGHT / RES);
    draw(WIDTH / RES, HEIGHT / RES);
  }
});

/**
 * Changes the speed of the simulation: The higher the value,
 * the faster the simulation
 */
document.getElementById('speed').addEventListener('change', function() {
  if (isRunning) {
    clearInterval(interval);
    interval = setInterval(function() {
      const cols = WIDTH / RES;
      const rows = HEIGHT / RES;
      draw(cols, rows);

      // Count generation and show user
      updateGeneration();
      generation++;
    }, (1 / document.getElementById('speed').value) * 1000);
  }
});

/**
 * Random pattern on the canvas
 */
document.getElementById('random').addEventListener('click', function() {
  // Case user wants to start simulation
  if (!isRunning) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();

    const cols = WIDTH / RES;
    const rows = HEIGHT / RES;
    grid = createArray(cols, rows);
    createRandom(cols, rows);

    draw(cols, rows);
  }
});

/**
 * Creates a Lightweight spaceship
 * https://www.conwaylife.com/wiki/Lightweight_spaceship
 */
document.getElementById('lwss').addEventListener('click', function() {
  // Case user wants to start simulation
  if (!isRunning) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();

    const cols = WIDTH / RES;
    const rows = HEIGHT / RES;

    createGrid(cols, rows);

    const x = Math.floor(cols / 2);
    const y = Math.floor(rows / 2);

    createLwss(x, y);

    draw(cols, rows);
  }
});

/**
 * Creates a r-pentomino
 * https://www.conwaylife.com/wiki/R-pentomino
 */
document.getElementById('r-pentomino').addEventListener('click', function() {
  // Case user wants to start simulation
  if (!isRunning) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();

    const cols = WIDTH / RES;
    const rows = HEIGHT / RES;

    createGrid(cols, rows);

    const x = Math.floor(cols / 2);
    const y = Math.floor(rows / 2);

    createRPentomino(x, y);

    draw(cols, rows);
  }
});

/**
 * Create an acorn
 * https://www.conwaylife.com/wiki/Acorn
 */
document.getElementById('acorn').addEventListener('click', function() {
  // Case user wants to start simulation
  if (!isRunning) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();

    const cols = WIDTH / RES;
    const rows = HEIGHT / RES;

    createGrid(cols, rows);

    const x = Math.floor(cols / 2);
    const y = Math.floor(rows / 2);

    createAcorn(x, y);

    draw(cols, rows);
  }
});

/**
 * Creates a gosper glider gun
 * https://www.conwaylife.com/wiki/Gosper_glider_gun
 */
document.getElementById('gosper').addEventListener('click', function() {
  // Case user wants to start simulation
  if (!isRunning) {
    // Generation needs to be resetted
    generation = 0;
    updateGeneration();

    const cols = WIDTH / RES;
    const rows = HEIGHT / RES;

    createGrid(cols, rows);

    const x = Math.floor(cols / 2);
    const y = Math.floor(rows / 2);

    createGosperGliderGun(x, y);

    draw(cols, rows);
  }
});

/**
 * Draws the initial state of the canvas
 */
function initialDraw() {
  const cols = WIDTH / RES;
  const rows = HEIGHT / RES;
  grid = createArray(cols, rows);

  // Field edge is getting filled up with "2"
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      grid[i][j] = BORDER;
    }
  }

  // Field gets filled up with "0" = dead cells
  for (let i = 1; i < cols - 1; i++) {
    for (let j = 1; j < rows - 1; j++) {
      grid[i][j] = DEAD_CELL;
    }
  }

  draw(cols, rows);
}

/**
 * Draws the canvas
 *
 * @param {int} cols      Number of columns
 * @param {int} rows      Number of rows
 */
function draw(cols, rows) {
  // Draw
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      const x = i * RES;
      const y = j * RES;
      if (grid[i][j] === DEAD_CELL) {
        // Dead cells are white
        drawCell('white', 'LightGray', 1, x, y, RES, RES);
      } else if (grid[i][j] === LIVING_CELL) {
        // Living cells are #2B823A (green)
        drawCell('#2B823A', 'LightGray', 1, x, y, RES, RES);
      } else {
        // Border cells are #00410B (red)
        drawCell('#00410B', 'LightGray', 1, x, y, RES, RES);
      }
    }
  }
  // Updates grid with values of next generation
  applyRules(cols, rows);
}

/**
 * Draws one cell of the canvas
 *
 * @param {string} fillStyle       Color of the cell
 * @param {string} strokeStyle     Color of the border of the cell
 * @param {int} lineWidth          linewidth of the border
 * @param {int} x                  x-coordinate of upper-left corner
 * @param {int} y                  y-coordinate
 * @param {int} width              width of cell
 * @param {int} height             height of cell
 */
function drawCell(fillStyle, strokeStyle, lineWidth, x, y, width, height) {
  const ctx = document.getElementById('canvas').getContext('2d');
  ctx.fillStyle = fillStyle;
  ctx.strokeStyle = strokeStyle;
  ctx.lineWidth = lineWidth;
  ctx.beginPath();

  // Draw border of color strokeStyle
  ctx.rect(x, y, width, height);

  // Fill cell with color fillStyle
  ctx.fill();
  ctx.stroke();
}

/**
 * Updates the grid with the next generation:
 * Living cells with 2 or 3 neighbors stay alive.
 * Living cells with less than two or more than 3 neighbors die.
 * Dead cells with exactly 3 neighbors are "re-born"
 *
 * @param {int} cols      Number of columns
 * @param {int} rows      Number of rows
 */
function applyRules(cols, rows) {
  // Temporary array to save new values (next generation)
  const newArray = createArray(cols, rows);

  for (let i = 1; i < cols - 1; i++) {
    for (let j = 1; j < rows - 1; j++) {
      // how many living cells does the current cell (grid[i][j]) have
      const neighbors = countNeighbors(i, j);

      if (grid[i][j] == LIVING_CELL) {
        // Living cells with less than two or more than 3 neighbors die
        if ((neighbors < 2) || (neighbors > 3)) {
          newArray[i][j] = DEAD_CELL;
        }

        // Living cells with 2 or 3 neighbors stay alive
        if ((neighbors == 2) || (neighbors == 3)) {
          newArray[i][j] = LIVING_CELL;
        }
      } else if (grid[i][j] == DEAD_CELL) {
        // Dead cells with exactly 3 neighbors are "re-born"
        if (neighbors == 3) {
          newArray[i][j] = LIVING_CELL;
        } else {
          newArray[i][j] = DEAD_CELL;
        }
      }
    }
  }

  // Save temporary variables in grid so that grid now represents
  // the next generation
  for (let i = 1; i < cols - 1; i++) {
    for (let j = 1; j < rows - 1; j++) {
      grid[i][j] = newArray[i][j];
    }
  }
}

/**
 * Counts the neighbors of cell in grid at
 * position (x,y)
 *
 * @param {int} x                  x-coordinate of cell
 * @param {int} y                  y-coordinate of cell
 *
 * @return {int} neighbors         Number of neighbors
 */
function countNeighbors(x, y) {
  let neighbors = 0;
  /*
      001
      0X1
      101

      -> X has four neighbors
  */
  for (let i = x - 1; i <= x + 1; i++) {
    for (let j = y - 1; j <= y + 1; j++) {
      if (grid[i][j] == LIVING_CELL) {
        neighbors = neighbors + 1;
      }
    }
  }

  // Cell is not neighbor of itself
  if (grid[x][y] == LIVING_CELL) {
    neighbors = neighbors - 1;
  }

  return neighbors;
}

/**
 * Creates an empty 2d-array
 *
 * @param {int} cols      Number of columns
 * @param {int} rows      Number of rows
 *
 * @return {Obj} array    newly created array
 */
function createArray(cols, rows) {
  const array = new Array(cols);
  for (let i = 0; i < cols; i++) {
    array[i] = new Array(rows);
  }
  return array;
}

/**
 * Creates a grid with initial state
 *
 * @param {int} cols      Number of columns
 * @param {int} rows      Number of rows
 */
function createGrid(cols, rows) {
  grid = createArray(cols, rows);

  // Field edge is getting filled up with "2"
  for (let i = 0; i < cols; i++) {
    for (let j = 0; j < rows; j++) {
      grid[i][j] = BORDER;
    }
  }

  // Field gets filled up with "0" = dead cells
  for (let i = 1; i < cols - 1; i++) {
    for (let j = 1; j < rows - 1; j++) {
      grid[i][j] = DEAD_CELL;
    }
  }
}

/**
 * Updates the generation-counter
 */
function updateGeneration() {
  $(document).ready(function() {
    $('#generation').text(generation);
  });
}

/**
 * Filling grid with random numbers (0 or 1)
 *
 * @param {int} cols      Number of columns
 * @param {int} rows      Number of rows
 */
function createRandom(cols, rows) {
  for (let i = 1; i < cols - 1; i++) {
    for (let j = 1; j < rows - 1; j++) {
      grid[i][j] = (Math.random() * 2 | 0);
    }
  }
}

/**
 * Enables buttons when user stops simulation,
 * disables buttons when user starts simulation
 */
function switchButtons() {
  document.getElementById('clear').disabled = isRunning;
  document.getElementById('random').disabled = isRunning;
  document.getElementById('lwss').disabled = isRunning;
  document.getElementById('r-pentomino').disabled = isRunning;
  document.getElementById('acorn').disabled = isRunning;
  document.getElementById('gosper').disabled = isRunning;
}

/**
 * Helper function to create a
 * lightweight spaceship
 *
 * @param {int} x                  x-coordinate of cell in the middle of grid
 * @param {int} y                  y-coordinate of cell in the middle of grid
 */
function createLwss(x, y) {
  const arr = [[0, 1], [-1, 1], [1, 1], [-2, 1], [-2, 0],
    [2, 0], [-2, -1], [-1, -2], [2, -2]];
  createPattern(arr, x, y);
}

/**
 * Helper function to create a
 * r-pentomino
 *
 * @param {int} x                  x-coordinate of cell in the middle of grid
 * @param {int} y                  y-coordinate of cell in the middle of grid
 */
function createRPentomino(x, y) {
  const arr = [[0, 0], [0, -1], [0, 1], [-1, 0], [1, -1]];
  createPattern(arr, x, y);
}

/**
 * Helper function to create a
 * acorn
 *
 * @param {int} x                  x-coordinate of cell in the middle of grid
 * @param {int} y                  y-coordinate of cell in the middle of grid
 */
function createAcorn(x, y) {
  const arr = [[1, 0], [2, 0], [3, 0], [-2, 0], [-3, 0], [0, -1], [-2, -2]];
  createPattern(arr, x, y);
}

/**
 * Helper function to create a
 * gosper glider gun
 *
 * @param {int} x                  x-coordinate of cell in the middle of grid
 * @param {int} y                  y-coordinate of cell in the middle of grid
 */
function createGosperGliderGun(x, y) {
  const arr = [[-13, 0], [-12, 0], [-3, 0], [1, 0], [3, 0], [4, 0],
    [9, 0], [11, 0], [-3, 1], [3, 1], [11, 1], [-2, 2],
    [2, 2], [-1, 3], [0, 3], [-13, -1], [-12, -1], [-3, -1],
    [3, -1], [7, -1], [8, -1], [-2, -2], [2, -2], [7, -2],
    [8, -2], [-1, -3], [0, -3], [7, -3], [8, -3], [21, -2],
    [22, -2], [9, -4], [11, -4], [21, -3], [22, -3],
    [11, -5]];
  createPattern(arr, x, y);
}

/**
 * Helper function to write pattern saved in
 * arr to grid
 *
 * @param {Obj} arr                array of coordinates of cells to fill with 1
 * @param {int} x                  x-coordinate of cell in the middle of grid
 * @param {int} y                  y-coordinate of cell in the middle of grid
 */
function createPattern(arr, x, y) {
  for (let i = 0; i < arr.length; i++) {
    grid[x + arr[i][0]][y + arr[i][1]] = 1;
  }
}
/*
   =========================================================================
   Main style
   =========================================================================
*/

/* color palette */
:root {
  --active: #00410b;
  --light: #2b823a;
  --lighter: #82c38d;
}

html,
body {
  padding: 0;
  font-family: "Open Sans", sans-serif;
}

/* Content in middle of page */
.inner {
  width: 840px;
  margin: 0 auto;
  padding: 0;
  position: relative;
}

/* main content not as wide as inner-div */
.content {
  padding-left: 20px;
  padding-right: 20px;
  padding-bottom: 100px;
}

/*
   =========================================================================
   footer
   =========================================================================
*/

.footer {
  text-align: center;
  width: 840px;
}

hr {
  color: var(--active);
}

/*
   =========================================================================
   number input
   =========================================================================
*/

label {
  display: block;
}

input[type="number"] {
  width: 18%;
  padding: 10px 10px;
  display: block;
  border: 1px solid black;
  border-radius: 5px;
  box-sizing: border-box;
  margin-bottom: 10px;
  margin-right: 12px;
}

.tooltip {
  display: inline-block;
  position: relative;
  margin-bottom: 10px;
}

.tooltip:hover::before {
  content: attr(tooltip-title);
  position: absolute;
  width: 250px;
  background: var(--active);
  color: white;
  padding: 10px;
  border-radius: 5px;
  box-sizing: border-box;
  bottom: 20px;
  left: 20px;
  opacity: 0.9;
}

/*
   =========================================================================
   Other
   =========================================================================
*/

canvas {
  border: 1px solid LightGray;
  border-radius: 5px;
  box-sizing: border-box;
}

button {
  width: 18%;
  padding: 10px 10px;
  display: inline;
  border: 1px solid black;
  border-radius: 5px;
  box-sizing: border-box;
  margin-bottom: 10px;
  margin-right: 12px;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: var(--active);
  font-family: "Baloo 2", sans-serif;
}

a.link {
  display: inline-block;
  margin: 0;
  color: var(--light);
  text-decoration: none;
}

a.link::after {
  display: block;
  content: "";
  width: 0;
  border-bottom: 2px solid var(--light);
  transition: width 200ms;
}

a.link:hover::after {
  width: 100%;
}

/*
   =========================================================================
   responsive
   =========================================================================
*/

@media (max-width: 840px) {
  .inner {
    min-height: 100%;
    width: 100%;
    margin: 0 auto;
    padding: 0;
    position: relative;
  }

  .footer {
    text-align: center;
    width: 100%;
  }

  .pattern {
    width: 100%;
    padding: 10px 10px;
    display: inline;
    border: 1px solid black;
    border-radius: 5px;
    box-sizing: border-box;
    float: left;
    margin-bottom: 10px;
  }

  button {
    width: 50%;
  }

  canvas {
    height: calc(((100% - 100px) / 100) * 100);
    width: calc(((100% - 40px) / 100) * 100);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="inner">
  <div class="content">
    <h1>Conway's Game Of Life</h1>
    <canvas id="canvas" width="800" height="500"></canvas>

    <h3>Information</h3>
    <p>Generation: <span id="generation"></span></p>

    <h3>Control</h3>
    <button id="startStop">Start</button>
    <button id="clear">Clear</button>

    <label>
      <span class="tooltip" tooltip-title="The higher the number, the faster the simulation" style="border-bottom: 1px dotted black">Speed:</span>
      <input id="speed" type="number" min=1 max=20 value="5" onkeydown="return false;">
    </label>

    <h3>Pattern</h3>
    <button id="random">Random</button>
    <button id="lwss">LWSS</button>
    <button id="r-pentomino">R-pentomino</button>
    <button id="acorn">Acorn</button>
    <button id="gosper">Gosper glider gun</button>

    <h3>What is this?</h3>
    <p>Conway's Game of Life is a mathematical game invented by English Mathematician John Horton Conway. The game is based on the theory of cellular automatons. It is a so-called zero-player game because once the "player" set the initial state of the automaton, the game is just about to watch the deterministic outcome of the game.</p>
    <p>The rules are as follows:</p>
    <ol>
      <li>Living cells with 2 or 3 neighbors stay alive.</li>
      <li>Living cells with less than two or more than 3 neighbors die.</li>
      <li>Dead cells with exactly 3 neighbors are "re-born"</li>
    </ol>
    <p>In this implementation, the living cells are green and the dead cells are white.</p>
    <p>If you want to learn more about the Game of Life, reading the following articles will help:</p>
    <ul>
      <li><a class="link" href="https://en.wikipedia.org/wiki/Conway's_Game_of_Life">Conway's Game of Life</a></li>
      <li><a class="link" href="https://en.wikipedia.org/wiki/Cellular_automaton">Cellular automaton</a></li>
      <li><a class="link" href="https://en.wikipedia.org/wiki/John_Horton_Conway">John Conway</a></li>
    </ul>
  </div>
  <footer>
    <hr>
    <div class="footer">
      footer-text
    </div>
  </footer>
</div>

Question

All suggestions are welcome.

\$\endgroup\$
4
\$\begingroup\$

I really like the DEAD_CELL and LIVING_CELL variables, they're a great approach to distinguishing cells without using just numbers. Here are some suggestions for the JavaScript:

Use modules for large scripts While writing everything in a single .js file can work fine in smaller scripts, when it gets to be more than a couple hundred lines long, it can start to get more difficult to manage than is ideal. For example, let's say you realize that cells aren't growing and dying as is intended. Currently, to debug it, unless you've memorized the function name, you'd probably scroll through the (long) code until you found a section that looks related, then examine it to see if it's really what you were looking for. That's not a very scalable approach.

It would be better if you could do something like go into a grid folder, inside which you can see different files containing functions related to the grid, and then you could browse it and go to the one file that's causing the issue, applyRules.js, and fix it up. Modules make large scripts a whole lot easier to write, maintain, and debug. Other benefits include explicit dependencies and very narrow scope (narrow scope helps a whole lot).

Use something like Webpack to coalesce all the separate .js files into a single one to serve on your HTML. Using a build process like this will also help you in other ways:

  • Easy, automatic transpilation: you'll be able to write in as modern syntax as you want, and automatically transpile your code down to ES5 for production (allowing older browsers to understand your code, while keeping your source code modern and concise)
  • Easy, automatic minification - A bit similar to above, reduces network payloads

Declare variables close to where they'll be used The thought process of someone reading the following might get somewhat disjointed - you make a row and col variable, and then return and don't use them if a condition is met.

document.getElementById('canvas').addEventListener('click', function(e) {
  // Which box did user click on?
  const row = Math.floor(e.offsetX / RES);
  const col = Math.floor(e.offsetY / RES);

  // Don't allow changes while simulation is running
  if (isRunning) {
    return;
  }

  // Case user clicked on a dead cell
  if (grid[row][col] === DEAD_CELL) {

Consider returning as soon as possible instead, and only declaring the variables when you're going to use them in code below. Also consider renaming row and col to clickedRow and clickedCol for precision. This makes the code more self-documenting, and will mean that you can remove some of the comments:

document.getElementById('canvas').addEventListener('click', function(e) {
  // Don't allow changes while simulation is running
  if (isRunning) {
    return;
  }

  const clickedRow = Math.floor(e.offsetX / RES);
  const clickedCol = Math.floor(e.offsetY / RES);

  if (grid[clickedRow][clickedCol] === DEAD_CELL) {

IDs You have a number of elements with IDs on the page. Unfortunately, such IDs become global variables. This has the potential to cause bugs due to variable name collisions. For example, you have speed and random IDs. What if, elsewhere, you tried to use a variable named speed or random, and/or accidentally refer to the standalone variables without doing querySelector or getElementById first? Then there could be problems that would have to be fixed (and could well be quite confusing).

While you could reduce the chance of collisions by changing the IDs to be less likely to be referred to accidentally (and by using a linter to warn you against the use of undefined variables), I’d personally prefer to avoid IDs entirely - either use classes instead to select elements:

<button class="clear">Clear</button>
const clearButton = document.querySelector('.clear');

Or use a framework like React so that the elements are created at the same time as their handlers - that way, no elements have to be selected from the DOM at all. Just an idea - it makes the management of more complicated interfaces a bit easier.

generation The meaning of this variable wasn’t entirely clear to me until I saw how it was being used. Consider naming it something more precise like generationCount, or generationCountSinceLastChange.

updateGeneration: Throughout the code, you're frequently incrementing or setting to 0 the generation variable, and then calling updateGeneration, which does:

function updateGeneration() {
  $(document).ready(function() {
    $('#generation').text(generation);
  });
}

Consider passing updateGeneration the new generation count instead:

function updateGeneration(newCount) {
  generationCountSinceLastChange = newCount;
  document.querySelector('#generation').textContent = newCount;
}

This lets you do updateGeneration(generationCountSinceLastChange + 1) or updateGeneration(0).

(This is the only place you're using jQuery. Feel free to remove it entirely - it isn't accomplishing anything useful. The document is already going to be loaded by the time this runs.)

Waiting for the window to load Rather than wrapping the entry point in a load listener, you may consider either giving the <script> tag the defer attribute or putting it at the bottom of the <body> - I think it's a bit easier to direct it from the HTML, rather than from the JS.

initialDraw Best to name functions based on what they do, rather than when they're called. This function looks to draws the border, so maybe call it drawBorder.

Avoid sloppy comparison with == and !=, they have strange coercion rules. Better to always use === and !==. Consider using ESLint and the rule eqeqeq.

DRYing

There are many places in the current code that are pretty repetitive.

On canvas click, rather than if elses, you can use the conditional operator and call updateGeneration at once:

updateGeneration(0);
const setAlive = grid[row][col] === DEAD_CELL;
grid[row][col] = setAlive ? LIVING_CELL : DEAD_CELL;
drawCell(setAlive ? 'white' : '2B823A', 'LightGray', 1, row * RES, col * RES, RES, RES);

createGrid Rather than calculating and passing createGrid the global constants every single time it's called:

const cols = WIDTH / RES;
const rows = HEIGHT / RES;
createGrid(cols, rows);

Consider having createGrid itself calculate the grid dimensions needed:

function createGrid() {
    const cols = WIDTH / RES;
    const rows = HEIGHT / RES;

You can do the same sort of thing for the draw function. (If the arguments to a function are always the same, that's an indication that it's something that should be handled inside the function, not something that gets passed to the function.)

applyRules can be refactored to avoid redundant checks. For example, given if ((neighbors < 2) || (neighbors > 3)) {, rather than testing against 2 and 3 for the other branch, you can use else. Or, even better, use the conditional operator.

for (let i = 1; i < cols - 1; i++) {
  for (let j = 1; j < rows - 1; j++) {
    const neighborCount = countNeighbors(i, j);
    if (grid[i][j] === LIVING_CELL) {
      // Living cells with less than two or more than 3 neighbors die
      newArray[i][j] = (neighborCount < 2 || neighborCount > 3) ? DEAD_CELL : LIVING_CELL;
    } else {
      // Dead cells with exactly 3 neighbors are "re-born"
      newArray[i][j] = neighborCount === 3 ? LIVING_CELL : DEAD_CELL;
    }
  }
}

Concise shape insertion When you have lots of shapes that can be inserted, having separate functions for each of them gets a bit repetitive. Consider making an object indexed by shape name instead, eg:

const patterns = {
  lwss: [[0, 1], [-1, 1], [1, 1], [-2, 1], [-2, 0],
    [2, 0], [-2, -1], [-1, -2], [2, -2]]
  // ...
};

Then, rather than createLwss and all the other separate functions, you can do:

createPattern(patterns.lwss, x, y);

Avoid new Array, since it creates sparse arrays (which are only guaranteed to have a length property, but may or may not have own-properties from 0 up to the length) - if you forget to assign to every index of the array, iteration over it can be very weird. Consider using Array.from instead, or using .fill after calling new Array.

function createArray(cols: number, rows: number) {
  return Array.from(
    { length: cols },
    () => new Array(rows).fill(null)
  );
}
\$\endgroup\$
  • \$\begingroup\$ Thank you for your answer. In your last snippet, the line () => does give me the error "Argument type function(): this is not assignable to parameter type (v: unknown, k: number)". Do you know what the problem is here? \$\endgroup\$ – Philipp Wilhelm Nov 9 '20 at 11:48
  • 1
    \$\begingroup\$ Forgot to type the arguments - they're numbers, so use :number and it should work. \$\endgroup\$ – CertainPerformance Nov 9 '20 at 14:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.