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.
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 |
|
Tous les projets utilisant Pygame contiennent ces imports.
Initialisation du Jeu
🐍 Script Python | |
---|---|
1 |
|
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 |
|
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
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
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 |
|
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
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
for i in range(7):
pygame.draw.circle(screen, WHITE, (50 + 100*i, 50), 40)
Col
Mais finalement pourquoi ne pas boucler aussi sur la hauteur??
Col
Construction des emplacements
for j in range(6):
for i in range(7):
pygame.draw.circle(screen, WHITE, (50 + 100*i, 50 + 100*j), 40)
Col
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:
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:
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:
- si l'abscisse du clic de souris est entre
0
et100
, on doit le remplacer par50
- si l'abscisse du clic de souris est entre
100
et200
, on doit le remplacer par150
- si l'abscisse du clic de souris est entre
200
et300
, on doit le remplacer par250
- si l'abscisse du clic de souris est entre
300
et400
, on doit le remplacer par350
- si l'abscisse du clic de souris est entre
400
et500
, on doit le remplacer par450
- si l'abscisse du clic de souris est entre
500
et600
, on doit le remplacer par550
- si l'abscisse du clic de souris est entre
600
et700
, on doit le remplacer par650
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 7
fois. 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.
def cible(valeur):
for i in range(7):
if 100*i < valeur < 100*(i + 1):
valeur = (100*i + 100*(i + 1))//2
return valeur
pygame.draw.circle(screen, JAUNE, (cible(event.pos[0]), cible(event.pos[1])), 40)
Voici le code:
Utiliser une fonction
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.
mem = [0, 0, 0, 0, 0, 0, 0]
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
mem = [0, 1, 2, 4, 1, 2, 0]
Col
Cette liste me sert à déterminer les ordonnées des jetons jaunes (ou rouges).
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 enlever100
autant de fois qu'il y a de jetons présents dans la colonne: or ce nombre est précisémentmem[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
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:
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
- Ajouter la variable
tour
initialisée à 0 au début du programme principal. - 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...
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.
place_jeton_jaune = []
place_jeton_rouge = []
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:
#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:
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:
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:
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
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 fondbruit.wav
pour un bruitage quand vous positionner un jetonapplaudissement.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:
pygame.mixer.init()
pygame.mixer.set_num_channels(3) # creation de 3 channels sonores
puis charger la musique dans une variable:
musique_de_fond = "./son/musique_ambiance.mp3"
ambiance = pygame.mixer.Sound(musique_de_fond)
ambiance.set_volume(0.2)
ch1 = pygame.mixer.find_channel()
ch1.play(ambiance)
ch1.stop()
Animation musicale
- 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. - 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...