Snake, in 500 characters

In the second week of May 2024, there was a small game jam called TweetTweetJam 9, where participants have to write a game with 500 characters of code or less. Most participants made their entries using PICO-8, a retro-style “fantasy console” with “cartridges” written in Lua. And although I missed the deadline, I did write a small PICO-8 cart for the jam, which you can play online here.

A pixelated Snake game, with a light gray snake moving on a black and dark blue checkered background while collecting orange food.
It's Snake, on the PICO-8. What more did you expect?

This isn’t going to win me any Game of the Year awards anytime soon – it’s just a clone of the classic game Snake – but I think it plays decently.

Here is the entire source code of the game, which ended up being 498 characters.

cls(2)for i=0,41do
for j=0,41do
?"■",i*3+1,j*3,i+j&1
end
end
o=0r=0s=5a=62b=62e=62f=62g=pget
d=pset?"□",61,60,6
::x::repeat
m=rnd(42)\1*3+1n=rnd(42)\1*3until g(m,n+1)<2?"⁙",m,n,9
::_::?"⁶3",0,0
p=btn()q=p*.6&.75if(p>0and(q-r)%1!=.5)r=q
h=cos(r)v=sin(r)a+=h*3b+=v*3w=g(a-h,b-v)<2?"⁵fe□",a,b,6
d(a-h,b-v,0)d(a-h*2,b-v*2)z=g(a,b)d(a,b)if(z>1)d(o,0,8)o+=1s+=3goto x
if s>0do
s-=1else
t=g(e-1,f)-g(e+1,f)u=g(e,f-1)-g(e,f+1)?"⁵fe■",e,f,e\3+f\3&1
d(e+t/3,f+u/3,6)e+=t/2f+=u/2end
if(w)goto _
?"⁶c0★: "..o,8

…okay, perhaps I need to explain what’s going on.

Theorizing

Most clones of Snake use a list or a table to store and update the segments of the snake. This makes sense conceptually, but this can make updating the snake and checking whether to end the game get slower over time.

For this game, I wanted to use a clever technique from this Snake clone for the TI-83+, which only stores the positions of the head and tail of the snake, and uses the pixels on the screen to detect walls, snake segments, and fruits.

Previously, I made a close port of that version of Snake in 1,034 characters, but it wasn’t very optimized. So this time, I started from scratch.

Initialization

Normally, PICO-8 cartridges include three specially named functions called _init, _update, and _draw, which then run in a specific order as the cartridge is running. But that takes up a lot of characters, so I use labels and goto for control flow instead.

The first few lines of code initialize everything I need for the game.

cls(2)for i=0,41do
for j=0,41do
?"■",i*3+1,j*3,i+j&1
end
end
o=0r=0s=5a=62b=62e=62f=62g=pget
d=pset?"□",61,60,6

First, the screen is filled with maroon (color 2) using cls(2). Then, a nested loop prints1 a checkerboard pattern with the character (which renders as a 3×3 pixel filled box), in black (color 0) and dark blue (color 1). The checkerboard pattern should make it easier for the player to see which row and column they’re on in the grid.

Then, a few game variables are set.

Finally, the character (which renders as a 3×3 pixel outlined box) is printed at the snake’s starting position.

Fruit spawning

::x::repeat
m=rnd(42)\1*3+1n=rnd(42)\1*3until g(m,n+1)<2?"⁙",m,n,9

In this part, I generate a random grid cell, and get the color of its top left corner. If it’s black or dark blue, this grid cell is empty, and a fruit can spawn here; if it’s not, I repeat until it is. Then the character (which renders as a 3×3 pixel X shape) is printed in orange (color 9).

This part of the code is given a label, and placed just before the main game loop, so jumping to this label will spawn a fruit. This saves characters over defining a function.

Main loop

The first thing in the main game loop – besides a label, of course – is a weird-looking string that’s printed to the screen:

::_::?"⁶3",0,0

That tiny number 6 is actually character code 6 in PICO-8’s character set, affectionately nicknamed P8SCII. And much like how ASCII control characters were made to cause effects other than printing, P8SCII control codes exist that can do all sorts of non-printing things when printed. In this case, it simply waits 4 frames before continuing.2

p=btn()q=p*.6&.75if(p>0and(q-r)%1!=.5)r=q
h=cos(r)v=sin(r)a+=h*3b+=v*3

This is where the snake moves based on button input. btn() returns a bitfield of the player’s inputs, and pancelor discovered a neat way to convert this bitfield into an angle from 0 to 1 – where 0 is right, 0.25 is up, 0.5 is left, and 0.75 is down. The potential new angle is stored into q.

Storing the snake’s direction as an angle is convenient, because the cos and sin functions can convert an angle to an X and Y offset,3 which I store into h and v.

Another benefit of storing the snake’s direction as an angle is being able to easily test whether the player is going backwards, by checking the difference between the old and new angles. Here, I update the snake’s direction only if the player is pressing any button, and the new angle isn’t exactly half a turn away from the old one.

w=g(a-h,b-v)<2?"⁵fe□",a,b,6
d(a-h,b-v,0)d(a-h*2,b-v*2)z=g(a,b)d(a,b)

Before the snake moves forward, I check the color of the pixel behind where the snake’s head will go. If it’s black or dark blue, then we’re moving into an empty grid cell; otherwise, we’re moving into a wall (or a snake segment), and the game should end. I store the result of this check into w.

Then, the head of the snake is drawn in its new position by printing a character in light gray (color 6). I use some more P8SCII control codes to offset the position, in a way that saves a character over ?"□",a-1,b-2,6.

Once the head is drawn, a line of 3 pixels behind (and including) the head is erased, and I chweck the color of the pixel at the head before being erased and store it into z.

if(z>1)d(o,0,8)o+=1s+=3goto x

If the color is not black or dark blue, then it’s the middle of a fruit. I draw a pixel in red (color 8) at the top of the screen, add 1 point and 3 snake segments,4 and spawn a new fruit.

if s>0do
s-=1else
t=g(e-1,f)-g(e+1,f)u=g(e,f-1)-g(e,f+1)?"⁵fe■",e,f,e\3+f\3&1
d(e+t/3,f+u/3,6)e+=t/2f+=u/2end

Here, I check whether there are any snake segments to add. If there are, I decrease the number by 1 and do nothing; otherwise, I move the tail forward.

The tail segment should move in the direction of whichever pixel around it is empty. The horizontal and vertical offsets are calculated from the color values, and stored into t and u.

Note

Because the snake is in color 6, the calculations for t and u will result in ±6 if the tail should move along that axis, and 0 if it shouldn’t.

To erase the tail, the character is printed at the tail’s old position, in whichever color the underlying checkerboard pattern should be. Then, to close the tail at its new position, a pixel in color 6 is drawn 2 (= 6/3) pixels forward from the tail’s old position. Finally, the tail’s position is moved 3 (= 6/2) pixels forward.

if(w)goto _
?"⁶c0★: "..o,8

If the w check from earlier says we’re moving into an empty grid cell, I jump back to the start of the main game loop; otherwise, the game is over, I clear the screen (with a P8SCII control code), and print the score in red (color 8).

TweetTweetJam 9

While I couldn’t get this game into TweetTweetJam 9, I recommend that you check out the entries submitted by others. My favorite entry is Make Ten by pancelor, because it’s cleverly made, and it’s a very good arcade-style game even with its 500 character limit.

Here’s hoping I can muster up an entry of my own on time next time… 😅


  1. Printing is normally done with the print function, but PICO-8 provides ? as a shorthand operator for printing. It requires a newline afterwards, but for saving characters, this is almost always worth it. 

  2. For P8SCII control codes to work, I don’t need to give the print command any coordinates. But in this case, if a fruit was just printed near the bottom of the screen, printing this would cause the screen to scroll down by one line, which will cause glitches. 

  3. Technically, the sign of PICO-8’s sin command is inverted from what it mathematically should be. However, because the Y coordinate goes up the further you go down on the screen, the inversion is cancelled out. It’s weird, but it works. 

  4. This code actually has the effect of spawning 4 snake segments, because this runs after moving the head forward, but before moving the tail forward. This is a side effect of turning the fruit spawning code into a goto, instead of a function call.