Aller au contenu

CHAPITRE 7


Gérer les pommes⚓︎

Les pommes⚓︎

L'objet Pomme en lui-même est très simple, ne disposant que de peu de propriétés et méthodes. Mais dans le modèle, il faudra rajouter toute la gestion du côté de l'Arène et du côté du Serpent.

Une pomme, c'est une position, ainsi qu'une référence vers l'arene et son ecran et une méthode pour se dessiner. Plus tard nous améliorerons le modèle en y ajoutant la possibilité de voir la pomme disparaître au bout d'un certain temps si elle n'a pas été mangée.

Une première classe Pomme simple⚓︎

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

L'Arène crée les pommes⚓︎

On ajoute une propriété pommes à notre objet Arene. Cette propriété va stocker les pommes ; ici dans un dictionnaire dont les clés sont des positions dans l'arène et les valeurs associées les pommes. AU départ ce dictionnaire est vide.

class Arene:
    ...
    def __init__(self, ...):
        ...
        self.pommes = {}
Les concepts

Quelques méthodes du module random.

Les pommes seront ajoutées une par une toutes les \(n\) secondes, pour faire simple, nous prendrons \(n = 10\) ; on pourra faire en sorte que ce délai soit tiré aléatoirement dans un intervalle. Pour simuler le temps qui passe nous utiliserons des appels à pygame.time.get_ticks() qui renvoie le nombre de millisecondes depuis l'appel à pygame.init(). En divisant cette valeur par 1000, nous obtenons un chronomètre (pas d'une précision incroyable, mais suffisante dans ce cadre).

class Jeu:
    ...
    def time(self):
        return pygame.time.get_ticks() // 1000

L'idée est donc d'ajouter à l'objet Arene une propriété date initialisée avec la valeur self.jeu.time(). Ensuite, nous mesurons le temps écoulé depuis la dernière génération de pomme et si ce temps est supérieur au délai1 nous générons une pomme et nous réinitialisons date :

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

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

La méthode ajoute_pomme permet de générer une position aléatoire dans l'arène et d'y placer la nouvelle pomme. Le module random offre des méthodes pour obtenir des entiers dans un intervalle donné :

  • Si a et b sont deux entiers alors random.randint(a, b) donne un entier n pris aléatoirement tel que a <= n <= b
  • random.randrange(b) est équivalent à random.randint(0, b-1)
class Arene:
    ...

    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)

Il faudra aussi ajouter dans la méthode dessine l'ordre à chaque pomme présente de se dessiner :

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

Le serpent mange les pommes⚓︎

Lorsque le serpent se déplace, il demande à l'arène si une pomme se trouve au niveau de sa tête. Si c'est le cas, il la mange. Manger consiste juste à croitre de quelques cases en insérant au début de la liste des positions, quelques coordonnées de la position 0 (celle de la queue) :

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

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

Et du côté de l'Arène, la suppression de la pomme se fait au moment où l'Arène répond True à la question du serpent sur l'existence d'une pomme au niveau de sa tête :

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

En Programmation Orientée Objet, ce que fait l'arène avec self.serpent.pos[...] est considérée comme une mauvaise pratique. Les propriétés d'un objet ne concernent que l'objet lui-même. Il s'agit d'un principe clé en POO : l'encapsulation.

Les concepts

L'encapsulation et les moyens d'y parvenir en Python

Mais pour l'instant, nous nous contenterons de cette version.

Exercice⚓︎

À faire vous-même

Intégrer dans un fichier snake_09.py la gestion des pommes.

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 bouge(self):
        x, y = self.pos[self.tete]
        dx, dy = self.direction
        self.pos.append((x+dx, y+dy))
        if self.arene.une_pomme():
            self.mange()
        self.pos.pop(0)

    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 = pygame.time.get_ticks()

    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 = pygame.time.get_ticks()
        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   

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

    def gerer_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key in DIRECTIONS:
                dx, dy = DIRECTIONS[event.key]
                self.arene.serpent_change_direction(dx, dy)
            elif event.key == pygame.K_q:
                self.fini = True

    def pause(self):
        pygame.time.delay(DELAI_MS)

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

    def loop(self):
        while not self.fini:
            for event in pygame.event.get():
                self.gerer_event(event)
            self.pause()
            self.effacer()
            self.arene.anime()
            self.arene.se_dessine()
            pygame.display.flip()
        pygame.quit()    

snake_game = Jeu()
snake_game.start()
import pygame

# CONSTANTES POUR LA VUE

LARGEUR = 600
HAUTEUR = 450
SIZE = 10
LENGTH = 4

COLS = LARGEUR // SIZE
ROWS = HAUTEUR // SIZE

DELAI_MS = 100

COULEUR_TETE = pygame.Color('lightcoral')
COULEUR_CORPS = pygame.Color('lightslategrey')
COULEUR_FOND = pygame.Color('sienna')
COULEUR_POMME = pygame.Color('greenyellow')


# CONSTANTES POUR LE MODÈLE

DIRECTIONS = {pygame.K_DOWN: (0, 1), 
              pygame.K_UP: (0, -1),
              pygame.K_LEFT: (-1, 0),
              pygame.K_RIGHT: (1, 0)}

DELAI_POMME = 10

CROISSANCE = 4

Le résultat de snake_09.py

animation snake

Pour aller plus loin...⚓︎

Vous avez noté comme dans ce chapitre, alors que nous avons ajouté une entité au jeu, cela n'a pas du tout modifié l'objet Jeu (ou très peu, simplement l'ajout d'une méthode time). Comme ce dernier ne s'adresse qu'à l'Arène, l'impact d'un changement du modèle peut ne pas perturber le contrôleur.

Dans le dernier chapitre, nous allons ajouter quelques fonctionnalités mises en œuvre par une facilité des événements : la possibilité de créer des événements utilisateur.


  1. Encore une constante à ajouter, par exemple on peut décider de sortir une pomme toutes les 10s : DELAI_POMME = 10 

Retour en haut de la page