Adam Richardson's Site

PICO-8 Snake

snake_1.gif

Figure 1: Snake implemented in PICO-8

I chose to make an implementation of Snake because I had never done it before and I wanted to understand how the slither algorithm worked. My gut told me that it had to be very straight forward since my first exposure to the game was on my TI-86 graphing calculator in high school. So my first task was to try to determine what the data structures for the game should be.

I wanted to keep the overall design very simplistic so I set the rule that as the snake eats one apple it grows by one cell. I also setup the game state to be in a grid that is made up of snake cells and apples. In the case of the head of the snake the renderer draws it in a brighter green color. I used a linked list structure for the snake. After each game tick the direction that the previous snake cell moved is passed to the next cell.

One of the common pitfalls with the Snake game revolves around how you find the random location for the apple once the snake has eaten the current apple. A naive approach will just pick a random grid cell, if it is not empty pick another one. The problem with this method is that as the snake grows larger the number of collisions will grow. An unbounded search will be vulnerable to a stack overflow. Additionally a perfect game of snake would end with the snake occupying all of the cells in the grid. My solution to this problem is to exhaustively build an array that has only the indices of the empty cells, then pick a random index from that array.

For the finished game I ended up keeping the game speed fixed. I originally was going to have the speed be adjustable based on how long the snake got. After play testing the game I found the first speed to be fast enough. The game keeps track of the amount of frames that have elapsed and if it is greater than the tick amount it resets the frame count and advances the world. Initially I was only polling the input when the world was advancing. This created a very narrow window for the player to input their actions. So instead of doing this I opted to store the user input in a global and poll the controller every frame. Snake needs to be as responsive as possible! You can play it here.

pico-8 cartridge // http://www.pico-8.com
version 27
__lua__
-- SNAKE
-- BY ADAM RICHARDSON

function _init()
   size=4
   cols =128/size
   snake_v=1
   apple_v=2
   dir_n=1
   dir_s=2
   dir_e=3
   dir_w=4
   starting_len=3
   grid={}
   update_ticks=6
   ticks=0
   score=0
   head=snake_cell()
   is_gameover=false
   next_dir=0
   apple_idx=0
   for i=1,(cols*cols) do
      add(grid,0)
   end
   setup()
end

function snake_cell()
   return {
      x=0,
      y=0,
      n=nil,
      d=0,
   }
end

function idx_xy(x,y)
   return x+(y*cols)
end

function setup()
   for i=1,(cols*cols) do
      grid[i]=0
   end
   next_dir=dir_e
   head.x=flr(rnd(cols/2))+starting_len
   head.y=flr(rnd(cols/2))+starting_len
   head.d=next_dir
   head.n=nil
   for i=1,starting_len do
      grow_snake()
   end

   local cell=head
   while cell do
      grid[idx_xy(cell.x,cell.y)]=snake_v
      cell=cell.n
   end

   new_apple()

   is_gameover=false
   ticks=0
   score=0
end

function grow_snake()
   local cell=head
   while cell.n do
      cell=cell.n
   end

   local grow=snake_cell()
   grow.x=cell.x
   grow.y=cell.y
   grow.n=nil
   grow.d=cell.d
   cell.n=grow
   local growpos={
      {x=grow.x,y=grow.y+1},
      {x=grow.x,y=grow.y-1},
      {x=grow.x-1,y=grow.y},
      {x=grow.x+1,y=grow.y}
   }
   local xy=growpos[grow.d]
   grow.x=xy.x
   grow.y=xy.y
end

function new_apple()
   local check_grid={}
   local ncheck=0
   for i=1,(cols*cols) do
      if grid[i] != snake_v then
         add(check_grid,i)
         ncheck += 1
      end
   end
   local idx=flr(rnd(ncheck-1))+1
   apple_idx=check_grid[idx]
   grid[apple_idx] = apple_v
end

function gameover()
   is_gameover=true
   sfx(1)
end

function handle_input(d)
   local new_dir=d
   if btn(⬅️) then
      if d!=dir_e and d!=dir_w then
         new_dir=dir_w
      end
   end

   if btn(➡️) then
      if d!=dir_e and d!=dir_w then
         new_dir=dir_e
      end
   end

   if btn(⬆️) then
      if d!=dir_n and d!=dir_s then
         new_dir=dir_n
      end
   end

   if btn(⬇️) then
      if d!=dir_n and d!=dir_s then
         new_dir=dir_s
      end
   end
   return new_dir
end

function should_wait()
   ticks+=1
   if ticks!=update_ticks then
      return true
   end
   ticks=0

   return false
end

function _update60()
   if is_gameover then
      if btn(❎) then
         setup()
      end
      return
   end

   next_dir = handle_input(next_dir)
   if should_wait() then
      return
   end

   local cell=head
   local d=next_dir
   while cell do
      grid[idx_xy(cell.x,cell.y)]=0
      local last_dir=cell.d
      local cellpos={
         {x=cell.x,y=cell.y-1},
         {x=cell.x,y=cell.y+1},
         {x=cell.x+1,y=cell.y},
         {x=cell.x-1,y=cell.y},
      }
      local xy=cellpos[d]
      cell.x=xy.x
      cell.y=xy.y
      cell.d=d
      if cell.x >= cols or cell.x < 0 or
         cell.y >= cols or cell.y < 0 then
         gameover()
         return
      end

      if grid[idx_xy(cell.x,cell.y)] == snake_v then
         gameover()
         return
      end

      grid[idx_xy(cell.x,cell.y)]=snake_v
      cell=cell.n
      d=last_dir
   end

   if idx_xy(head.x,head.y) == apple_idx then
      new_apple()
      grow_snake()
      sfx(0)
      score += 1
   end
end

function _draw()
   cls(3)
   map(0,0,0,0,128,64)
   for i=0,cols-1 do
      for j=0,cols-1 do
         local v=grid[idx_xy(i,j)]
         if v==snake_v then
            local c=1
            if i==head.x and j==head.y then
               c=11
            end
            rectfill(
               i*size,
               j*size,
               i*size+size-1,
               j*size+size-1,
               c)
         elseif v==apple_v then
            rectfill(
               i*size,
               j*size,
               i*size+size-1,
               j*size+size-1,
               8)
         end
      end
   end

   print(score,7)
   if is_gameover then
      print("game over",7)
      print("press ❎ to restart",7)
   end
end