Monday 8 August 2016

Snake

Here's our second game, a version of the classic snake game. A bit of a challenge to fit this onto a 5x5 display with only two buttons but we think we've managed it. Drive the snake around the screen using the A and B buttons to turn left and right. The aim is to eat the food (the flashing pixel) but your snake gets longer each time it eats something. Eventually your snake tail will be so long that you'll crash into it and it's game over!


The code uses a list to store the snakes body. Each element in the list is itself a two element list with two entries: X and Y. Each update we draw the player's current position to the screen, push it to the front of the list and then pop the last position off and clear it. This makes it looks like the snake is slithering across the screen. The list starts out at one element long and each time we eat some food we push another, extra element to it - making the snake's body a little longer.

We use another list of X/Y pairs, called directions, to tell us how to move from the current position to the next, based on player_direction. The variable player_direction is an index into that list, which is four elements long, so player_direction must be an integer between 0 and 3. We manage this using the modulus (written as '%') operator:

        # Change direction if the player pressed a button
        if button_a.was_pressed():
            player_direction = (player_direction + 4 - 1) % 4
        if button_b.was_pressed():
            player_direction = (player_direction + 1) % 4

Here is the complete listing:

"""
 Snake

 Steer the snake aroud the screen, eating food, which makes the snake's body grow longer and 
 longer. Use the A and B buttons to change the snake's direction left and right. Game ends when 
 the snake collides with it's own body.

 This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 
 International License. https://creativecommons.org/licenses/by-nc-sa/4.0/
"""


from microbit import *
import random


def show_titles():
    """
    Show title image and wait for the A button to be pressed
    """
    display.show(Image.SNAKE)
    while not button_a.was_pressed():
        pass


def play_game():
    """
    Play a single round of the game and return the score achieved
    """

    def find_empty_space():
        """
        Finds an empty space on the screen
        """
        while True:
            x = random.randint(0, 4)
            y = random.randint(0, 4)
            if display.get_pixel(x, y) == 0:
                return [x, y]

    # Player's current position. Stored as a pair of x and y values in a list
    player_position = [2, 2]

    # This will hold a list of x/y pairs for each segment of the snake's body
    player_position_stack = []

    # Players direction. Will be a number between 0 and 3
    player_direction = 0

    # How to move in each of the four directions. One of these x/y pairs will be added to the 
    # player's position each frame to make them move
    directions = [
        [0, -1],    # up
        [+1, 0],    # right
        [0, +1],    # down
        [-1, 0],    # left
    ]

    # Clear the display and set the flag to put a piece of food down
    display.clear()
    food_position = None

    # The main game loop. We break out if the snake crashes into it's own body
    while True:
        # Draw player to the screen
        display.set_pixel(player_position[0], player_position[1], 9)

        # Place a new piece of food if the flag is set
        if food_position == None:
            food_position = find_empty_space()
            
        # Flash the food and slow down the game
        for i in range(4):
            display.set_pixel(food_position[0], food_position[1], 3 if i & 1 else 7)
            sleep(100)

        # Push the player's position onto the stack
        player_position_stack.insert(0, list(player_position))

        # Change direction if the player pressed a button
        if button_a.was_pressed():
            player_direction = (player_direction + 4 - 1) % 4
        if button_b.was_pressed():
            player_direction = (player_direction + 1) % 4

        # Move the player forward in the direction they are heading
        direction = directions[player_direction]
        player_position[0] = (player_position[0] + 5 + direction[0]) % 5
        player_position[1] = (player_position[1] + 5 + direction[1]) % 5

        # What value is the pixel that the player is about to move to?
        value = display.get_pixel(player_position[0], player_position[1])
        if value == 9:
            # 9 is the snake's own body colour, so break out of the main game loop
            break
        elif value == 0:
            # 0 is background, so we don't need to do anything
            pass
        else:
            # Anything else must be food
            # Make the stack one element longer so that the snake body grows
            player_position_stack.insert(0, list(player_position))
            # And clear the food position so that another one will be generated

            food_position = None

        # Pop the position off the end of the stack and clear it on the screen
        erase_position = player_position_stack.pop()
        display.set_pixel(erase_position[0], erase_position[1], 0)

    # The score returned is the length of the snake's body
    return len(player_position_stack) - 1


def show_game_over():
    """
    Show a game over screen
    """
    display.show([Image(), Image.SNAKE, Image(), Image.HEART, Image.HEART_SMALL, Image(), Image.SKULL, Image()])


def show_score(score):
    """
    Show score and wait for the A button to be pressed
    """
    # Clear inputs
    button_a.was_pressed()
    
    # Show message and wait
    display.scroll('Score: ' + str(score), wait = False, loop = True)
    while not button_a.was_pressed():
        pass


# Main loop
while True:
    show_titles()
    score = play_game()
    show_game_over()
    show_score(score)


Note that it's not possible to complete the game - if you're good enough then you'll eventually run out of space, there will be nowhere to generate new food and the game will hang. Maybe you'd like to add the code to handle this case yourself? You'll need to ask yourself some questions:
  • What is the maximum possible score?
  • What does completing the game mean - is it a certain score, a certain length of snake or is it when the screen is full?
  • How do you tell if the player has completed the game?
  • What will you do when the game is complete?
Here's the code (and the hex file) if you'd like to tinker:
snake.py
snake.hex


No comments:

Post a Comment