Skip to content

252. Snake!

December 11, 2015

This is the old snake game, as shown below. Joao_Lopes posted his version on the Codea forum, but got stuck on making the snake longer when it ate food. So, Joao, this is for you.

If you are fairly new to Codea, this may help you understand things like tables a little better.

How the Snake game works

You use a joystick (up/down/left/right) to steer a snake (green squares) around the screen, trying to touch the food (red square). Each time you touch the food, your length increases by 1, and new food appears. If you go off the edge of the screen, you lose.

This used to be a popular game on old “feature” phones, you know, those really old ones with about 10 pixels on the screen, because this game didn’t use many pixels. In fact, the snake was made up of 1 pixel squares.

If you did that with your iPad, you wouldn’t see the snake, it would be so tiny, so we have to make it bigger. We’ll pretend the iPad screen is made up of little squares, each 20 pixels wide. The snake and food will also be squares which are 20 pixels wide.

Follow the leader

Now, when you touch the joystick controls, the head of the snake needs to turn left, right, up or down, and the rest of the snake will follow.

snake1So suppose the snake is 4 squares long, and is going from right to left. Then we turn down.

The picture shows how the first square goes down and is followed by the other squares. But the other squares don’t all start going down immediately – they follow exactly the same path as the first square, so the last square (number 4 in the picture) goes left 3 squares before it starts going down.

How do we do this? How do we get the squares to remember the path that the first square took?

The answer is amazingly easy, and it’s probably why this game was made originally, because it is so simple.

If you want to test yourself, have a think about it before reading the answer below.

Our program will have a list of the positions for the squares of the snake. So suppose the list for the picture above looks like this, before we turn down the screen:

(200,100), (220,100), (240,100), (260,100)

You can see the x values of the squares are 20 pixels apart, so they are in a row next to each other (as you see above).

Now we turn down. The list for the second picture above will look like this. The first square has gone down 20 pixels and the other squares have moved left

(200,80), (200,100), (220,100), (240,100)

and the list for the third picture will look like this (the second square has now started going down)

(200,60), (200,80), (200,100), (220,100)

Can you see the pattern?

The first item in each list is the new position of the head of the snake.

To get the positions of the rest of the snake, you take the list for the previous move, and move them all one to the left, because the second square is moving to where the first square was, the third square moves to where the second square was, and so on.

So if you have a four-square snake with a list of positions (a,b,c,d), and the snake moves to e, then the list of positions will become (e,a,b,c). This means you add the new position at the front of the list, and delete the last item on the list.

This does exactly what we need.

And it gets better, because there is an easy solution for our next problem. How do we add a square to the tail of the snake if it eats some food? Should it go above, below, left or right of the end of the snake? The obvious answer is that it should go where the tail of the snake has just been, ie the empty square where it was one move ago. And this is incredibly easy to do.

When we are adding a new position to our list, we look at whether we ate food, and if we did, we don’t delete the last item on the list (but if we didn’t eat food, we do delete it). This effectively adds a new item on the end, exactly where the tail of the snake was one move ago. Perfect.

Adjusting the speed

If we moved the snake by one square (20 pixels) every frame, it would get across the whole screen in about a second, which is way too fast. We need to slow it down.

We can do this by using a counter that increases by 1 at each frame. Then, every (say) 10th frame, we move the snake.

Adjusting every 10th frame means the picture only changes 6 times a second, and it will look jumpy. I’ll fix this below, but first, let’s program it like this.

Here is my setup function

function setup()
    speed=20 --a bigger number makes it slower and easier
    size=20 --size of each squares (pixels)
    --start our snake in the middle of the screen
    -- // is integer division, ie WIDTH/2//size=math.floor(WIDTH/2)
    --we do this because the snake must be exactly on a square
    start=vec2(WIDTH/2//size,HEIGHT/2//size)*size 
    S={start} --table holding snake squares
    --JOYSTICK SETUP CODE GOES HERE (we'll look at this later)
    AddFood() --add a food item
    --set the direction we are moving (nothing at first)
    counter=0
end

And here is my draw function. When you start out programming, you will probably have most of your code in the draw function, and it starts getting very messy. It’s a good idea to put the code into separate functions, making it easier to debug.

function draw()
    background(150, 192, 209, 255)
    DrawSnake()
    DrawFood()
    DrawJoyStick()
end

Here is the DrawSnake code – this is the important function. See the numbered notes underneath

function DrawSnake()
    --update snake position
    counter=counter+1
    if counter%speed==0 then  --[1]
        local p=S[1]+direction*size  --[2]
        if p.x<0 or p.x>WIDTH or p.y<0 or p.y>HEIGHT then  --[3]
            --LOSE GAME - NOT PROGRAMMED
        end
        table.insert(S,1,p) --[4]
        --eat food
        if S[1]:dist(food)<0.1 then --[5]
            --UPDATE SCORE -- NOT PROGRAMMED
            AddFood() --create new food somewhere else
        else
            table.remove(S,#S) --[6]
        end
    end
    
    pushStyle() --[7]
    fill(75, 140, 72, 255)
    for i=1,#S do  --[8]
        rect(S[i].x,S[i].y,size)
    end
    popStyle()
end

[1] counter%speed = remainder of counter/speed. It will be 0 if counter is an exact multiple of speed, so if speed=10, then this will give an answer of 0 when counter =10, 20, 30,….  So this is an easy way of doing something every 10 frames.

[2] I haven’t shown you this yet, but touching the joystick gives you a direction vector, Left is (-1,0), right is (1,0), up is (0,1) and down is (0,-1). We multiply this by the size of our squares, and add it to the position of the first item in our snake list.

[3] if we go off the edge off the screen, we lose. I didn’t program what happens if you lose.

[4] if we are still on the screen, we insert our new position p into the first place in our snake table.

[5] if we are very close to the position of the food (the dist function calculates distance in pixels between two points), then we ate the food. I didn’t program the score, but I add new food somewhere else.

[6] I delete the last item in the snake table, but only if we didn’t eat food (as explained above).

[7] whenever I change colours, I put pushStyle() first, and popStyle() when I’ve finished. This makes Codea save the previous settings and put them back afterwards.

[8] now I draw the squares of the snake using a for loop (#S is the number of items in the table S)

Below is my function for adding food. I’ve included it to show how I can make sure it is not too close to the player, and it is not too close to the joystick controls. As with the snake, the food position needs to be a multiple of the square size (ie 20), so I start by calculating how many squares there are in the width and height, choosing one at random, and doing my checks. If it’s too close to the snake or the joystick, I do it again, and break (ie exit) if everything is OK.

function AddFood()
    while true do --keep looping until we get a position we want
        --positions must be a multiple of the size, so 
        --first figure out how many we can fit into the screen
        local w,h=WIDTH//size,HEIGHT//size
        --choose one that isn't on the edge
        food=vec2(math.random(2,w-1),math.random(2,h-1))*size
        --make sure it is further than 50 pixels from our snake head
        --and also that it is not inside our joystick
        if food:dist(S[1])>50 and food:dist(joyCentre)>joyLength then 
            break 
        end
    end
end

Finally, the joystick code. This code goes in setup. What is all this? I set up a table with 4 items, one for each of the paddle arms. Each of these 4 items is a table, made up of the centre position of that joystick arm, its length and width, and the direction to go in, if that arm is touched.

joyCentre=vec2(WIDTH-140,140) --centre of joystick paddle
joyWidth,joyLength=40,80 --length and width of paddle arms
joyArms={
  {joyCentre-vec2(joyLength,joyWidth)/2,joyLength,joyWidth,vec2(-1,0)},
  {joyCentre+vec2(joyLength,joyWidth)/2,joyLength,joyWidth,vec2(1,0)},
  {joyCentre-vec2(joyWidth,joyLength)/2,joyWidth,joyLength,vec2(0,-1)},
  {joyCentre+vec2(joyWidth,joyLength)/2,joyWidth,joyLength,vec2(0,1)}
 }

The reason I do this here, is that it makes the touch code much simpler.

function touched(t)
    if t.state==ENDED then --if we have finished touching
        for i=1,4 do --check if we touched each arm
            local j=joyArms[i] --just to make the next line shorter
            --check if we touched inside this arm
            if math.abs(t.x-j[1].x)<j[2] and 
               math.abs(t.y-j[1].y)<j[3] then
                  direction=j[4] 
                  break
            end
        end
    end
end

Drawing the joystick is also simple. I’m lazy, and I draw the 4 arms as two thick lines instead of 4 rectangles.

function DrawJoyStick()
    pushStyle()
    stroke(86, 133, 199, 255)
    strokeWidth(joyWidth)
    line(joyCentre.x-joyLength,joyCentre.y,
       joyCentre.x+joyLength,joyCentre.y)
    line(joyCentre.x,joyCentre.y-joyLength,
       joyCentre.x,joyCentre.y+joyLength)
    popStyle()
end

What about the jumpy updating?

So far, our program looks like this. Can we make it less jumpy?

The answer is yes.

Instead of just jumping from one square to the next, we can interpolate, so the movement is smooth. We will still only eat food or change direction every S frames, where S is the speed we have chosen, but we will move the squares in between.

This is easy to do. Imagine we have a 4-square snake with positions (a,b,c,d), and it is going to move to e (it doesn’t matter whether this is in the same direction or not).

In the program above, we would wait for S frames (where S is the speed), and then change the list to (e,a,b,c).

What we will do now, is to add e to the front of the list, and delete the last item. Then when we draw, we use the counter to interpolate. So if the speed is 10 (ie it takes 10 frames to move one square, or 20 pixels), the position of the first snake square will be as follows for the next 3 frames

frame 1 = e*1/10 + a*9/10
frame 2 = e*2/10 + a*8/10
frame 3 = e*3/10 + a*7/10
....
frame 10 = e*10/10 + a*0/10

So we calculate each square’s position by interpolating between its previous position and its next position. It means one small change to our initial table, so instead of S={start}, we have S={start,start}, so we have two positions to interpolate between, even if they are the same at the beginning.

This is the code for drawing the snake

local f=(counter%speed)/speed --fraction
for i=1,#S-1 do
    local x,y=S[i].x*f+S[i+1].x*(1-f),S[i].y*f+S[i+1].y*(1-f)
    rect(x,y,size)
end

Note how my loop stops one before the end of the table S. That’s because I included an extra item in it at the beginning for interpolation. The first snake square interpolates between the first and second item, the second square interpolates between the second and third list items, and the last snake square interpolates between the second to last and last list items.

And when you do all this, you get the result shown in the video at the top of this post.

Here is the final code. I’m not sure the joystick code is working perfectly, but I mainly did this post to show how to program snake, so I’m not going to go back and look at it.

I hope you enjoy it, and maybe try changing it.

 

 

Leave a Comment

Leave a comment