Microbit Programming: The Development of a Snake Eating Apple Game and AI (Version 1 – Snake Does Not Grow)


Last week, we bring the intelligence to Microbit (Microbit Programming: Introduction to AI – Letting Computer Play the Game), letting it play the apple-catching game. This week, we’ll design a classic snake-eating-apple game by using the two Sprites: the Snake and the Apple.

The classic snake game has four control buttons to let the snake going into four directions (up, left, right, and down). However, on Microbit, there are only 2 buttons (A and B). But if you think carefully, we only need two buttons to control the snake. The snake can turn left, or turn right, as corresponding to button A and B. If no buttons are pressed, the snake will keep moving towards its direction.

To make the game simpler, we now define the snake only having a pixel which can be easily represented using a Sprite (more specifically, the ledSprite). And the snake does not grow after eating apples. These assumptions help to simply the game a little bit.

Global Variables (Game Elements)

Let’s define a few variables:

1
2
3
4
5
6
7
let direction = 0;
let dxOffset = [[1, 0], [0, 1], [-1, 0], [0, -1]];
let score = 0;
let px = 0;
let py = 0;
let snake = game.createSprite(px, py);
let apple = game.createSprite(2, 2);
let direction = 0;
let dxOffset = [[1, 0], [0, 1], [-1, 0], [0, -1]];
let score = 0;
let px = 0;
let py = 0;
let snake = game.createSprite(px, py);
let apple = game.createSprite(2, 2);

The direction variable stores the direction index – only four possibilities:

  • 0: Right
  • 1: Down
  • 2: Left
  • 3: Up

The directions are defined as clock-wise (however, you can also make it anti-clockwise). To apply the direction change (e.g. turn left), we need to know the direction offsets, which is an array that contains four sets of coordinate offsets – both X and Y.

For example, when direction = 0, which is right. The dxOffset[direction] will be [1, 0] that means next pixel will be X+1 and Y+0, that is the right pixel next to it.

We define the score to store the number of apples currently the snake has eaten. And coordinate px and py stores where currently the snake is. The snake and apple are two Sprite objects on the Microbit LED.

Placing Next Apple

When the snake eats an apple, we need to place next apple. We can generate two random integers from 0 to 4 (inclusive). However, we need to make sure that the apple should not be placed on the snake body. We can do this using the following Javascript function:

1
2
3
4
5
6
7
8
9
10
function placeNextApple() {
    let x = Math.randomRange(0, 4);
    let y = Math.randomRange(0, 4);
    while (x == snake.x() && y == snake.y()) {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    }
    apple.goTo(x, y);
    apple.setBrightness(100);
}
function placeNextApple() {
    let x = Math.randomRange(0, 4);
    let y = Math.randomRange(0, 4);
    while (x == snake.x() && y == snake.y()) {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    }
    apple.goTo(x, y);
    apple.setBrightness(100);
}

Here, when we generate random numbers in x and y, we make the apple go to it by using the Sprite.goTo() method. In order to distinguish between the snake and apple pixel, we change the brightness of the apple pixel to 100 (from 0 to 255, where 255 is the brightest).

As you can see above that we first generate x and y, and use a while loop when it happens to be on the snake. And there is a bit of code duplication for example, generating x as a random integer appears twice. We can also use the do-while loop, to rewrite the above logics – which is concise as we the duplication is eliminated.

1
2
3
4
5
6
7
8
9
function placeNextApple() {
    let x, y;
    do {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    } while ((x == snake.x()) && (y == snake.y()))
    apple.goTo(x, y);
    apple.setBrightness(100);
}
function placeNextApple() {
    let x, y;
    do {
        x = Math.randomRange(0, 4);
        y = Math.randomRange(0, 4);
    } while ((x == snake.x()) && (y == snake.y()))
    apple.goTo(x, y);
    apple.setBrightness(100);
}

We can call placeNextApple function in the game initialization part – which is right after apple Sprite is created.

Snake’s three actions

As a snake, it can do three things at the current state: Do Nothing (moving forward), Turn Left, or Turn Right. To turn right, we need to change the direction variable – moving it next to it – which is the clockwise right direction.

1
2
3
function turnRight() {
    direction = (direction + 1) % 4;
}
function turnRight() {
    direction = (direction + 1) % 4;
}

The modulous operator make sure that it goes round and round. For example, when direction = 3 (UP), its right direction is 0 which is RIGHT. Similarly, here is the Javascript function to make the snake turn left.

1
2
3
function turnLeft() {
    direction = (direction + 3) % 4;
}
function turnLeft() {
    direction = (direction + 3) % 4;
}

Game Functions: Game Over and Reset Game

Here are two helper functions to end a game (when the snake moves out of the LED screen)

1
2
3
4
5
6
function gameOver() {
    game.setScore(score);
    game.pause();
    basic.pause(1000);
    game.gameOver();
}
function gameOver() {
    game.setScore(score);
    game.pause();
    basic.pause(1000);
    game.gameOver();
}

and reset a game (set score to zero etc).

1
2
3
4
5
6
7
8
9
10
function resetGame() {
    game.setScore(0);
    score = 0;
    direction = 0;    
    px = 0;
    py = 0;
    snake.goTo(px, py);
    placeNextApple();
    game.resume();
}
function resetGame() {
    game.setScore(0);
    score = 0;
    direction = 0;    
    px = 0;
    py = 0;
    snake.goTo(px, py);
    placeNextApple();
    game.resume();
}

Moving Forward

Moving forward means we need to apply the direction offsets to the current coordinate px and py.

1
2
3
4
5
6
7
8
function moveForward() {
    let dx = dxOffset[direction];
    px += dx[0];
    py += dx[1];
    if (!validPixelCoordinate(px, py)) {
        gameOver();
    }
}
function moveForward() {
    let dx = dxOffset[direction];
    px += dx[0];
    py += dx[1];
    if (!validPixelCoordinate(px, py)) {
        gameOver();
    }
}

Here, we use a function to check if the next position is out of the LED:

1
2
3
function validPixelCoordinate(nx: Number, ny: Number): boolean {
    return (nx >= 0 && nx <= 4 && ny >= 0 && ny <= 4);
}
function validPixelCoordinate(nx: Number, ny: Number): boolean {
    return (nx >= 0 && nx <= 4 && ny >= 0 && ny <= 4);
}

The coordinates nx and ny are only valid when they are in the range of [0, 4] inclusive.

Game Controls

This seems the easiest part. A for turn left, B for turn right. and AB together for reset game.

1
2
3
4
5
6
7
8
9
10
11
input.onButtonPressed(Button.A, function () {
    turnLeft();
})
 
input.onButtonPressed(Button.B, function () {
    turnRight();
})
 
input.onButtonPressed(Button.AB, function () {
    resetGame();
})
input.onButtonPressed(Button.A, function () {
    turnLeft();
})

input.onButtonPressed(Button.B, function () {
    turnRight();
})

input.onButtonPressed(Button.AB, function () {
    resetGame();
})

These three functions are defined above: turnLeft, turnRight and resetGame.

Main Game Loop

Putting all these pieces together. To make the snake move faster and faster, we reduce the delay and setting a threshold to minimal 100 ms.

1
2
3
4
5
6
7
8
9
10
11
12
13
basic.forever(function () {
    if (game.isGameOver()) {
        return;
    }
    let delay = Math.max(100, 1000 - score * 50);
    basic.pause(delay);
    moveForward();
    snake.goTo(px, py);
    if (snake.isTouching(apple)) {
        score++;
        placeNextApple();
    }
})
basic.forever(function () {
    if (game.isGameOver()) {
        return;
    }
    let delay = Math.max(100, 1000 - score * 50);
    basic.pause(delay);
    moveForward();
    snake.goTo(px, py);
    if (snake.isTouching(apple)) {
        score++;
        placeNextApple();
    }
})

If the snake hits (eats) apple (via isTouching method), we increment the score and place next apple.

The Snake Game (version 1) and Simulator on Microbit: https://makecode.microbit.org/_2eE1EseWyFs5

Playing Game with AI

We let the computer play this snake game on Microbit. Computer will never makes mistake or get tired. The AI strategy is easy for snake to make a decision. It just needs to compute the cost for three actions: do nothing, turn left, or turn right.

The cost function can be estimated as the new distance to the apple. And it can be calculated via the sum of delta X and delta Y offsets.

1
let dist = Math.abs(nextX - apple.x()) + Math.abs(nextY - apple.y());
let dist = Math.abs(nextX - apple.x()) + Math.abs(nextY - apple.y());

We also need to make sure the next pixel is valid. If it is not valid, the cost will be simply set to a very large number e.g. 9999 so that it will not get picked. The strategy is to pick the action with the minimal cost. And at least one action is valid e.g. when the sake is in the corner, 2 actions will be invalid making the snake out of the LED screen.

Let’s calculate the costs for three actions and pick the smallest cost, which is known as the Greedy Algorithm.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function letComputerPlay() {
    let x = snake.x();
    let y = snake.y();
 
    // next distance to apple if moving forward
    let dx = dxOffset[direction];
    let nx1 = x + dx[0];
    let ny1 = y + dx[1];
    let dist1 = 9999;
    if (validPixelCoordinate(nx1, ny1)) {
        dist1 = Math.abs(nx1 - apple.x()) + Math.abs(ny1 - apple.y());
    }
 
    // next distance to apple if turning right
    let dx1 = (direction + 1) % 4;
    dx = dxOffset[dx1];
    let nx2 = x + dx[0];
    let ny2 = y + dx[1];
    let dist2 = 9999;
    if (validPixelCoordinate(nx2, ny2)) {
        dist2 = Math.abs(nx2 - apple.x()) + Math.abs(ny2 - apple.y());
    }
 
    // next distance to apple if turning left
    let dx2 = (direction + 3) % 4;
    dx = dxOffset[dx2];
    let nx3 = x + dx[0];
    let ny3 = y + dx[1];
    let dist3 = 9999;
    if (validPixelCoordinate(nx3, ny3)) {
        dist3 = Math.abs(nx3 - apple.x()) + Math.abs(ny3 - apple.y());
    }
 
    if (dist1 <= dist2 && dist1 <= dist3) {
        // best strategy is moving forward without turning
        return;
    } else if (dist2 <= dist1 && dist2 <= dist3) {
        turnRight();
    } else if (dist3 <= dist1 && dist3 <= dist2) {
        turnLeft();
    }
}
function letComputerPlay() {
    let x = snake.x();
    let y = snake.y();

    // next distance to apple if moving forward
    let dx = dxOffset[direction];
    let nx1 = x + dx[0];
    let ny1 = y + dx[1];
    let dist1 = 9999;
    if (validPixelCoordinate(nx1, ny1)) {
        dist1 = Math.abs(nx1 - apple.x()) + Math.abs(ny1 - apple.y());
    }

    // next distance to apple if turning right
    let dx1 = (direction + 1) % 4;
    dx = dxOffset[dx1];
    let nx2 = x + dx[0];
    let ny2 = y + dx[1];
    let dist2 = 9999;
    if (validPixelCoordinate(nx2, ny2)) {
        dist2 = Math.abs(nx2 - apple.x()) + Math.abs(ny2 - apple.y());
    }

    // next distance to apple if turning left
    let dx2 = (direction + 3) % 4;
    dx = dxOffset[dx2];
    let nx3 = x + dx[0];
    let ny3 = y + dx[1];
    let dist3 = 9999;
    if (validPixelCoordinate(nx3, ny3)) {
        dist3 = Math.abs(nx3 - apple.x()) + Math.abs(ny3 - apple.y());
    }

    if (dist1 <= dist2 && dist1 <= dist3) {
        // best strategy is moving forward without turning
        return;
    } else if (dist2 <= dist1 && dist2 <= dist3) {
        turnRight();
    } else if (dist3 <= dist1 && dist3 <= dist2) {
        turnLeft();
    }
}

Then, we just need to plug-in the letComputerPlay method in the main game loop.

The snake game with AI and the Microbit simulator: https://makecode.microbit.org/_7CJeJrEMv3Ts

Stay right there, next week, we’ll implement the snake that grows after eating apples. It is going to be much fun!

Below is the video of Microbit playing the Snake game. It is very good at it. It keeps eating, never gets tired…

Play Snake Game

Want to play the snake game? Here are two good options:

–EOF (The Ultimate Computing & Technology Blog) —

GD Star Rating
loading...
1721 words
Last Post: Compute the Angle of the Hour and Minute Hand on a Clock
Next Post: Bruteforce or Line Sweep Algorithms to Remove Covered Intervals

The Permanent URL is: Microbit Programming: The Development of a Snake Eating Apple Game and AI (Version 1 – Snake Does Not Grow)

Leave a Reply