Skip to content

Instantly share code, notes, and snippets.

@misterussell
Created April 13, 2018 22:28
Show Gist options
  • Save misterussell/20f3b766517a5d4b1d001508932c5431 to your computer and use it in GitHub Desktop.
Save misterussell/20f3b766517a5d4b1d001508932c5431 to your computer and use it in GitHub Desktop.
4 13 18 Blog Post

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

Array manipulation, and tigers, and bears oh my!

The Issue

One aspect of my game grid that I did not like was that if a user selected some cells and then changed the size of the grid the cells jumped all around the board. I understood when creating the rendering logic that this would happen but I choose to leave it in the icebox until things were working with ease.

Let's say the user selected this nice little T.

// normal image

If they grew the size of the grid once the nice little T becomes this mess.

// normal grown once image

Or, if someone selected this little cross with a square from a larger grid.

// normal large

And chose to reduce the size of the grid quite drastically they end up things not only being chopped up but also rearranged.

// normal large shrank

Now that doesn't look very nice does it?

Fix 1 - Growing the Grid

Fixing this was relatively easy because I already had a function that turned the flat grid of cells:

[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,]

Into a stacked array:

[
  [1, 1, 1, 1, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0]
]

At first, I had only been thinking in terms of updating the flat array, pinpointing how many indices would get inserted before the first cell, and then between the 6th and 7th etc... I quickly realized that if I just turned them into separate chunks I could probably just concat each changed array together faster.

My growCellArray(array, alpha) takes two arguments. The original flat array and the alpha of how many cells need added to the total width/height of the array. I start with a 5x5 array, and pass in an alpha of 2, generating a 7x7 array.

I know that we will need to stack an array of dead cells at the top and bottom so first create that.

const newRow = Array.from(new Array(Math.sqrt(array.length) + alpha), () => {
  return 0;
});
  • I use the Array.from() method pretty frequently through out the project so stuck with that.
  • We can get the dimensions of the grid by getting the square root of the flattened array's length, which I can then add my alpha to get the length of a new row.

Next we need to handle updating the arrays that already existed, that may or may not have active cells that the user has selected. All that changes within these rows is the front and end of the arrays. There will always be an additional number of alpha/2 cells tacked on to each side.

const rowPadding = Array.from(new Array(alpha / 2), () => {
  return 0;
});

const updatedRows = createToroidalArray(array, Math.sqrt(array.length)).map(row => {
  return [...rowPadding, ...row, ...rowPadding];
});

Thus I convert the flat array into its subarrays and then pad each side with the new cells. The spread operator makes this a breeze.

Returning was a bit more complicated because I can just spread these together because my updatedRows section is a single array of other arrays. So, I need to concat them together.

return newRow.concat(...updatedRows, newRow);

So now, this:

// normal

Becomes a smaller T, maintaining it's neighbors and relative position:

// normal grown once fixed

Fix 2 - Shrinking the Grid

Shrinking works different because pieces of the array are being destroyed at each turn. The same arguments are needed though. The array of the current state and the alpha representing what we will be chopping off.

return createToroidalArray(array, Math.sqrt(array.length)).map((row, i, arr) => {
  return row.slice((alpha / 2), (arr[i].length - (alpha / 2)));
}).filter((row, i, arr) => {
  return i === 0 || i === arr.length - (alpha/2) ? false : true;
}).reduce((a, b) => {
  return a.concat(b);
}, [] );
  1. I first break the everything down to their subarrays.
  2. I then return an array with the first and last sections removed, for each subarray.
  3. I then filter on all of the subarrays and get rid of the first and last.
  4. Then I put all of the arrays back together to get the flattened array that my rendering uses.

Side Note

I'm chaining a few different methods together here and using a ternary as well, but I strive to keep these sorts of things readable. Proper spacing and naming is crucial for being able to understand things later. I'll sometimes be reading through stack overflow and see posts that boast the few characters used to create something. How the hell is that helpful? I suppose the example above could be complicated if one weren't familiar with the different array methods, so commenting could be useful in that case. Avoiding the semantic concerns of aptly naming each step of editing saves some confusion, should I picked ridiculous variable names. I like a nice blend of properly named variables to store things and chained together methods in more complicated scenarios.

Here's the product of our finalized shrink function. Our T and Square change from:

// normal large

To a bigger relatively positioned:

// normal large shrank fixed 2

In the current iteration, if cells move off the gameboard they are deleted permanently. I may decide to persist these deleted cells until the Start button is clicked but I'll leave that in my icebox for now.

More to come!

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