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:
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.
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 🙂
Alors laisse Python le faire à ta place.