T2.2 Programmation Orientée Objet : TP Morpion

1. Introduction

Le but de ce TP est ici de modéliser le jeu du Morpion, en utilisant le paradigme de la programmation orientée objet, et de mettre en pratique les différents éléments vus dans le cours précédent.

Nous ferons une version sans interface graphique.

Rappel des règles du morpion

Deux joueurs s'affrontent. Ils doivent remplir chacun à leur tour une case de la grille avec le symbole qui leur est attribué : O ou X. Le gagnant est celui qui arrive à aligner trois symboles identiques, horizontalement, verticalement ou en diagonale. Il est coutume de laisser le joueur jouant X effectuer le premier coup de la partie.

Une partie gagnée par le joueur X :

Une partie nulle :

En raison du nombre de combinaisons limité, l'analyse complète du jeu est facile à réaliser : si les deux joueurs jouent chacun de manière optimale, la partie doit toujours se terminer par un match nul.

Visualisez l'extrait vidéo ci-dessous du film Wargames, dans lequel une IA va jouer contre elle même au morpion, pour arriver à la conclusion qu'il n'y a pas de possibilité de gagner et ainsi sauver le monde de la 3e guerre mondiale ...

2. Implémentation, création d'une classe Morpion

Nous allons donc ici créer une classe Morpion, qui contiendra toutes les méthodes et tous les attributs nécessaires à la réalisation du jeu.

Cette classe contiendra les méthodes suivantes (en dehors de la fonction d'initialisation) :

  1. Fonction jouer, qui permet à un joueur de jouer son tour. Cette fonction prend 2 arguments en entrée :
    • le joueur
    • la case jouée.
  2. Fonction afficher_plateau, qui comme son nom l'indique affichera le plateau du jeu (qui ne prend aucun argument en entrée).
  3. Fonction test_fin_jeu, qui teste si un joueur est vainqueur, ou s'il n'y a plus de mouvement possible (qui prend en argument le joueur en entrée).
  4. Fonction jeu, qui contiendra le code qui assure le bon déroulement du jeu en lui-même (qui ne prend aucun argument en entrée).
1. Créer la classe Morpion

Exercice de code : Créez votre classe morpion

(n'oubliez pas la fonction d'initialisation **__init__** )


**Rappel:** les fonctions qui n'ont pas d'argument en entrée ne doivent pas s'écrire () mais (self)

Créez simplement les fonctions avec les bons arguments, et dans le corps de la fonction mettez seulement l'instruction pass, nous allons les remplir après.

Quand vous allez exécuter ce code, il ne se passera rien, ce qui est normal. Mais si lors de l'exécution, votre programme ne passe pas les tests, c'est qu'il y a des erreurs de syntaxe, et vous devez les corriger.

3. Implémentation, création des différentes fonctions de la classe Morpion

Maintenant, nous allons devoir écrire le contenu de chacune de ces fonctions.

2. Créer la Fonction __init__

Dans la fonction __init__, le plateau de jeu est initialisé avec des “-”, le joueur 1 est initialisé avec une croix X et le joueur 2 est initialisé avec un rond O.

Cela signifie que partout sur le plateau ou il y a des tirets, les joueurs n'ont pas encore choisi ces emplacements.

Partout ou il y a des croix cela signifie que le joueur 1 a joué et enfin, partout où il y a des ronds, cela signifie que le joueur 2 a joué.

Nous allons donc déclarer 3 attributs :

  • un attribut nommé plateau, qui contiendra le plateau du jeu. Comme vu ci-dessus, il sera rempli de tirets. Donc ici, nous initialiserons notre plateau avec des '-', de façon à afficher un plateau de 3*3 tirets .
    (Pensez à une liste de listes, avec une liste comprenant 3 listes qui contiennent chacun les 3 tirets).
    Voici ci-dessous le plateau au début du jeu :
                   ---
                   ---
                   ---
  • un attribut J1, qui contiendra un 'X'
  • un attribut J2, qui contiendra un 'O'

Avant d'implementer cette fonction , recopier et coller ci-dessous le code de la classe morpion

Exercice de code : Créez le constructeur
complétez la fonction __init__ Aide 1
3. Créer la fonction jouer

Avant d'appeler la fonction jouer, on commence (dans le programme principal) par lire au clavier 2 nombres (séparés par une virgule) qui sont les n° de ligne, de colonne et de la position où le joueur courant souhaite jouer (1 à 3 en abscisse, puis 1 à 3 en ordonnée). Le joueur entrera par exemple 2,2 pour jouer la case au centre du plateau.

(attention, s'il joue la case 1,1, en python il faudra transcrire : ligne = 0, colonne = 0).

La méthode jouer prend deux arguments :

joueur : str : "X" ou "O"

case : une chaine lue au clavier telle que décrit ci-dessus

Le signe (X ou O) du joueur courant est par la suite écrit sur le plateau à la position précédemment indiquée.

Danc cette fonction vous devrez donc affecter la marque du joueur à la position x et y du plateau (attention aux indices). joueur et case_jouee sont 2 arguments de la fonction. Il faut décoder la case, par exemple "1,1" doit être compris comme ligne=0 et colonne=0.

Ecrivez bien les spécification de la fonction.

Implémentez, ci-dessous, cette fonction (qui prend en argument le joueur en entrée 'X' ou 'O')


(Vous recopierez la classe définie précédemment dans cet exercice (classe + fonction init + les autres fonctions non complétée sauf la fonction jouer).

Exercice de code : Implémenter la méthode jouer
complétez la fonction jouer Aide2 Aide1
4. Créer la Fonction afficher_plateau

La fonction afficher_plateau parcourt le plateau et l’affiche en veillant à bien séparer chaque signe par deux barres “|” pour que cela soit plus lisible

Voilà comment le plateau sera affiché au début du jeu :

|-|-|-|
|-|-|-|
|-|-|-|
Comme le plateau est stocké dans une liste de listes, il faudra utiliser 2 boucles imbriquées. Dans le premier exercice vous avez un exemple, qui donne un résultat assez proche de ce qui est demandé ici. Inspirez vous de cet exemple pour implementer la fonction afficher_plateau

N'oubliez pas de recopier la classe et les fonctions déjà définies.
Exercice de code : Implémenter afficher_plateau
Complétez la fonction afficher_plateau Aide
5. Créer la Fonction test_fin_jeu

test_fin_jeu va vérifier si l’un des deux joueurs a gagné ou s’il n’y a pas égalité.

Pour cela elle va vérifier qu’il n’y a pas 3 fois le même signe aligné ni en horizontal, ni en vertical, ni en diagonal.

Elle va également vérifier qu'il reste de la place pour jouer en vérifiant qu’il reste des signes “-” sur le plateau.

Il y aura 3 cas possibles :

  • Soit le joueur passé en argument aura gagné, et dans ce cas là cette fonction renverra ce joueur.
  • Soit il n'y a plus de place sur le plateau, et dans ce cas on renverra True
  • Soit le jeu n'est pas encore fini, et dans ce cas on retournera False

Ces valeurs de retour seront utilisées dans la fonction principale, jeu, que nous implémenterons en dernier.

Cas où un joueur gagne :

  • Un joueur gagne s'il a aligné 3 fois son signe en horizontal
  • Un joueur gagne s'il a aligné 3 fois son signe en vertical
  • Un joueur gagne s'il a aligné 3 fois son signe en diagonale Pour tester ce dernier cas, vous pourrez si vous voulez faire 2 tests : 1 pour la première diagonale, et un pour la seconde (attention aux indices de vos boucles imbriquées).

A vous de jouer ...
Pour la mise au point, n'hésitez pas à insérer des print dans votre fonction pour voir les résultats intermédiaires, vous les enlèverez ensuite....

Exercice de code : Implémenter la méthode test_fin_jeu
Comme pour les autres exercices, recopiez votre classe, puis ajoutez les instructions pour la fonction test_fin_jeu(). Aide Diagonales Aide Diagonales++ Aide lignes Aide colonnes Aide jeu terminé ? Aide rien ne marche
6. Créer la Fonction jeu

La fonction jeu va lancer le jeu en affichant premièrement le tableau, puis en appelant les fonctions définies plus haut.

Tant que l'un des deux joueurs n’aura pas gagné, elle va les faire jouer chacun leur tour (vous pouvez commencer par faire jouer J1, puis ensuite J2, et ainsi que suite), puis afficher le nouveau plateau et tester que le jeu n’est pas fini.

Vous pourrez utiliser une boucle infinie, en utilisant un booléen qui changera de valeur quand la partie sera terminée.

Si le jeu est fini on sort de la fonction en renvoyant le signe du joueur gagnant (X ou O) ou en renvoyant égalité si c’est une égalité.

Exercice de code : Implémenter la méthode jeu qui lance le jeu
Ajoutez les instructions pour la fonction jeu(). Aide

Améliorations possibles

  • Quand le joueur entre son abscisse et son ordonnée, il faut vérifier que:
    • L'abscisse et l'ordonnée entrés par le joueur sont bien des nombres compris entre 1 et 3, sans quoi il faut leur redemander.
    • Qu'il n'y a pas déjà de X ou de O à l'abscisse et l'ordonnée entrés par le joueur, sans quoi il faut leur redemander.
  • Peut être mettre un menu, puis un choix de niveau de difficulté qui pourrait mettre un temps limite pour répondre.
  • Utiliser la librairie Pygame pour améliorer l'aspect graphique (demander au professeur de vérifier votre travail avant d'attaquer la partie graphique avec Pygame).

Interface graphique

4. Partie graphique avec PyGame

Le but est ici de retranscrire notre travail effectué ici, pour l'afficher proprement avec Pygame.

Nous améliorerons également l'ergonomie du logiciel : ici plus rien à taper au clavier, on pourra directement cliquer sur la case choisie avec la souris pour choisir où mettre son X ou son O

4.1 Changements à apporter dans la structure du programme

  • La première chose à faire sera de copier votre code (fonctionnel) dans edupython, ce sera votre base de départ. Nous ne continuons pas à coder dans Google Coolab car la librairie Pygame n'est pas compatible (l'environnement Python est sur une machine distante, et Pygame a besin d'accéder à la carte graphique de votre ordinateur, ce qui est impossible).
  • Nous allons également faire des changements dans la structure du programme. Nous allons garder notre classe Morpion, mais nous allons créer une 2e classe, intitulée Grille, qui s'occupera uniquement de gérer l'affichage. Et nous créerons une instance de cette classe à l'initilisation de la classe Morpion.
  • Dernière chose, n'oubliez pas d'importer la librairire pygame (évidemment) mais également la librairie sys.

Commençons par le commencement ...

4.2 Création de la fenêtre et de la grille

Pour commencer, nous allons donc créer une nouvelle classe nommée Grille

La fonction d'initialisation prendra un argument en entrée (nommé ecran). Nous verrons son utilité plus tard.

Cettte fonction comprendra :

  • Un attribut ecran que l'on initialisera avec la valeur ecran passée en paramètre.
  • Un attribut lignes qui sera une liste qui contient 4 tuples, avec des coordonnées de points pour tracer les 4 lignes de la grille. Utilisez les valeurs suivantes :[( (200,0),(200,600)), ((400,0),(400,600)), ((0,200),(600,200)), ((0,400),(600,400))]
  • Un attribut grille qui sera un tableau de tableau, qui correspond à notre plateau créé ci-dessus. On l'initialisera avec les valeurs None cette fois-ci (et non plus avec les tirets)

On ajoutera également une fonction afficher pour afficher la grille (qui ne prendra pas d'arguments).

On parcourera juste notre attribut lignes créé ci-dessus, pour tracer des lignes en utilisant la méthode draw.line de pygame :

for ligne in self.lignes :

            pygame.draw.line(self.ecran,(0,0,0),ligne[0],ligne[1],2)
Maintenant, intéressons nous à notre classe Morpion

Nous allons modifier la fonction d'initialisation, en créant la fenêtre Pygame (rappelez vous son fonctionnement)

Dans cette fonction (sans argument ), ajouter :

  • Un attribut ecran, qui créera la fenêtre Pygame:
pygame.display.set_mode((600,600))
  • Un attribut grille, qui sera une instance de la classe Grille (avec en paramètre l'attribut ecran)

Nous allons également modifier la fonction jeu;

Rappelez vous, Pygame fonctionne avec des événements. Il faut donc rajouter une boucle For sur ces événements, et prévoir la sortie :

for event in pygame.event.get():

                if event.type == pygame.QUIT:
                    sys.exit()
Rajouter également ces 2 instructions à la suite de la boucle for, pour ajouter une couleur et rafraichir l'affichage :

self.ecran.fill((240,240,240))
pygame.display.flip()
Enfin, à la suite de ces 2 lignes, nous allons appeler la méthode afficher de l'attribut grille (qui est une instance de la classe Grille, dans laquelle nous avons une méthode afficher)

4.3 Création et affichage des X/O

Nous allons utiliser la souris pour placer nos X et nos O Pour cela, nous utiliserons la méthode mouse.getpos() de pygame. Cette fonction nous renvoie les coordonnées de l'endroit où l'on clique. Or, nos cases font 200 pixels de large et 200 pixels de long.

Comment faire pour convertir facilement ces coordonnées sous forme d'indices qui seront plus facilement utilisables ? Nous allons utiliser la division entière par 200 : nous aurons alors des indices de position.

Rajoutez ces lignes dans la boucle principale de la fonction jeu (nous testons si on a cliqué avec les bouton gauche de la souris):

if event.type == pygame.MOUSEBUTTONDOWN and pygame.mouse.get_pressed()[0]:                 
                    position = pygame.mouse.get_pos() 
                    position_x ,position_y = position[0]//200 ,position[1]//200 

                    if self.compteur % 2 == 0 :
                                  self.grille.fixer_la_valeur(position_x, position_y, self.J1)
                    else:
                                  self.grille.fixer_la_valeur(position_x, position_y, self.J2)
Si vous regardez les dernières lignes, nous testons la parité d'un attribut compteur. Quand il est pair, c'est J1 qui joue, sinon c'est l'inverse.

Nous allons définir la méthode fixer_la_valeur de notre attribut grille juste après.

N'oubliez pas de rajouter également dans la fonction d'initialisation de la classe Morpion l'attribut compteur, qui sera initialisé à 0.

Retour à la classe Grille :

  • Rajouter une nouvelle méthode : fixer_la_valeur.

Cette méthode sera utilisée pour modifier une valeur de la grille, avec X ou O. Elle prendra 3 paramètres : x,y, et la valeur

Testez si la valeur de la grille d'abscisse x et d'ordonnée y est égale à None. Si c'est le cas, on modifie cette valeur de la grille avec valeur

  • Rajoutez un attribut compteur_on, qui sera initialisé à False, qui va nous permettre de définir si le compteur est actif ou pas
    • Mettez le à True quand vous avez modifié une valeur dans fixer_le_valeur
    • Mettez le à False dans la boucle principale, en testant si le compteur est actif, à savoir juste après avoir testé la parité du compteur :
if self.compteur % 2 == 0 :#1
                    self.grille.fixer_la_valeur(position_x, position_y, self.J1)
else:
                    self.grille.fixer_la_valeur(position_x, position_y, self.J2)

if self.grille.compteur_on: #1
                    self.compteur += 1
                    self.grille.compteur_on = False
Enfin, dernière chose avant de tester :

Dans la méthode afficher de la classe Grille, nous affichons déjà les 4 lignes qui font la grille.

Mais il serait bien d'afficher les X et les O également, suivant les valeur de la grille ?

Pour cela, nous allons parcourir les éléments de la grille (double boucle), puis quand nous allons trouver un 'X', nous allons dessiner un X avec la méthode draw.line, et idem pour O avec la méthode draw.circle. Nous utiliserons les fonctions de dessins de Pygame.

Je vous épargne cette recherche fastidieuse qui n'est pas le but du TP ici, et je vous donne les instructions à rajouter :

for y in range(0,len(self.grille)):
            for x in range(0,len(self.grille)):


                    if self.grille[y][x] == 'X' :

                        pygame.draw.line(self.ecran, (0, 0, 0), (x * 200, y * 200), (200 + (x * 200), 200 + (y * 200)), 3) 
                        pygame.draw.line(self.ecran, (0, 0, 0), ((x * 200), 200 + (y * 200)), (200 + (x * 200), (y * 200)),
                                     3)

                    elif self.grille[y][x] == 'O' :

                        pygame.draw.circle(self.ecran, (0, 0, 0), (100 + (x * 200), 100 + (y * 200)), 100, 3)

4.4 Vainqueur et fin de partie

Nous y sommes presque. Il vous reste à réutiliser la méthode test_fin_jeu définie ci-dessus, qui nous permettait de savoir qui était vainqueur.

En effet, nous ne parcourons plus le plateau, mais la grille

Attention cependant si vous remplacez juste plateau par grille, cela risque de ne pas fonctionner et de générer le message d'erreur suivant :

TypeError: 'Grille' object does not support indexing

A vous de voir pourquoi il y a ce message d'erreur, et quelle modification, liée à la nature de l'attribut grille il faut effectuer

Un peu de ménage de code :

N'oubliez pas de supprimer certaines fonctions qui ne sont plus utiles, comme afficher_plateau et jouer de la classe Morpion qui ne sont plus utilisées.

BON JEU !!!

4.5 Aller plus loin

Pour ceux qui ont une version fonctionnelle, vous pouvez faire un menu de démarrage, avec la possiblité de rentrer des noms pour les joueurs, et de recommencer une partie.

Et également, pour ceux qui le souhaitent, programmer une IA qui pourrait jouer contre vous.

Mais ceci est une autre histoire ...