Imposing rules

Step 4

Constraining the playing area.
Detect when the snake bites its tail.
Apples display on the game scene.
Detect when the snake swallows an apple.

Close the field

We're going to start by locking the snake in and making sure that if it hits one of the walls of the enclosure, the game restarts instantly. We'll deal with the end of the game later on.

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

def didSnakeHitTheWall():
    h = snake['head']
    x = snake['x'][h]
    y = snake['y'][h]
    return x < 0 or x == COLS or y < 0 or y == ROWS

The didSnakeHitTheWall() function is very simple: it compares the coordinates of the snake's head to the ends of the grid. If the snake crosses one of these limits, then the function returns True, otherwise it returns False.

It can be immediately integrated into the task scheduling:

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

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

    draw()

You see that, if the function didSnakeHitTheWall() returns the value True, the game switches back to MODE_START.

Let's test it right now!

Démo

When the snake bites its tail

To detect that the snake bites its tail, it will be necessary to compare the current position of the head with all those it occupied at previous times stored in snake['x'] and snake['y'] lists. We will therefore create a didSnakeBiteBiteItsTail() function to perform these comparisons:

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

def didSnakeBiteItsTail():
    h = snake['head']
    n = snake['len']
    x = snake['x'][h]
    y = snake['y'][h]
    i = (h + 1) % n
    for _ in range(n-1):
        if snake['x'][i] == x and snake['y'][i] == y:
            return True
        i = (i + 1) % n
    return False

We start by retrieving the coordinates of the snake's head. Then the reading head is shifted one notch to position itself on the coordinates of the last section of the snake's tail (thus the oldest position of the head):

h = snake['head']
n = snake['len']
x = snake['x'][h]
y = snake['y'][h]
i = (h + 1) % n

From there, each section will be examined by moving the reading head towards the snake's head. As soon as it is detected that a section is in the same position as the snake head, True is returned. If nothing has been detected, the function returns False:

for _ in range(n-1):
    if snake['x'][i] == x and snake['y'][i] == y:
        return True
    i = (i + 1) % n
return False

We can now integrate this new condition into the management of the game phases:

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

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

    draw()

And to make it easier to perform a test, we're going to lengthen the snake's tail:

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

SNAKE_LENGTH = 15
Démo

You see that the game is restarted as soon as the snake:

  • bites his tail,
  • turns back,
  • or hits a wall.

Well, once you've finished your tests, don't forget to reassign the value 4 to the global variable that represents the default queue length:

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

SNAKE_LENGTH = 4

The forbidden fruit

Come on, we're now going to introduce the management of apples coveted by the snake. In fact, there is no need to create more than one. We will always use the same one: it will just have to be repositioned on the field after the snake has swallowed it.

To model the apple, nothing could be easier:

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

apple = { 'x': 0, 'y': 0 }

A function will then be defined to randomly draw coordinates to place the apple randomly in the field. To be able to draw numbers at random with Python, there are several functions that allow you to do so. We will use here the random.randint function, defined by the random library:

random.randint(a, b)

This function returns a random integer in the interval [a,b]. And to be able to use it, you will have to import it into the script, from the random library. So, after the import line of gamebuino_meta library components, adds the line corresponding to the import of randint:

from gamebuino_meta import waitForUpdate, display, color, buttons
from random import randint

To randomly position the apple in the field, we will create a specific function:

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

def spawnApple():
    apple['x'] = randint(0, COLS - 1)
    apple['y'] = randint(0, ROWS - 1)

Nothing surprising, we simply assign random values to the coordinates of the apple on the grid.

Then, it will be necessary to detect the situation where the snake swallows the apple:

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

def didSnakeEatApple():
    h = snake['head']
    return snake['x'][h] == apple['x'] and snake['y'][h] == apple['y']

We simply compare the position of the snake's head with that of the apple.

Now it is necessary to integrate these routines into the management of the different phases of the game, in the scheduler:

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

def tick():
    if game['mode'] == MODE_START:
        resetSnake()
        spawnApple()
        game['mode'] = MODE_PLAY
    elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()
        if didSnakeEatApple():
            spawnApple()
        if didSnakeBiteItsTail() or didSnakeHitTheWall():
            game['mode'] = MODE_START

    draw()

As soon as the apple is swallowed by the snake, it is immediately randomly repositioned in the field as a new fresh apple:

if didSnakeEatApple():
    spawnApple()

Finally, we still have to do one last thing: create the apple display routine and add a call to this routine in the function in charge of rendering the game scene.

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

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

def drawApple():
    display.setColor(COLOR_APPLE)
    x = apple['x']
    y = apple['y']
    display.fillRect(OX + x * SNAKE_SIZE, OY + y * SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE)

It is therefore necessary to define a new global variable for the color of the apple:

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

COLOR_APPLE = 0x07f0

Come on! Let's see what it looks like:

Démo

Well... okay, we still don't fully manage what's supposed to happen when the snake swallows the apple (its tail is supposed to stretch)... but apart from that... how do you analyze what's happening here? Why does the apple keep changing position until the snake is set in motion?

Take a break and think about it....  

I give up the sponge...

This phenomenon is quite normal! Remember that we added in the scheduler the detection of the situation where the snake bites its tail:

if didSnakeBiteItsTail() or didSnakeHitTheWall():
    game['mode'] = MODE_START

Now... when the game starts, the snake is wrapped around itself in the center of the grid... in other words, its head occupies the same cell as all the sections of its tail!

So what's going on? The game phase is reset to MODE_START... so? Well, the game has to start again:

if game['mode'] == MODE_START:
        resetSnake()
        spawnApple()
        game['mode'] = MODE_PLAY

The snake is reset and the apple is randomly repositioned in the field. Then the game phase changes to MODE_PLAY... and what happens? Well, it's happening again, since the snake is still in the middle of the grid, rolled up on itself....

That's why the apple seems to change position continuously... in fact, it's because the game is constantly restarted as long as the snake doesn't move!  

How can we get out of this situation?

Well, it is enough to introduce an intermediate play phase between MODE_START and MODE_PLAY in which we wait for the snake to move and during which we do not perform the didSnakeBiteItsTail() test! Besides, we also don't need to perform the didSnakeHitTheWall() and didSnakeEatApple() tests...

Okay, so let's define a new global variable to describe this new phase of play:

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

MODE_START = 0
MODE_READY = 1
MODE_PLAY  = 2

In this way, we can modify our scheduler and unlock the situation:

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

def tick():
    if game['mode'] == MODE_START:
        resetSnake()
        spawnApple()
        game['mode'] = MODE_READY
    elif game['mode'] == MODE_READY:
        handleButtons()
        moveSnake()
        if snakeHasMoved():
            game['mode'] = MODE_PLAY
    elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()
        if didSnakeEatApple():
            spawnApple()
        if didSnakeBiteItsTail() or didSnakeHitTheWall():
            game['mode'] = MODE_START

    draw()

Here the game phase starts with MODE_START and then switches to MODE_READY as soon as the snake has been reset and the apple has been positioned on the field:

if game['mode'] == MODE_START:
    resetSnake()
    spawnApple()
    game['mode'] = MODE_READY

When entering the MODE_READY phase, you stay there until the snake is set in motion:

elif game['mode'] == MODE_READY:
    handleButtons()
    moveSnake()
    if snakeHasMoved():
        game['mode'] = MODE_PLAY

This situation is detected by the snakeHasMoved() function that we need to implement:

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

def snakeHasMoved():
    return snake['vx'] or snake['vy']

As long as the velocity is zero, the function returns False. But as soon as one of the velocity components is not zero, then the function returns True.

In this way, at the scheduler level, the game phase changes to MODE_PLAY and that's it:

Démo

There, you have just completed this step and you will be able to move on to the next one. We'll take care of triggering the extension of the snake's tail every time it swallows an apple. And we will also take the opportunity to introduce the player's score management.

Here is the complete code of the code.py script which gathers everything we have implemented since the beginning of the workshop:

from gamebuino_meta import waitForUpdate, display, color, buttons
from random import randint

# ----------------------------------------------------------
# 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
COLOR_APPLE   = 0x07f0
MODE_START    = 0
MODE_READY    = 1
MODE_PLAY     = 2

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

def tick():
    if game['mode'] == MODE_START:
        resetSnake()
        spawnApple()
        game['mode'] = MODE_READY
    elif game['mode'] == MODE_READY:
        handleButtons()
        moveSnake()
        if snakeHasMoved():
            game['mode'] = MODE_PLAY
    elif game['mode'] == MODE_PLAY:
        handleButtons()
        moveSnake()
        if didSnakeEatApple():
            spawnApple()
        if didSnakeBiteItsTail() or didSnakeHitTheWall():
            game['mode'] = MODE_START

    draw()

def spawnApple():
    apple['x'] = randint(0, COLS - 1)
    apple['y'] = randint(0, ROWS - 1)

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

def snakeHasMoved():
    return snake['vx'] or snake['vy']

def didSnakeEatApple():
    h = snake['head']
    return snake['x'][h] == apple['x'] and snake['y'][h] == apple['y']

def didSnakeBiteItsTail():
    h = snake['head']
    n = snake['len']
    x = snake['x'][h]
    y = snake['y'][h]
    i = (h + 1) % n
    for _ in range(n-1):
        if snake['x'][i] == x and snake['y'][i] == y:
            return True
        i = (i + 1) % n
    return False

def didSnakeHitTheWall():
    h = snake['head']
    x = snake['x'][h]
    y = snake['y'][h]
    return x < 0 or x == COLS or y < 0 or y == ROWS

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

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

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)

def drawApple():
    display.setColor(COLOR_APPLE)
    x = apple['x']
    y = apple['y']
    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
}

apple = { 'x': 0, 'y': 0 }

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

while True:
    waitForUpdate()
    tick()

Next step