Skip to content

Instantly share code, notes, and snippets.

@misterussell
Created January 19, 2018 19:32
Show Gist options
  • Save misterussell/5084b9cc170f1f21499fab2e780543c1 to your computer and use it in GitHub Desktop.
Save misterussell/5084b9cc170f1f21499fab2e780543c1 to your computer and use it in GitHub Desktop.
1 19 18 Blog Post

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

In Part I and Part II I worked through the basic logic of analyzing a game of life. I created hash maps to calculate the living state of cells, and set up a toroidal array structure. In this post I will be diving into how I have built a visual structure in React, utilizing state to manage and render each generation of life for a set of cells.

Provisioning

I used Create-React-App as my build tool to jumpstart the project and dive in to coding, in an attempt to minimize the time spent provisioning the project. Switching out my testing utilities to Mocha/Chai added some time to provisioning. You can read about that here.

I'm utilizing React-Bootstrap for most of my basic components in this project, primarily because I'm building this into a hub of practice exercises that I can both visualize and test with React. Things like Buttons, Menus, and Modals will all be pulled bootstrapped, with a few minor tweaks to keep them reusable based on the state of the parent container.

Knowing that I was going to move this to React, I was very mindful when building the initial game logic. I kept everything pretty functional. Functions received their inputs and returned new copies of the data needed, with no side-effects nor mutations to the parent data.

Handling Data

I considered implementing a state container to manage my data, but settled on having React state be the top level of data. Changes made to the data would be set to the container state and trickle down accordingly. There is one piece of data that I am handling with an outside data-store, which I will discuss briefly.

The state of my container is handling a few things:

  1. Conditional aspects for reusable components: Button text, Button callbacks, Modal display and text
  2. Game related rendering: The dimensions of the CSS Grid Game Board, what Cells are active, what Cells are inactive, the activity state of the game
  3. Game related data: The initial Seed structure, the hashMap for each generation, and the interval for calculating the next stage of life

Simple State Updates

For updates to data I am no longer directly updating state with the mutable function of:

this.setState({ foo: 'bar' });

I am in turn, returning full copies of the state that needs mutated. I like this pattern because changes are set to the state on completion, meaning that if I were to call upon a state property after the function is complete, the new state would be registered.

this.setState((prevState) => {
  let foo = { foo: 'newBar' };
  return { foo };
});

Let's dive into an example of this when a cell is clicked. When a cell is clicked the array that the React component is rendering needs to either a) change the cell colour to active, or b) change it back to inactive. This is handled with inline styling based on the value of the index of the cell array. I immediately return a state change, because clicking a cell doesn't have any other side-effects.

this.setState((prevState) => {
  const cellState = prevState.cells[cell] === 0 ? 1 : 0;
  const activeCells = prevState.cells[cell] === 0
    ? prevState.activeCells += 1
    : prevState.activeCells -=1;
  const copy = [...prevState.cells];
  copy[cell] = cellState;
  return { cells: copy, activeCells };
});
  • The first thing that is handled is the new value of the cell. Activity is conditionally set based on the current array value; 0 for empty, 1 for active.
  • The next thing that I'm taking into account when clicking a cell is the total number of active cells on the Game Board. If a cell is inactivated, it is removed from this list.
  • A mutable copy of the prevState array is then saved so that I can make changes to the array of cells to be updated.
  • Finally a new state object that updates cells and activeCells is returned.
  • An important detail of this function is that it actually receives an argument cell which is the array value of the cell that was clicked. This is sent through the props to the cell and to this function onClick.

Handling state in this requires you to be more literal with changes. For example: Let's say I accidentally sliced some indices from my cells array, if I return only the new array values, the missing values won't persist in state. I would end up with a new array that would render a Game Board with cells missing.

  • Note, this.setState() takes two arguments, the second being currentProps. As my main container has no props related to the game, I'm not including this optional argument.

Conditional State Updates

Let's look at a more complicated example, the updateGameBoard() method. This returns conditional state changes. Rather than returning an object with my new changes defined in the return, I'm saving updates to the final state changes in a nextState object. There are two things that need returned for each condition, hashMap and cells, so those are defined at the highest level of the function. Everything is saved in nextState so that needs to go ahead and be defined as well.

let hashMap;
let cells;
let nextState;

On the condition that the game has been ended due to no more cells being changed, or because the user has paused the game the current state of the cells is returned.

if (prevState.gameState === false) {
  cells = [...prevState.cells]
}

If the game is still running each pass of updateGameBoard, which is run every 175 ms, will generate a new hashMap based on the current array's values. The hashMap generation is pretty clunky, so I may pipe these together later.

hashMap = generateNextGenState(
            generateGenState(
              createHashableArray(prevState.cells, prevState.totalBound)));
cells = [...prevState.cells];

Here is where we hit our first hidden side effect. The generateNextGenState function is actually saving any cell whose value has changed into a Store variable defined in a high level module. Since I'm not using a state container tool, I'm reaching out to that in updateGameBoard which seems like a bit of an anti-pattern, but as I'm not returning the changed value from the function, but rather relying on a global variable the function will break if the Store isn't computed correctly. If I was using a state container, I'd probably throw a listener on the store and have it deal with all the changes and remove my state updaters from the component all together. The snipped that updates the store is pretty simple:

if (cell.cellState !== nextState) {
  Store.changes[cell.arrayPosition] = nextState;
}

If the Store doesn't register any changes to the cells, the game is turned off, and the interval that was updated the cells is cleared, and a modal shown to let the user know why the game has paused. There is a bit of a bug here because the modal isn't shown immediately when the game dies but rather on the pass after the game has died. This lag is noticeable enough to pursue a fix.

nextState = { gameState: false, interval: clearInterval(prevState.interval), modal };

If the Store does have pending changes then edits are made to the copy of the current cells array. The store is then cleared out for the next pass.

Object.keys(Store.changes).forEach(key => {
  cells[key] = Store.changes[key];
});
Store.changes = {};
nextState = { hashMap, cells };

After cycling through each of the conditions setState returns nextState with the appropriate changes to state stored in the new object. It feels so clean!

Nested Object Property Updates in State

One last pattern for the setState() function that I want to mention is that you cannot update certain properties, as would seem logical. I.e:

this.setState((prevChanges) => {
  return { modal.show: true };
})

Single properties cannot be adjusted, most likely because this pattern of updating state requires the complete state of the object to be updated. I'm using the babel spread transform to handle this update:

this.setState((prevChanges) => {
  let show = true;
  let modal = { ...prevState.modal, show }
  return { modal }
});

By handling the update this way, any of the other properties of the modal state object are saved, like the text that was last generated for the modal. I'm jumping a bit ahead of myself for this, but I'm planning to track the number of times the user has played a dying game, etc..., and will likely need to save this info after each game ends.

Conclusion

All functions in the Game Board container only return updates to state. None have side-effects (beyond the one function that updates the store of pending changes).

Next Steps

I have some decisions to make for what I handle next.

  • When the size of the array is grown, how do I want currently selected cells to be handled? Currently they just keep their index in the array and move due to the shift in columns and rows
  • Do I want to implement a state container to get more practice with one of these?
  • Should I add transitions to the cells, for pzazz
  • How do I want to handle data tracking? Should I build a dash so that the user can see what happened in the game? Would stats be interesting or complicated? (I'm probably going to do this because I'm a data Junkie)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment