1. Programmation orientée objet

Document Professeur

1.1. Objectif

Notre objectif est développer une version Python du fameux jeu Snake en s’appuyant sur les concepts de programmation orientée objet.

1.2. Prérequis

  • Utilisation d’un environnement de développement (pycharm)

  • Types élémentaires : numériques et chaines de caractères

  • Types construis : listes, tuples

  • Structure algorithmique de base : boucle, test

  • Concepts de programmation objet : classes, objets, attribus et méthodes

1.3. Conditions

  • Activité sur PC avec connexion internet

  • Niveau première

  • Type d’activité TD/TP

  • Durée : 4h

1.4. Rubriques du programme :

  • Langage et programmation :

    • Constructions élémentaires

    • Spécification

    • Mise au point

    • Paradigme de programmation objet

2. Introduction : Snake

Le snake, de l’anglais signifiant « serpent », est un genre de jeu vidéo dans lequel le joueur dirige une ligne qui grandit et constitue ainsi elle-même un obstacle. Bien que le concept tire son origine du jeu vidéo d’arcade Blockade développé et édité par Gremlin Industries en 1976, il n’existe pas de version standard. Son concept simple l’a amené à être porté sur l’ensemble des plates-formes de jeu existantes sous des noms de clone.

Le joueur contrôle une longue et fine ligne semblable à un serpent, qui doit slalomer entre les bords de l’écran et les obstacles qui parsèment le niveau. Pour gagner chacun des niveaux, le joueur doit faire manger à son serpent un certain nombre de pastilles similaire à de la nourriture, allongeant à chaque fois la taille du serpent. Alors que le serpent avance inexorablement, le joueur ne peut que lui indiquer une direction à suivre (en haut, en bas, à gauche, à droite) afin d’éviter que la tête du serpent ne touche les murs ou son propre corps, auquel cas il risque de mourir.

— wikipedia
https://fr.wikipedia.org/wiki/Snake_(genre_de_jeu_vid%C3%A9o)

Dans notre version, le lancement du jeu devra :

  • Afficher une bannière contenant le nom du jeu PYTHON SNAKE pendant 2 secondes avant d’afficher l’aire de jeu

  • Afficher l’aire de jeu avec le score et le niveau

  • L’aire de jeu contient un serpent et au moins une pomme.

  • Le serpent se déplace dans l’aire de jeu.

  • Il réapparait sur le bord opposé lorsqu’il sort de l’aire de jeu.

  • Il grandit d’un anneau chaque fois qu’il mange une pomme.

  • A chaque niveau, il doit manger 5 pommes.

  • Au premier niveau, l’aire de jeu contient 1 pomme à la fois, au second niveau, 2 pommes, 3 au troisième, …​.

  • La vitesse de déplacement du serpent augmente à chaque niveau.

  • Le jeu prend fin lorsque le joueur appui sur la touche Echap ou lorsque le serpent se mord la queue

snake

3. Au travail

  • Sur votre espace github, créer un nouveau dépôt PythonSnake

  • Sur pycharm, créer un nouveau projet associé à votre dépôt dans votre environnement de développement

  • Créer un nouveau fichier python snake.py

  • Ne pas oublier de commiter puis de pousser le code après chaque étape du développement.

3.1. Analyse

Question :

D’après vous, quels sont les objets qui constituent le jeu ?

Question :

Quels sont les attributs de chacun de ces objets ?

Question :

Quels sont les comportements ou méthodes de chacun de ces objets ?

3.2. Codage du squelette de l’application

L’aire de jeu s’appuie sur l’utilisation du module curses dont l’utilisation sera détaillée plus loin.

Le modèle retenu est réprésenté ci-dessous :

modele

Question :

  • Coder le squelette des classes avec leur constructeur

3.2.1. Instantiation de la classe jeu

Question :

  • Ajouter dans le constructeur de la classe Jeu la ligne suivante :

print(f"Construction d'un objet de la classe Jeu de dimension {hauteur}x{largeur}")
  • Créer un objet mon_jeu de la classe Jeu avec pour arguments 40x120

Question :

  • Excuter le programme et observer la création de l’objet mon_jeu

pythonSnake0

3.2.2. Affichage de la bannière d’accueil

La bannière d’accueil PYTHON SNAKE est un petit clin d’oeil au jeu, au serpent (le python) et au langage avec lequel on le programme !

Pour la réaliser, nous allons utiliser le concept de l'ASCII art

ASCII art

L’ASCII art consiste à réaliser des images uniquement à l’aide des lettres et caractères spéciaux contenus dans le code ASCII. Vous utilisez souvent cet art pour générer des émoticons. Nous allons ici pousser plus loin le concept pour générer une image du titre de notre jeu :

titre

Vous êtes libre de réaliser le design que vous souhaitez du moment qu’il utilise les caractères de la table ASCII

On trouve sur le net des sites de génération d’image ASCII art, par exemple : http://www.patorjk.com/software/taag

Question :

  • Réaliser votre bannière et l’insérer dans le code du constructeur du jeu sous la forme d’un tuple` comme ci-dessous :

titre =('  _______     _________ _    _  ____  _   _    _____ _   _          _  ________ ',
        ' |  __ \ \   / |__   __| |  | |/ __ \| \ | |  / ____| \ | |   /\   | |/ |  ____|',
        ' | |__) \ \_/ /   | |  | |__| | |  | |  \| | | (___ |  \| |  /  \  |   /| |__   ',
        ' |  ___/ \   /    | |  |  __  | |  | | . ` |  \___ \| . ` | / /\ \ |  < |  __|  ',
        ' | |      | |     | |  | |  | | |__| | |\  |  ____) | |\  |/ ____ \| . \| |____ ',
        ' |_|      |_|     |_|  |_|  |_|\____/|_| \_| |_____/|_| \_/_/    \_|_|\_|______|')

Question :

  • Ecrire une methode afficher_banniere(self, banniere) qui parcours le tuple banniere et affiche chacune de ces lignes puis attend 2 secondes avant de passer à la suite (lancement du jeu).

Remarque : L’attente peut être obtenue en utilisant la fonction sleep() du module time

Question :

  • Dans le programme principal, appeler la méthode afficher_banniere() de l’objet mon_jeu.

  • Excuter le programme et observer l’affichage de la bannière.

Les caractéristiques d’un terminal peuvent être contrôler au moyen du codage Escape ANSI. La liste de ces codes est données dans la documentation Screen User’s Manual du projet GNU :

Quelques codes utiles :

  • Effacer le terminal : \x1b[1J

  • Redimensionner le terminal (10x80 caractères) : \x1b[8;10;80t

Remarque : Le caractère Escape à pour code ASCII 0x1b

Question :

  • Modifier la méthode afficher_banniere() pour :

    • effacer le terminal avant l’affichage de la bannière,

    • le redimensionner à la hauteur de la bannière \+ 2 caractères et à la largeur de l’aire de jeu,

    • attendre 2 seconde

    • le redimensionner aux dimensions de l’aire de jeu

    • effacer de nouveau le terminal,

    • attendre 1 seconde

  • Excuter le programme et observer l’affichage de la bannière.

3.2.3. Création de l’aire de jeu

Le module curses

Curses est le nom d’une bibliothèque logicielle permettant le développement sous Unix d’environnements plein écran en mode texte, indépendamment du terminal informatique utilisé, et pour tout type d’applications.

Elle est issue de la distribution BSD d’Unix, dans laquelle elle était employée pour l’éditeur de texte vi et le jeu Rogue.

Avec curses, il est possible d’écrire à tout moment en toute position de l’écran. Les programmeurs peuvent ainsi concevoir des applications basées sur le mode texte, sans tenir compte des particularités de chaque terminal. La bibliothèque curses s’occupe d’envoyer les caractères de contrôle appropriés vers le moniteur lors de l’exécution du programme, en essayant d’optimiser ce qu’elle peut quand elle le peut.

Rester en mode texte, mais en y utilisant ainsi l’adressage direct de tout endroit de l’écran, permet aussi de ne pas avoir à s’occuper du gestionnaire graphique utilisé parmi les nombreux que peut utiliser Linux, Unix ou Windows.

— wikipedia
https://fr.wikipedia.org/wiki/Curses
Utilisation basic de Curses

Les méthodes principales sont très simples :

  • initscr() initialise le mode plein écran.

  • move(ligne, colonne) déplace virtuellement le curseur

  • newwin(height, width, begin_y, begin_x) création d’une fenêtre dans le terminal

  • addstr(chaîne) écrit une chaîne de caractères là où est le curseur

  • refresh() met en conformité l’écran réel et l’affichage virtuel

  • endwin() met fin au mode plein écran et restaure l’état où était l’écran avant que l’on ne s’y place.

La méthode newwin() permet de créer une fenêtre qui est une abstraction de base de curses. Un objet fenêtre représente une zone rectangulaire de l’écran et prend en charge des méthodes pour afficher du texte, l’effacer, permettre à l’utilisateur de saisir des chaînes, etc.

Le détail de toutes les méthodes de curses : https://docs.python.org/fr/3/library/curses.html

Comment utiliser efficacement le module curses : https://docs.python.org/3/howto/curses.html

Question :

  • Ecrire une méthode affichage_aire_de_jeu(titre) de la classe Jeu qui doit :

    • afficher l’aire de jeu en commençant par le bord supérieur gauche du terminal (x=0, y=0)

    • Afficher un titre en rouge sur fond blanc au milieu de la bordure supérieure

    • Pas de curseur apparent

    • Emettre un bip à la fin de l’affichage

    • Retourner la fenêtre créée

  • Appeler la méthode affichage_aire_de_jeu(hauteur, largeur, titre)

  • Excuter le programme et observer l’affichage de l’aire de jeu.

fenetre vide

Exemple de codage des couleurs avec curses :

# Définition de la couleur 1
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)

# Utilisation de la couleur 1 pour afficher du texte
win.addstr(0, 0, "du texte", curses.color_pair(1))

Le retour au terminal ne se fait pas correctement car la fonction endwin() du module curses n’a pas été appelé.

Question :

  • Ecrire une méthode fin(self) de la classe Jeu qui doit :

    • Emettre un bip

    • Mettre fin à la fenêtre de jeu dans le terminal

    • Redimensionner le terminal avec 10 lignes et 80 colonnes

    • Afficher le score du joueur. Il faut donc aussi créer un attribut score à la classe Jeu et l’initialiser à 0.

  • Appeler la méthode fin() de l’objet mon_jeu. Pour s’assurer de voir l’aire de jeu, utiliser la fonction napms() du module curses qui prend en argument une durée en millisecondes.

  • Excuter le programme et observer l’affichage de l’aire de jeu et le retour normal au terminal avec l’affichage su score.

3.3. Ajout d’une pomme

Une pomme est caractérisée par sa localisation, sa couleur et sa forme. Nous allons pour le moment laisser la couleur de côté et générer aléatoirement le positionnement de la pomme qui s’affichera en vert sur fond noir. Nous utilserons le caractère ASCII étendu 211 : Ó ou 210 : Ò pour représenter une pomme.

3.3.1. Afficher la pomme

Question :

  • Ajouter au constructeur de la classe :

    • la définition de la couleur de la pomme

    • un attribut coordonnees dont la valeur initiale est une liste de deux éléments à 0. Le premier est la position en ligne et le second en colonne.

    • un attribut window initialisé avec la fenêtre de jeu obtenue du constructeur.

  • Ecrire une méthode afficher() de la classe Pomme qui doit :

    • Obtenir aléatoirement de nouvelles coordonnées pour la pomme. Utiliser la fonction randint du module random.

    • afficher la pomme dans l’aire de jeu obtenue du constructeur.

  • Créer un objet la_pomme de la classe Pomme dans le programme principal avant la fin de l’affichage de l’aire de jeu.

  • Appeler la méthode afficher de l’objet la_pomme

  • Excuter le programme et observer l’affichage de l’aire de jeu avec une pomme.

apple

3.3.2. Obtenir les coordonnées de la pomme

Question :

  • Ecrire une méthode get_xy() de la classe Pomme qui retourne la liste des coordonnées de la pomme.

  • Appeler la méthode get_xy() de l’objet la_pomme et faire afficher après la fin du jeu la localisation de la pomme.

  • Excuter le programme et observer l’affichage de l’aire de jeu avec une pomme.

position pomme

3.4. Ajout du serpent

Le serpent est caractérisé par sa taille, sa localisation, sa couleur et le caractère utilisé pour matérialisé ses anneaux.

Nous allons fixer sa taille de départ à 3 anneaux, sa couleur sera jaune sur fond noir, chaque anneau contiendra le caractère * et sa localisation de départ sur la colone 8, et la ligne 4.

Pour matérialiser sa position, on utilisera une liste de listes de deux éléments :

snake0

3.4.1. Afficher le serpent

Question :

  • Ajouter au constructeur de la classe :

    • la définition de la couleur du serpent

    • un attribut snake dont la valeur initiale est une liste de listes de deux éléments dont le premier est la position en ligne et le second en colonne de chaque anneau du serpent.

    • un attribut window initialisé avec la fenêtre de jeu obtenue du constructeur.

  • Ecrire une méthode afficher() de la classe Serpent qui doit :

    • afficher le serpent dans l’aire de jeu obtenue du constructeur au coordonnées contenues dans snake.

  • Créer un objet le_serpent de la classe Serpent dans le programme principal avant la fin de l’affichage de l’aire de jeu.

  • Appeler la méthode afficher de l’objet le_serpent

  • Excuter le programme et observer l’affichage de l’aire de jeu avec le serpent.

apple snake

3.4.2. Déplacer le serpent

On utilise les touches de directions pour déplacer le serpent. Les seules touches qui auront un effet sur le jeu sont :

  • UP : Le serpent va vers le haut

  • DOWN : Le serpent va vers le bas

  • LEFT : Le robot va vers la gauche

  • RIGHT : Le robot va vers la droite

  • Escape : quitter la partie en cours.

L’acquisition des touches pressées par le joueur est du ressort de la classe Jeu.

Question :

  • Rechercher les constantes correspondantes au touches directionnelles (KEYS) dans la documentation :

Rappel : le code ASCII de la touche Escape est 0x1d soit 27.

  • Ajouter la méthode controle() à classe Jeu qui retourne la dernière touche valide utilisée.

def controle(self,
         key: int = KEY_RIGHT,
         keys: list = [KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_UP, 27]) -> int:

    old_key = key
    key = self.window.getch()
    if key == -1 or key not in keys:
        key = old_key
    return key

Remarque : cette méthode fait appel à l’attribut window qui contient la fenêtre de jeu, or cet attribut n’exite pas ! La fenêtre de jeu est définie dans la méthode affichage_aire_de_jeu().

Question :

  • Modifier la méthode affichage_aire_de_jeu() pour créer l’attribut window de la classe Jeu.

  • Modifier le programme principal pour contrôler le jeu. La sortie du jeu ne peut se produire que si la touche utilisée est Escape

  • Excuter le programme et observer l’affichage de l’aire de jeu jusqu’à l’appui sur la touche Escape.

Principe

Les déplacements sont obtenus en agissant sur les coordonnés du serpent. Lorsque le serpent avance dans une direction donnée, on insère un nouvel élément dans sa liste à la position 0 (la tête). Le serpent à donc grandi d’un anneau. Lorsque nous traiterons du cas ou il a mangé une pomme, nous ne lui retirerons pas cet anneau mais pour le moment, nous traitons seulement le cas de l’avance, donc, il nous faut supprimer son dernier anneau pour conserver sa longueur :

Au départ

snake0

Le serpent se déplace vers la droite, on insère la tête …​

snake1

...et on supprime le dernier anneau

snake2

Codage des déplacements

Question :

  • Ajouter une méthode tete() à classe serpent qui retourne une liste contenant les coordonnées de sa tête (premier élément de la liste snake) :

def tete(self) -> list:
    return ???

Question :

  • Ajouter une méthode corps() à classe serpent qui retourne une liste contenant les coordonnées de tous ses éléments sauf la tête :

def corps(self) -> list:
    return ???

Question :

  • Compléter la méthode deplacement(win, key, niveau) de la classe serpent qui gère les déplacents du serpent dans l’aire de jeu en fonction de la touche de contrôle active. Si le serpent atteint une limite de l’air de jeu, il réapparait sur le bord opposé :

class Serpent:
    ...

    def deplacement(self, key: int, niveau: int) -> list:

        # si la touche est KEY_RIGHT : ajouter la tête une colonne à droite
        if key == KEY_RIGHT:
            self.snake.insert(0, [self.tete()[0], self.tete()[1] + 1])

        # si la touche est KEY_LEFT : ajouter la tête une colonne à gauche
        elif key == KEY_LEFT:
            ???

        # si la touche est KEY_UP : ajouter la tête une ligne au dessus
        elif key == KEY_UP:
            ???
        # si la touche est KEY_DOWN : ajouter la tête une ligne en dessous
        elif key == KEY_DOWN:
            ???

        # Si la tête du serpent touche les bords de l'aire de jeu
        if self.tete()[0] == 0:
            ???
        if self.tete()[1] == 0:
            ???
        if self.tete()[0] == self.window.getmaxyx()[0] - 1:
            ???
        if self.tete()[1] == self.window.getmaxyx()[1] - 1:
            self.tete()[1] = 1

        # Supprimer le dernier élément du serpent
        self.window.addstr(self.snake[???][0], self.snake[???][1], ' ', curses.color_pair(1))
        self.snake.pop()

        # Attendre un peu en fonction du niveau
        self.window.timeout(int(150 * (1 / niveau + 1 / (niveau * 2))))

        return self.snake

Question :

  • Compléter le programme principal pour faire avancer le serpent en fonction de la touche utilisée tant que le joueur n’a pas quitter le jeu.
    Ne pas oublier d’afficher le serpent après le calcul des dépacements et d’initialiser le niveau à 1.

  • Vérifier que le déplacement du serpent est fonctionnel dans l’aire de jeu. Modifier le niveau et constater la rapidité du déplacement.

3.5. On a mangé la pomme ?

On cherche ici à savoir si au cours du déplacement, le serpent a mangé la pomme. Dans ce cas, les coordonnées de sa tête sont égales à celle de la pomme.

3.5.1. Principe

La position de la tête est donnée par la méthode tete() de la classe Serpent et celle de d’une pomme par sa méthode get_xy() de la classe Pomme. Il suffit que les listes retournées par ces deux méthodes soient égales pour que l’on en déduise que le serpent à manger la pomme !

Attention : Il pourrait y avoir plusieurs pommes dans l’air de jeu.

3.5.2. Codage du test "le serpent a manger la pomme"

Lorsque le serpent mange une pomme, le score du joueur est incrémenté et on pourra par la suite changer le niveau par exemple tous les 5 points.

Question :

  • Ajouter la méthode afficher_score() à la classe Jeu. Ajouter également l’attribut niveau à la classe Jeu

  • Appeler cette méthode dans le programme principal et constater l’affichage du score et du niveau en haut de l’aire jeu.

class Jeu:

    ...

    self.niveau = 1

    ...

    def affiche_score(self):
        self.window.addstr(0, 2, 'Score : ' + str(self.score) + ' ')
        self.window.addstr(0, self.largeur - 16, 'Niveau : ' + str(self.niveau) + ' ')
        self.window.refresh()

Question :

  • Compléter le code de la méthode mange_pomme(win, pommes) de la classe Serpent qui retourne Vrai si le serpent a mangé la pomme et Faux si ce n’est pas le cas.

class Serpent:
    ...

    def mange_pomme(self, win: curses, pommes: list) -> bool:

        # initialisation du flag à Faux
        miam = ???

        # la liste <pommes> contient toutes les pommes de l'air de jeu
        # parcour de la liste pommes
        for pomme in ???:

            # si la tête du serpent est sur la pomme
            ???
                curses.beep()

                # flag passe à Vrai
                miam = ???

                # afficher une nouvelle pomme dans la fenêtre de jeu
                pomme.???

                # Recommencer tant la nouvelle pomme apparait dans le serpent
                while pomme.get_xy() in self.snake:
                    pomme.afficher(win)

        # Si le serpent n'a pas manger la pomme
        ???:
            # Supprimer le dernier élément du serpent
            win.addstr(self.snake[-1][0], self.snake[-1][1], ' ', curses.color_pair(1))
            self.snake.pop()
        return miam

Question :

  • Modifier le programme principale pour incrémenter le score du jeu lorsque le serpent mange une pomme.

Remarque : On pourra accéder directement à l’attribut score de la classe jeu depuis le programme principal. Ce n’est pas très indiqué, la meilleur solution serait de créer une méthode pour incrémenter le score.

  • Tester le programme et constater l’incrémentation du score lorsque le serpent mange une pomme. Un nouvelle pomme doit également apparaitre de façon aléatoire.

3.6. Quand est-ce qu’on perd ?

Si tout va bien pour le moment, on peut jouer mais pas perdre. En effet, nous n’avons pas prévu de cas perdant. Dans le scénario initial de Snake, le joueur perd si le serpent se mord la queue ou s’il entre en contact avec une bordure. Nous ne traiterons ici que du premier cas.

3.6.1. Principe

Lorsque le serpent se mord la queue, les coordonnées de sa tête sont identiques à celles d’une de ses cellules. La position de la tête est donnée par la méthode tete() et celle du reste du corps par la méthode corps(). Il suffit que la tête soit dans le corps pour que l’on en déduise que le serpent s’est mordu la queue !

3.6.2. Codage de la fin du jeu

Question :

  • Compléter la méthode perdu(serpent) de la classe Jeu :

def perdu(self, snake):

    # flag initialisé à Faux
    end = ???

    # Si la tête du serpent est dans le corps
    ???
        self.window.addstr(self.hauteur // 2,
                           self.largeur // 2 - 4,
                           'GAME OVER !',
                           curses.color_pair(4))
    self.window.refresh()
    curses.napms(2000)

        # flag devient Vrai
    end = ???
    return end

Question :

  • Modifier le programme principal pour mettre fin au jeu lorsque le serpent se mord la queue. Pour le moment, le jeu ne peut se terminer que si le joueur appui sur la touche Echap

3.7. Gestion des niveaux de jeu

3.7.1. Principe

Pour faire simple, nous allons changer de niveau chaque fois que le serpent aura mangé 5 pommes. Le changement de niveau aura un effet sur :

  • la vitesse de déplacement du serpent : self.window.timeout(int(150 * (1 / niveau + 1 / (niveau * 2)))) dans la méthode deplacement() de la classe Serpent

  • le nombre de pommes : on ajoutera une pomme à chaque nouveau niveau

3.7.2. Codage de la gestion du changement de niveau

Question :

  • Compléter la méthode calcul_niveau() de la classe Jeu qui retourne le niveau augmenté de 1 tous les 5 points (attribut score).

Rappel : La classe jeu dispose d’un attibut niveau affiché par la méthode affiche_score()

def calcul_niveau(self) -> int:
    # niveau augmente de 1 tous les 5 points
    ???

    # 10 est le dernier niveau
    # après ça va trop vite !
    if self.niveau > 10:
        self.niveau = ???

    return ???

Question :

  • Modifier le programme principal pour acquérir le niveau courant, le sauvegarder et le comparer au niveau précédant. Si le niveau a augmenté, on instancie une nouvelle pomme que l’on ajoute à la liste des pommes et que l’on affiche dans l’aire de jeu.

  • Tester, corriger les problèmes éventuels et jouer à volonté !

snake final

4. On joue !

Amusez vous et quand vous en aurez assez, modifiez le scénario, les bords de la fenêtre deviennent mortel, on affiche plusieurs pommes qu’il faut mangées pour changer de niveau, on sauvegarde dans un fichier texte les meilleurs scores, …​.

Faites preuve d’imagination !