Aller au contenu

CHAPITRE 8


Finaliser le jeu⚓︎

  • Gérer les collisions
  • Ajouter un score
  • Ajouter un timer

Le concept de mode⚓︎

Lorsque le serpent entre en collision avec un des bords de l'arène ou avec lui-même, le jeu prend fin. Jusqu'à présent nous ne pouvons quitter le jeu qu'en utilisant la touche Q de notre clavier. Même le bouton de fermeture de la fenêtre est inactif.

Si le jeu se termine à cause d'une collision, il n'est pas souhaitable que tout s'arrête brutalement, avec fermeture de la fenêtre. En effet, le joueur voudra probablement voir son score, éventuellement avoir accès à un menu pour recommencer une partie etc.

Basculer dans un mode stop serait approprié. Dans ce mode on pourrait avoir l'affichage d'un menu par exemple. On en tout cas, dans sa version la plus simple, le mode stop attend tout simplement que l'utilisateur décide de quitter le jeu.

Différents modes⚓︎

Nous pourrions profiter de ce concept pour créer un mode start qui serait celui dans lequel le jeu démarrerait, empêchant ainsi le serpent de commencer à se mouvoir sans l'intervention du joueur (vous avez remarqué que le serpent se replie sur lui-même au tout début ?), empêchant le timer de se lancer, etc.

Un mode pause pourrait être activé pendant la partie sur décision du joueur (un appui sur la barre espace par exemple) : dans ce mode, le serpent ne bouge pas mais le timer continue de défiler

Le mode move ou mode normal serait celui de la partie qui se déroule, avec le serpent qui bouge.

Enfin le mode stop que nous avons déjà évoqué.

Ajout d'un mode au Jeu⚓︎

Les constantes de modes sont :

MODE_START = 0 # le serpent ne bouge pas
MODE_MOVE = 1 # mode normal, le serpent bouge
MODE_PAUSE = 2 # le joueur a mis le jeu en pause
MODE_STOP = 3

Et l'impact sur le contrôleur :

class Jeu:

    def __init__(self):
        ...
        self.mode = MODE_START 

    def gerer_event(self, event):
        if event.type == pygame.KEYDOWN:
            if self.mode == MODE_START:
                self.debut = int(time.time())
                self.mode = MODE_MOVE
            if self.mode != MODE_STOP:
                self.move_events(event)
            elif event.key == pygame.K_q:
                self.fini = True
        elif # ici une condition pour savoir qu'un crash a eu lieu 
             and self.mode == MODE_MOVE:
            self.game_over()
            self.mode = MODE_STOP
        elif event.type == pygame.QUIT:
            self.fini = True

Les collisions⚓︎

Dès que le serpent a bougé, il doit vérifier qu'il n'a pas tenté de sortir de l'arène, ni heurté son propre corps. Si c'est le cas, il faut avertir le jeu (le contrôleur) que la partie est terminée (basculer en mode stop).

Le souci : le Serpent ne connaît que l'Arène, pas le Jeu. Alors nous pourrions faire une succession de renvois de valeur pour signifier au Jeu, par l'intermédiaire de l'Arène que la partie est finie.

Cette façon de faire est artificielle et surtout, pourrait être très fastidieuse si les intermédiaires entre l'objet qui doit faire l'annonce et le contrôleur s'avéraient nombreux (ce qui peut être le cas dans des projets plus importants).

Les événements utilisateur sont là pour ça.

Les événements utilisateur⚓︎

Lorsque le contrôleur teste le type d'un événement avec event.type == pygame.KEYDOWN, la constante pygame.KEYDOWN est un simple numéro identifiant un événement prédéfini ; ici le fait qu'une touche du clavier a été enfoncée.

Il existe une constante pygame.USEREVENT qui est un identifiant libre, que l'on peut utiliser. Si on a besoin d'un deuxième événement utilisateur, il suffit d'incrémenter cette valeur : pygame.USEREVENT + 1.

Ainsi, dans les constantes nous allons déclarer un événement crash :

CRASH_EVENT = pygame.USERVENT

Ensuite, il suffira au serpent de déclencher cet événement lorsqu'il aura détecté une collision. Avant de détailler cela, nous pouvons compléter la partie manquante de notre méthode gerer_event :

class Jeu:
    ...

    def gerer_event(self, event):
        if event.type == pygame.KEYDOWN:
            if self.mode == MODE_START:
                self.debut = int(time.time())
                self.mode = MODE_MOVE
            if self.mode != MODE_STOP:
                self.move_events(event)
            elif event.key == pygame.K_q:
                self.fini = True
        elif event.type == CRASH_EVENT and self.mode == MODE_MOVE:
            self.game_over()
            self.mode = MODE_STOP
        elif event.type == pygame.QUIT:
            self.fini = True

Note

La méthode game_over pourra afficher un message au milieu de l'écran et attendre que le joueur quitte le jeu.

Du côté du Serpent⚓︎

Il faut déjà vérifier la collision éventuelle :

Le serpent se mord ?⚓︎

class Serpent:
    ...

    def se_mord(self):
        return any(self.pos[i] == self.pos[self.tete] 
                              for i in range(len(self.pos)-1))
Les concepts
  • Les itérateurs
  • Les fonctions qui les utilisent : sum, max, sorted, all, any...

Le serpent est dans l'arène ?⚓︎

class Serpent:
    ...
    def dans_arene(self):
        """Renvoie True si le serpent est bien dans l'arène"""
        tete_x, tete_y = self.pos[self.tete]
        return 0 <= tete_x < COLS and 0 <= tete_y < ROWS

Le crash ?⚓︎

class Serpent:
    ...

    def crash(self):
        """Renvoie True si le serpent s'est crashé qq part"""
        if self.se_mord() or not self.dans_arene():
            self.declenche_event(CRASH_EVENT)
            return True
        return False

Le déclenchement d'un event se fait en deux temps :

  1. création d'un objet événement avec le bon identifiant : pygame.event.Event(event_id)
  2. mettre l'événement dans le buffer des événements : pygame.event.post(event)
class Serpent:
    ...

    def declencher_event(self, event_id):
        pygame.event.post(pygame.event.Event(event_id))

Exercice⚓︎

À faire vous-même

Intégrer la gestion des crashes dans une version snake_10.py. Pour le game_over pour l'instant fait juste un pass

import pygame
import random
import time
from constantes import *

def xy_vers_pixels(coords):
    x, y = coords
    return x * SIZE, y * SIZE

class Pomme:

    def __init__(self, pos, arene):
        self.arene = arene
        self.ecran = arene.ecran
        self.pos = pos

    def se_dessine(self):
        px, py = xy_vers_pixels(self.pos)
        pygame.draw.rect(self.ecran, COULEUR_POMME, pygame.Rect(px, py, SIZE, SIZE))

class Serpent:
    def __init__(self, arene):
        self.arene = arene
        self.ecran = arene.ecran
        self.pos = [(COLS//2 + i, ROWS//2) for i in range(-LENGTH//2, LENGTH//2)]
        self.tete = -1
        self.direction = 0, 0

    def change_direction(self, dx, dy):
        self.direction = dx, dy

    def mange(self):
        pos_queue = self.pos[0]
        for _ in range(CROISSANCE):
            self.pos.insert(0, pos_queue)

    def declenche_event(self, event_id):
        pygame.event.post(pygame.event.Event(event_id))

    def se_mord(self):
        """Renvoie True si la position de la tête se retrouve à une des positions du corps, False sinon"""
        return any(self.pos[i] == self.pos[self.tete] for i in range(len(self.pos)-1))

    def dans_arene(self):
        """Renvoie True si le serpent est bien dans l'arène"""
        tete_x, tete_y = self.pos[self.tete]
        return 0 <= tete_x < COLS and 0 <= tete_y < ROWS

    def crash(self):
        """Renvoie True si le serpent s'est crashé qq part"""
        if self.se_mord() or not self.dans_arene():
            self.declenche_event(CRASH_EVENT)
            return True
        return False

    def bouge(self):
        x, y = self.pos[self.tete]
        dx, dy = self.direction
        self.pos.append((x+dx, y+dy))
        self.pos.pop(0)
        if not self.crash():
            if self.arene.une_pomme():
                self.mange()

    def se_dessine(self):
        for coords in self.pos:
            px, py = xy_vers_pixels(coords)
            pygame.draw.rect(self.ecran, COULEUR_CORPS, pygame.Rect(px, py, SIZE, SIZE))
        px, py = xy_vers_pixels(self.pos[self.tete]) 
        pygame.draw.rect(self.ecran, COULEUR_TETE, pygame.Rect(px, py, SIZE, SIZE))


class Arene:
    def __init__(self, jeu):
        self.jeu = jeu
        self.ecran = jeu.ecran
        self.serpent = Serpent(self)
        self.pommes = {}
        self.date = int(time.time())

    def anime(self):
        self.gestion_pommes()
        self.serpent.bouge()

    def serpent_change_direction(self, dx, dy):
        self.serpent.change_direction(dx, dy)

    def une_pomme(self):
        position_serpent = self.serpent.pos[self.serpent.tete]
        if position_serpent in self.pommes:
            self.pommes.pop(position_serpent)
            return True
        return False

    def random_position(self):
        return random.randrange(COLS), random.randrange(ROWS)

    def ajoute_pomme(self):
        pos = self.random_position()
        self.pommes[pos] = Pomme(pos, self)

    def gestion_pommes(self):
        date = int(time.time())
        if date - self.date > DELAI_POMME:
            self.ajoute_pomme()
            self.date = date

    def se_dessine(self):
        self.serpent.se_dessine()
        for pomme in self.pommes.values():
            pomme.se_dessine()


class Jeu:
    def __init__(self):
        self.ecran = pygame.display.set_mode((LARGEUR, HAUTEUR)) 
        self.arene = Arene(self)
        self.fini = False
        self.mode = MODE_START   

    def start(self):
        pygame.init()
        pygame.display.set_caption('Another SNAKE game...')
        self.ecran.fill(COULEUR_FOND)
        self.loop()

    def move_events(self, event):
        if event.key in DIRECTIONS:
            dx, dy = DIRECTIONS[event.key]
            self.arene.serpent_change_direction(dx, dy)
        elif event.key == pygame.K_SPACE:
            self.mode = 3 - self.mode # bascule de PAUSE à MOVE et vice-versa

    def gerer_event(self, event):
        if event.type == pygame.KEYDOWN:
            if self.mode == MODE_START:
                self.debut = int(time.time())
                self.mode = MODE_MOVE
            if self.mode != MODE_STOP:
                self.move_events(event)
            elif event.key == pygame.K_q:
                self.fini = True
        elif event.type == CRASH_EVENT and self.mode == MODE_MOVE:
            self.game_over()
            self.mode = MODE_STOP
        elif event.type == pygame.QUIT:
            self.fini = True

    def game_over(self):
        pass

    def effacer(self):
        self.ecran.fill(COULEUR_FOND)

    def refresh_and_pause(self):
        pygame.display.flip()
        pygame.time.delay(DELAI_MS)


    def loop(self):
        while not self.fini:
            for event in pygame.event.get():
                self.gerer_event(event)
            if self.mode != MODE_STOP:
                self.effacer()
                if self.mode == MODE_MOVE:
                    self.arene.anime()
                self.arene.se_dessine()
                self.refresh_and_pause()

        pygame.quit()    

snake_game = Jeu()
snake_game.start()

Ajout d'un score⚓︎

En nous servant du principe de l'événement utilisateur, nous allons ajouter un score au jeu : propriété initialisée à 0, à chaque pomme mangée, ce score augmente de 1.

Il nous faut utiliser les objets texte de pygame pour afficher des informations à l'écran.

Les textes de Pygame⚓︎

Probablement un des points faibles de Pygame1. Pour afficher un texte il faut :

  1. Définir une fonte,
  2. faire un rendu de cette fonte sur un texte : ce qui donne une image
  3. afficher cette image

Heureusement, il est possible d'utiliser des valeurs par défaut rendant le processus un peu moins pénible. L'étape 1 se fait au démarrage, avec une fonte par défaut présente sur le système (c'est la signification de l'argument None) :

class Jeu:
    ...
    def start(self):
        pygame.init()
        self.font = pygame.font.SysFont(None, 24)
        ...

Les étapes 2 et 3 se font au moment de l'affichage :

class Jeu:
    ...
    def infos(self):
        score = self.font.render(f'{self.score:03}', True, COULEUR_TEXTE)
        self.ecran.blit(score, POSITION_SCORE)

La méthode blit permet d'afficher une image.

La propriété score⚓︎

Elle fait partie des propriétés de l'objet Jeu :

class Jeu:
    ...
    def __init__(self):
        ...
        self.score = 0

et la mise à jour se fait par événement utilisateur :

SCORE_EVENT = pygame.USEREVENT + 1

qui sera déclenché par le Serpent :

class Serpent:
    ...

    def bouge(self):
        x, y = self.pos[self.tete]
        dx, dy = self.direction
        self.pos.append((x+dx, y+dy))
        self.pos.pop(0)
        if not self.crash():
            if self.arene.une_pomme():
                self.declencher_event(SCORE_EVENT)
                self.mange()

et intercepté par le contrôleur :

class Jeu:
    ...
    def gerer_event(self, event):
        ...
        elif event.type == SCORE_EVENT:
            self.score += 1
        ...

Exercice final⚓︎

À faire vous-même

Réaliser la version finale du SNAKE en intégrant :

  1. les modes et les collisions
  2. le score
  3. le timer
  4. on pourra aussi ajouter plsu d'aléatoire sur l'apparition des pommes, faire en sorte que les pommes disparaissent au bout d'un temps aléatoire aussi, faire accélérer régulièrement le serpent...
import pygame
import random
from constantes import *

def xy_vers_pixels(coords):
    x, y = coords
    return x * SIZE, y * SIZE

class Pomme:

    def __init__(self, pos, arene, date_peremption):
        self.arene = arene
        self.ecran = arene.ecran
        self.pos = pos
        self.date_peremption = date_peremption

    def se_dessine(self):
        px, py = xy_vers_pixels(self.pos)
        pygame.draw.rect(self.ecran, COULEUR_POMME, pygame.Rect(px, py, SIZE, SIZE))

class Serpent:
    def __init__(self, arene):
        self.arene = arene
        self.ecran = arene.ecran
        self.pos = [(COLS//2 + i, ROWS//2) for i in range(-LENGTH//2, LENGTH//2)]
        self.tete = -1
        self.direction = 0, 0

    def change_direction(self, dx, dy):
        self.direction = dx, dy

    def mange(self):
        pos_queue = self.pos[0]
        for _ in range(CROISSANCE):
            self.pos.insert(0, pos_queue)

    def declenche_event(self, event_id):
        pygame.event.post(pygame.event.Event(event_id))

    def se_mord(self):
        """Renvoie True si la position de la tête se retrouve à une des positions du corps, False sinon"""
        return any(self.pos[i] == self.pos[self.tete] for i in range(len(self.pos)-1))

    def dans_arene(self):
        """Renvoie True si le serpent est bien dans l'arène"""
        tete_x, tete_y = self.pos[self.tete]
        return 0 <= tete_x < COLS and 0 <= tete_y < ROWS

    def crash(self):
        """Renvoie True si le serpent s'est crashé qq part"""
        if self.se_mord() or not self.dans_arene():
            self.declenche_event(CRASH_EVENT)
            return True
        return False

    def bouge(self):
        x, y = self.pos[self.tete]
        dx, dy = self.direction
        self.pos.append((x+dx, y+dy))
        self.pos.pop(0)
        if not self.crash():
            if self.arene.une_pomme():
                self.declenche_event(SCORE_EVENT)
                self.mange()

    def se_dessine(self):
        for coords in self.pos:
            px, py = xy_vers_pixels(coords)
            pygame.draw.rect(self.ecran, COULEUR_CORPS, pygame.Rect(px, py, SIZE, SIZE))
        px, py = xy_vers_pixels(self.pos[self.tete]) 
        pygame.draw.rect(self.ecran, COULEUR_TETE, pygame.Rect(px, py, SIZE, SIZE))


class Arene:
    def __init__(self, jeu):
        self.jeu = jeu
        self.ecran = jeu.ecran
        self.serpent = Serpent(self)
        self.pommes = {}
        self.date = jeu.time()

    def anime(self):
        self.gestion_pommes()
        self.serpent.bouge()

    def serpent_change_direction(self, dx, dy):
        self.serpent.change_direction(dx, dy)

    def retrait_pomme(self, pos):
        self.pommes.pop(pos)

    def ajout_pomme(self, date_peremption):
        pos = self.random_position()
        self.pommes[pos] = Pomme(pos, self, date_peremption)

    def une_pomme(self):
        position_serpent = self.serpent.pos[self.serpent.tete]
        if position_serpent in self.pommes:
            self.retrait_pomme(position_serpent)
            return True
        return False

    def random_position(self):
        return random.randrange(COLS), random.randrange(ROWS)

    def retire_pommes(self):
        """retire les pommes périmées"""
        date = self.jeu.time()
        for pos in list(self.pommes.keys()):
            pomme = self.pommes[pos]
            if pomme.date_peremption < date:
                self.retrait_pomme(pos)

    def gestion_pommes(self):
        date = self.jeu.time()
        delai_apparition = random.randint(*DELAI_POMME)
        delai_peremption = random.randint(*VALIDITE)
        if (date - self.date) > delai_apparition:
            self.ajout_pomme(date + delai_peremption)
            self.date = date
        self.retire_pommes()

    def se_dessine(self):
        self.serpent.se_dessine()
        for pomme in self.pommes.values():
            pomme.se_dessine()

    def reset(self):
        self.serpent = Serpent(self)
        self.pommes = {}
        self.date = self.jeu.time()


class Jeu:
    def __init__(self):
        self.ecran = pygame.display.set_mode((LARGEUR, HAUTEUR)) 
        self.arene = Arene(self)
        self.fini = False
        self.mode = MODE_START
        self.debut = None # sera initialisé au moment du lancement  
        self.score = 0
        self.font = None
        self.delai = DELAI_MS
        self.deja_accelere = False

    def start(self):
        pygame.init()
        pygame.display.set_caption('Another SNAKE game...')
        self.font = pygame.font.SysFont(None, 24)
        self.ecran.fill(COULEUR_FOND)
        self.loop()

    def move_events(self, event):
        if event.key in DIRECTIONS:
            dx, dy = DIRECTIONS[event.key]
            self.arene.serpent_change_direction(dx, dy)
        elif event.key == pygame.K_SPACE:
            self.mode = 3 - self.mode # bascule de PAUSE à MOVE et vice-versa
        elif event.key == pygame.K_q:
            self.fini = True

    def time(self):
#        return int(time.time())
        return pygame.time.get_ticks() // 1000

    def gerer_event(self, event):
        if event.type == pygame.KEYDOWN:
            if self.mode == MODE_START:
                self.debut = self.time()
                self.mode = MODE_MOVE
            if self.mode != MODE_STOP:
                self.move_events(event)
            elif event.key == pygame.K_q:
                self.fini = True
            elif event.key == pygame.K_r:
                self.reset()
                self.mode = MODE_START
        elif event.type == SCORE_EVENT:
            self.score += 1
        elif event.type == CRASH_EVENT and self.mode == MODE_MOVE:
            self.game_over()
            self.mode = MODE_STOP
        elif event.type == pygame.QUIT:
            self.fini = True

    def accelere(self):
        if self.temps_ecoule() % DELAI_ACC == 0 and not self.deja_accelere:
            self.deja_accelere = True
            self.delai -= DELTA_DELAI
            print(self.delai)
        elif self.temps_ecoule() % DELAI_ACC != 0:
            self.deja_accelere = False

    def temps_ecoule(self):
        return 0 if self.debut is None else self.time() - self.debut

    def infos(self):
        ecoule = self.temps_ecoule()
        temps = self.font.render(f'{ecoule//60:02}:{ecoule%60:02}', True, COULEUR_TEXTE)
        score = self.font.render(f'{self.score:03}', True, COULEUR_TEXTE)
        self.ecran.blit(temps, POSITION_TEMPS)
        self.ecran.blit(score, POSITION_SCORE)

    def game_over(self):
        big = pygame.font.SysFont(None, 36)
        fin = big.render('GAME OVER', True, COULEUR_TEXTE)
        self.ecran.blit(fin, CENTRE)
        self.refresh_and_pause()

    def effacer(self):
        self.ecran.fill(COULEUR_FOND)

    def refresh_and_pause(self):
        pygame.display.flip()
        pygame.time.delay(DELAI_MS)


    def loop(self):
        while not self.fini:
            for event in pygame.event.get():
                self.gerer_event(event)
            if self.mode != MODE_STOP:
                if self.delai > DELAI_MIN:
                    self.accelere()
                self.effacer()
                if self.mode == MODE_MOVE:
                    self.arene.anime()
                self.arene.se_dessine()
                self.infos()
                self.refresh_and_pause()
        pygame.quit()    

    def reset(self):
        self.arene.reset()

snake_game = Jeu()
snake_game.start()

  1. En réalite cela signifie que si on a beaucoup de textes à gérer le mieux est de créer un objet texte pour encapsuler les différentes étapes. 

Retour en haut de la page