The Relicans

Cover image for Life. As in Conway's Game. And a little GitHub Copilot.
Kirk Haines
Kirk Haines

Posted on

Life. As in Conway's Game. And a little GitHub Copilot.

The Olden Days

It was the mid 1980s. I had an Apple][+ computer that my parents had spent, in 1982 dollars, a small fortune on. I knew everything about that computer. Hardware implementation, low level details about the OS, 6502 microprocessor assembly language, Applesoft Basic (of course), and so much more. I was a guru in the niche that was that ecosystem of hardware and softare.

As people did in those days, I read actual print magazines about programming, and friends traded actual physical 5.25" floppy discs to share software. Those were the days....

I remember reading in a magazine about Conway's Game of Life. I do not recall which magazine it was, unfortunately. However, I was intrigued by it. I wanted to build it!

Wait. Conway's Game of Life?

Calling it a game is deceptive. It is really a very simple simulation that operates by applying a few simple rules to a stateful grid, generating a new transformation of the prior state.

Some implementations provide an infinite grid, but most, in the interest of generating something that can be displayed on a statically sized display, use a grid that wraps around. That is, if something moves off of the right size, it will wrap around to the left.

Conway's Game of Life, itself, is described in great detail on Wikipedia. The key information, though, is the rules.

Given a grid on which to render the state transformations, each cell on the grid will have 8 neighboring cells.

If the cell has less than two live neighbors, or more than three live neighbors, the cell's new state will be dead, or empty.

If the cell has two neighbors, and it was previously alive, or filled, it will remain alive.

If the cell has three neighbors, it is alive regardless of it's previous state.

Each generation transforms the state from the previous generation.

Those are the only rules.

Now, Back To The Olden Days

I don't recall with certainty how long it took for me to build it, but I wrote an implementation, in Applesoft Basic, using the Lowres graphics mode for the Apple ][. The screen was a 40x40 grid of very large pixels, with 4 lines of text below it, or one could eliminate the text to get a display resolution of 40x48 pixels. Each pixel could have 16 colors in lowres mode.

There was also a hires mode, which was 280x192 pixels, or 280x160 if one wanted 4 lines of text below the graphic.

Hires mode technically supported 8 colors, but the design was quirky -- Steve Wozniak did some very clever things to save on the number of chips needed, and thus the cost. The colors were arranged into two banks of 4 colors:

  • black, green, purple, white
  • black, orange, blue, white

Black and white were repeated in both banks. These two color banks actually had a half-pixel offset in how they were displayed, and this led to some interesting effects, since it meant that the two blacks and the two whites, while both black and white, were not the same, and colors between the two banks, when they were adjacent, could produce interesting interference patterns. It was possible to, effectively, display a wide variety of colors using just this small palate, though intentional exploitation of the interference patterns between the two banks, and by placing different colored pixels next to each other, which would effectively generate the perception of a third color.

In any event, while I would have liked to have used hires graphics for my game of life, Apple ][+ computers came stock with 48k (yes, kilobytes, or 49152 bytes, which had to contain the full OS plus the userspace program), though mine had an expensive 16k expander to bring it to an amazing 64k of RAM. Thus, the memory constraints, along with the performance constraints from running an interpreted Basic language on a computer with a 1Mhz clock speed meant that I targeted lowres graphics. Alas.

But....it worked. And it was cool. It was very slow, of course, but that is just how it was. The pixels would change color as we moved through the generations, but it probably took 15 or 20 seconds to run each generation!

Living In The Future!

Fast forward to today. I was thinking about Conway's Game of Life, and I wondered how long it would take for me to implement a version of the game that I was happy with today, using a modern language. I also wondered if I could cheat on that.

I have been previewing GitHub's Copilot, and I wondered if Copilot could write a working Life implementation, with only minimal guidance from me. For those who are not aware, copilot is a Generative Pretrained Transformer, which is a type of AI which, when trained on a large body of inputs, can be used to generate new output based on a new partial input.

Basically, when given an input of some sort, it guesses at what will come next. GitHub/Microsoft trained one of these models on the public code stored with GitHub, and released this as Copilot. It provides code completions, where it generates code based on either the prompt that it is given, in a comment or series of comments, or on the context of the other code around it. In this way it is a little bit like pair programming with someone who is both very smart and very stupid at the same time.

To start this timed attempt at writing an implementation of Conway's Game of Life, I chose to use Ruby, both because I know it very well, and because it is one of the languages that Copilot has very good support for. I wrote the following comment as a prompt to get started, and then I asked Copilot to generate code based on that prompt.

# I want to write a Ruby script to play Conway's Game of Life with a random board, in a terminal.
Enter fullscreen mode Exit fullscreen mode

Copilot generates, by default, 10 different multiline completions, since they sometimes vary quite substantially, allowing one to see a number of different options. For Life, several of the options seemed to be both complete-enough and correct-enough that they night run. I picked the one that started like this:

class Life
  attr_reader :board, :size

  def initialize(size)
    @size = size
    @board = Array.new(size) { Array.new(size) }
  end

  def randomize
    @board.each_index do |y|
      @board[y].each_index do |x|
        @board[y][x] = rand(2) == 0 ? false : true
      end
    end
  end

  def tick
    new_board = Array.new(@size) { Array.new(@size) }
      @board.each_index do |y|
        @board[y].each_index do |x|
        neighbors = 0
        (-1..1).each do |dy|
          (-1..1).each do |dx|
            if @board[(y + dy) % @size][(x + dx) % @size]
              neighbors += 1
            end
          end
        end
Enter fullscreen mode Exit fullscreen mode

On pasting the entire program, though, it failed. A little debugging revealed that Copilot had written a method to display the board that looked like this:

def print
  @board.each_index do |y|
    @board[y].each_index do |x|
      print "#{@board[y][x] ? '*' : ' '}"
    end
    puts
  end
end
Enter fullscreen mode Exit fullscreen mode

Ruby has a #print method, which outputs what it is passed, without appending a newline the way that #puts does. However, this code defined a new Life#print method. The problem there is that the new method supersedes the default #print method for any code running in a Life object's instance methods. This meant that the line in there to print the element of the board would not work, as it would just call into our own #print method, instead.

This was easy to fix. I just renamed that method to print_board.

Victory! I ran it, and....

The board never changed.

Hmmm. Copilot wrote a method, #tick, that would calculate the next generation, reading from the current board, and building a new board to replace it. It, however, failed to assign that new board to the instance variable that held the current board. Easy fix:

  @board = new_board
Enter fullscreen mode Exit fullscreen mode

Interestingly, on positioning my cursor where I wanted to insert that line, that exact code was the completion that Copilot suggested. The multiline completion lacked the line, but within the context of the rest of the generated code, Copilot was able to suggest the correct missing code.

It was done now, right?

No. Not quite. On rerunning the program, the board still wasn't changing.

As it turned out, in the same way that Wikipedia's description of the rules of Life are a bit obtuse, and a lot of humans struggle to understand it on the first read-through, so true did Copilot's Generative Transformer struggle to get those rules just right.

It's understanding of the rules looked like this:

if @board[y][x] && neighbors < 2
  new_board[y][x] = false
elsif !@board[y][x] && neighbors == 3
  new_board[y][x] = true
else
  new_board[y][x] = @board[y][x]
end
Enter fullscreen mode Exit fullscreen mode

It's so close to correct a correct implementation. There is a clause there to causes a cell to die from loneliness, but what is missing is a clause to let it die from overcrowding.

This was fun, thouogh, is that I put my cursor after the first clause and hit enter:

if @board[y][x] && neighbors < 2
  new_board[y][x] = false

elsif !@board[y][x] && neighbors == 3
  new_board[y][x] = true
else
  new_board[y][x] = @board[y][x]
end
Enter fullscreen mode Exit fullscreen mode

And a second later, there it was:

if @board[y][x] && neighbors < 2
  new_board[y][x] = false
elsif @board[y][x] && neighbors > 3
  new_board[y][x] = false
elsif !@board[y][x] && neighbors == 3
  new_board[y][x] = true
else
  new_board[y][x] = @board[y][x]
end
Enter fullscreen mode Exit fullscreen mode

Copilot fixed its own mistake.

I also made two additional changes. I asked copilot to write a method to clear the screen:

def clear_screen
  puts "\e[H\e[2J"
end
Enter fullscreen mode Exit fullscreen mode

And I added the colorize gem, and created a #print_block method for the board printing method to use to print a colored block, since we don't have easy access to Apple ][ style Lowres graphics.

def print_block(x, y)
  print @board[y][x] ? " ".black.on_white : " "
end

def print_board
  clear_screen
  @board.each_index do |y|
    @board[y].each_index do |x|
      print_block(x, y)
    end
    puts
  end
end
Enter fullscreen mode Exit fullscreen mode

All in all, I spent about 15 minutes, with Copilot's help, and I had a working Life implementation, with pseudo-graphical output. The implementation has a lot of room for improvement, and it might be interesting to compare this implementation to the timed one that I did myself, to see how long it would take to produce something that I was happy with as an implementation myself. That's another blog post for another day, though.

Github Copilot shows the potential for this technology to provide some powerful assistance to developers in the future, and I am curious to see where this exploration takes us.

The complete code for this version of Conway's Game of Life can be found on Github at https://github.com/wyhaines/relicans-gpt-life.

Discussion (0)