Search code examples
javascriptarrayscellular-automata

The rules of Conway's Game of Life aren't working in my Javascript version. What am I doing wrong?


I'm working on some code for a javascript implemenation of Conway's Game of Life Cellular Automata for a personal project, and I've reached the point of encoding the rules. I am applying the rules to each cell, then storing the new version in a copy of the grid. Then, when I'm finished calculating each cell's next state, I set the first grid's state to the second's one, empty the second grid, and start over. Here's the code I used for the rules:

//10x10 grid
let ecells = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]];

let cells = empty_cells;

let new_cells = cells;

let paused = true;

function Cell(x, y) {
    return cells[y][x];
}

function Nsum(i, j) {
    if (i >= 1 && j >= 1) {
        return Cell(i - 1, j) + Cell(i + 1, j) + Cell(i, j - 1) + Cell(i - 1, j - 1) + Cell(i + 1, j - 1) + Cell(i, j + 1) + Cell(i - 1, j + 1) + Cell(i + 1, j + 1);
    }
}

//One can manually change the state of the cells in the "cells" grid, 
//which works correctly. Then, one can run the CA by changing the "paused"
//value to false.

function simulation() {
    for (i = 0; i < cells[0].length; i++) {
        for (j = 0; j < cells.length; j++) {
            if (Cell(i, j)) {
                ctx.fillRect(20*i - 0.5, 20*j, 20, 20);
                if (!paused) {
                    if (Nsum(i, j) == 2 || Nsum(i, j) == 3) new_cells[j][i] = 1;
                    else new_cells[j][i] = 0;
                }
            }
            else {
                ctx.clearRect(20*i - 0.5, 20*j, 20, 20);
                if (!paused) {
                    if (Nsum(i, j) == 3) new_cells[j][i] = 1;
                    else new_cells[j][i] = 0;
                }
            }
        }
    }
    if (!paused) cells = new_cells;
    new_cells = empty_cells;
    requestAnimationFrame(simulation);
}

simulation();

The rule logic is inside the nested for loop, Nsum is the function that calculates the neighborhood sum of the current cell. I say ncells[j][i] instead of ncells[i][j] because in a 2d array you address the row first.

I didn't try much, but I can't imagine a solution. Help!


Solution

  • Here's the core of your problem:

    let cells = empty_cells;
    let new_cells = cells;
    

    These statements don't do what you think: instead of making copies of the arrays, they just assign the same array to multiple variables.

    This would be fine if your array was immutable (like, say, strings and numbers are in JS), but since the array is mutable, any changes made to it via one variable will be visible through all the variables. In effect, all those variables are just different names for the same array.


    To show the problem more clearly, here's a quick snippet you can run right here:

    const arrayOne = [0, 0, 0];
    const arrayTwo = arrayOne;
    arrayOne[0] = 37;
    arrayTwo[2] = 42;
    console.log("arrayOne =", arrayOne);
    console.log("arrayTwo =", arrayTwo);

    What do you think arrayOne and arrayTwo will look like after this code runs?

    The correct answer is that they will both look like [37, 0, 42], because they are just two names for the same array.


    So what should you do instead?

    Probably the most efficient way would be something like this:

    1. Before starting your simulation, create two separate arrays with the same dimensions. One way to do that (which doesn't require deep-copying objects) is to write a function that creates and returns a new empty array, and then call it twice:

      function makeCellGrid() {
        return [
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
          [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        ];
      }
      
      let cells = makeCellGrid();
      let newCells = makeCellGrid();
      

      The [ ... ] syntax in JavaScript creates a new array every time it is evaluated, so each call to the function will return a completely new array of arrays.

      (Of course it would be nicer if the makeCellGrid function took the width and the height of the grid you want as parameters and returned an array of the requested size. I'll leave implementing that as an exercise. Using a loop to create the rows is probably the simplest way, although you can also do it e.g. with the map method.)

    2. In your simulation function, you read only from cells and write only to newCells, just like you're currently doing. However, at the end of the function, what you do is swap the two arrays:

      const tempCells = cells;
      cells = newCells;
      newCells = tempCells;
      

      or, more compactly in modern JS:

      [cells, newCells] = [newCells, cells];
      

      Now you have the array containing the new cell states in cells, ready to be drawn. (You also still have the previous generation's cell states in the array that is now newCells, but that doesn't matter, since they will just be ignored and overwritten when the function is called again.)