7
\$\begingroup\$

It took some time to make Conway's Game of Life in HTML, CSS, JavaScript and jQuery. I'd like suggestions, criticisms, and discussions on how it can be done better.

JSFiddle link

/*
 * Conway's - Game of Life.
 * Any live cell with fewer than two live neighbours dies, as if caused by under-population.
 * Any live cell with two or three live neighbours lives on to the next generation.
 * Any live cell with more than three live neighbours dies, as if by over-population.
 * Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
 */

'use strict';

/*
 * Representation of each cell on the canvas.
 * "row" and "col" stores the location of the cell.
 * The "alive" property stores whether the cell is alive or dead.
 */
function Cell(row, col) {

  var _this = this,
    $this = null,
    alive = false;

  this.activate = function () {
    alive = true;
    $this.addClass('alive');
  };

  this.deActivate = function () {
    alive = false;
    $this.removeClass('alive');
  };

  this.isAlive = function () {
    return alive;
  };

  this.getRow = function () {
    return row;
  };

  this.getCol = function () {
    return col;
  };

  this.getJqueryElement = function () {
    return $this;
  };

  // If $this is not yet defined, create a new HTML element.
  if (null === $this) {
    $this = $('<div>').addClass('conway-cell').data('cell', _this);
  }

  return this;
}

/**
 * The main logic of the game goes here.
 */
function ConwayGame(selector, numRows, numCols) {

  var $parent = $(selector),
    _this = this,
    rows = numRows,
    cols = numCols,
    cells = [],
    lifeMap = [],
    intervalTime = 500,
    intervalId;

  this.getSpeed = function () {
    return intervalTime;
  };

  // Initialize the list of cells required. Add the same to the HTML parent element.
  var initialize = function () {

    for (var i = 0; i < rows; i++) {

      cells[i] = [];
      var $row = $('<div>').addClass('conway-row');

      for (var j = 0; j < cols; j++) {

        var cell = cells[i][j] = new Cell(i, j);
        var $cell = cell.getJqueryElement();

        // Add click handler for the Cell.
        $cell.on('click', function (event) {
          var cellObj = $(this).data('cell');
          if (cellObj.isAlive()) {
            cellObj.deActivate()
          } else {
            cellObj.activate();
          }

          _this.reMap();
        });

        $row.append($cell);
      }

      // Add the HTML Row to the Parent element.
      $parent.append($row);
    }
  };

  // Re-draw the Elements based on their status, if they are alive or not.
  this.reDraw = function () {

    for (var i = 0; i < rows; i++) {
      for (var j = 0; j < cols; j++) {

        var cell = cells[i][j];
        cell.isAlive() ? cell.activate() : cell.deActivate();
      }
    }

  };

  // Get the count of immediate neighbors for the cell.
  this.getNeighborsCount = function (cell) {

    var neighbors = 0,
      row = cell.getRow(),
      col = cell.getCol();

    // Top Left to Top Right
    if (cells[row - 1]) {
      if (cells[row - 1][col - 1] && cells[row - 1][col - 1].isAlive()) neighbors++;
      if (cells[row - 1][col] && cells[row - 1][col].isAlive()) neighbors++;
      if (cells[row - 1][col + 1] && cells[row - 1][col + 1].isAlive()) neighbors++;
    }

    // Middle Left to Middle Right. Ignore the current cell.
    if (cells[row][col - 1] && cells[row][col - 1].isAlive()) neighbors++;
    if (cells[row][col + 1] && cells[row][col + 1].isAlive()) neighbors++;

    // Bottom Left to Bottom Right.
    if (cells[row + 1]) {
      if (cells[row + 1][col - 1] && cells[row + 1][col - 1].isAlive()) neighbors++;
      if (cells[row + 1][col] && cells[row + 1][col].isAlive()) neighbors++;
      if (cells[row + 1][col + 1] && cells[row + 1][col + 1].isAlive()) neighbors++;
    }

    return neighbors;
  };

  this.reMap = function () {

    for (var i = 0; i < rows; i++) {
      lifeMap[i] = [];

      for (var j = 0; j < cols; j++) {

        var cell = cells[i][j];
        lifeMap[i][j] = _this.getNeighborsCount(cell);
      }
    }
  };

  this.getNextLife = function () {

    for (var i = 0; i < rows; i++) {
      for (var j = 0; j < cols; j++) {

        var cell = cells[i][j];
        var lifeValue = lifeMap[i][j];

        if (cell.isAlive()) {
          if (lifeValue < 2 || lifeValue > 3) {
            cell.deActivate();
          }
        } else {
          if (lifeValue === 3) {
            cell.activate();
          }
        }

      }
    }

    _this.reMap();
  };

  this.next = function () {
    _this.getNextLife();
    _this.reDraw();
  };

  this.play = function () {
    intervalId = setInterval(_this.next, intervalTime);
  };

  this.pause = function () {
    clearInterval(intervalId);
  };

  this.increaseSpeed = function () {
    if (intervalTime > 100) {
      intervalTime -= 100;
    }
    _this.pause();
    _this.play();
  };

  this.decreaseSpeed = function () {
    if (intervalTime < 2000) {
      intervalTime += 100;
    }
    _this.pause();
    _this.play();
  };

  if (cells.length === 0) {
    initialize();
  }

  return this;
}


// Run as soon as the Document is Ready.
$(function () {

  var game = new ConwayGame('.conway-game', 20, 20);

  $('#nextButton').on('click', function () {
    game.next();
  });

  $('#playButton').on('click', function () {
    game.play();
  });

  $('#pauseButton').on('click', function () {
    game.pause();
  });

  $('#speedUpButton').on('click', function () {
    game.increaseSpeed();
  });

  $('#slowDownButton').on('click', function () {
    game.decreaseSpeed();
  });

});
.conway-game {
  display: table;
}

.conway-row {
  display: table-row;
}

.conway-cell {
  display: table-column;
  float: left;
  width: 12px;
  height: 12px;
  border: 1px solid #CCE8AF;
}

.alive {
  background-color: #7FC539;
}

.controls {
  margin-top: 10px;
}

.controls button {
  float: left;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="conway-game"></div>
<div class="controls">
  <button id="playButton">Play</button>
  <button id="pauseButton">Pause</button>
  <button id="nextButton">Step</button>

  <button id="speedUpButton">Speed Up</button>
  <button id="slowDownButton">Slow Down</button>
</div>
\$\endgroup\$
  • \$\begingroup\$ Don't use an interval. Use requestanimationframe and recur in the callback \$\endgroup\$ – Zorgatone Dec 2 '15 at 7:39
  • \$\begingroup\$ You can also consider to use ES6 for OOP/Classes and modularity, and transpile it using BabelJS. That's also good for reusability in future, IMHO \$\endgroup\$ – Zorgatone Dec 2 '15 at 7:48
  • 1
    \$\begingroup\$ @Zorgatone, yeah am looking into the requestanimationframe method. I did take a look at ES6, I don't think I'll be changing my code for that. Thanks for the comments. :) \$\endgroup\$ – LearningDeveloper Dec 2 '15 at 10:16
  • \$\begingroup\$ No problem, however having a good grasp at ES6 can be really helpful \$\endgroup\$ – Zorgatone Dec 2 '15 at 10:36
  • \$\begingroup\$ You will get better performance out of a canvas/SVG than table cells. \$\endgroup\$ – Roamer-1888 Dec 3 '15 at 9:25
5
\$\begingroup\$

I cloned your code and created a website using the GitHub repo that I created with it so you can see some of the other changes that I make to the code as well. please feel free to fork it.


In the initialize function of your ConwayGame class you used the length if then statement

if (cellObj.isAlive()) {
  cellObj.deActivate()
} else {
  cellObj.activate();
}

But then in the this.reDraw function you use a ternary statement for the same call, so I changed that right away. I know that these are reversed, but the ternary will operate in a similar fashion. It looks like this

cellObj.isAlive() ? cellObj.deActivate() : cellObj.activate();

I also removed some of the comments because they were redundant when I looked at the line(s) of code that they were referring to.


I also lightly touched the getNeightborscount function and changed it slightly, I pulled out rowAbove and rowBelow so that I could dry it up a little bit, but I was only able to pull these out so far, this is what it looks like currently

this.getNeighborsCount = function (cell) {

    var neighbors = 0,
    row = cell.getRow(),
    col = cell.getCol();

    var rowAbove = cells[row - 1];
    var rowBelow = cells[row + 1];

    //if (cells[row - 1]) {
    if (rowAbove) {
        if (rowAbove[col - 1] && rowAbove[col - 1].isAlive()) neighbors++;
        if (rowAbove[col] && rowAbove[col].isAlive()) neighbors++;
        if (rowAbove[col + 1] && rowAbove[col + 1].isAlive()) neighbors++;
    }

    if (cells[row][col - 1] && cells[row][col - 1].isAlive()) neighbors++;
    if (cells[row][col + 1] && cells[row][col + 1].isAlive()) neighbors++;

    //if (cells[row + 1]) {
    if (rowBelow) {
        if (rowBelow[col - 1] && rowBelow[col - 1].isAlive()) neighbors++;
        if (rowBelow[col] && rowBelow[col].isAlive()) neighbors++;
        if (rowBelow[col + 1] && rowBelow[col + 1].isAlive()) neighbors++;
    }

    return neighbors;
};

Let's move on to the this.getNextLife function.

if (cell.isAlive()) {
  if (lifeValue < 2 || lifeValue > 3) {
      cell.deActivate();
      cellsDestroyed++;
  }
} else {
  if (lifeValue === 3) {
      cell.activate();
      cellsCreated++;
  }
}

this looks a little clunky to me, at the very least that else statement should be an else if statement like this

if (cell.isAlive()) {
    if (lifeValue < 2 || lifeValue > 3) {
        cell.deActivate();
        cellsDestroyed++;
    }
} else if (lifeValue === 3) {
    cell.activate();
    cellsCreated++;
}

I liked the way that you created classes and objects to handle the different functions of the Game itself so you could just call them on a click event.


Edit: I found a bug in your code, you were counting surviving cells as newly created cells inside the getNextLife function. Here is how I fixed that.

if (cell.isAlive()) {
    if (lifeValue < 2 || lifeValue > 3) {
        cell.deActivate();
        cellsDestroyed++;
    } else if (lifeValue === 3) {
        cell.activate();
    }
} else if (lifeValue === 3){
        cell.activate();
        cellsCreated++;
    }
}

And really I don't think that you need to actually activate that cell or do anything to it if it is active and has lifeValue === 3

so we could just write it like this instead

if (cell.isAlive() && (lifeValue < 2 || lifeValue > 3) {
    cell.deActivate();
    cellsDestroyed++;
} else if (lifeValue === 3){
    cell.activate();
    cellsCreated++;
}

All these updates are in the code @GitHub as well.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the wonderful review. Cool, the GITHub repo looks nice with the user entering the number of cells. I agree with all the code changes that you have mentioned and updated the same in my Fiddle. It's been a while since I've looked at this code, I guess few things have been left unfinished and needs to be taken care of. Yup, this was my first real JavaScript application with OOP in mind.. \$\endgroup\$ – LearningDeveloper Apr 6 '16 at 2:41
  • \$\begingroup\$ please feel free to fork that repo, and do some pull requests. I am thinking about how i would write the checking algorithm recursively, then I might post the code here for a review. \$\endgroup\$ – Malachi Apr 6 '16 at 2:59

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.