Built-in project · Arduino Uno R3

Game of Life on an Arduino Uno

Conway's Game of Life on a 32x8 LED sign that wraps round at the edges: draw cells with a thumb stick, run or pause, pour in a random soup, or drop a glider. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $34. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Displays
  • Games
  • Controllers
  • Reading a button
  • Difficulty: Advanced
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 7

How it works

Conway's Game of Life on a 32x8 LED sign that wraps round at the edges: draw cells with a thumb stick, run or pause, pour in a random soup, or drop a glider.

Arduino Uno R3 runs the sketch below. The circuit around it: LED matrix, 8x8, Thumb stick, Pushbutton. pinMode sets RUN_PIN as an input with its pull-up on. digitalRead reads whether pin is HIGH or LOW. delay waits 10 milliseconds. Serial.begin opens the serial port at 115200 baud. millis reads the time since power-up, so the sketch can time things without stopping.

Parts list

7 parts, plus the jumper wires. They retail for about $34; on Mokxi they're free.

Kits that can build it

This starter kit has every part in the circuit. Each kit's page lists what else it can build.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
LED matrix, 8x8 pin GNDArduino Uno R3 pin GND
Thumb stick pin GNDArduino Uno R3 pin GND
Pushbutton (1) pin 2aArduino Uno R3 pin GND
Pushbutton (2) pin 2aArduino Uno R3 pin GND
Pushbutton (3) pin 2aArduino Uno R3 pin GND
LED matrix, 8x8 pin CLKArduino Uno R3 pin 13
LED matrix, 8x8 pin DINArduino Uno R3 pin 11
LED matrix, 8x8 pin CSArduino Uno R3 pin 10
Pushbutton (3) pin 1aArduino Uno R3 pin 5
Pushbutton (2) pin 1aArduino Uno R3 pin 4
Pushbutton (1) pin 1aArduino Uno R3 pin 3
Thumb stick pin SWArduino Uno R3 pin 2
LED matrix, 8x8 pin VCCArduino Uno R3 pin 5V
Thumb stick pin VCCArduino Uno R3 pin 5V
Thumb stick pin VRXArduino Uno R3 pin A0
Thumb stick pin VRYArduino Uno R3 pin A1

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Game of Life: Conway's cellular automaton on a 32x8 MAX7219 sign, with a
// thumb stick to draw cells and three buttons.
//
// Wiring, board to modules:
//   MAX7219 DIN -> pin 11   CLK -> pin 13   CS -> pin 10   VCC -> 5 V   GND -> GND
//   stick   VRX -> A0   VRY -> A1   SW -> pin 2   VCC -> 5 V   GND -> GND
//   RUN button -> pin 3     SOUP button -> pin 4     GLIDER button -> pin 5
//   (each button joins its pin to GND; the chip's pull-up does the rest)
//
// # The rules
//
// Every cell is alive or dead. Once a generation, each cell counts its eight
// neighbours and then:
//
//   a live cell with 2 or 3 live neighbours lives on, otherwise it dies
//   a dead cell with exactly 3 live neighbours is born
//
// That is all. Out of those two lines come blinkers, blocks and gliders: a
// glider is five cells that walk across the sign one step diagonally every
// four generations, forever.
//
// # The world is a ring
//
// The sign is 32 columns by 8 rows. A cell on the right edge counts the left
// edge as its neighbour, and the top counts the bottom. A glider that leaves
// on the right comes back in on the left, so the board never runs out of room.
//
// # Controls
//
//   stick       moves the cursor (it blinks while the game is paused)
//   stick press toggles the cell under the cursor; hold it for a second to
//               wipe the board
//   RUN         runs or pauses
//   SOUP        fills the board with random cells, about one in three
//   GLIDER      drops a glider at the cursor, heading a new way each press
//
// The game starts running with one glider already on its way.
//
// # How the board is stored
//
// One row is 32 cells, so one row fits in one 32-bit number, a bit a cell.
// The whole world is eight numbers, 32 bytes. Counting neighbours is a loop
// over the three rows above, at and below, with the column wrapped by hand.

#include "mokxi_joystick.h"
#include "mokxi_max7219.h"

const uint8_t DIN_PIN = 11;
const uint8_t CLK_PIN = 13;
const uint8_t CS_PIN = 10;
const uint8_t RUN_PIN = 3;
const uint8_t SOUP_PIN = 4;
const uint8_t GLIDER_PIN = 5;

const int8_t W = 32;
const int8_t H = 8;

Max7219 sign(DIN_PIN, CS_PIN, CLK_PIN, 4);
Joystick stick(A0, A1, 2);

/** The world now, and the one being worked out. Bit x of a row is column x. */
uint32_t world[H];
uint32_t next[H];

bool running = true;
int8_t cx = 16;
int8_t cy = 4;
uint8_t gliderTurn = 0;
uint16_t lfsr = 0xACE1u;
uint32_t generation = 0;

const uint32_t STEP_MS = 200;
const uint32_t MOVE_MS = 110;
const uint32_t BLINK_MS = 250;
const int16_t THROW = 160;

uint32_t lastStep = 0;
uint32_t lastMove = 0;
uint32_t lastBlink = 0;
bool cursorOn = true;

static uint8_t rnd() {
  // A 16-bit maximal-length shift register: cheap, and random enough to look like soup.
  uint16_t bit = (uint16_t)(((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 5)) & 1u);
  lfsr = (uint16_t)((lfsr >> 1) | (bit << 15));
  return (uint8_t)lfsr;
}

static bool alive(int8_t x, int8_t y) {
  // The ring: add the size before the remainder so -1 becomes the last cell.
  x = (int8_t)((x + W) % W);
  y = (int8_t)((y + H) % H);
  return (world[y] >> x) & 1u;
}

static void setCell(int8_t x, int8_t y, bool on) {
  x = (int8_t)((x + W) % W);
  y = (int8_t)((y + H) % H);
  if (on) {
    world[y] |= (uint32_t)1 << x;
  } else {
    world[y] &= ~((uint32_t)1 << x);
  }
}

static uint8_t population() {
  uint8_t n = 0;
  for (int8_t y = 0; y < H; y++) {
    for (int8_t x = 0; x < W; x++) {
      n += alive(x, y) ? 1 : 0;
    }
  }
  return n;
}

/** One generation: the two rules, applied to every cell at once. */
static void step() {
  for (int8_t y = 0; y < H; y++) {
    uint32_t row = 0;
    for (int8_t x = 0; x < W; x++) {
      uint8_t n = 0;
      for (int8_t dy = -1; dy <= 1; dy++) {
        for (int8_t dx = -1; dx <= 1; dx++) {
          if (dx != 0 || dy != 0) {
            n += alive((int8_t)(x + dx), (int8_t)(y + dy)) ? 1 : 0;
          }
        }
      }
      bool now = alive(x, y);
      if (n == 3 || (now && n == 2)) {
        row |= (uint32_t)1 << x;
      }
    }
    next[y] = row;
  }
  // Only now copy over: a cell must be judged on the old world, not a half-new one.
  for (int8_t y = 0; y < H; y++) {
    world[y] = next[y];
  }
  generation++;
}

/** Draw the world, and the cursor on top of it when asked. */
static void draw(bool cursor) {
  for (int8_t y = 0; y < H; y++) {
    for (int8_t x = 0; x < W; x++) {
      bool on = alive(x, y);
      if (cursor && x == cx && y == cy) {
        on = !on;
      }
      sign.setPixel(x, y, on);
    }
  }
  sign.show();
}

static void clearWorld() {
  for (int8_t y = 0; y < H; y++) {
    world[y] = 0;
  }
}

static void soup() {
  clearWorld();
  for (int8_t y = 0; y < H; y++) {
    for (int8_t x = 0; x < W; x++) {
      // A byte under 85 is one chance in three.
      setCell(x, y, rnd() < 85);
    }
  }
}

/**
 * Five cells that walk. The picture is the glider heading down and right;
 * flipping x and y gives the other three headings.
 *
 *   . # .
 *   . . #
 *   # # #
 */
static void glider(int8_t ox, int8_t oy, uint8_t turn) {
  static const int8_t shape[5][2] = {{1, 0}, {2, 1}, {0, 2}, {1, 2}, {2, 2}};
  for (uint8_t i = 0; i < 5; i++) {
    int8_t gx = shape[i][0];
    int8_t gy = shape[i][1];
    if (turn & 1) gx = (int8_t)(2 - gx);
    if (turn & 2) gy = (int8_t)(2 - gy);
    setCell((int8_t)(ox + gx), (int8_t)(oy + gy), true);
  }
}

static bool pressedEdge(uint8_t pin, bool &was) {
  bool now = digitalRead(pin) == LOW;
  bool edge = now && !was;
  was = now;
  return edge;
}

bool wasRun = false;
bool wasSoup = false;
bool wasGlider = false;
bool wasClick = false;
uint32_t clickAt = 0;
bool wiped = false;

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Uno: Game of Life on a 32x8 MAX7219 sign");
  stick.begin();
  pinMode(RUN_PIN, INPUT_PULLUP);
  pinMode(SOUP_PIN, INPUT_PULLUP);
  pinMode(GLIDER_PIN, INPUT_PULLUP);
  sign.begin(4);
  clearWorld();
  glider(2, 1, 0);
  draw(false);
  lastStep = millis();
}

void loop() {
  uint32_t now = millis();

  // Mixing the clock into the shift register on every press is what makes the
  // soup differ each time: nobody presses at the same millisecond twice.
  if (pressedEdge(RUN_PIN, wasRun)) {
    running = !running;
    lfsr ^= (uint16_t)now;
    Serial.println(running ? "run" : "pause");
    lastStep = now;
    draw(!running);
  }
  if (pressedEdge(SOUP_PIN, wasSoup)) {
    lfsr ^= (uint16_t)(now * 31u);
    if (lfsr == 0u) lfsr = 0xACE1u;
    soup();
    Serial.print("soup ");
    Serial.println(population());
    draw(!running);
  }
  if (pressedEdge(GLIDER_PIN, wasGlider)) {
    glider((int8_t)(cx - 1), (int8_t)(cy - 1), gliderTurn);
    gliderTurn = (uint8_t)((gliderTurn + 1) & 3);
    Serial.println("glider");
    draw(!running);
  }

  // The stick: a press toggles a cell on release-free edge, a long press wipes.
  bool down = stick.pressed();
  if (down && !wasClick) {
    clickAt = now;
    wiped = false;
    if (!running) {
      setCell(cx, cy, !alive(cx, cy));
      draw(true);
    }
  }
  if (down && !wiped && (uint32_t)(now - clickAt) > 1000u) {
    clearWorld();
    wiped = true;
    Serial.println("wipe");
    draw(!running);
  }
  wasClick = down;

  // The cursor only moves when paused, so a run cannot be spoilt by a nudge.
  if (!running && (uint32_t)(now - lastMove) >= MOVE_MS) {
    int16_t x = stick.dx();
    int16_t y = stick.dy();
    int8_t mx = (int8_t)(x > THROW ? 1 : (x < -THROW ? -1 : 0));
    int8_t my = (int8_t)(y > THROW ? 1 : (y < -THROW ? -1 : 0));
    if (mx != 0 || my != 0) {
      cx = (int8_t)((cx + mx + W) % W);
      cy = (int8_t)((cy + my + H) % H);
      lastMove = now;
      draw(true);
    }
  }

  if (running) {
    if ((uint32_t)(now - lastStep) >= STEP_MS) {
      lastStep = now;
      step();
      draw(false);
    }
  } else if ((uint32_t)(now - lastBlink) >= BLINK_MS) {
    // Blink the cursor: flip it every quarter second.
    lastBlink = now;
    cursorOn = !cursorOn;
    draw(cursorOn);
  }
  delay(10);
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Uno: Game of Life on a 32x8 MAX7219 sign");
  stick.begin();
  pinMode(RUN_PIN, INPUT_PULLUP);
  pinMode(SOUP_PIN, INPUT_PULLUP);
  pinMode(GLIDER_PIN, INPUT_PULLUP);
  sign.begin(4);
  clearWorld();
  glider(2, 1, 0);
  draw(false);
  lastStep = millis();
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop() {
  uint32_t now = millis();

  // Mixing the clock into the shift register on every press is what makes the
  // soup differ each time: nobody presses at the same millisecond twice.
  if (pressedEdge(RUN_PIN, wasRun)) {
    running = !running;
    lfsr ^= (uint16_t)now;
    Serial.println(running ? "run" : "pause");
    lastStep = now;
    draw(!running);
  }
  if (pressedEdge(SOUP_PIN, wasSoup)) {
    lfsr ^= (uint16_t)(now * 31u);
    if (lfsr == 0u) lfsr = 0xACE1u;
    soup();
    Serial.print("soup ");
    Serial.println(population());
    draw(!running);
  }
  if (pressedEdge(GLIDER_PIN, wasGlider)) {
    glider((int8_t)(cx - 1), (int8_t)(cy - 1), gliderTurn);
    gliderTurn = (uint8_t)((gliderTurn + 1) & 3);
    Serial.println("glider");
    draw(!running);
  }

  // The stick: a press toggles a cell on release-free edge, a long press wipes.
  // ... 45 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The matrix is blank after upload. LED matrix
    On power up the MAX7219 is in shutdown mode with intensity 0. Wake it, set intensity, set scan limit to all 8 digits, and clear the display.
  • Dim, flickering, or wrong rows. LED matrix
    Set the ISET resistor right for your LEDs (about 10 kohm for 40 mA). Power from 5 V with a 0.1 uF capacitor, and chain DOUT to the next DIN.
  • The axis reads about half scale at rest instead of exactly zero, and the values drift a little, so the cursor or motor creeps. Thumb stick
    Center is about half of the ADC range (about 512 on a 10 bit Arduino). Read it at startup and subtract that, then add a dead zone of a few counts.
  • The push button reads random values or always looks pressed. The switch pin is a plain contact to ground and has no pull-up on the stick. Thumb stick
    Set the switch pin to INPUT_PULLUP and treat LOW as pressed. Debounce it in code. Power the stick from the same voltage as the ADC reference.
  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.
  • The button seems always pressed, or never changes. Pushbutton
    A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
  • Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
    Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.