Two months or so ago, the kids and I bought a few bits and pieces at www.adafruit.com to build small LED badges. They had been staying in their box since then, but I eventually got to it with my youngest this Sunday afternoon.

Animated GIF!

Animated GIF!

The idea was to create simple animated badges using the cute 8×8 “mini” LED matrix displays that Adafruit sells, and using some of their small ATTiny45 boards to run the ARV Arduino sketch they described at http://learn.adafruit.com/trinket-slash-gemma-space-invader-pendant/overview .

While the kids got busy drawing animation frames for their badges, I thought I would come up with something a bit different – that and the fact I really can’t draw – and started looking at Conway’s “Life” algorithm that sounded perfect for such a tiny display. Someone had naturally already done this on the Arduino, so my job really was to adapt the code to the ATTiny85 and its limited memory space, and find a good way to generate good pseudo random sequences on the Adafruit “Gemma” board.

Gemma is a small round board with just a few pads, and the unique pad that can do ADC is used for the I2C bus in this particular “badge” configuration. Fortunately, one of the pins that is used for the USB connection also has ADC capabilities: though it is not actually possible to use it to do analog to digital configurations, this pin is floating when the USB plug is not connected, which is exactly what we want to seed our random number generator!

So without further ado, below is the Arduino Sketch for running a mini game of life on a 8×8 mini display. If the program detects the game is ended, i.e. all pixels dead or static, it will wait 10 seconds and restart. If the game ends up oscillating, the game always resets after 255 iterations anyway, as a precaution.

Enjoy!

// A Game of life badge, based on the Adafruit tutorial at
// http://learn.adafruit.com/trinket-slash-gemma-space-invader-pendant/overview
// plus code inspired from https://gist.github.com/remcoder/6007361
// in particular, I have kept the multicolor capabilities (cell age) in the code
// though the matrix is monochrome in my case.

// Original work below is (c) Edouard Lafargue 2014 License GPLv3


#define BRIGHTNESS   5 // 0=min, 15=max
#define I2C_ADDR   0x70 // Edit if backpack A0/A1 jumpers set

#include <TinyWireM.h>
#include <avr/power.h>
#include <avr/sleep.h>

// This table is necessary because we are not using the Adafruit backpack library but we're driving
// the display directly
static const uint8_t PROGMEM reorder[] = { // Column-reordering table
    0x00,0x40,0x20,0x60,0x10,0x50,0x30,0x70,0x08,0x48,0x28,0x68,0x18,0x58,0x38,0x78,
    0x04,0x44,0x24,0x64,0x14,0x54,0x34,0x74,0x0c,0x4c,0x2c,0x6c,0x1c,0x5c,0x3c,0x7c,
    0x02,0x42,0x22,0x62,0x12,0x52,0x32,0x72,0x0a,0x4a,0x2a,0x6a,0x1a,0x5a,0x3a,0x7a,
    0x06,0x46,0x26,0x66,0x16,0x56,0x36,0x76,0x0e,0x4e,0x2e,0x6e,0x1e,0x5e,0x3e,0x7e,
    0x01,0x41,0x21,0x61,0x11,0x51,0x31,0x71,0x09,0x49,0x29,0x69,0x19,0x59,0x39,0x79,
    0x05,0x45,0x25,0x65,0x15,0x55,0x35,0x75,0x0d,0x4d,0x2d,0x6d,0x1d,0x5d,0x3d,0x7d,
    0x03,0x43,0x23,0x63,0x13,0x53,0x33,0x73,0x0b,0x4b,0x2b,0x6b,0x1b,0x5b,0x3b,0x7b,
    0x07,0x47,0x27,0x67,0x17,0x57,0x37,0x77,0x0f,0x4f,0x2f,0x6f,0x1f,0x5f,0x3f,0x7f,
    0x80,0xc0,0xa0,0xe0,0x90,0xd0,0xb0,0xf0,0x88,0xc8,0xa8,0xe8,0x98,0xd8,0xb8,0xf8,
    0x84,0xc4,0xa4,0xe4,0x94,0xd4,0xb4,0xf4,0x8c,0xcc,0xac,0xec,0x9c,0xdc,0xbc,0xfc,
    0x82,0xc2,0xa2,0xe2,0x92,0xd2,0xb2,0xf2,0x8a,0xca,0xaa,0xea,0x9a,0xda,0xba,0xfa,
    0x86,0xc6,0xa6,0xe6,0x96,0xd6,0xb6,0xf6,0x8e,0xce,0xae,0xee,0x9e,0xde,0xbe,0xfe,
    0x81,0xc1,0xa1,0xe1,0x91,0xd1,0xb1,0xf1,0x89,0xc9,0xa9,0xe9,0x99,0xd9,0xb9,0xf9,
    0x85,0xc5,0xa5,0xe5,0x95,0xd5,0xb5,0xf5,0x8d,0xcd,0xad,0xed,0x9d,0xdd,0xbd,0xfd,
    0x83,0xc3,0xa3,0xe3,0x93,0xd3,0xb3,0xf3,0x8b,0xcb,0xab,0xeb,0x9b,0xdb,0xbb,0xfb,
    0x87,0xc7,0xa7,0xe7,0x97,0xd7,0xb7,0xf7,0x8f,0xcf,0xaf,0xef,0x9f,0xdf,0xbf,0xff };

void ledCmd(uint8_t x) { // Issue command to LED backback driver
  TinyWireM.beginTransmission(I2C_ADDR);
  TinyWireM.write(x);
  TinyWireM.endTransmission();
}

void clear(void) { // Clear display buffer
  TinyWireM.beginTransmission(I2C_ADDR);
  for(uint8_t i=0; i<17; i++) TinyWireM.write(0);
  TinyWireM.endTransmission();
}


// Game of life variables: 
// We need two tables: current and next
int next[8][8];
boolean still = false; // used to detect if we are still


// We initialize current with a known nice pattern, the "R-pentomino", see Wikipedia.
// We seed it with a bit of randomness anyway to make things more interesting.
int current[8][8] =
{ {0,0,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,1,1,0,0,0},
  {0,0,0,0,1,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} };

// Utility function
int mod(int a) { return (a+8)%8; }

// Called by Arduino at startup
void setup() {
  power_timer1_disable();    // Disable unused peripherals

  // We want to seed from an analog read to an unconnected pin. The
  // way the Gemma is wired, pin 2 is actually used by the USB bus, so it is not
  // usable for real analog reading. But when the USB bus is not connected, pin2 is
  // actually floating, which is exactly what we want for hardware randomness! Took me
  // a while to figure this one...
  randomSeed(analogRead(2));
  power_adc_disable();       // to save power
  PCMSK |= _BV(PCINT1);      // Set change mask for pin 1
  TinyWireM.begin();         // I2C init
  clear();                   // Blank display
  ledCmd(0x21);              // Turn on oscillator
  ledCmd(0xE0 | BRIGHTNESS); // Set brightness
  ledCmd(0x81);              // Display on, no blink

  randomize();
}

void randomize() {
     // Add a bit of randomness to our grid:
    for (uint8_t row = 0 ; row < 8 ; row++) {
      for (uint8_t col = 0 ; col < 8 ; col++) {
        if (random(6) > 4) 
            current[row][col] ^= 1;
      }
    }  
}

// Maximum of cycles before randomizing, so that in case we end up with
// an oscillator, we won't be stuck there forever...
uint8_t rep = 255;

// Writes the data to the tiny matrix:
void writeDisplay() {
    TinyWireM.beginTransmission(I2C_ADDR);
    TinyWireM.write(0);             // Start address
    for (uint8_t row = 0; row < 8; row++) { // 8 rows
      uint8_t cv = 0;
      for(uint8_t col = 0; col < 8; col++) {    // 8 columns
        cv |= ((next[row][col] == 0) ? 0:1) << col;
      }
      TinyWireM.write(pgm_read_byte(&reorder[cv]));
      TinyWireM.write(0);
    }
    TinyWireM.endTransmission();
}

// Compute the next step on the grid using Conway's algorithm:
// For more info on "Life": http://en.wikipedia.org/wiki/Conway's_Game_of_Life

void game_of_life() {
    // calc next state
  for (uint8_t row = 0 ; row < 8 ; row++) {
    for (uint8_t col = 0 ; col < 8 ; col++) {    
      // count alive neighbors
      int alive = 0;
      alive += current[mod(row+1)][mod(col)  ] != 0;
      alive += current[mod(row)  ][mod(col+1)] != 0;
      alive += current[mod(row-1)][mod(col)  ] != 0;
      alive += current[mod(row)  ][mod(col-1)] != 0;
      alive += current[mod(row+1)][mod(col+1)] != 0;
      alive += current[mod(row-1)][mod(col-1)] != 0;
      alive += current[mod(row+1)][mod(col-1)] != 0;
      alive += current[mod(row-1)][mod(col+1)] != 0;
      if (current[row][col])
// - Any live cell with fewer than two live neighbours dies, as if caused by under-population.
// - Any live cell with more than three live neighbours dies, as if by overcrowding.
        if (alive < 2 || alive > 3)
          next[row][col] = 0;
        else
// - Any live cell with two or three live neighbours lives on to the next generation.
          next[row][col] = current[row][col] + 1; // Could just be "1" here, since our display is monochrome.
      else
        if (alive == 3)
// - Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
          next[row][col] = 1;
    }
  }    
  
  // Move forward, copy next into current.
  still = true;
  for (uint8_t row = 0 ; row < 8 ; row++) {
    for (uint8_t col = 0 ; col < 8 ; col++) {
      still &= ((current[row][col]>0) == (next[row][col]>0)); // A bit tricky because value can be > 1
      current[row][col] = next[row][col];
    }
  }
}


void loop() {

    writeDisplay();
    delay(400); // 400 ms
    game_of_life();

  if(!--rep || still) {
    // Leave last screen for a while and stir things up a bit
    delay(10000);
    randomize();
    randomize();
    rep = 255;    
  }
}

ISR(PCINT0_vect) {} // Button tap

2 comments on “Mini game of life

  • October 26, 2014 at 01:12
    Permalink

    Thank you for sharing this pleasant and fun tutorial. The code is clean and easy to read.

    Reply
  • November 18, 2014 at 04:31
    Permalink

    Fantastic! I just got my space invaders working. Gotta try this. I think I used Conway’s Life many years ago fro Scientific American in Basic.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *