Feature flags are decision points in an application that change it's behavior based on context and a set of rules. This is a fun little project that implements Conway's Game of Life using feature flag rules, a raspberry pi, and a neopixel matrix.

Surprisingly complex and fascinating behavior emerges from having the cells follow just 3 simple rules

Infinite complexity from simple rules

The hardware

This being a quick weekend project, we'll start by hastily wiring a raspberry pi to a level shifter and finally to our LED matrix.

Level shifter required to change the Pi's 3.3v GPIO signal to 5v
16x16 Neopixel LED Matrix with a very professional diffuser made of paper and packing tape

The flags

Our first step is to create a couple feature flags for controlling the grid. Our application will run in a loop, evaluating feature flags for each cell to determine it's next state. We will pass a couple of custom attributes to power the targeting rules:

  • column and row: indicating the x and y coordinates of the cell on the grid
  • alive: boolean indicating whether or not the cell is "alive"
  • neighbors: the number of neighboring cells that are alive
  • iteration: total number of iterations (loops) have run so far
  • since: the iteration when the cells state last changed
  • age: how long the cell has been in this state (iteration - since)

Config: Cell Alive

Determine if the cell is alive

Cells in rows 7-9 are alive

Config: Cell Color

Determine the color of the LED representing the cell

Config: Steps Per Second

Determine how many iterations should be run per second

Rules to Live By

We now have an excellent base to implement our automaton. During each loop, we will evaluate the config-cell-alive flag for every cell to determine its next state:

 const nextState = await variation('config-cell-alive', this.cellUser({
                    column, row,
                    alive,
                    since,
                    neighbors: liveNeighbors,
                    age: Math.min(0, this.iteration - since),
                }), defaultState)

                return {
                    alive: nextState,
                    since: nextState == alive ? since : this.iteration
                }

In Conway's Game of Life, cells follow a set of simple rules:

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

These rules, which compare the behavior of the automaton to real life, can be condensed into the following:

  1. Any live cell with two or three live neighbours survives.
  2. Any dead cell with three live neighbours becomes a live cell.
  3. All other live cells die in the next generation. Similarly, all other dead cells stay dead.

source: Wikipedia

We can implement the simplified rules using the targeting rules in Config: Cell Alive:

Since we start with an empty world, let's add one more rule that randomly spawns live cells on the first iteration:

Behold, life!
⚠️
Detour: Bug in some versions of the hardware

Neopixels are represented by a flat array with their indices determined by how they are wired together. In my smaller 8x8 matrix, converting x and y coordinates to a pixel index is very simple:

   cellCoordinatesToIndex(x, y) {
        return (y * this.width) + x
    }

With my larger 16x16 matrix, it turned out that every other row gets flipped! I visualized this by setting all cells in the first column (`x = 0`) to green:

Every even row starts on the opposite end of the matrix

In order to support many types of panels without changing the code-base, I opted to create a feature flag to enable remapping the column of even rows in the renderPixels function:

async function renderPixels() {
        for(let i = 0; i < game.state.length; i++) {
            const cell = game.state[i];
            const [x,y] = game.cellIndexToCoordinates(i)
            const color = await variation(`config-cell-color`,
            	game.cellUser({
                	row: y,
                	column: x,
                	age: game.iteration - cell.since, 
                	...cell
            	}), cell.alive ? 0x00ff00 : 0x000000);
            // the feature flag returns a hex string
            const colorNumber = typeof color == 'string' ? parseInt(color, 16) : color

            // Seperentine matrixes require remapping on even rows
            if (await variation('enable-serpentine-matrix-remap', createContext(), false) 
                && y % 2 == 0) {
                const remapX = (game.width - x - 1) % game.width
                pixels[game.cellCoordinatesToIndex(remapX, y)] = colorNumber
            } else {
                pixels[i] = colorNumber
            }
        }
        neopixel.render()
    }

Once we toggle the flag on, everything works as expected:

Other tricks

Glider in action

Using segments, we can easily save common patterns such as:

  • Still lifes: patterns that live forever
  • Oscillators: a cycle of patterns that return to their original state after a finite number of generations
  • Spaceships: patterns that travel across the grid
  • Generators: patterns that spawn other patterns/cells

Finishing up

To liven up the visuals a bit, I am going to add some additional rules to the cell color flag:

And finally, make the game automatically reset to iteration 0 when no cells are alive on the grid:

async function runGame() {
    while(running) {
        game.step()
        await renderPixels()
        const population = game.state.filter(x => x.alive).length
        if(population == 0) game.reset()
        await sleep(1000 / await conf('steps-per-second', 1))
    }
    console.log('done running')
}

Now we have a nice loop that will keep running without human intervention.

Future ideas

I plan to combine four 16x16 grids for a total of 1024 cells and a pretty frame to hang on my wall as a digital art piece.  Eventually, I'd like to create a Web UI that allows users to see the grid and paint cells to life by clicking on them.