Conway's Game of Life in PICO-8
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.
Figure 2: Slow 1x1 Conway's Game of Life
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