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.
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.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × LED matrix, 8x8
- 1 × Thumb stickIn Claw Machine Kit, Spaceship Panel Kit.
- 3 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
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 pin | Connects to |
|---|---|
| LED matrix, 8x8 pin GND | Arduino Uno R3 pin GND |
| Thumb stick pin GND | Arduino Uno R3 pin GND |
| Pushbutton (1) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (2) pin 2a | Arduino Uno R3 pin GND |
| Pushbutton (3) pin 2a | Arduino Uno R3 pin GND |
| LED matrix, 8x8 pin CLK | Arduino Uno R3 pin 13 |
| LED matrix, 8x8 pin DIN | Arduino Uno R3 pin 11 |
| LED matrix, 8x8 pin CS | Arduino Uno R3 pin 10 |
| Pushbutton (3) pin 1a | Arduino Uno R3 pin 5 |
| Pushbutton (2) pin 1a | Arduino Uno R3 pin 4 |
| Pushbutton (1) pin 1a | Arduino Uno R3 pin 3 |
| Thumb stick pin SW | Arduino Uno R3 pin 2 |
| LED matrix, 8x8 pin VCC | Arduino Uno R3 pin 5V |
| Thumb stick pin VCC | Arduino Uno R3 pin 5V |
| Thumb stick pin VRX | Arduino Uno R3 pin A0 |
| Thumb stick pin VRY | Arduino 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.
// 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 editorTroubleshooting
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.