0

I am trying to make Conway's Game of Life in JavaScript, but cannot find what is wrong with a specific function after hours of debugging.

The program works by making a 2d array based of global "row" and "column" variables, and then pushes "Square" objects to each space in the array. The program then sets an interval for the "draw" function for a specific interval, (the global variable "rate").

I apologize if this is hard to understand, but basically, every specific time interval, like every 1000 milliseconds, the program checks every "Square" object in the array, updates the amount of neighbors it has, and then draws it on the screen.

This is where I am stuck; the update function is supposed to check all 8 neighbors that a square has, (or 3-5 if it is an edge square) but it will only check 4 neighbors. No matter what I do, if I click a square to populate it, only the top, top left, top right, and left neighbors of the now populated square will register that their neighbor has become populated.

Other than this bug the code is working fine, and i'm 99% sure the problem is in this one function, because the code will still function as a cellular automata currently, just not as Conway's Game of Life.

var canvas = document.getElementById("life");
var ctx = canvas.getContext('2d');
var timer;
var rate = 1000;
var rows = 20;
var columns = 20;
var width = 20;
var clickX;
var clickY;
var board;
var running = false;
var checkArray = [
  [-1, 0, 1, 1, 1, 0, -1, -1],
  [-1, -1, -1, 0, 1, 1, 1, 0]
];
var visuals = true;
var gridColor = "#000000";
/*
live cell with < 2 neighbors = death
live cell with 2 or 3 neighbors = live for 1 generation
live cell with > 4 neighbors = death
dead cell with 3 neighbors = live for 1 generation
      
0 = death
1 = death
2 = continues life if alive
3 = continues life if alive OR brings to life if dead
4 = death
5 = death
6 = death
7 = death
8 = death
*/
window.onload = function() {
  makeBoard();
  var timer = setInterval(draw, rate);
  window.addEventListener("mousedown", clickHandler);
  // for(var i = 0; i < 8; i++){
  //     var checkIndexX = checkArray[0][i];
  //     var checkIndexY = checkArray[1][i];
  //   console.log(checkIndexX, checkIndexY);
  // }
}

function makeBoard() {
  board = new Array(columns);
  for (var i = 0; i < rows; i++) {
    var intRow = new Array(rows);
    for (var j = 0; j < columns; j++) {
      intRow[j] = new Square(false, j, i);
    }
    board[i] = intRow;
  }
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (var y = 0; y < rows; y++) {
    for (var x = 0; x < columns; x++) {
      if (running) {
        board[y][x].update();
      }
      board[y][x].draw();
      if (visuals) {
        board[y][x].visuals();
      }
    }
  }
  drawRunButton();
}

function Square(alive, PARX, PARY) {
  this.alive = false;
  this.X = PARX;
  this.Y = PARY;
  this.neighbors = 0;

  this.update = function() {
    this.neighbors = 0;
    for (var i = 0; i < 8; i++) {
      var checkIndexX = checkArray[0][i];
      var checkIndexY = checkArray[1][i];
      if ((this.X + checkIndexX) >= 0 && (this.X + checkIndexX) < columns &&
        (this.Y + checkIndexY) >= 0 && (this.Y + checkIndexY) < rows) {
        var check = board[this.Y + checkIndexY][this.X + checkIndexX];
        // console.log(this.X, this.Y, check.X, check.Y, checkIndexX, checkIndexY);
        if (check.alive) {
          this.neighbors++;
        }
      }
    }
    if (this.alive) {
      if (this.neighbors < 2 || this.neighbors > 3) {
        this.alive = false;
      }
    } else {
      if (this.neighbors == 3) {
        this.alive = true;
      }
    }
  };
  this.visuals = function() {
    drawVisuals(this.neighbors, this.X * width, this.Y * width);
  };
  this.draw = function(alive) {
    drawSquare(this.alive, this.X * width, this.Y * width, width);
  }
}

function clickHandler(e) {
  var clickX = e.screenX - 68;
  var clickY = e.screenY - 112;
  mapClick(clickX, clickY);
  manageRun(clickX, clickY);
}

function mapClick(x, y) {
  var indexX = Math.floor(x / width);
  var indexY = Math.floor(y / width);
  if (indexX >= 0 && indexX < columns && indexY >= 0 && indexY < rows) {
    board[indexY][indexX].alive = true;
  }
}

function manageRun(x, y) {
  if (x >= (columns * width) + 5 && x <= (columns * width) + 45 && y >= 5 && y <= 45) {
    if (running) {
      running = false;
    } else {
      running = true;
    }
    console.log(running);
  }
}

function drawRunButton() {
  drawSquare(false, (columns * width) + 5, 5, 40)
}

function drawSquare(fill, x, y, width) {
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x + width, y);
  ctx.lineTo(x + width, y + width);
  ctx.lineTo(x, y + width);
  ctx.lineTo(x, y);
  if (fill) {
    ctx.fillStyle = gridColor;
    ctx.fill();
  } else {
    ctx.strokeStyle = gridColor;
    ctx.stroke();
  }
}

function drawVisuals(neighbors, x, y) {
  ctx.beginPath();
  ctx.fillStyle = "#ff0000";
  ctx.font = '20px serif';
  ctx.fillText(neighbors, x + (width / 3), y + (width / 1.25));
}
<!DOCTYPE html>
<html>

<head>
  <title>template.com</title>
  <link href="style.css" type="text/css" rel="stylesheet">
</head>

<body>
  <canvas id="life" width="1400" height="700" style="border: 1px solid black"></canvas>

</body>

</html>
jrswgtr
  • 2,287
  • 8
  • 23
  • 49
  • EDIT: sorry, first ever question on this site. I trying to debug the update function in the "Square" object constructor function. – Bear Grylles Dec 09 '18 at 05:05
  • 1
    Please give your question a more specific title. Everyone who posts here can't figure out what's wrong with their code. – Barmar Dec 09 '18 at 05:05
  • Why does `Square()` take `alive` as a parameter, but then it always sets `this.alive = false;`? – Barmar Dec 09 '18 at 05:08
  • 1
    You can't modify the liveness of each cell during the loop that's calculating the next generation. Each cell needs to determine its next state from the current liveness of its neighbors. If you modify them as you're going, the neighbors will get the wrong counts. – Barmar Dec 09 '18 at 05:11
  • 1
    You either need two grids, or another property that holds the state for the next generation, which you then copy into the real liveness after the loop. – Barmar Dec 09 '18 at 05:12
  • @Barmar I used to set each object to alive at its initialization, but decided it was redundant and haven’t changed to function yet – Bear Grylles Dec 09 '18 at 14:55
  • @Barmar sorry, I’m afraid I don’t understand, how am I “modifying the liveness of each cell during the loop that’s calculating the next generation?” – Bear Grylles Dec 09 '18 at 14:58
  • You do it here: `if (this.neighbors < 2 || this.neighbors > 3) { this.alive = false; }` – Barmar Dec 10 '18 at 04:47
  • That's in `Square.update()`, which is called in the loop in `draw()`. – Barmar Dec 10 '18 at 04:48
  • @Barmar thanks for your help, and sorry for the slow response, I think I live in a different time zone than you (it’s ~10:45 here). I took your advice and fixed the problem, I am now calculating liveliness after calculating the neighbors, and the program works well. I’ve made a lot more progress on other peripheral stuff like buttons and hot keys, so that I can clear the board, stop or start the simulation, etc.. now trying to adapt it to use other rules outside of just Conway’s. Thanks for bearing with my somewhat-understandable questions, couldn’t have done it without you. – Bear Grylles Dec 10 '18 at 04:53
  • We're only an hour apart, it's 11:54pm here. But I've been away all day today. – Barmar Dec 10 '18 at 04:55

0 Answers0