Skip to content

Instantly share code, notes, and snippets.

@misterussell
Last active December 26, 2017 16:46
Show Gist options
  • Save misterussell/0f192634c2e77358d6aa69ecb8cc90d4 to your computer and use it in GitHub Desktop.
Save misterussell/0f192634c2e77358d6aa69ecb8cc90d4 to your computer and use it in GitHub Desktop.
12 22 Blog Post

Optimizing Conway's Game of Life in JavaScript - Part II

In Part I I tackled solving the Puzzle of Conway's way of life iteratively through arrays. I had a few idea's of the direction that I wanted to go and after working through this further some of my assumptions were correct. Largely the code looks completely different, and I hope, improved. It looks cleaner to me at least. Here was my expected process:

Process

  1. Get the program to correctly generate new generations
  2. Optimize the program so that it isn't wastefully processing - Slightly optimized this round
  3. Change the structure of the Life maps from a 2D config to a toroidal config
  4. Wireframe / Plan Visuals
  5. Deploy to a React app

For the full v2 code click here

I was able to successfully generate each subsequent generation with the array methods. My next goal was to tackle "optimization" as best I could. The initial array's that I'm starting with are small, so optimizing in this scenario is moving away from array enumeration. Semantics, right?

Before changing the model of my data, I will note, that it was quite easy to change the initial function from a for loop to a recursive function. This pleases me, because I sometimes struggle with recursion.

Changing the data

I made drastic shifts to the form of the data and how it is stored, and some minor adjustments to what I wanted to keep track of for each cell.

Cell Creation - From Constructor to Factory

function Cell(x, y, cellState, neighbors = 0) {
  this.x = x;
  this.y = y;
  this.hash = (this.x * 15486047) + (this.y * 15487429);
  this.isAlive = cellState;
  this.neighbors = neighbors
 };

This let me save and access properties that I thought I would need. I only ended up needing two of these properties in the initail run of the program, isAlive and neighbors. The value isAlive kept track of if the cell was a true or false, and neighbors kept track of calculated value where any adjacent cells were also alive.

In v2 I have moved away from using a Constructor to using a factory. The factory picks up it's values from the function arguments and returns the new object. You'll note I'm also passing in a toroidalLimits arg, as I have changed the data structure to toroidal arrays. I will talk through why later. The main thing to note about this factory function is the extra parenthesis around the function definition. () => ({ }) disambiguates the return so that we can return an object literal

function createCell(x, y, cellState, toroidalLimits){
 const cellHash = (x * 15486047) + (y * 15487429);
 const Cell = () => ({
   x,
   y,
   cellHash,
   cellState,
   toroidalLimits
 });

 return Cell();
};

Generation Management - From Array of Arrays to Object/Hashmap

To avoid needless array enumeration I have decided to move to an object of hashed cell objects to store my data. This allows me to target specific cells to verify their life/death state.

One run of enumeration is required to generate the gen hashmap from our arrays:

let genState = {};
  rawCells.forEach((terrian, i, arr) => {
    terrian.forEach((cell, p, arr2) => {
      const toroidalLimits = genState.toroidalLimits === undefined ? genState.toroidalLimits = [arr.length, arr2.length] : null;
      const cellState = cell;
      const cellObj = createCell(i + 1, p + 1, cellState, toroidalLimits);
      return genState[cellObj.cellHash] === undefined ? genState[cellObj.cellHash] = cellObj : null
    });
 });
return genState;
  • I'm currently passing in the max horizontal and vertical index values to each cell. I will likely end up moving this to the main object.

Our second run of enumeration is over the hashmap object. As I've converted the cell's world into an infinite toroidal map the object setup did help considerably in being able to avoid temporarily cached arrays for the "mirrored" layers of the world. The getNextCellState() function ends up needing 2 things.

  1. The information about the current cell, cell- This gives the function access to the hash for that cell, as well as its position.
  2. The hashmap of all cells, genState- This gives the function access to the information about the current cell, as well as every other cell.

Calculating inner cell values non-egocentrically

I use ternary operations to assist me in building the pattern that finds whether or not a cell is on the "edge" of the toroidal array. If the x or y value of the current cell is one original array's bounds, then these operators will cycle to the next correct array values for their neighbors.

const lowLimit = cell.x === cell.toroidalLimits[0] ? 1 : cell.x + 1;
const highLimit = cell.x === 1 ? cell.toroidalLimits[0] : cell.x - 1;
const rightLimit = cell.y === cell.toroidalLimits[1] ? 1 : cell.y + 1;
const leftLimit = cell.y === 1 ? cell.toroidalLimits[1] : cell.y - 1;

I pass in these with a combination of the current cell's x and y values to get all of its surrounding neighbors, as an array based on the calculated hash values of each neighbor's position. Since this is now a toroidal array, all cells have the same number of neighbors so there aren't any edge cases.

const blockSum = [
      [highLimit, leftLimit], [highLimit, cell.y], [highLimit, rightLimit],
      [cell.x, leftLimit], /* current cell */ [cell.x, rightLimit],
      [lowLimit, leftLimit], [lowLimit, cell.y], [lowLimit, rightLimit]
      ].map(neighbor => {
        return genState[(neighbor[0] * 15486047) + (neighbor[1] * 15487429)].cellState;
      }).reduce((a, b) => a + b);

The next big change is that the cells no longer egocentrically calculate their value. All that matters is the sum of the 9 cells in total. If the total of the cell is 3 the inner cell lives. If the total is 4 the inner cell's state does not change, and any other sum makes the inner cell dead regardless of its state. My map/reduce only spits back the total sum for the moment, but there is a possibility that I may adjust this down the line should I choose to cache cell states to further minimize unnecessary calculation.

My favorite part about the getNextCellState() function is that it led me to found a nifty babel plugin that allows you to use the spread operator to add information to an object. Originally I had given each cell a method that updated their nextState property based on the config of their current neighbors. Rather, I now return the cell that was passed into the getNextCellState() function with a new additional property nextState. Note: nextState is destructured so it is actually a key/value pair, not just a variable in this scenario.

return { ...cell, nextState }

Expectations for the next step of the process

  1. Now that the data format has been changed, do I want to log a history of changes? If so what does this look like?
  2. Testing to see if this format will work with shapes that aren't parallelograms
  3. If I do create visualizations for this, how will animations be handled so that they aren't just immediate flashes of data? Assuming react will make this easier, but I don't have a state container set up.

Other Process Changes

Another big change is that I've moved these practice activities into one place where I can now test with mocha/chai. I've been using browser assertions for these smaller projects, which has probably been a waste of learning opportunity. Provisioning and setting up my test environment was not a quick process, but now that I have it set up I think provisioning my next project will be a bit smoother as I'll most likely continue to use CRA. My priority at the moment is exposure and practice. The initial tests for these functions can be found here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment