Academy
Snake (Python)
Controls and display

Controls and display

Step 3

Interception of user interactions.
Controlling snake movements.
Displaying snake on the game scene.
Scheduling of game engine tasks.

Control the movement of the snake

The player controls the snake's movements using the META directional PAD, i.e. the LEFT, RIGHT, UP and DOWN buttons:

Gamebuino META

Depending on the button the player pressed, it will be sufficient to call a dirSnake() function to apply a directional control to change the snake's movement.

Commandes directionnelles

This function is very simply defined:

# ----------------------------------------------------------
# Snake management
# ----------------------------------------------------------

def dirSnake(dx, dy):
    snake['vx'] = dx
    snake['vy'] = dy 

dx and dy are directly associated with the snake velocity, since they correspond exactly to instantaneous variations in positions.

There are several ways to intercept the way the player acts on the console buttons. We will use one of them, which simply detects if the player presses a particular button. For example:

buttons.pressed(buttons.LEFT)

returns a Boolean (True or False) depending on whether the LEFT button is pressed or not.

The directional controls can then be easily applied as follows:

# ----------------------------------------------------------
# Game management
# ----------------------------------------------------------

def handleButtons():
    if buttons.pressed(buttons.LEFT):
        dirSnake(-1, 0)
    elif buttons.pressed(buttons.RIGHT):
        dirSnake(1, 0)
    elif buttons.pressed(buttons.UP):
        dirSnake(0, -1)
    elif buttons.pressed(buttons.DOWN):
        dirSnake(0, 1)

elif is the contraction of else if and allows to restart the test on a new condition.

At this stage, the snake is not yet really moving... indeed, it has only been assigned a velocity. It is now necessary to implement its motion by modifying the position of its head according to the velociy that has just been applied to it:

# ----------------------------------------------------------
# Snake management
# ----------------------------------------------------------

def moveSnake():
    h = snake['head']
    x = snake['x'][h]
    y = snake['y'][h]
    h = (h + 1) % snake['len']
    snake['x'][h] = x + snake['vx']
    snake['y'][h] = y + snake['vy']
    snake['head'] = h

The moveSnake() function first retrieves the coordinates of the snake's head from the snake['x'] and snake['y'] lists:

h = snake['head']
x = snake['x'][h]
y = snake['y'][h]

Then it advances the reading head one notch, rotating thanks to the modulo operator %:

h = (h + 1) % snake['len']

The new position of the snake head is then calculated and saved in the lists at the place pointed by the reading head:

snake['x'][h] = x + snake['vx']
snake['y'][h] = y + snake['vy']

Finally, the new position of the reading head is saved:

snake['head'] = h

Display the snake on the game scene

In the previous step, we had left it at the following definition for the draw() function, which is responsible for rendering the game scene:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def draw():
    display.clear(COLOR_BG)
    display.setColor(COLOR_WALL)
    display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)

We will need to define a new global variable for the snake color:

# ----------------------------------------------------------
# Global variables
# ----------------------------------------------------------

COLOR_SNAKE = 0xfd40

And now that the elements on the game scene are starting to multiply, we will also structure the display by creating more specialized functions:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def draw():
    clearScreen()
    drawWalls()

def clearScreen():
    display.clear(COLOR_BG)

def drawWalls():
    display.setColor(COLOR_WALL)
    display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)

We will add a new drawSnake() function that will display the snake on the grid. The easiest way to display the snake is to draw one by one each of its sections:

def drawSnake():
    display.setColor(COLOR_SNAKE)
    n = snake['len']
    for i in range(n):
        x = snake['x'][i]
        y = snake['y'][i]
        display.fillRect(
            OX + x * SNAKE_SIZE,
            OY + y * SNAKE_SIZE,
            SNAKE_SIZE,
            SNAKE_SIZE
        )

To do this, we simply go through the snake['x'] and snake['y'] lists to pick the coordinates of each section and draw a rectangle with the size of a grid cell. However, this is not the fastest way to execute this plot. We will see in the next step how to optimize it.

For now, let's add a call to this new function in our draw() function:

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def draw():
    clearScreen()
    drawWalls()
    drawSnake()

Integration with the scheduler

Now we have to integrate everything we've just done into the main scheduler so that the game finally starts to take shape.

To do this, we will organize the tasks to be performed according to the phases of the game. At the beginning, when the game starts, you have to reset the snake. Then, we start the game and activate the controller in charge of observing the player's behaviour by examining the buttons on the console. The display of the game scene must also be done.

To model these different phases of the game, we will define some global variables:

# ----------------------------------------------------------
# Global variables
# ----------------------------------------------------------

MODE_START = 0
MODE_PLAY  = 1

Then we will define a new dictionary to represent, in a way, the game engine:

# ----------------------------------------------------------
# Initialization
# ----------------------------------------------------------

game = {
    'mode': MODE_START
}

The game engine will therefore manage the different phases of the game.

Now, let's go back to our tick() function, which is responsible for applying the scheduling of the different tasks:

# ----------------------------------------------------------
# Game management
# ----------------------------------------------------------

def tick():
    if game['mode'] == MODE_START:
        resetSnake()
        game['mode'] = MODE_PLAY
    elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()

    draw()

And let's see what it looks like:

Démo

It's working pretty well, isn't it?

Well... okay, wall detection is not yet implemented... nor the fact that the snake can't turn back... because, in that case, it would bite its tail. We will deal with these cases in the next step!

Here is the complete code.py script, which integrates everything we have done so far:

from gamebuino_meta import waitForUpdate, display, color, buttons

# ----------------------------------------------------------
# Global variables
# ----------------------------------------------------------

SCREEN_WIDTH  = 80
SCREEN_HEIGHT = 64
SNAKE_SIZE    = 2
SNAKE_LENGTH  = 4
COLS          = (SCREEN_WIDTH  - 4) // SNAKE_SIZE
ROWS          = (SCREEN_HEIGHT - 4) // SNAKE_SIZE
OX            = (SCREEN_WIDTH  - COLS * SNAKE_SIZE) // 2
OY            = (SCREEN_HEIGHT - ROWS * SNAKE_SIZE) // 2
COLOR_BG      = 0x69c0
COLOR_WALL    = 0xed40
COLOR_SNAKE   = 0xfd40
MODE_START    = 0
MODE_PLAY     = 1

# ----------------------------------------------------------
# Game management
# ----------------------------------------------------------

def tick():
    if game['mode'] == MODE_START:
        resetSnake()
        game['mode'] = MODE_PLAY
    elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()

    draw()

def handleButtons():
    if buttons.pressed(buttons.LEFT):
        dirSnake(-1, 0)
    elif buttons.pressed(buttons.RIGHT):
        dirSnake(1, 0)
    elif buttons.pressed(buttons.UP):
        dirSnake(0, -1)
    elif buttons.pressed(buttons.DOWN):
        dirSnake(0, 1)

# ----------------------------------------------------------
# Snake management
# ----------------------------------------------------------

def resetSnake():
    x = COLS // 2
    y = ROWS // 2
    snake['x'] = []
    snake['y'] = []
    for _ in range(SNAKE_LENGTH):
        snake['x'].append(x)
        snake['y'].append(y)
    snake['head'] = SNAKE_LENGTH - 1
    snake['len']  = SNAKE_LENGTH
    snake['vx'] = 0
    snake['vy'] = 0

def dirSnake(dx, dy):
    snake['vx'] = dx
    snake['vy'] = dy

def moveSnake():
    h = snake['head']
    x = snake['x'][h]
    y = snake['y'][h]
    h = (h + 1) % snake['len']
    snake['x'][h] = x + snake['vx']
    snake['y'][h] = y + snake['vy']
    snake['head'] = h

# ----------------------------------------------------------
# Graphic display
# ----------------------------------------------------------

def draw():
    clearScreen()
    drawWalls()
    drawSnake()

def clearScreen():
    display.clear(COLOR_BG)

def drawWalls():
    display.setColor(COLOR_WALL)
    display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)

def drawSnake():
    display.setColor(COLOR_SNAKE)
    n = snake['len']
    for i in range(n):
        x = snake['x'][i]
        y = snake['y'][i]
        display.fillRect(OX + x * SNAKE_SIZE, OY + y * SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE)

# ----------------------------------------------------------
# Initialization
# ----------------------------------------------------------

game = {
    'mode': MODE_START
}

snake = {
    'x':    [],
    'y':    [],
    'head': 0,
    'len':  0,
    'vx':   0,
    'vy':   0
}

# ----------------------------------------------------------
# Main loop
# ----------------------------------------------------------

while True:
    waitForUpdate()
    tick()

Next step