Game of Life

Caoimhe

I exhibited at a showcase for local game makers yesterday. It was the first time I had done something like that and it was extremely fun and utterly exhausting. Working on games is an occasionally hobby; I don’t intend to make it my job and I haven’t worked on any game projects since the game jam last year, but I dusted off Snolf Robo Blast 2 and Conway’s Garden and set them up with some controllers for people to try out.

Someone “enjoying” Snolf.

I had not thought about how exhibiting these would work before the event but I quickly realised that I was going to have to give a lot of context to people as to what they were playing. The vast majority of people attending never heard of Sonic Robo Blast 2 before so I had to give a quick rundown to everyone on what it was and sometimes the concept of fangames and mods.

Even with that explained, though, it became apparent how awkward it was to try to convey what the mod is doing when people seeing it do not have a context of the base game and how the mod is creating an experience on top of that. They are coming in and seeing and playing this game for the first time and expecting it to be a coherent whole, not this deliberate awkward layer on top of a base game. You don’t have the context that the joy in it is that you are playing the game “wrong”, layering a control scheme and method of play that the world was not intended for. Many people when seeing it as a golf-type thing started looking for a hole or guidance on where they are shooting for. The levels have a fairly legible linear structure when played normally, but when you have never seen them before and can freely shoot yourself much farther and higher than the levels for designed for, bouncing every which way, it is become very difficult to parse the structure of the space. More than one person suggested there should be guiding arrows of some sort to help.

Some people, though, did gel with it straight away and were delighted by blasting Snolf around, which was really nice to see. I was quick to give credit to the SRB2 team for the game itself and Dr. Melon for the concept. A line I fell into saying that got a laugh from a lot of people is that “all I did was make the controls worse.” A lot of people also got a kick out of it when I pointed out that the game was technically an extremely heavily modified version of Doom. Some people did have more of a concept of an antigame and compared it to QWOP and Getting Over It when I tried to explain the awkwardness of it not being entirely unintentional and the other game makers there generally got it and found it interesting. There were a few parents who brought children, including a Sonic fan or two, and they gravitated over to it being one of the more brightly coloured, exciting looking things on display, only to get pretty frustrated with it. For the kids at least I did quickly quit back to the main menu and restart the game with normal Sonic so that they could have a bit of fun trying it out and gave on the details of SRB2 to their parents if they were interested in it, pointing out that it was free. I think if exhibiting it again it would be useful to have a second computer set up with the unmodified version of the game, both to give context and to allow any child who sees Sonic and gets excited to play something that would actually be fun for them.

Similarly for Conway’s Garden I had to let people know that there wasn’t really any goal or point to it and it was more of a piece of art and a challenge to make something in a tiny amount of code. I had the code open in a second in a second window to the side so people could see how small it was for themselves. A lot of people, quite fairly, lost interest in it quickly, but some people were fascinated by the highly condensed code and the idea of tweetcarts and Pico-8 itself and a surprising number of people were already familiar with and recognised Conway’s Game of Life.

There was one man in particular I had a lovely chat with who was unfamiliar with games generally (he had to ask to clarify if “mods” was short for “modifications”) but enthusiastic about discussing the things on display as art. He said the mix of chaotic generation with the player’s simple, deliberate movements in Conway’s Garden reminded him of Joseph Beuys’s I Like America and America Likes Me and immediately recognised the Sisyphean nature of Snolf.

I don’t know if I will do anything like this again any time soon (and I won’t be working on anything new to show for the moment) but I had a great time and met some very cool people.


Caoimhe
k=2^13::s::for a=0,k do
n=0 for x=0,8 do
n+=peek(k*3+a+x/3+x%3*64-65)end
poke(a,n==12 and 4 or n==16 and peek(a))end
memcpy(k*3,0,k)goto s

That is the entire source code for my my Pico-8 implementation of Conway’s Game of Life, which you can run here:

Control with , and

I made this as a tweetcart back when tweets were still 140 characters and I am still pretty proud of it. Let’s run through how this works!

If you have never heard of it, the Game of Life is a type of cellular automation, a simulation consisting of a grid of cells. Each cell is either alive or dead and with each iteration of the simulation the state of each cell changes based on the following rules, as copied from Wikipedia:

  • 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.

It’s pretty hard to see how those rules translate to the above code, so I’ll try to explain it as best I can. It relies on some tricks specific to Pico-8.

The first thing in the code is is setting k=2^13, that is 8,192. This is a magic number that is going to have a few uses.

Then we set a label ::s::. This is a label we can use goto later on in the code to return to.

Then we start a for loop, for a=0,k do, counting from 0 to 8,192. This is the length, in bytes, of the part of the the Pico-8 virtual machine’s memory that holds the current screen state. Pico-8 has a resolution of 128×128 pixels, for 16,384 pixels total, and each pixel can have one of 16 colours. That means that the colour of each pixel can be stored in 4 bits—half a byte—and therefore the full screen information can be stored in 8,192 bytes. We are going to treat every byte as a single cell, which means every cell is actually made of two pixels. You can see how every cell in the simulation is one brown and one black pixel side-by-side.

The reason we’re doing this is so that we can iterate over the current screen state in order to determine the next iteration and which cells survive or die. By examining the screen memory directly we don’t have to create a different data structure for storing the cell information; it’s just whatever’s currently on screen. This saves an awful lot of characters.

Then, inside the loop, we start by setting n=0, this is going to be used to track the number of neighbours that every cell has.

Then we start up a new loop, for x=0,8 do, to iterate over all the neighbouring cells.

Which brings us to the first complicated expression, n+=peek(k*3+a+x/3+x%3*64-65). This is adding up the values of all the neighbours (for the purposes of this implementation, each cell is considered to be be one of its own neighbours). It peeks at the memory addresses of each of the neighbours, then adds it to the running total, stored in n.

k*3 (24,576 or 0x6000) is the start of the screen memory, the starting point for iterating over the screen data. Adding a to it is iterating over each cell in sequence in the memory.

Every byte (cell) is going to have a value of 0, for dead, or 4, for alive. I’ll explain why I used the value of 4 rather than the more obvious value of 1 later.

The screen memory is arranged in the order you would probably expect, it starts in the top left corner and scans left to right, top to bottom. The first 64 bytes are the first line of the screen, the next 64 bytes are the second line, etc. As such getting the cells to the left and right of the left and right of the current one is just a matter of taking away or adding one to the address we’re querying. Getting the cells above and below means subtracting or adding 64, respectively.

To get all nine neighbours (including the current cell itself) we need to check nine memory addresses: The current address, k*3+a, and offsets of -1, +1, -64, +64, -65, -63, +63, +65. This is accomplished with the +x/3+x%3*64-65 part of the expression as it iterates from 0 to 8. +x/3 will go from +0, +1, +2 over the course of the loop1. +x%3*64 cycles through +0, +64 and +128. Then the -65 corrects these to be centred around 0 (-1 for the horizontal and -64 for the vertical).

You may notice that this is going to behave weirdly at screen edges: For the first and last row it will be checking for “neighbours” in memory before and after the screen data. These are simply going to be blank as they aren’t otherwise being used for this programme. Additionally, due to the layout of the memory the left and right edges will wrap around and be treated as neighbours of each other, with a one pixel vertical offset. This weird behaviour is going to be the price we have to pay for trying to fit a mathematical simulation into a tweet2.

Once we have the total value of n totted up it’s time to write the next iteration of the simulation: poke(a,n==12 and 4 or n==16 and peek(a)).

Poke is the opposite of peak, it lets us write to a value in memory, and we’re going to just starting writing at address 0x0, which in Pico-8 is normally spritesheet data, but we’re not using sprites in this programme so we can treat it as effectively free memory.

The poke expression is a bit weird but effectively we’re either going to poke a value of 4, 0 or the current value of the cell (peek(a)).

Let’s go back to our bullet points from Wikipedia for a second. We need to rewrite them slightly because we’ve changed the way we counted living neighbours to include the cell itself as its own neighbour. Rephrasing it in those terms we have:

  • A live cell with fewer than three live neighbours dies.
  • A live cell with three or four neighbours lives.
  • A live cell with more than four neighbours dies.
  • A dead cell with exactly three live neighbours lives.

Or, simplifying it:

  • Any cell with three live neighbours is alive in the next iteration.
  • Any cell with four live neighbours keeps its current value in the next iteration.
  • Any other cell is dead in the next iteration.

Or, n==12 and 4 or n==16 and peek(a). If n==12 this evaluates as 4, the value we’re using for a living cell. If n==16 this evaluates as peek(a), write whatever the value for the cell is currently. Otherwise this evaluates as nil, and fortunately for us Pico-8 treats poke(a,nil) as being equivalent to poke(a,0), meaning every other case produces a dead cell.

Now we’ve iterated across the entire screen and made the next iteration of our simulation, the only thing left to do is copy the new iteration into the screen memory (memcpy(k*3,0,k)) and then start our loop again (goto s).

That’s it! Except, you might wonder, what about our starting state? We didn’t set any initial data, so why is there anything here in the first place when you run it? Well, our initial data is whatever is on screen when we run the code. You can have any starting state you want simply by having something different drawn to the screen when you hit run.

And that’s also why I used 4 for the value of a living cell: it produced interesting results when running directly after Pico-8’s boot screen rather than dying instantly, which happens if you used 13. 4 was chosen by trial and error. It’s also a bit easier to look at than using 1, which uses a dark blue colour rather than brown that’s harder to see against the black.

You can play around with this code yourself using Pico-8 Education Edition4 or check out my other Pico-8 projects

And that’s (actually) it! I hope you found this interesting.

  1. peek thankfully ignores any fractional values, so we can act as if these are being rounded. 

  2. At least to fit into what the size a tweet was back in the good old days, when men were men and tweets were tweets. Now I’m a woman and I’ve deleted my Twitter account… Actually, those might have been the bad old days. 

  3. Which would have shaved off two two more characters as the poke command would have been changed to n==3 and 1 or n==4 and peek(a)

  4. Click the button to start it and then hit Esc twice to open the editor. To run it again hit Esc again to bring up the command line, enter run and hit Enter