More Game of Life

At Recurse Center we are highly encouraged to pair program. By means of an introduction we did a workshop today to practice. The challenge was Game of Life, which already came up today in a different conversation, and I wrote about how to do it in Grasshopper a while back. Are Cellular Automata having a moment? Or are they just awesome?

Either way I worked with Joseph Martinez on a JavaScript implementation. Here's what we cooked up:

// grab a DOM element to use for output
// and initialize the grid state
const target = document.querySelector('#life');
let grid = [
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0],
];

// how many of the eight surrounding cells
// at row, column are currently living?
function countLiveNeighbors(row, col) {
  let w = grid[0].length - 1;
  let h = grid.length - 1;
  let neighbors = [
    row >= 1 && col >= 1 ? grid[row - 1][col - 1] : 0,
    row >= 1             ? grid[row - 1][col]     : 0,
    row >= 1 && col <  w ? grid[row - 1][col + 1] : 0,
                col >= 1 ? grid[row    ][col - 1] : 0,
                col <  w ? grid[row    ][col + 1] : 0,
    row < h  && col >= 1 ? grid[row + 1][col - 1] : 0,
    row < h              ? grid[row + 1][col]     : 0,
    row < h  && col <  w ? grid[row + 1][col + 1] : 0,
  ];
  let count = neighbors.reduce((accum, n) => accum + n, 0);
  return count;
};

// loop over the grid, apply rules to each cell
// build up the next grid state, make that current
function applyRules() {
  let newGrid = [];
  for (let i = 0; i < grid.length; i++) {
    newGrid[i] = [];
    for (let j = 0; j < grid.length; j++) {
      let alive = grid[i][j];
      let neighbors = countLiveNeighbors(i, j);
      if (neighbors < 2 && alive) alive = 0;
      else if (neighbors == (2 || 3) && alive) alive = 1;
      else if (neighbors > 3 && alive) alive = 0;
      else if (neighbors == 3 && !alive) alive = 1;
      newGrid[i][j] = alive;
    }
  }
  grid = newGrid;
}

// translate the grid state data to something readable
function draw() {
  const gridAsString = grid.map((row) => row.join(' ')).join('\n');
  target.innerHTML = gridAsString;
}

// every second, apply the rules and redraw
setInterval(() => {
  applyRules()
  draw();
}, "1000");

Here it is in action:

0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0

I'm sure there are more elegant ways to deal with the neighbor checking, and it would be fun to be able to use a different input state (maybe from a user-provided bitmap?). But that's good enough for today!