PICO-8 Snake
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