Aller au contenu

Le module Pygame de Python

Le module Pygame de Python permet de construire des jeux d'arcade très simple.

Je vous propose ici de découvrir ce module et quelques unes de ses fonctionnalité avec l'objectif de construire le jeu de puissance 4.

puissance 4

Ce que vous devez faire!

Presque tout le code est donné pour fabriquer ce jeu. Vous devez lire ce document, comprendre les stratégies utilisées puis copier et compléter le code Python lorsque les exercices le demandent. Ne soyez pas trop impatient!

Configuration de base

Comme dans tout projet Python, il faut importer des bibliothèque nécessaires:

Import du module

🐍 Script Python
1
2
import pygame, sys
from pygame.locals import *

Tous les projets utilisant Pygame contiennent ces imports.

Initialisation du Jeu

🐍 Script Python
1
pygame.init()

La fonction init() initialise le moteur du jeu. Elle aussi reste obligatoire

La boucle de jeu

La boucle de jeu est l'endroit où tous les événements du jeu se produisent, se mettent à jour et sont affichés à l'écran.

La boucle de Jeu

🐍 Script Python
1
2
3
4
5
6
while True:                             #boucle infinie
    for event in pygame.event.get():    #pour tous les événements
        if event.type == QUIT:          #si l'événement quit est déclenché
            pygame.quit()
            sys.exit()                  #fin du jeu
    pygame.display.update()

Une fois la configuration initiale et l'initialisation des variables terminées, la boucle de jeu commence là où le programme continue de boucler encore et encore jusqu'à ce qu'un événement de type QUIT se produise.

Un « événement » Pygame se produit lorsque l'utilisateur effectue une action spécifique, comme cliquer sur sa souris ou appuyer sur un bouton du clavier: la fonction pygame.event.get() retourne la liste des événements créés.

Creation de l'écran de jeu

Pour chaque jeu, il faut créer une fenêtre de dimension donnée:

Création de la fenêtre

🐍 Script Python
screen = pygame.display.set_mode((700, 600))  # largeur: 700 pixels hauteur: 600 pixels
icon = pygame.image.load("logoNSI.png")       # charger une nouvelle image comme icone

pygame.display.set_icon(icon)                 #changer l'icone
pygame.display.set_caption("Puissance 4")     #changer le titre

La fenêtre a par défaut un icône et un titre que j'ai changé ici.

Je vais créer deux constantes BLEU et WHITE pour colorer le background d'une part et les emplacements des jetons d'autre part:

Création des couleurs

🐍 Script Python
BLEU = (0, 0, 255)      
WHITE = (255, 255, 255)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)

À ce stade, le code complet est donc:

Code complet
🐍 Script Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pygame, sys
from pygame.locals import *

#----- CONSTANTE DU JEU --------------------#
BLEU = (0, 0, 255)
WHITE = (255, 255, 255)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
# ----- INITIALISATION CONSTRUCTEURS-------------------  #
pygame.init()

screen = pygame.display.set_mode( (700, 600))
icon = pygame.image.load("logoNSI.png")

pygame.display.set_icon(icon)
pygame.display.set_caption("Puissance 4")

screen.fill(BLEU)

#  ---------- BOUCLES  DE JEU----------------  #
while True:                             #boucle infinie
    for event in pygame.event.get():    #pour tous les événements
        if event.type == QUIT:          #si l'événement quit est déclenché
            pygame.quit()
            sys.exit()                  #fin du jeu
    pygame.display.update()

Code à recopier

Copier le code dans un fichier Python.

Définir les emplacements des jetons

Le puissance 4 traditionnel est composé de 7 colonnes et 6 lignes: c'est la raison pour laquelle j'ai choisi la taille de l'écran de largeur 700pixels (\(7\times 100\)) et de hauteur 600 pixels(\(6\times 100\)).

Maintenant, nous allons définir les 42 emplacements pour les jetons.

Pour déssiner un cercle dans Pygame, il faut utiliser la commande pygame.draw.circle() qui prend en paramètres l'endroit où on le construit, la couleur, les coordonnées du centre et le rayon. On obtient:

Premier emplacement

🐍 Script Python
pygame.draw.circle(screen, WHITE, (50, 50), 40)

Pour construire les autres emplacements, nous pourrions le faire à la main: il y aurait autant d'instructions pygame.draw.circle() que de jetons, soit 42. Il y a certainement mieux à faire...

Astuce

En informatique, chaque fois que l'on répète du code on utilise une boucle.

Nous allons donc utiliser une première boucle pour construire les 7 premiers jetons de la ligne du haut.

Remarquons que les coordonnées de ces disques sont (50, 50), (100, 50), (150, 50), (200, 50), ..., (650, 50): en mathématiques, nous dirions que les abscisses de ces points sont en progression arithmétique (ou linéaire...) et que le i-ième jeton a pour coordonnées (50 + 100*i, 50).

Voici donc la construction des 7 premiers jetons avec une boucle:

Col

Première ligne

🐍 Script Python
for i in range(7):
    pygame.draw.circle(screen, WHITE, (50 + 100*i, 50), 40)

Col

fonction

Mais finalement pourquoi ne pas boucler aussi sur la hauteur??

Col

Construction des emplacements

🐍 Script Python
for j in range(6):
    for i in range(7):
        pygame.draw.circle(screen, WHITE, (50 + 100*i, 50 + 100*j), 40)

Col

fonction

Code à compléter

Ajouter les deux boucles for au code précédent à la fin de la partie Initialisation.

À ce stade, l'interface graphique est prête à recevoir les instructions des joueurs...

Un jeton sur le canvas

Nous allons jouer à la souris, c'est plus simple. Une question vient alors...

Souris et Canvas

Comment tracer des cercles jaunes ou rouges dans le canvas?

Il faut se renseigner pour cela et on trouve rapidement comment faire.

La souris dans Pygame

Les clics de souris génèrent des événements dont le type est pygame.MOUSEBUTTONUP et pygame.MOUSEBUTTONDOWN.

Ces deux événements disposent d'un attribut pos qui permet de récupérer sous forme de tuple, les coordonnées du clic de souris dans le canvas.

Code à modifier

Modifier la boucle d'événements du précédent code pour obtenir ceci:

🐍 Script Python
while True:                         #boucle infinie
    for event in pygame.event.get():    #pour tous les événements
        if event.type == QUIT:          #si l'événement quit est déclenché
            pygame.quit()
            sys.exit()                  #fin du jeu
        if event.type == pygame.MOUSEBUTTONDOWN: # si clic de souris
            print(event.pos)                     #alors affichage des coordonnées dans la console
    pygame.display.update()

On peut donc finalement tracer un jeton jaune ou rouge au clic de souris en ajoutant l'instruction suivante:

Code à compléter

Ajouter dans le if précédent, après le print, l'instruction suivante:

🐍 Script Python
pygame.draw.circle(screen, JAUNE, event.pos, 40)

Bon, on se rapproche mais quelque chose ne va pas!

Jeton mal positionné!

Il faudrait que le jeton soit au milieu de son emplacement blanc et pas à cheval sur la grille.

Pas de soucis, les mathématiques vont encore nous aider...

La grille est divisée en sept colonnes et il faudrait que quand je clique sur une colonne le disque soit automatiquement centré dans cette colonne. Donc:

  1. si l'abscisse du clic de souris est entre 0 et 100, on doit le remplacer par 50
  2. si l'abscisse du clic de souris est entre 100 et 200, on doit le remplacer par 150
  3. si l'abscisse du clic de souris est entre 200 et 300, on doit le remplacer par 250
  4. si l'abscisse du clic de souris est entre 300 et 400, on doit le remplacer par 350
  5. si l'abscisse du clic de souris est entre 400 et 500, on doit le remplacer par 450
  6. si l'abscisse du clic de souris est entre 500 et 600, on doit le remplacer par 550
  7. si l'abscisse du clic de souris est entre 600 et 700, on doit le remplacer par 650

Question

Que fait-on en informatique lorsqu'on est amené à répéter des instruction??

ON BOUCLE

On peut donc réduire les instructions suivantes par une boucle qui commencerait par for i in range(7): car on répète 7fois. Remarquons enfin que le remplaçant est la moyenne des deux bornes de l'intervalle.

Je vais donc créer une fonction Python, nommée cible qui prend en paramètre un entier(l'abscisse ou l'ordonnée du clic de souris) et qui retourne l'une des valeurs précédentes.

🐍 Script Python
def cible(valeur):
    for i in range(7):
        if 100*i < valeur < 100*(i + 1):
            valeur = (100*i + 100*(i + 1))//2
    return valeur
J'appelle ensuite cette fonction dans la construction des disques jaunes.

🐍 Script Python
pygame.draw.circle(screen, JAUNE, (cible(event.pos[0]), cible(event.pos[1])), 40)

Voici le code:

Utiliser une fonction
🐍 Script Python
import pygame, sys
from pygame.locals import *

#----- CONSTANTE DU JEU --------------------#
BLEU = (0, 0, 255)
WHITE = (255, 255, 255)
ROUGE = (255, 0, 0)
JAUNE = (255, 255, 0)

# ----- INITIALISATION -------------------  #
pygame.init()

screen = pygame.display.set_mode( (700, 600))
icon = pygame.image.load("logoNSI.png")

pygame.display.set_icon(icon)
pygame.display.set_caption("Puissance 4")

screen.fill(BLEU)
for j in range(6):
    for i in range(7):
        pygame.draw.circle(screen, WHITE, (50 + 100*i, 50 + 100*j), 40)
#  ---     FONCTIONS    -----------         #
def cible(valeur):
    for i in range(7):
        if 100*i < valeur <100*(i + 1):
            valeur = (100*i + 100*(i + 1))//2
    return valeur

#  ----------------------------------------  #
while True:                             #boucle infinie
    for event in pygame.event.get():    #pour tous les événements
        if event.type == QUIT:          #si l'événement quit est déclenché
            pygame.quit()
            sys.exit()                  #fin du jeu
        if event.type == pygame.MOUSEBUTTONDOWN:
            print(event.pos)
            pygame.draw.circle(screen, JAUNE, (cible(event.pos[0]), cible(event.pos[1])), 40)
    pygame.display.update()

C'est mieux mais ce n'est pas encore gagné...

Jeton encore mal positionné!

Il faudrait que le jeton soit au milieu et au plus bas de la colonne dans laquelle je clique pour simuler la gravité...

Il me faudrait une mémoire qui retient le nombre de disque construit dans chaque colonne. Pour cela, je vais utiliser une liste mem de 7 éléments( car 7 colonnes)initialisés à 0.

🐍 Script Python
mem = [0, 0, 0, 0, 0, 0, 0]
ou mieux, liste construite par compréhension:

🐍 Script Python
mem = [0 for i in range(7)]

Astuce

À chaque fois que je construis un jeton dans une colonne, j'incrémente le nombre dans la liste correspondant à la colonne!

Voici alors l'état de la liste après quelques coups joués:

Col

🐍 Script Python
mem = [0, 1, 2, 4, 1, 2, 0]

Col

fonction

Cette liste me sert à déterminer les ordonnées des jetons jaunes (ou rouges).

🐍 Script Python
pygame.draw.circle(screen, JAUNE, (cible(event.pos[0]), 550 - mem[event.pos[0]//100]*100), 40)
mem[event.pos[0]//100] += 1

Code pas facile à comprendre mais tentons tout de même une explication.

  • l'abscisse d'un jeton est donnée par l'instruction cible(event.pos[0])
  • l'ordonnée est par défaut, lorsque la colonne est vide, à 550. D'où le 550. Mais il faut lui enlever 100 autant de fois qu'il y a de jetons présents dans la colonne: or ce nombre est précisément mem[event.pos[0]//100].

Division entière

L'instruction event.pos[0]//100 réalise la division entière de event.pos[0] par 100 qui donnera dans notre cas soit 0,1,... ou 6, les indices des colonnes!

Donc au final, l'ordonnée d'un jeton dans le canvas est 550 - mem[event.pos[0]//100]*100.

Re-voici le code complet pour l'instant:

Code complet
🐍 Script Python
import pygame, sys
from pygame.locals import *

#----- CONSTANTE DU JEU --------------------#
BLEU = (0, 0, 255)
WHITE = (255, 255, 255)
ROUGE = (255, 0, 0)
JAUNE = (255, 255, 0)
mem = [0 for i in range(7)]

# ----- INITIALISATION -------------------  #
pygame.init()

screen = pygame.display.set_mode( (700, 600))
icon = pygame.image.load("logoNSI.png")

pygame.display.set_icon(icon)
pygame.display.set_caption("Puissance 4")

screen.fill(BLEU)
for j in range(6):
    for i in range(7):
        pygame.draw.circle(screen, WHITE, (50 + 100*i, 50 + 100*j), 40)
#  ---     FONCTIONS    -----------         #
def cible(valeur):
    for i in range(7):
        if 100*i < valeur <100*(i + 1):
            valeur = (100*i + 100*(i + 1))//2
    return valeur

#  ----------------------------------------  #
while True:                             #boucle infinie
    for event in pygame.event.get():    #pour tous les événements
        if event.type == QUIT:          #si l'événement quit est déclenché
            pygame.quit()
            sys.exit()                  #fin du jeu
        if event.type == pygame.MOUSEBUTTONDOWN:
            pygame.draw.circle(screen, JAUNE, (cible(event.pos[0]), 550 - mem[event.pos[0]//100]*100), 40)
            mem[event.pos[0]//100] += 1
            print(mem)
    pygame.display.update()

Alternance des couleurs

Les deux joueurs jouent l'un après l'autre: il faut donc coder l'alternance des couleurs. Voici ma stratégie (il en existe d'autres!):

Jaune ou rouge

Je crée une variable tour initialisée à 0 et incrémentée à chaque placement d'un jeton.

Si la valeur de la variable est paire alors la couleur sera JAUNE. Sinon elle sera ROUGE. Voici mon code:

🐍 Script Python
if tour%2 == 0:
    pygame.draw.circle(screen, JAUNE, (cible(event.pos[0]), 550 - mem[event.pos[0]//100]*100), 40)
else:
    pygame.draw.circle(screen, ROUGE, (cible(event.pos[0]), 550 - mem[event.pos[0]//100]*100), 40)
tour += 1

Astuce

Pour savoir si la variable tour contient une valeur paire on peut utiliser le modulo 2 (tour%2), reste de la division euclidienne du nombre par 2. Si le reste de la division euclidienne d'un nombre par 2 est 0 alors ce nombre est pair. Sinon il est impair!

Alternance des couleurs

  1. Ajouter la variable tour initialisée à 0 au début du programme principal.
  2. Modifier le programme afin d'ajouter le code précédent.

Maintenant, il reste à savoir si un joueur a gagné: pas évident à coder.

Et le gagnant est...

Un joueur gagne lorsqu'il aligne quatre jetons les uns à côtés des autres. Regardons de plus près...

gain possible

On peut gagner en horizontal, en vertical ou en diagonal.

Le cas vertical

Le cas du gain vertical est facile à gérer: on ne peut gagner dans ce mode si le dernier jeton est positionné sur une pile de trois autres.

Un jeton est positionné dans le canvas en fonction des coordonnées de son centre: ce sont ces données que nous exploiterons.

Memoriser les emplacements

Memoriser les emplacements

Il faut une structure pour conserver les coordonnées des jetons, pour les JAUNES comme pour les ROUGES.

Nous allons donc créer naturellement deux listes place_jeton_jaune et place_jeton_rouge de coordonnées, qui seront complétées à chaque construction d'un jeton.

🐍 Script Python
place_jeton_jaune = []
place_jeton_rouge = []
Les listes sont évidemment vide au départ! À chaque construction on ajoute les coordonnées du centre du jeton:

🐍 Script Python
x, y = cible(event.pos[0]), 550 - mem[event.pos[0]//100]*100
if tour%2 == 0:
    pygame.draw.circle(screen, JAUNE, (x, y), 40)
    place_jeton_jaune.append((x, y))
else:
    pygame.draw.circle(screen, ROUGE, (x, y), 40)
    place_jeton_rouge.append((x, y))

Compléter le code

Ajouter les deux précédentes listes au programme et ainsi que les instructions précédentes.

Gain vertical

Un jeton étant placé, il suffit de tester si il y en a un en dessous, puis un autre puis un autre. Voici mon code:

🐍 Script Python
#exemple pour les jaunes
gain = 0
x, y = cible(event.pos[0]), 550 - mem[event.pos[0]//100]*100
for i in range(4):
    if (x, y + 100*i) in place_jeton_jaune:
        gain += 1
    else:
        break

Le break casse la boucle: si il n'y a pas de jeton en dessous, inutile de continuer la boucle pour tester d'autres cas. En revanche, si à la fin de la boucle la variable gain vaut 4 alors c'est gagné!

La fonction gain_vertical prend en paramètres les coordonnées x,y du clic et une liste puis retourne le booléen True en cas de gain vertical et False sinon:

🐍 Script Python
def gain_vertical(x, y , liste):
    gain = 0    
    for i in range(4):
        if (x, y + 100*i) in liste:
            gain += 1
            if gain == 4:
                return True            
        else:
            return False

Gain horizontal

Un jeton étant positionné, il faut regarder à sa droite et éventuellement à sa gauche si il devient le quatrième jeton d'un alignement. Voici la fonction correspondante:

🐍 Script Python
def gain_horizontal(x, y, liste):
    gain = 0
    for i in range(4):
        if (x + 100*i, y) in liste:
            gain += 1
        else:
            break
    for i in range(3):
        if (x - 100*(i + 1), y) in liste:
            gain += 1
        else:
            break
    if gain >= 4:
        return True
    else:
        return False 

Gain diagonal

Pour les diagonales, on s'inspire du code précédent et on obtient les deux fonctions suivantes:

🐍 Script Python
def gain_diagonal_1(x, y, liste):
    gain = 0
    for i in range(4):
        if (x + 100*i, y + 100*i) in liste:
            gain += 1
        else:
            break
    for i in range(3):
        if (x - 100*(i + 1), y - 100*(i + 1)) in liste:
            gain += 1
        else:
            break
    if gain >= 4:
        return True
    else:
        return False

def gain_diagonal_2(x, y, liste):
    gain = 0
    for i in range(4):
        if (x + 100*i, y - 100*i) in liste:
            gain += 1
        else:
            break
    for i in range(3):
        if (x - 100*(i + 1), y + 100*(i + 1)) in liste:
            gain += 1
        else:
            break
    if gain >= 4:
        return True
    else:
        return False

Ajouter des fonctions

À l'endroit indiqué, ajouter les fonctions précédentes au programme principal.

Déclaration du gagnant

Nous allons maintenant utiliser ces fonctions pour tester à chaque coup si un joueur a gagné. Avant cela, nous allons créer une variable globale flag initialisée à 0. Elle sert à contrôler le déroulement du jeu:

  • si flag est à 0 je jeu continue
  • si flag passe à 1 (ou autre chose...) le jeu s'arrête!

Question

Comment arrêter le jeu?

On pourrait stopper la boucle infinie while True mais les événements de type QUIT ne seraient accessibles. On va cependant conditionner les actions de type pygame.MOUSEBUTTONDOWN à la valeur de la variable flag. On obtient alors le code maintenant complet ci-dessous:

Code complet pour jouer
🐍 Script Python
import pygame, sys
from pygame.locals import *
#----- CONSTANTE DU JEU --------------------#
BLEU = (0, 0, 255)
WHITE = (255, 255, 255)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)
tour = 0
mem = [0 for i in range(7)]
place_jeton_jaune = []
place_jeton_rouge = []
gain_j = 0
gain_r = 0
flag = 0
# ----- INITIALISATION CONSTRUCTEURS-------------------  #
pygame.init()

screen = pygame.display.set_mode( (700, 600))
icon = pygame.image.load("logoNSI.ico")

pygame.display.set_icon(icon)
pygame.display.set_caption("Puissance 4")

screen.fill(BLEU)
for j in range(6):
    for i in range(7):
        pygame.draw.circle(screen, WHITE, (50 + 100*i, 50 + 100*j), 40)

#  ---     FONCTIONS    -----------         #
def cible(valeur):
    for i in range(7):
        if 100*i < valeur <100*(i + 1):
            valeur = (100*i + 100*(i + 1))//2
    return valeur

def gain_vertical(x, y , liste):
    gain = 0    
    for i in range(4):
        if (x, y + 100*i) in liste:
            gain += 1
            if gain == 4:
                return True            
        else:
            return False

def gain_horizontal(x, y, liste):
    gain = 0
    for i in range(4):
        if (x + 100*i, y) in liste:
            gain += 1
        else:
            break
    for i in range(3):
        if (x - 100*(i + 1), y) in liste:
            gain += 1
        else:
            break
    if gain >= 4:
        return True
    else:
        return False

def gain_diagonal_1(x, y, liste):
    gain = 0
    for i in range(4):
        if (x + 100*i, y + 100*i) in liste:
            gain += 1
        else:
            break
    for i in range(3):
        if (x - 100*(i + 1), y - 100*(i + 1)) in liste:
            gain += 1
        else:
            break
    if gain >= 4:
        return True
    else:
        return False

def gain_diagonal_2(x, y, liste):
    gain = 0
    for i in range(4):
        if (x + 100*i, y - 100*i) in liste:
            gain += 1
        else:
            break
    for i in range(3):
        if (x - 100*(i + 1), y + 100*(i + 1)) in liste:
            gain += 1
        else:
            break
    if gain >= 4:
        return True
    else:
        return False

while True:                             #boucle infinie
    for event in pygame.event.get():    #pour tous les événements
        if event.type == QUIT:          #si l'événement quit est déclenché
            pygame.quit()
            sys.exit()                  #fin du jeu
        if event.type == pygame.MOUSEBUTTONDOWN:
            if flag == 0:
                x, y = cible(event.pos[0]), 550 - mem[event.pos[0]//100]*100
                if tour%2 == 0:
                    pygame.draw.circle(screen, JAUNE, (x, y), 40)
                    place_jeton_jaune.append((x, y))


                else:
                    pygame.draw.circle(screen, ROUGE, (x, y), 40)
                    place_jeton_rouge.append((x, y))


                if gain_vertical(x, y, place_jeton_jaune) or gain_horizontal(x, y, place_jeton_jaune) or gain_diagonal_1(x, y, place_jeton_jaune) or gain_diagonal_2(x, y, place_jeton_jaune):
                    print("Jaune a gagné")
                    flag = 1
                if gain_vertical(x, y, place_jeton_rouge) or gain_horizontal(x, y, place_jeton_rouge) or gain_diagonal_1(x, y, place_jeton_rouge) or gain_diagonal_2(x, y, place_jeton_rouge):
                    print("Rouge a gagné")
                    flag = 1

                tour += 1
                mem[event.pos[0]//100] += 1
    pygame.display.update()

Un peu de musique et de bruitage

Pygame permet d'utiliser de la musique pour dynamiser votre jeu.

On suppose que vous avez trois fichiers musicaux:

  • musique_ambiance.mp3 pour une musique de fond
  • bruit.wav pour un bruitage quand vous positionner un jeton
  • applaudissement.wav pour applaudir quand un joueur a gagné

J'ai pour ma part, rangé ces trois musiques dans un dossier son.

Pour jouer de la musique, il faut d'abord initialiser le contexte avec les instructions suivantes:

🐍 Script Python
pygame.mixer.init()
pygame.mixer.set_num_channels(3) # creation de 3 channels sonores

puis charger la musique dans une variable:

🐍 Script Python
musique_de_fond = "./son/musique_ambiance.mp3"
ambiance = pygame.mixer.Sound(musique_de_fond)
puis régler le niveau de son:
🐍 Script Python
ambiance.set_volume(0.2)
puis trouver un channel libre:
🐍 Script Python
ch1 = pygame.mixer.find_channel()
et enfin jouer la musique souhaitée dans ce canal:
🐍 Script Python
ch1.play(ambiance)
Bien entendu, la musique s'arrêtera à l'instruction:
🐍 Script Python
ch1.stop()

Animation musicale

  1. Chercher sur la toile, trois musiques libres de droits que vous renommerez et sauvegarderez dans un dossier son au même niveau d'arborescence que votre fichier principal Python. Moi j'ai pioché ici.
  2. En s'inspirant des routines précédentes, jouer les musiques en les associant aux actions correspondantes. Par exemple, le bruitage bruit est associer au placement d'un jeton...