Académie
Snake (Python)
Imposer des règles

Imposer des règles

Étape 4

Contraindre l'espace de jeu.
Détecter quand le serpent se mord la queue.
Gestion de l'apparition des pommes sur la scène de jeu.
Détecter quand le serpent avale une pomme.

Fermer l'enceinte du terrain

On va commencer par enfermer le serpent et faire en sorte que, s'il heurte un des murs de l'enceinte, le jeu redémarre instantanément. On s'occupera plus tard de gérer la fin de la partie proprement.

# ----------------------------------------------------------
# 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

La fonction didSnakeHitTheWall() est très simple : elle compare simplement les coordonnées de la tête du serpent aux extrémités de la grille. Si le serpent franchit l'une de ces limites, alors la fonction renvoie la valeur True, sinon elle renvoie False.

On peut immédiatement l'intégrer à l'ordonnancement des tâches :

# ----------------------------------------------------------
# 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()

Tu vois que, si la fonction didSnakeHitTheWall() renvoie la valeur True, le jeu rebascule en phase MODE_START.

Testons ça tout de suite !

Démo

Quand le serpent se mord la queue

Pour détecter que le serpent se mord la queue, il va falloir comparer la position courante de la tête avec toutes celles qu'elle a occupées aux instants antérieurs mémorisés dans les listes snake['x'] et snake['y']. On va donc créer une fonction didSnakeBiteItsTail() chargée d'effectuer ces comparaisons :

# ----------------------------------------------------------
# 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

On commence par récupérer les coordonnées de la tête du serpent. Puis on décale la tête de lecture d'un cran pour se positionner sur les coordonnées du dernier tronçon de la queue du serpent (donc la position la plus ancienne de la tête) :

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

À partir de là, on va examiner chaque tronçon en faisant progresser la tête de lecture vers la tête du serpent. Dès qu'on détecte qu'un tronçon occupe la même position que la tête du serpent, la valeur True est retournée. Si rien n'a été détecté, la fonction finit par retourner la valeur False :

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

On peut maintenant intégrer cette nouvelle condition dans la gestion des phases du jeu :

# ----------------------------------------------------------
# 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()

Et pour qu'on puisse effectuer un test plus facilement, on va allonger la queue du serpent :

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

SNAKE_LENGTH = 15
Démo

Tu vois que la partie est relancée dès lors que le serpent :

  • se mord la queue,
  • rebrousse chemin,
  • ou heurte un mur.

Bien, une fois que t'auras terminé tes tests, n'oublie pas de réaffecter la valeur 4 à la variable globale qui représente la longueur de la queue par défaut :

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

SNAKE_LENGTH = 4

Le fruit défendu

Allez, on va maintenant introduire la gestion des pommes convoitées par le serpent. En fait, il est inutile d'en créer plusieurs. On utilisera toujours la même : il suffira de la repositionner sur le terrain après que le serpent l'ait avalée.

Pour modéliser la pomme, rien de plus simple :

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

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

On va ensuite définir une fonction chargée de tirer des coordonnées au hasard pour placer la pomme alétoirement sur le terrain. Pour pouvoir tirer des nombres au hasard avec Python, il existe plusieurs fonctions qui te permettent de le faire. On utilisera ici la fonction random.randint définie par la bibliothèque random :

random.randint(a, b)

Cette fonction retourne un entier aléatoire dans l'intervalle [a,b]. Et pour pouvoir l'utiliser, il va falloir l'importer dans notre script, à partir de la bibliothèque random. Donc, après la ligne d'import des éléments de la bibliothèque gamebuino_meta, ajoute la ligne correspondant à l'import de randint :

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

Pour positionner aléatoirement la pomme sur le terrain, on va créer une fonction spécifique :

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

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

Rien de suprenant, on affecte simplement des valeurs aléatoires aux coordonnées de la pomme sur la grille.

Ensuite, il va falloir détecter la situation où le serpent avale la pomme :

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

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

On compare simplement la position de la tête du serpent avec celle de la pomme.

Maintenant il faut intégrer ces deux routines dans la gestion des différentes phases du jeu, au niveau de l'ordonnanceur :

# ----------------------------------------------------------
# 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()

Dès que la pomme est avalée par le serpent, elle est immédiatement repositionnée aléatoirement sur le terrain comme une nouvelle pomme toute fraîche :

if didSnakeEatApple():
    spawnApple()

Pour finir, il nous reste à faire une dernière chose : créer la routine d'affichage de la pomme et ajouter un appel à cette routine dans la fonction chargée du rendu graphique de la scène de jeu.

# ----------------------------------------------------------
# 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)

Il faut donc définir une nouvelle variable globale pour la couleur de la pomme :

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

COLOR_APPLE = 0x07f0

Allez !... Voyons un peu ce que ça donne :

Démo

Bon... ok, on ne gère pas encore complètement ce qui est censé se passer au moment où le serpent avale la pomme (sa queue est censée s'allonger)... mais en dehors de ça... comment analyses-tu ce qui se passe ici ? Pourquoi la pomme change continuellement de position tant que le serpent n'est pas mis en mouvement ?

Pose-toi un peu et réfléchis...  

J'donne ma langue au chat...

Ce phénomène est tout à fait normal !... Souviens toi qu'on a ajouté dans l'ordonnanceur la détection de la situation où le serpent se mord la queue :

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

Or... lorsque le jeu démarre, le serpent est enroulé sur lui-même au centre de la grille... autrement dit sa tête occupe la même cellule que tous les tronçons de sa queue !

Donc qu'est-ce qui se passe ? La phase de jeu est réinitialisée à MODE_START... donc ? Et ben forcément le jeu redémarre :

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

Le serpent est réinitialisé et la pomme repositionnée aléatoirement sur le terrain. Puis la phase de jeu passe à MODE_PLAY... et qu'est-ce qui se passe ? Et ben ça recommence, puisque le serpent est toujours au centre la grille, enroulé sur lui-même...

Voilà pourquoi la pomme semble changer de position continuellement... en fait, c'est parce-que le jeu est sans cesse relancé tant que le serpent ne bouge pas !  

Comment peut-on se sortir de cette situation ?

Et ben il suffit d'introduire une phase de jeu intermédiaire entre MODE_START et MODE_PLAY dans laquelle on attend que le serpent bouge et pendant laquelle on n'effectue pas le test didSnakeBiteItsTail() ! D'ailleurs on n'a pas besoin non plus d'effectuer les tests didSnakeHitTheWall() et didSnakeEatApple()...

Ok, donc définissons une nouvelle variable globale pour décrire cette nouvelle phase de jeu :

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

MODE_START = 0
MODE_READY = 1
MODE_PLAY  = 2

De cette manière, on peut modifier notre ordonnanceur et débloquer la 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()

Ici la phase de jeu démarre avec MODE_START puis passe à MODE_READY dès que le serpent a été réinitialisé et que la pomme a été positionnée sur le terrain :

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

En entrant dans la phase MODE_READY, on y reste tant que le serpent n'est pas mis en mouvement :

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

Cette situation est détectée par la fonction snakeHasMoved() qu'il nous faut implémenter :

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

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

Tant que la vitesse est nulle, la fonction renvoie False. Mais dès que l'une des composantes de la vitesse est non nulle, alors la fonction renvoie True.

De cette manière, au niveau de l'ordonnanceur, la phase de jeu passe à MODE_PLAY et le tour est joué :

Démo

Voilà, tu viens de terminer cette étape et tu vas pouvoir passer à la suivante. On va s'occuper de déclencher l'allongement de la queue du serpent à chaque fois qu'il avale une pomme. Et on en profitera également pour introduire la gestion du score effectué par le joueur.

Voici le code complet du script code.py qui rassemble tout ce que nous avons implémenté depuis le début de l'atelier :

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()

Etape suivante