← Back to list

An Arduino Version: The Game of Life

Polish your coding skills using a Seeed ESP32C6

Mark Lucking in Level Up Coding · 2024-10-24 01:05 · 111 claps · 8.1 min read paywalled
#arduino #seeed #conways-game-of-life #coding #games
Open on Medium ↗
Wiki topics: 💻 · Programming 📟 · Gadgets & IoT

An Arduino Version: The Game of Life

Polish your coding skills using a Seeed ESP32C6

An animated GIF of the game of LIFE by John Horton Conway

An animated GIF of the game of LIFE by John Horton Conway

One of the millions of people who sadly lost their lives to the COVID-19 pandemic was the brilliant mathematician John Horton Conway. Active in several theoretical areas of math, he became famous in the early seventies when a friend, Martin Gardner, published his invention, the cellular automaton he called the Game of Life. It was a game that quickly made its way to the computers of the time and became a favourite to try to implement. Join me in this article to learn how to do so on a Seeed ESP32C6 running on their Seeed Round Display, as illustrated.

Game of Life

A cellular automaton is a grid of cells that can be either alive or dead, depending on four rules. Once defined, it does just as the game suggests: it comes alive, mutates, and continues to get bigger or smaller. Here are the four rules.

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

A zero-player game, it is an interesting game to implement because it contains many elements that you could/would reuse in developing other games.

Requirements

Initially, I need to set up a means to input a cell variant I want to try, such as a touch-sensitive grid. But since touch is my only control, I must be creative about how it works. Beyond that, I need to set up a toroidal array to represent the cell space, which will wrap around itself. In the main loop, I need a window on the array that we will update with each new generation, applying the stated rules.

Seeed Studio Round Display

A few very important words on this — the Seeed wiki page describes a nightmare procedure in my mind, suggesting you downgrade your environment to get things working; it isn't an approach I recommend. I don’t recommend it partly bc you won’t be able to use the ESP32C6 should you follow their instructions to the letter, nor will you find yourself in a happy place when the “indexing till death bug, crops up halfway though your development” and you have to reset and restart the process.

A better approach you’ll find with this forum link, in which you’ll find an unofficial but very functional TFT library that has been edited to work on the latest chip, the ESP32C6: no Arduino environment downgrade is needed, although you should still read through their installation instructions to make sure you dotted all the i’s and crossed all the t’s.

[embed]Getting Started with Seeed Studio Round Display for XIAO | Seeed Studio Wiki XIAO Round Dislay Basic Tutorialwiki.seeedstudio.com

[embed]Support for the Seeed Round Display? I foolishly purchased several round displays for projects, including some based on the ESP32C6, only to discover when…forum.seeedstudio.com

You won’t be able to install the unofficial library as a zip. You must do so manually by unzipping it and copying it into the filesystem by hand [renaming it in the process, TFT_eSPI]. Turn verbose mode on the compiler when you do, and ensure the versions shown here match what you’ve got at a minimum.

Using library TFT_eSPI at version 2.5.43 in folder: /Users/localuser/Arduino/libraries/TFT_eSPI 
Using library SPI at version 3.0.5 in folder: /Users/localuser/Library/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/SPI 
Using library FS at version 3.0.5 in folder: /Users/localuser/Library/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/FS 
Using library SPIFFS at version 3.0.5 in folder: /Users/localuser/Library/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/SPIFFS 
Using library Seeed Arduino Round display at version 1.0.0 in folder: /Users/localuser/Arduino/libraries/Seeed_Arduino_Round_display 
Using library lvgl at version 9.2.0 in folder: /Users/localuser/Arduino/libraries/lvgl 
Using library Wire at version 3.0.5 in folder: /Users/localuser/Library/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/Wire 
/Users/localuser/Library/Arduino15/packages/esp32/tools/esp-rv32/2302/bin/riscv32-esp-elf-size -A /private/var/folders/sc/4y094kzs1rs26_qzh6wfr5qr0000gp/T/arduino/sketches/DA19B67043BCB5F915BA03751CA6D229/touchRoundDisplay.ino.elf
Sketch uses 271918 bytes (20%) of program storage space. Maximum is 1310720 bytes.
Global variables use 13344 bytes (4%) of dynamic memory, leaving 314336 bytes for local variables. Maximum is 327680 bytes.

Steps

When trying to code something from scratch, I typically look for the most challenging aspects, the ones I will find most difficult to code, and write some code to prototype those issues first. I am sure you can figure out my rationale: I don’t want to get three-quarters through a project and find myself facing something that is beyond my coding skills.

Structure

The basic structure behind the dynamics in this game will be a two-dimensional array representing the cell space. Within said array, I need to set up an index that will look at a three-by-three box of nine squares. The requirement is a ready-made structure to quickly determine how many neighbours each cell has as I pass them. The code to do that looks like this.

int LeftX = abs((i - 1) % maxCell);
int CentX = i;
int RightX = abs((i + 1) % maxCell);

int HighY = abs((k + 1) % maxCell);
int CentY = k;
int LowY = abs((k - 1) % maxCell);

You can think of this as a spreadsheet. I need three rows and lines to capture combinations of all the boxes in my three-by-three box. Having decomposed the problem into its smallest part, I can now create some super sets, which I do here.

int northLine = cells[LeftX][HighY] + 
         cells[CentX][HighY] + 
         cells[RightX][HighY];
int midLine = cells[LeftX][CentY] +
              cells[RightX][CentY];
int southLine = cells[LeftX][LowY] +
                cells[CentX][LowY] +
                cells[RightX][LowY];
int neighbours = northLine + midLine + southLine;

The variable neighbours contain a number representing how many live cells I have around the cell at the centre of the three-by-three box.

Rules

This code uses the neighbourhood value to determine who should and shouldn’t live in the next generation. The four rules condense down to just three. [Yes, I looked this up; I confess]

if ((neighbours == 2 || neighbours == 3) && cells[i][k] == 1) {
          shadows[i][k] = 1;
        } else if (cells[i][k] == 0 && neighbours == 3) {
            shadows[i][k] = 1;
          }  else {
            shadows[i][k] = 0;
        }

As I mentioned, this was and remains a favourite in computer science, and testing it is a breeze, too. I can use one of the known patterns and compare the result with the online wiki version, which you can find here.

[embed]Conway's Game of Life - Wikipedia The Game of Life, also known as Conway's Game of Life or simply Life, is a cellular automaton devised by the British…en.wikipedia.org

Toroidal Array

But as I tested it, I noted the toroidal array aspect wasn’t working. If I chose a “glider,” for example, it would make its way to the edge of the matrix and fall off. I must confess it took some hours to work out the fix. To correct it, I needed to ensure every access to the array went through the same code rule by creating a process to gateway the array access.

int returnInnerIndex(int i) {
  return(i % (MAXCELL -1));
}

int returnOuterIndex(int k) {
  return(k % (MAXCELL -1));
}

More importantly, meaning I had to change all accesses to said array.

int LeftX = returnInnerIndex(i -1);
int CentX = returnInnerIndex(i);
int RightX = returnInnerIndex(i + 1);

int HighY = returnOuterIndex(k + 1);
int CentY = returnOuterIndex(k);
int LowY = returnOuterIndex(k - 1);

...

if (cells[returnInnerIndex(i)][returnOuterIndex(k)] == 1) {
  tft.fillCircle(returnInnerIndex(i)*MAXSPACE +SHIFT,returnOuterIndex(k)*MAXSPACE +SHIFT,MAXSIZE,TFT_GREEN);
} else {
  tft.fillCircle(returnInnerIndex(i)*MAXSPACE +SHIFT,returnOuterIndex(k)*MAXSPACE +SHIFT,MAXSIZE,TFT_BLACK);
}

if ((neighbours == 2 || neighbours == 3) && cells[returnInnerIndex(i)][returnOuterIndex(k)] == 1) {
  shadows[returnInnerIndex(i)][returnOuterIndex(k)] = 1;
} else if (cells[returnInnerIndex(i)][returnOuterIndex(k)] == 0 && neighbours == 3) {
    shadows[returnInnerIndex(i)][returnOuterIndex(k)] = 1;
  }  else {
    shadows[returnInnerIndex(i)][returnOuterIndex(k)] = 0;
}

Touch, all is forgiven iOS

At this point, I thought I was home dry; how wrong could I be?

Unfortunately, as I already mentioned, the version of TFT I am restricted to using here is 2.5.x. Certainly not the current one. And on 2.x, they have minimal touch management, making the implementation a real challenge.

I soon discovered that although I could detect a touch event, they would come in clusters. I would need nano reflexes to get a single-touch event.

Since the plan was for the first touch to say yes, put a cell here, and the second touch to say no, that was a mistake — I needed to set up my own gating mechanism.

Beyond that, I needed a little wizard math to work out which cell had been touched and a lot of trial and error to get things big enough for my fat fingers to be able to touch the cell I wanted but small enough to be useful.

I settled on a grid of 6x6, which isn’t big enough, but given the restrictions, will have to do it. I may go back to the drawing board on this and look at a more Arduino-ish solution—more of that later.

Finally, how to switch to the main arena after defining your life form. I achieved this by using a timing loop. I used a delayed count that I reset every time you touch the display — if you don’t touch it for 128 ticks, then I assume you're done, and we’re off to the races. This is what all that looks like.

void defineCell() {
  int stepX = tft.width() / 8;
  int stepY = tft.height() / 8;

  for (int i = stepX * 1; i < stepX * 7; i += stepX) { 
    for (int k = stepY * 1; k < stepY * 7; k += stepY) { 
      tft.drawCircle(i+FIX,k+FIX,16,TFT_DARKGREY);
    }
  }

  int gogo = 0;
  int prior = true;
  int lastX = 0;
  int lastY = 0;

  while (gogo < 128) {
    gogo = 0;
    while (!chsc6x_is_pressed() && gogo < 128) {
      tft.fillRect(stepX * 3.0, stepY * 7, 48, 48, TFT_BLACK);
      tft.drawNumber(gogo, stepX * 3.5, stepY * 7, true);
      if (prior && gogo < 128) {
        prior = false;
        chsc6x_get_xy(&touchX, &touchY);
        int cellX = int(touchX / stepX);
        int cellY = int(touchY / stepY);
        lastX = touchX;
        lastY = touchY;
        int calcX = cellX * stepX;
        int calcY = cellY * stepY;
        if (rex[cellX][cellY] == 0) {
          if (cellX > 0 && cellY > 0 && cellX < 7 && cellY < 7) {
            tft.fillCircle(calcX+FIX,calcY+FIX,16,TFT_YELLOW);
            rex[cellX][cellY] = 1;
          }
        } else {
          tft.fillCircle(calcX+FIX,calcY+FIX,16,TFT_BLACK);
          tft.drawCircle(calcX+FIX,calcY+FIX,16,TFT_DARKGREY);
          rex[cellX][cellY] = 0;
        }
      }
      delay(96);
      gogo++;
      chsc6x_get_xy(&touchX, &touchY);
      if (touchX != lastX && touchY != lastY) {
        prior = true;
      }
    }
  }
}

This animated GIF shows the game in action —

Conclusion

My greatest mistake was thinking the main event would be the hardest. The biggest challenge was making touch work effectively. The Arduino solution would have been introducing more hardware for the controls. So, a second processor connected using ESP NOW and wired up to some touch pads, perhaps— but really —

In retrospect, maybe the Seeed ESP32C6 Round Display is a poor candidate for this application — the touch controls are so rudimentary they are almost unusable, certainly to those of us in the iOS world where you have a whole galaxy of controls and other libraries. TFT has worked on this; sadly, our friends at Seeed have not. Touch in the newer version of TFT is better. Please leave a comment below if your hardware supports TFT 3.0; I would like to take a look at it, too.

Finally, I didn’t include the entire codebase here because it doesn’t do my reading stats any good—stats that dictate how much if anything, medium.com pays me for this article. If you want the entire thing, please find me on LinkedIn and request a connection with a note asking for it. I have hundreds of articles published on medium.com, primarily focused on Swift development, the Arduino series a new muse. I am based in Europe, which is the other big giveaway.

An animated GIF of the whole show, speeded up just a little

An animated GIF of the whole show, speeded up just a little

And there you have it again, the game of life by mathematician extraordinaire John Horton Conway, brought to life running on an ESP32C6 running on a Seeed Round Display.


메타데이터
post_id
8949fffc8ee5
slug
an-arduino-version-the-game-of-life-8949fffc8ee5
url
https://levelup.gitconnected.com/an-arduino-version-the-game-of-life-8949fffc8ee5
canonical_url
https://levelup.gitconnected.com/an-arduino-version-the-game-of-life-8949fffc8ee5
author_url
https://medium.com/@marklucking
status
ok
fetched_at
2026-07-22 10:44:00