Créer le jeu du Morpion en Python

python jeu morpion

Créer le jeu du Morpion en Python

Vous souhaitez créer le jeu du Morpion en Python ? Je m’y suis collé… et ce ne fut pas de tout repos!

Mais comme je suis très gentil, je vous explique tout !

Le jeu du Morpion en Python: réflexions préliminaires

Je ne veux absolument pas un jeu tout pourri en mode console… Seul le mode graphique sera accepté.

Il faudrait aussi deux niveaux : “Facile” et “Difficile”.

Il faut aussi que le joueur ait la possibilité de commencer le jeu ou de laisser la main à l’ordinateur selon ses envies.

Je veux tenir des statistiques sur les parties (gagnées, perdues et les matchs nuls).

Voilà. Avec ça, j’ai matière à faire…

Maintenant, quel paradigme de programmation vais-je utiliser ? Bon, je vais m’en tenir à la POO parce que j’ai plus l’habitude. Quant à la solution graphique, je vais utiliser Tkinter pare que QT… je ne suis pas assez doué ! 🙂

Le jeu du Morpion: la base

Je vais créer une classe Morpion qui représentera mon jeu.

Qui dit classe dit constructeur. Je vais y aller doucement en définissant d’abord la fenêtre:

from tkinter import Tk, Canvas

class Morpion:
    def __init__(self, window):
        window.geometry("750x505")
        window.title("Le jeu du Morpion - Stéphane Pasquet (mathweb.fr)")
        Label(window, text="Le jeu du Morpion (OXO)", font=("Helvetica", 20)).place(x=190,y=1)
        
        self.game = Canvas(window, width = 405, height = 405, bg = "white")        

if __name__ == '__main__':    
    root = Tk()
    Morpion(root)  

Il faut maintenant que je place une grille… je vais définir une méthode pour cela :

    def draw_grid(self):
        self.game.create_rectangle(0,0,405,405,fill='white')
        for x in [ 4 , 135 , 270 , 405 ]:
            self.game.create_line(0 , x , 405 , x , fill = 'black' , width = 4)
            self.game.create_line(x , 0 , x , 405 , fill = 'black' , width = 4)
        
        self.game.place(x = 0 , y = 50)

Bien entendu, il faut que j’ajoute la ligne suivante dans le constructeur :

self.draw_grid()

Maintenant, je dois ajouter les menus; je vais donc ajouter à mon appel à tk quelques commandes :

from tkinter import Tk, Canvas, Label, StringVar, Button, OptionMenu, DISABLED, NORMAL

et à mon constructeur les lignes suivantes :

        # Menu de droite
        
        Label(text="Quel niveau ?", font=('Helvetica', 12), fg='red').place(x = 420, y = 50)
        self.LevelList = [ "?????" , "Facile", "Difficile" ]
        self.LevelVar = StringVar(window)
        self.LevelVar.set(self.LevelList[0])
        self.LevelOptions = OptionMenu(window, self.LevelVar, *self.LevelList)
        self.LevelOptions.config(width = 10, font=('Helvetica', 12))
        self.LevelOptions.place(x = 600 , y = 44)
        self.niveau = self.LevelVar.trace("w", self.jeu)
    
        Label(text="Qui commence à jouer ?", font=('Helvetica', 12), fg='red').place(x = 420, y = 100)
        self.FirstPlayerList = [ "?????" , "Joueur", "Ordinateur" ]
        self.FirstPlayerVar = StringVar(window)
        self.FirstPlayerVar.set(self.FirstPlayerList[0])
        self.FirstPlayerOptions = OptionMenu(window, self.FirstPlayerVar, *self.FirstPlayerList)
        self.FirstPlayerOptions.config(width = 10, font=('Helvetica', 12))
        self.FirstPlayerOptions.place(x = 600 , y = 94)
        self.player_one = self.FirstPlayerVar.trace("w", self.jeu)
        
        # bouton "rejouer"
        
        self.rejouer = Button(window , text = 'Rejouer' , state=DISABLED , command=self.restart)
        self.rejouer.place(x = 560 , y = 400)
        
        # bouton "voir les stats"
        
        self.stats = Button(window , text = 'Voir les statistiques'  , command=self.visu_stats)
        self.stats.place(x = 530 , y = 450)

Bien entendu, les méthodes appelées par les boutons ne sont pas encore écrites et tel quel, le programme ne tourne pas encore… Je vais donc ajouter les méthodes vides jeu, restart et visu_stats:

    def jeu(self):
        return None
    
    def restart(self):
        return None
    
    def visu_stats(self):
        return None

Là, on peut lancer le programme:

N’essayez pas encore de choisir le niveau du jeu, car cela ne fonctionne pas encore.

Avant de définir les méthodes vides, j’aimerais définir les symboles (croix et cercles). Cela se fait toujours dans le constructeur:

        self.image_croix = Image.open("croix.png")
        self.croix = ImageTk.PhotoImage(self.image_croix)
        
        self.image_cercle= Image.open("cercle.png")
        self.cercle = ImageTk.PhotoImage(self.image_cercle)
        
        self.text_gamer = StringVar()
        self.texte_joueur = Label(root , textvariable=self.text_gamer , font=('Helvetica', 12), fg='blue').place(x = 420, y = 150)
        
        self.text_result = StringVar()
        self.texte_result = Label(root , textvariable=self.text_result , font=('Helvetica', 12), fg='purple').place(x = 420, y = 200)

Bien entendu, les images “croix.png” et “cercle.png” seront à placer dans le répertoire courant.

Mais n’oublions pas d’appeler le module PIL :

from PIL import Image, ImageTk

Il ne reste plus qu’à conclure ce constructeur en appelant la méthode restart, qui initialise (ou réinitialise selon les cas) la grille.

self.restart() # initialisation

Le jeu du Morpion en Python: initialisation

Il faut avant tout penser à la représentation de la grille: comme souvent, cette dernière pourra être représentée par une matrice 3×3, ou si vous le souhaitez un array (tableau) à deux dimensions.

    def restart(self):
        self.G = [ [0,0,0] , [0,0,0] , [0,0,0] ]
        self.nb_cercles = 0
        self.nb_croix = 0
        self.text_gamer.set(' '*100)
        self.text_result.set(' '*100)
        self.draw_grid()
        self.FirstPlayerVar.set(self.FirstPlayerList[0])
        self.LevelVar.set(self.LevelList[0])
        self.rejouer.config(state=DISABLED , bg = 'gray')

La grille est donc ici représentée par le tableau self.G, initialement rempli de 0.

On définit par là même le nombre de cercles et de croix (0 pour commencer) car cela va nous être utile par la suite pour contrôler si le jeu est fini ou non.

J’ai aussi choisi de mettre un appel à la méthode draw_grid car c’est plus simple (pour rejouer une autre partie, il faudra vider la grille ou plus simplement la redessiner vierge). Il faudra donc supprimer cet appel qui apparaissait dans le constructeur (inutile de faire appel à la même fonction deux fois de suite).

Les autres lignes initialisent les variables self.FirstPlayerVar et self.LevelVar (le premier joueur et la niveau) à “rien”.
les variables self.text_gamer et self.text_result ne servent qu’à afficher du texte (qui va gagner et s’il y a match nul).

Le jeu du Morpion: le pointeur de la souris

Il faut bien entendu que le clic du bouton gauche de la souris soit détecté et affiche le symbole du cercle (celui du joueur). On écrit alors une méthode pour cela:

    def valid_pointeur(self,event):
        if self.gameOn == True:
            x , y = event.x//135, event.y//135
            self.G[y][x] = 'O'
            self.nb_cercles += 1
            
            self.game.create_image(68 + x * 135 , 68 + y * 135, anchor = 'center', image = self.cercle)
            
            if self.fin_partie()[0] == True:       
                self.final(self.fin_partie()[1])
            else:
                self.play(1 , self.LevelVar.get() )

J’ai ajouté une petite variable booléenne self.gameOn histoire de le pas pouvoir ajouter de cercle après que le jeu soit fini…

À chaque clic, on doit contrôler si le jeu est fini ou pas; c’est la mission de la méthode fin_partie qui renvoie un couple (bool , who): le premier nombre dit si la partie est finie (True) ou pas (False) et le second, qui a gagné (ordinateur ou joueur).

    def fin_partie(self):
        # on teste s'il y a alignement
        if ( self.G[0][0] == self.G[1][0] and self.G[1][0] == self.G[2][0] and self.G[0][0] != 0) \
                   or ( self.G[0][1] == self.G[1][1] and self.G[1][1] == self.G[2][1] and self.G[0][1] != 0) \
                   or ( self.G[0][2] == self.G[1][2] and self.G[1][2] == self.G[2][2] and self.G[0][2] != 0) \
                   or ( self.G[0][0] == self.G[0][1] and self.G[0][1] == self.G[0][2] and self.G[0][0] != 0) \
                   or ( self.G[1][0] == self.G[1][1] and self.G[1][1] == self.G[1][2] and self.G[1][0] != 0) \
                   or ( self.G[2][0] == self.G[2][1] and self.G[2][1] == self.G[2][2] and self.G[2][0] != 0) \
                   or ( self.G[0][0] == self.G[1][1] and self.G[1][1] == self.G[2][2] and self.G[0][0] != 0) \
                   or ( self.G[0][2] == self.G[1][1] and self.G[1][1] == self.G[2][0] and self.G[0][2] != 0):
            if self.FirstPlayerVar.get() == 'Ordinateur' and self.nb_croix > self.nb_cercles:
                return ( True , "Ordinateur" )
            elif self.FirstPlayerVar.get() == 'Ordinateur' and self.nb_croix < self.nb_cercles:
                return ( True , "Joueur" )
            elif self.FirstPlayerVar.get() == 'Joueur' and self.nb_croix < self.nb_cercles:
                return ( True , "Joueur" )
            else:
                return (True , "Ordinateur")
        # sinon on teste si la grille est pleine
        elif self.nb_croix + self.nb_cercles == 9:
            return (True , None)
        else:
            return ( False , None )

Pour le reste…

La méthode du jeu à proprement parlé est propre à chaque programmeur. Il y a quelque part de l’IA derrière le mode “difficile”. Je ne suis pas un expert en IA et ma façon de programmer cette partie est propre à ma façon de penser. Elle n’est pas optimale…

Les abonné·e·s de ce site trouveront les images et le code source Python du programme complet ci-dessous:

Cette partie est réservée aux abonné.e.s de ce site.
Si vous êtes abonné·e·s, merci de vous identifier pour visualiser le contenu dans son intégralité.
Stéphane Pasquet
Stéphane Pasquet

1 réflexion au sujet de « Créer le jeu du Morpion en Python »

Nicolas PatroisPublié le  9:35 - Sep 14, 2021

Il y a moins de 9!=362880 parties différentes, on peut donc toutes les explorer systématiquement.
L’ordinateur peut chercher une stratégie gagnante (ou non perdante) s’il en existe une.

    Stéphane Pasquet

    Stéphane PasquetPublié le  9:53 - Sep 14, 2021

    Pour ma part, je ne me vois pas explorer autant de parties différentes. C’est ce que je voulais dire dans l’article: tout dépend du programmeur… Moi, je ne suis pas le plus vaillant et le plus patient 🙂 Comme je ne suis pas payé pour ça, je ne passerais pas mes journées à le faire 🙂

Nicolas PatroisPublié le  3:37 - Sep 15, 2021

Alors laisse Python le faire à ta place.

Laissez votre message