Adam Richardson's Site

Conway's Game of Life in PICO-8

cogl_0.gif

Figure 1: Conway's Game of Life implemented in PICO-8

For my next PICO-8 project I wanted to implement Conway's Game of Life. I chose this project since I felt it would push me to use some of the more advanced memory features of PICO-8. You can check out my implementation here, press the right key to reseed the grid and restart (hold for a cool static effect). The code is accessible here on Github (as well as at the end of this post).

To properly implement Conway's Game of Life you need to store the grid position of each element for the current frame as well as the previous. I wanted to use binary data for this so I consulted the memory map. I determined that I should have enough memory in the General Use RAM (0x4300-0x5DFF) to store the current frame as well as the previous for each pixel in the 128x128 grid. To do this I needed to write a renderer for the data that reads each bit and uses its index to find the x and y coordinate of the pixel.

After initially completing this I found the performance to be very slow (1-2fps). I then refactored the code so I could easily adjust the scale of the grid. The only downside with this is that the size of each rectangle needs to be a power of 2 due to the way the data is stored in memory. Using a 2x2 cell size rather than 1x1 improved performance to around 5fps but it was still too slow for my tastes. I think the sweet spot for PICO-8 I found was 4x4.

cogl_2.gif

Figure 2: Slow 1x1 Conway's Game of Life

cogl_1.gif

Figure 3: Faster but still slow 2x2 Conway's Game of Life

pico-8 cartridge // http://www.pico-8.com
version 23
__lua__
-- CONWAY'S GAME OF LIFE
-- BY ADAM RICHARDSON

function _init()
   gaddr=0X4300
   size=4
   cols=128/size
   gsize=(cols*cols)/8
   laddr=gaddr+gsize
   rndg()
end

function rndg()
   memset(gaddr,0X0,gsize)
   for idx=0,gsize do
      local v=rnd(0Xff)
      poke(gaddr+idx,v)
      poke(laddr+idx,v)
   end
   for x=0,cols-1 do
      set_xy(x,0,0)
      set_xy(x,cols-1,0)
      set_xy(0,x,0)
      set_xy(cols-1,x,0)
   end
   lt=t()
end

function state_xy(x,y)
   local idx=(y*cols+x)
   local i=idx/8
   local o=(idx%8)
   local m=shl(1,o)
   local v=peek(laddr+i)

   return band(m,v) > 0
end

function set_xy(x,y,n)
   local idx=(y*cols+x)
   local i=idx/8
   local o=(idx%8)
   local m=shl(1,o)
   local v=peek(gaddr+i)
   if(n==0) then
      m=bnot(m)
      poke(gaddr+(i),band(m,v))
   else
      poke(gaddr+(i),bor(m,v))
   end
end

function score(x,y)
   local s=0
   -- north
   if state_xy(x,y-1) then
      s+=1
   end
   -- north east
   if state_xy(x+1,y-1) then
      s+=1
   end
   -- north west
   if state_xy(x-1,y-1) then
      s+=1
   end
   -- south
   if state_xy(x,y+1) then
      s+=1
   end
   -- south east
   if state_xy(x+1,y+1) then
      s+=1
   end
   -- south west
   if state_xy(x-1,y+1) then
      s+=1
   end
   -- east
   if state_xy(x+1,y) then
      s+=1
   end
   -- west
   if state_xy(x-1,y) then
      s+=1
   end
   return s
end

function _update60()
   if btn(1) then
      rndg()
      return
   end
   for x=1,cols-2 do
      for y=1,cols-2 do
         local s=score(x,y)
         local st=state_xy(x,y)

         if st then
            if s<2 or s>3 then
               set_xy(x,y,0)
            end
         else
            if s==3 then
               set_xy(x,y,1)
            end
         end
      end
   end

   memcpy(laddr, gaddr, gsize)
end

function _draw()
   cls()
   local x=0
   local y=0

   for idx=0,gsize do
      local v=peek(gaddr+idx)
      for o=0,7 do
         local m=shl(1,o)
         if band(m,v) > 0 then
            rectfill(
               x*size,
               y*size,
               x*size+size-1,
               y*size+size-1,
               11)
         end
         x+=1
         if(x==cols) then
            x=0
            y+=1
         end
      end
   end
end