02 Nov 2016

Snake on the BBC micro:bit

The BBC, in their first digital literacy project since the 1980's, recently gave Year 7 students (aged 11-12) in the UK a tiny single-board computer called the micro:bit. It has a 5x5 grid of LEDs, two buttons, plenty of sensors, and not much memory (16K) or processing power (16 MHz). Thankfully, that's still plenty enough computer to have some fun with.

Playing snake on the micro:bit
Figure 1: Playing snake on the micro:bit

To me, the 5x5 LEDs looked like they might display a small game of Snake. All I had to do was figure out the input. I originally used the accelerometer to control the snake by tilting the device, but it lacked the speed and precision of a good Snake game. What I really wanted was a way to move the snake in four directions using the two A/B buttons.

If you know binary, you'll know that two on/off values gives us four possible combinations. Of course, one of the combinations is off/off meaning no buttons are pressed. For this case, I decided it was most natural to have the snake constantly move 'down' until a button is pressed: A goes left, B right, and A+B up.

Controls

Direction Button A Button B  
Left X   Hold A
Right   X Hold B
Up X X Hold A+B
Down     Press nothing

Video

Despite a slightly odd control layout and tiny display, I found Snake on the micro:bit to be surprisingly playable. As proof, here's a video of me playing a full game. It's converted from a GIF so the timing looks a bit odd. In reality the movement is nice and smooth.

Source code

The game clocks in at around 120 lines of Python. To read, I suggest you start with the 'main game loop' at the end of the file.

from microbit import *
from random import randrange


class Snake():
    def __init__(self):
        self.length = 2
        self.direction = "down"
        self.head = (2, 2)
        self.tail = []

    def move(self):
        # extend tail
        self.tail.append(self.head)

        # check snake size
        if len(self.tail) > self.length - 1:
            self.tail = self.tail[-(self.length - 1):]

        if self.direction == "left":
            self.head = ((self.head[0] - 1) % 5, self.head[1])
        elif self.direction == "right":
            self.head = ((self.head[0] + 1) % 5, self.head[1])
        elif self.direction == "up":
            self.head = (self.head[0], (self.head[1] - 1) % 5)
        elif self.direction == "down":
            self.head = (self.head[0], (self.head[1] + 1) % 5)

    def grow(self):
        self.length += 1

    def collides_with(self, position):
        return position == self.head or position in self.tail

    def draw(self):
        # draw head
        display.set_pixel(self.head[0], self.head[1], 9)

        # draw tail
        brightness = 8
        for dot in reversed(self.tail):
            display.set_pixel(dot[0], dot[1], brightness)
            brightness = max(brightness - 1, 5)


class Fruit():
    def __init__(self):
        # place in a random position on the screen
        self.position = (randrange(0, 5), randrange(0, 5))

    def draw(self):
        display.set_pixel(self.position[0], self.position[1], 9)


class Game():
    def __init__(self):
        self.player = Snake()
        self.place_fruit()

    def place_fruit(self):
        while True:
            self.fruit = Fruit()
            # check it's in a free space on the screen
            if not self.player.collides_with(self.fruit.position):
                break

    def handle_input(self):
        # change direction? (no reversing)
        if button_a.is_pressed() and button_b.is_pressed():
            if self.player.direction != "down":
                self.player.direction = "up"
        elif button_a.is_pressed():
            if self.player.direction != "right":
                self.player.direction = "left"
        elif button_b.is_pressed():
            if self.player.direction != "left":
                self.player.direction = "right"
        else:
            if self.player.direction != "up":
                self.player.direction = "down"

    def update(self):
        # move snake
        self.player.move()

        # game over?
        if self.player.head in self.player.tail:
            self.game_over()

        # nom nom nom
        elif self.player.head == self.fruit.position:
            self.player.grow()

            # space for more fruit?
            if self.player.length < 5 * 5:
                self.place_fruit()
            else:
                self.game_over()

    def score(self):
        return self.player.length - 2

    def game_over(self):
        display.scroll("Score: %s" % self.score())
        reset()

    def draw(self):
        display.clear()
        self.player.draw()
        self.fruit.draw()


game = Game()

# main game loop
while True:
    game.handle_input()
    game.update()
    game.draw()
    sleep(500)

Installation

You can use the Mu editor to flash the above code to your micro:bit.