Formation I.S.N.

Un morpion pas à pas



Pour appréhender la notion d'événement, nous allons finaliser la programmation du jeu de morpion.

Tout d'abord, reprenons l'interface graphique programmée dans cette page et sauvegardée normalement dans un fichier intitulé morpion02.py. Ajoutons à ce programme l'événement « clic dans le canevas » qui permet d'afficher les coordonnées de la souris dans la zone de texte en haut de la fenêtre :


##-----Importation des Modules-----##
from tkinter import *


##----- Définition des Variables globales -----##



##----- Définition des Fonctions -----##
def afficher(event) :
    """Cette fonction affiche en temps réel les coordonnées de la souris
    obtenues par « event.x » et « event.y ».
    La zone de texte est mise à jour grâce à la méthode .configure()."""
    abscisse = event.x
    ordonnee = event.y
    message.configure(text='Clic en X = {0} et Y = {1}'.format(abscisse, ordonnee))

##-----Création de la fenêtre-----##
fen = Tk()
fen.title('Morpion')


##-----Création des zones de texte-----##
message=Label(fen, text='Ici du texte.')
message.grid(row = 0, column = 0, columnspan=2, padx=3, pady=3, sticky = W+E)


##-----Création des boutons-----##
bouton_quitter = Button(fen, text='Quitter', command=fen.destroy)
bouton_quitter.grid(row = 2, column = 1, padx=3, pady=3, sticky = S+W+E)

bouton_reload = Button(fen, text='Recommencer')
bouton_reload.grid(row = 2, column = 0, padx=3, pady=3, sticky = S+W+E)


##-----Création du canevas-----##
dessin=Canvas(fen, bg="white", width=301, height=301)
dessin.grid(row = 1, column = 0, columnspan = 2, padx=5, pady=5)


##-----La grille-----##
lignes = []
for i in range(4):
    lignes.append(dessin.create_line(0, 100*i+2, 303, 100*i+2, width=3))
    lignes.append(dessin.create_line(100*i+2, 0, 100*i+2, 303, width=3))


##-----Evenements-----##
dessin.bind('<Button-1>', afficher)


##-----Programme principal-----##
fen.mainloop()                      # Boucle d'attente des événements
				

Remarque

On rappelle que les captures d'écrans et les calculs de coordonnées dans le canevas ont été réalisées sous système d'exploitation Windows. Les décalages de pixels pour avoir un affichage optimal varient d'un système d'exploitation à l'autre. Sous Windows, ce décalage est de 2 pixels.

Coordonnées et clic dans la grille

Avant toute chose, enregistrez le travail précédent sour un nouveau nom, par exemple Morpion03.py, afin de ne pas perdre la trace du travail déjà réalisé...

Dans cette partie, il faut reprendre l'affichage pour que l'évènement « clic dans le canevas » retourne les numéros de ligne et de colonne du clic plutôt que les coordonnées de la souris (cf. illustration ci-contre).

  1. Les numéros de lignes sont-ils en abscisse ou en ordonnée ?
  2. Même question pour les numéros de colonnes :
  3. Dans les images précédentes, le clic a lieu, dans le canevas, aux coordonnées (175 ; 40). Il devient un clic en ligne 0 et colonne 1.
    Quel(s) calcul(s) permet(tent) de passer des valeurs (175 ; 40) aux valeurs (0 ; 1) ?

Modifier la fonction afficher() afin que, lors du clic de souris dans le canevas, s'affichent dans la zone de texte la ligne et la colonne de la case cliquée.

  • Une piste
  • Une solution

La division euclidienne, méthode de calcul fondamentale en programmation, doit devenir un réflexe. En Python, si a et b sont deux entiers alors :

  • a//b renvoie le quotient entier de la division euclidienne de a par b.
  • a%b renvoie le reste entier de la division euclidienne de a par b.

##----- Importation des Modules -----##
from tkinter import *


##----- Définition des Variables globales -----##



##----- Définition des Fonctions -----##
def afficher(event) :
    """ Entrées : Un événement de la souris
        Sortie : Affiche en temps réel les coordonnées de la case du clic de souris"""
    abscisse = event.x
    ordonnee = event.y
    l = (ordonnee-2)//100                  # Ligne du clic
    c = (abscisse-2)//100                  # Colonne du clic
    message.configure(text='Clic en ligne = {} et colonne = {}'.format(l, c))


##----- Création de la fenêtre -----##
fen = Tk()
fen.title('Morpion')


##----- Zones de texte -----##
message = Label(fen, text='Ici du texte.')
message.grid(row = 0, column = 0, columnspan=2, padx=3, pady=3, sticky = W+E)


##----- Boutons -----##
bouton_quitter = Button(fen, text='Quitter', command = fen.destroy)
bouton_quitter.grid(row = 2, column = 1, padx = 3, pady = 3, sticky = S+W+E)

bouton_reload = Button(fen, text='Recommencer')
bouton_reload.grid(row = 2, column = 0, padx = 3, pady = 3, sticky = S+W+E)


##----- Canevas -----##
dessin = Canvas(fen, bg="white", width=301, height=301)
dessin.grid(row = 1, column = 0, columnspan = 2, padx = 5, pady = 5)


##----- La grille -----##
lignes = []
for i in range(4):
    lignes.append(dessin.create_line(0, 100*i+2, 303, 100*i+2, width=3))
    lignes.append(dessin.create_line(100*i+2, 0, 100*i+2, 303, width=3))


##-----Evenements-----##
dessin.bind('<Button-1>', afficher)


##----- Programme principal -----##
dessin.bind('<Button-1>', afficher)
fen.mainloop()                      # Boucle d'attente des événements
						

Préparer le tracé des figures

L'image ci-contre représente la case située à la colonne c et à la ligne l. Les abscisses $x_1$ et $x_2$ sont celles des lignes de pixels gauche et droite encadrant la case, les ordonnées $y_1$ et $y_2$ sont celles des lignes encadrant au-dessus et en-dessous.

  1. Sur un papier, exprimez les valeurs des coordonnées $x_1$ ; $x_2$ ; $y_1$ et $y_2$ en fonction des valeurs c et/ou l.
  2. Quelle instruction permet alors, en fonction de c et l, de tracer un cercle rouge d'épaisseur 5 pixels dans le carré tracé en pointillés sur cette image ?
  3. Modifiez la fonction afficher() pour qu'à chaque clic dans une case un cercle rouge s'affiche dans cette case.
  4. Quelles instructions permettent, en fonction de c et l, de tracer une croix bleue d'épaisseur 5 pixels dans le carré tracé en pointillés sur cette image ?
  5. Modifiez la fonction afficher() pour qu'à chaque clic dans une case une croix bleue s'affiche dans cette case.
  • Question 1°/
  • Aide pour le cercle
  • Aide pour la croix
  • Une solution
  • $x_1 = $100*c + 3
  • $x_2 = $100*c + 101
  • $y_1 = $100*l + 3
  • $y_2 = $100*l + 101

dessin.create_oval(........., ........., ........., .........)
						

dessin.create_line(........., ........., ........., .........)
dessin.create_line(........., ........., ........., .........)
						

Pour l'instant, un rond et une croix s'afficheront de manière superposée à chaque clic dans une case.


##-----Importation des Modules-----##
from tkinter import *


##----- Définition des Variables globales -----##



##----- Définition des Fonctions -----##
def afficher(event) :
    """Cette fonction affiche en temps réel les coordonnées de la souris
    obtenues par « event.x » et « event.y ».
    La zone de texte est mise à jour grâce à la méthode .configure()."""
    abscisse = event.x
    ordonnee = event.y
    l = (ordonnee-2)//100                  # Ligne du clic
    c = (abscisse-2)//100                  # Colonne du clic
    message.configure(text='Clic en ligne = {} et colonne = {}'.format(l, c))
    
    dessin.create_oval(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, outline = 'red')
    dessin.create_line(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, fill = 'blue')
    dessin.create_line(100*c+8, 100*l+96, 100*c+96, 100*l+8, width = 5, fill = 'blue')
        
            

##-----Création de la fenêtre-----##
fen = Tk()
fen.title('Morpion')


##-----Création des zones de texte-----##
message=Label(fen, text='Ici du texte.')
message.grid(row = 0, column = 0, columnspan=2, padx=3, pady=3, sticky = W+E)


##-----Création des boutons-----##
bouton_quitter = Button(fen, text='Quitter', command=fen.destroy)
bouton_quitter.grid(row = 2, column = 1, padx=3, pady=3, sticky = S+W+E)

bouton_reload = Button(fen, text='Recommencer')
bouton_reload.grid(row = 2, column = 0, padx=3, pady=3, sticky = S+W+E)


##-----Création du canevas-----##
dessin=Canvas(fen, bg="white", width=301, height=301)
dessin.grid(row = 1, column = 0, columnspan = 2, padx=5, pady=5)


##-----La grille-----##
lignes = []
for i in range(4):
    lignes.append(dessin.create_line(0, 100*i+2, 303, 100*i+2, width=3))
    lignes.append(dessin.create_line(100*i+2, 0, 100*i+2, 303, width=3))


##-----Evenements-----##
dessin.bind('<Button-1>', afficher)


##-----Programme principal-----##
fen.mainloop()                      # Boucle d'attente des événements
						

Utiliser un drapeau

A chaque clic de souris, il faut pouvoir alterner l'affichage d'un rond ou d'une croix. Pour cela, on utilise un drapeau, c'est-à-dire une variable globale booléenne.

  1. Initialisez la variable globale drapeau à la valeur True.
  2. Modifiez la définition de la fonction afficher() pour qu'à chaque clic une croix soit tracée lorsque drapeau = True et un rond soit tracé sinon.
  3. Le message en haut de la fenêtre passe de 'Aux croix de jouer' à 'Aux ronds de jouer'.
  4. Chaque clic doit modifier la valeur du drapeau pour alterner les affichages.
  • Une solution

##-----Importation des Modules-----##
from tkinter import *


##----- Définition des Variables globales -----##
drapeau = True


##----- Définition des Fonctions -----##
def afficher(event) :
    """Cette fonction affiche en temps réel les coordonnées de la souris
    obtenues par « event.x » et « event.y ».
    La zone de texte est mise à jour grâce à la méthode .configure()."""
    global drapeau
    abscisse = event.x
    ordonnee = event.y
    l = (ordonnee-2)//100                    # Ligne du clic
    c = (abscisse-2)//100                    # Colonne du clic
    if drapeau:                              # drapeau == True
        dessin.create_line(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, fill = 'blue')
        dessin.create_line(100*c+8, 100*l+96, 100*c+96, 100*l+8, width = 5, fill = 'blue')
        message.configure(text='Aux ronds de jouer')
    else:
        dessin.create_oval(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, outline = 'red')
        message.configure(text='Aux croix de jouer')
    drapeau = not(drapeau)
        
            

##-----Création de la fenêtre-----##
fen = Tk()
fen.title('Morpion')


##-----Création des zones de texte-----##
message=Label(fen, text='Aux croix de jouer')
message.grid(row = 0, column = 0, columnspan=2, padx=3, pady=3, sticky = W+E)


##-----Création des boutons-----##
bouton_quitter = Button(fen, text='Quitter', command=fen.destroy)
bouton_quitter.grid(row = 2, column = 1, padx=3, pady=3, sticky = S+W+E)

bouton_reload = Button(fen, text='Recommencer')
bouton_reload.grid(row = 2, column = 0, padx=3, pady=3, sticky = S+W+E)


##-----Création du canevas-----##
dessin=Canvas(fen, bg="white", width=301, height=301)
dessin.grid(row = 1, column = 0, columnspan = 2, padx=5, pady=5)


##-----La grille-----##
lignes = []
for i in range(4):
    lignes.append(dessin.create_line(0, 100*i+2, 303, 100*i+2, width=3))
    lignes.append(dessin.create_line(100*i+2, 0, 100*i+2, 303, width=3))


##-----Evenements-----##
dessin.bind('<Button-1>', afficher)


##-----Programme principal-----##
fen.mainloop()                      # Boucle d'attente des événements

						

Fusionner le programme et l'interface

A présent nous avons :

  • D'un côté un programme fonctionnel en mode console ;
  • D'un autre côté tous les éléments nécessaires pour l'interface graphique.

Il ne reste qu'à insérer tous les éléments du programme de jeu « dans » cette interface graphique. Pour cela, on reprend les variables globales cases, joueur, n et somme définies dans le programme originel (Morpion01.py) de jeu de morpion.

  1. Modifiez la fonction afficher() pour qu'à chaque clic la cellule correspondante dans cases soit remplie par -1 lorsque la case est occupée par un rond et par 1 lorsqu'elle est occupée par une croix.
  2. Améliorez cette fonction pour qu'elle ne trace pas de nouvelle figure (rond ou croix) si le clic est effectué dans une case déjà « occupée ».
  3. Reprendre la fonction de vérification d'alignement verif() définie dans le programme originel.
  4. Modifiez à nouveau la fonction afficher() pour qu'elle teste la position nouvellement entrée à l'aide de la fonction verif() puis qu'elle affiche, selon les cas, qui des ronds ou des croix a gagné.
  • Une solution

##-----Importation des Modules-----##
from tkinter import *


##----- Définition des Variables globales -----##
cases=[ [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]
drapeau = True


##----- Définition des Fonctions -----##
def afficher(event) :
    """Cette fonction affiche en temps réel les coordonnées de la souris
    obtenues par « event.x » et « event.y ».
    La zone de texte est mise à jour grâce à la méthode .configure()."""
    global drapeau, cases
    l = (event.y-2)//100                    # Ligne du clic
    c = (event.x-2)//100                    # Colonne du clic
    
    if cases[l][c] == 0:
        if drapeau:                              # drapeau == True
            dessin.create_line(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, fill = 'blue')
            dessin.create_line(100*c+8, 100*l+96, 100*c+96, 100*l+8, width = 5, fill = 'blue')
            cases[l][c] = 1
            message.configure(text='Aux ronds de jouer')
            
        else:
            dessin.create_oval(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, outline = 'red')
            cases[l][c] = -1
            message.configure(text='Aux croix de jouer')
    
        drapeau = not(drapeau)
        somme = verif(cases)
        if somme == 1:
            message.configure(text='Les croix ont gagné !')
        elif somme == -1:
            message.configure(text='Les croix ont gagné !')
 


def verif(tableau):
    """Calcule des sommes de chaque ligne/colonne/diagonale pour
        vérifier l'alignement. Elle renvoie le n° du gagnant."""
    sommes = [0,0,0,0,0,0,0,0]             # Il y a 8 sommes à vérifier
    # Les lignes :
    sommes[0] = sum(tableau[0])
    sommes[1] = sum(tableau[1])
    sommes[2] = sum(tableau[2])
    # Les colonnes
    sommes[3] = tableau[0][0]+tableau[1][0]+tableau[2][0]
    sommes[4] = tableau[0][1]+tableau[1][1]+tableau[2][1]
    sommes[5] = tableau[0][2]+tableau[1][2]+tableau[2][2]
    # Les diagonales
    sommes[6] = tableau[0][0]+tableau[1][1]+tableau[2][2]
    sommes[7] = tableau[0][2]+tableau[1][1]+tableau[2][0]

    for i in range(8):                     # Parcours des sommes
        if sommes[i] == 3:
            return 1
        elif sommes[i] == -3:
            return -1
    return 0
            

##-----Création de la fenêtre-----##
fen = Tk()
fen.title('Morpion')


##-----Création des zones de texte-----##
message=Label(fen, text='Aux croix de jouer')
message.grid(row = 0, column = 0, columnspan=2, padx=3, pady=3, sticky = W+E)


##-----Création des boutons-----##
bouton_quitter = Button(fen, text='Quitter', command=fen.destroy)
bouton_quitter.grid(row = 2, column = 1, padx=3, pady=3, sticky = S+W+E)

bouton_reload = Button(fen, text='Recommencer')
bouton_reload.grid(row = 2, column = 0, padx=3, pady=3, sticky = S+W+E)


##-----Création du canevas-----##
dessin=Canvas(fen, bg="white", width=301, height=301)
dessin.grid(row = 1, column = 0, columnspan = 2, padx=5, pady=5)


##-----La grille-----##
lignes = []
for i in range(4):
    lignes.append(dessin.create_line(0, 100*i+2, 303, 100*i+2, width=3))
    lignes.append(dessin.create_line(100*i+2, 0, 100*i+2, 303, width=3))


##-----Evenements-----##
dessin.bind('<Button-1>', afficher)


##-----Programme principal-----##
fen.mainloop()                      # Boucle d'attente des événements
						

Remarque importante

Lorsque des élèves se lancent, par groupe, dans un projet de programmation qui comporte une interface graphique, il faut fortement leur déconseiller une répartition des tâches du type : « l'un travaille sur le programme originel et l'autre travaille sur l'interface graphique ».

En effet, accepter une telle répartition des tâches est le meilleur moyen pour obtenir :

  • aucun résultat : ni le programme originel, ni l'interface ne fonctionnent car au lieu de réfléchir à deux, les élèves n'ont pas coopéré ;
  • deux programmes non fusionnables car ils auront été pensés de manière bien trop différente ;

Pour des élèves débutants en programmation, il faut vraiment les conduire à travailler de manière coopérative d'abord sur un programme fonctionnel sans interface graphique puis l'ajout d'une interface s'il leur reste du temps (et pas d'abord l'interface puis les algorithmes et le programme...). L'interface graphique n'est généralement pas le coeur du programme et doit donc toujours être ajoutée dans un second temps.

Finaliser le morpion

Plus tard ! Cette partie n'est pas à réaliser le jour de la formation (sauf s'il vous reste du temps bien évidemment). Elle est là pour vous donner d'autres idées d'exercices, approfondir vos connaissances ou vous entraîner si besoin.

Le programme précédent permet aux joueurs de continuer à remplir la grille même lorsqu'un des joueurs a déjà gagné et les joueurs ne peuvent pas commencer une nouvelle partie. Voici quelques idées pour améliorer l'expérience de jeu :

  1. En utilisant la variable globale n qui compte le nombre de coups déjà joués, empêchez les joueurs de continuer lorsque l'un deux a gagné.
    D'ailleurs, cette variable n pourrait-elle jouer le rôle du drapeau ?
  2. A partir de quel « numéro de coup » est-il vraiment utile de vérifier l'alignement ? Modifiez la fonction affichage() en conséquence.
  3. Modifiez le texte affiché par la zone de texte s'il y a match nul.
  4. Programmer le bouton [Recommencer] pour qu'il appelle une fonction reinit() qui ré-initialise toutes les variables du jeu et efface les éventuelles figures déjà tracées dans le canevas.
  • Une solution

##-----Importation des Modules-----##
from tkinter import *


##----- Définition des Variables globales -----##
cases=[ [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]
drapeau = True                              # True pour les croix, False pour les ronds
n = 1                                       # Numéro du tour de jeu


##----- Définition des Fonctions -----##
def afficher(event) :
    """ Entrées : Un événement de la souris
        Sortie : Affiche en temps réel les coordonnées de la case du clic de souris"""
    global drapeau, cases, n
    l = (event.y-2)//100                    # Ligne du clic
    c = (event.x-2)//100                    # Colonne du clic

    if (n < 10) and (cases[l][c] == 0):
        if drapeau:                              # drapeau == True
            dessin.create_line(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, fill = 'blue')
            dessin.create_line(100*c+8, 100*l+96, 100*c+96, 100*l+8, width = 5, fill = 'blue')
            cases[l][c] = 1
            message.configure(text='Aux ronds de jouer')

        else:
            dessin.create_oval(100*c+8, 100*l+8, 100*c+96, 100*l+96, width = 5, outline = 'red')
            cases[l][c] = -1
            message.configure(text='Aux croix de jouer')

        drapeau = not(drapeau)
        if (n >= 5) and (n <= 9):
            somme = verif(cases)
            if somme == 1 or somme == -1:
                n = gagner(somme)
            elif n == 9:
                n = gagner(0)
        n += 1


def verif(tableau):
    """ Entrées : un tableau "carré"
        Sorties : Calcule les sommes de chaque ligne/colonne/diagonale
            et vérifie l'alignement."""
    sommes = [0,0,0,0,0,0,0,0]             # Il y a 8 sommes à vérifier
    # Les lignes :
    sommes[0] = sum(tableau[0])
    sommes[1] = sum(tableau[1])
    sommes[2] = sum(tableau[2])
    # Les colonnes
    sommes[3] = tableau[0][0]+tableau[1][0]+tableau[2][0]
    sommes[4] = tableau[0][1]+tableau[1][1]+tableau[2][1]
    sommes[5] = tableau[0][2]+tableau[1][2]+tableau[2][2]
    # Les diagonales
    sommes[6] = tableau[0][0]+tableau[1][1]+tableau[2][2]
    sommes[7] = tableau[0][2]+tableau[1][1]+tableau[2][0]

    for i in range(8):                     # Parcours des sommes
        if sommes[i] == 3:
            return 1
        elif sommes[i] == -3:
            return -1
    return 0



def gagner(a):
    """Cette fonction indique le gagnant en modifiant le message et en
        renvoyant la valeur 9."""
    if a == 1:
        message.configure(text = 'Les croix ont gagné !')
    elif a == -1:
        message.configure(text = 'Les ronds ont gagné !')
    elif a == 0:
        message.configure(text = 'Match nul !')
    return 9



def reinit():
    """Cette fonction ré-initialise les variables globales."""
    global drapeau, cases, n
    cases = [[0, 0, 0],
             [0, 0, 0],
             [0, 0, 0]]
    drapeau = True          # True pour les croix, False pour les ronds
    n = 1

    message.configure(text='Aux croix de jouer')
    dessin.delete(ALL)      # Efface toutes les figures
    lignes = []
    for i in range(4):
      lignes.append(dessin.create_line(0, 100*i+2, 303, 100*i+2, width=3))
      lignes.append(dessin.create_line(100*i+2, 0, 100*i+2, 303, width=3))


##-----Création de la fenêtre-----##
fen = Tk()
fen.title('Morpion')


##-----Création des zones de texte-----##
message=Label(fen, text='Aux croix de jouer')
message.grid(row = 0, column = 0, columnspan=2, padx=3, pady=3, sticky = W+E)


##-----Création des boutons-----##
bouton_quitter = Button(fen, text='Quitter', command=fen.destroy)
bouton_quitter.grid(row = 2, column = 1, padx=3, pady=3, sticky = S+W+E)

bouton_reload = Button(fen, text='Recommencer', command=reinit)
bouton_reload.grid(row = 2, column = 0, padx=3, pady=3, sticky = S+W+E)


##-----Création du canevas-----##
dessin=Canvas(fen, bg="white", width=301, height=301)
dessin.grid(row = 1, column = 0, columnspan = 2, padx=5, pady=5)


##-----La grille-----##
lignes = []


##-----Evenements-----##
dessin.bind('<Button-1>', afficher)


##-----Programme principal-----##
reinit()
fen.mainloop()                      # Boucle d'attente des événements