Squash

Nous allons commencer notre apprentissage de la programmation et de Python, en construisant un jeu de squash (c’est-à-dire un « pong », mais à une seule raquette).

L’aire de jeu

Commençons par créer une fenêtre pour notre jeu, et plaçons-y notre balle.

Créez un fichier squash.py vide. Vous pouvez facilement faire cette opération grâce à gedit:

  • démarrez gedit avec gedit squash.py &

  • ou bien démarrez gedit, et utilisez le menu open.

Introduisez alors le programme suivant dans l’éditeur:

import pygame

BLEU_CLAIR  = (  0, 191, 200)
JAUNE       = (255, 255,   0)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

pygame.init()

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]

pygame.event.pump()
pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
pygame.display.flip()

pygame.time.wait(5000)

pygame.display.quit()
pygame.quit()

Sauvegardez ce programme (menu save) et exécutez-le en tapant python3 squash.py dans un terminal.

Avertissement

Il faut toujours bien se rappeler de sauver le fichier avant de l’exécuter. En effet, oublier cette sauvegarde résulte en l’exécution de la version précédente (la dernière sauvée) du programme.

Si tout s’est bien passé, une nouvelle fenêtre est apparue, pendant 5 secondes. Cette fenêtre montrait une balle jaune sur fond bleu.

S’il s’est passé autre chose, c’est que vous avez fait une erreur lorsque vous avez édité le programme. Regardez le message d’erreur que l’interpréteur Python a imprimé sur le terminal: celui-ci devrait vous aider à localiser le ou les problèmes dans votre code.

Le programme est assez court, mais il contient déjà plusieurs notions importantes. Explorons-les!

La première ligne import pygame indique que votre programme utilise des fonctionalités fournies par pygame.

Variables

Notre programme utilise plusieurs variables. Une variable permet de mémoriser, et de réutiliser plus tard, une valeur ou un résultat. Chaque variable est caractérisée par un nom et une valeur. Une fois qu’il a été défini, le nom d’une variable ne peut plus changer. En revanche, la valeur d’une variable peut être changée par l’intermédiaire d’une affectation.

Par exemple, la ligne FENETRE_LARGEUR = 800 affecte la valeur 800 à la variable dont le nom est FENETRE_LARGEUR. Comme c’est la toute première fois que cette variable est utilisée dans le programme, cette ligne définit (c’est-à-dire crée) la variable.

Dans nos programmes, nous utilisons la convention que les variables dont la valeur ne change pas (c’est-à-dire, les variables qui représentent des constantes) ont un nom en majuscules.

Dans notre programme, il y a 8 variables: BLEU_CLAIR, JAUNE, FENETRE_LARGEUR, FENETRE_HAUTEUR, BALLE_RAYON, fenetre_taille, fenetre et balle_position.

Tuples et couleurs

Les variables BLEU_CLAIR et JAUNE stockent chacune une valeur qui représente une couleur. Dans Pygame, il y a plusieurs façons de représenter une couleur, mais dans ce tutoriel, nous nous limiterons à la représentation d’une couleur comme un mélange des trois couleurs fondamentales: rouge, vert et bleu. La quantité de chaque couleur fondamentale est représentée par un nombre entier de 0 à 255.

Par exemple, nous voyons que jaune est formé par la combinaison de quantités maximales de rouge et de vert, mais ne contient pas de bleu; alors que notre bleu clair ne contient pas de rouge mais contient un peu moins de vert que de bleu.

La notation (255, 255, 0) indique que la valeur de JAUNE est un tuple, c’est-à-dire une séquence immuable de valeurs. « Séquence immuable » veut dire que, non seulement les valeurs de la séquence ne peuvent pas changer, mais aussi que la taille de la séquence est fixe.

La position d’une valeur dans la séquence définit son index, qui peut être utilisé pour accéder à cette valeur. En informatique, l’habitude fait que l’index du premier élément d’une séquence possède souvent la valeur 0.

Par exemple, la valeur de la composante rouge du bleu clair de notre programme est BLEU_CLAIR[0] et vaut 0, la composante verte est BLEU_CLAIR[1] et vaut 191 et la composante bleue est BLEU_CLAIR[2] et vaut 200.

Notez que la valeur de la variable fenetre_taille est aussi un tuple, dont le premier élément fenetre_taille[0] vaut la valeur de FENETRE_LARGEUR (c’est-à-dire 800), et dont le deuxième élément fenetre_taille[1] vaut FENETRE_HAUTEUR (c’est-à-dire 600).

Créer une fenêtre

La commande pygame.display.set_mode(fenetre_taille) crée une fenêtre de la taille spécifiée par le tuple passé entre parenthèses. Le résultat de cette création de fenêtre est stocké dans la variable fenetre, qui représente donc la fenêtre de jeu.

La fenêtre représente l’aire de jeu. Celle-ci se compose de points (pixels) dans un plan dont le système de coordonnées est repésenté sur la figure.

système de coordonnées de l'écran

Système de coordonnées de l’écran.

Introduction aux listes

La variable balle_position contient une liste, c’est-à-dire une séquence mutable de valeurs. « Séquence mutable » signifie que, non seulement les valeurs de la séquence peuvent changer, mais aussi que le nombre de valeurs dans la séquence peut varier.

On peut aussi accéder aux valeurs d’une liste par indexation. Par example, dans notre programme, balle_position[0] vaut 10.

Nous reviendrons plus longuement sur l’utilisation et la manipulation des listes plus tard.

Quelques commandes de Pygame

La commande pygame.init() initialise Pygame, et doit toujours être la première commande de Pygame exécutée par un programme.

La commande pygame.event.pump() est nécessaire pour assurer le bon fonctionnement du programme, mais son explication peut être ignorée.

fenetre.fill(BLEU_CLAIR) colorie l’ensemble de notre fenêtre en bleu clair.

La commande pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON) trace dans la fenêtre un cercle de couleur jaune et de rayon BALLE_RAYON, dont le centre est à la position balle_position.

La commande pygame.display.flip() affiche le contenu de la fenêtre à l’écran.

La commande pygame.time.wait(5000) pause notre programme pendant 5000 milisecondes (c’est-à-dire 5 secondes).

Finallement, les commandes pygame.display.quit() et pygame.quit() permettent de quitter Pygame proprement.

Faire bouger la balle avec une boucle for

Nous allons modifier notre programme précédent, de sorte à faire bouger la balle sur le terrain.

Pour ce faire, nous intégrons les lignes pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON) et pygame.display.flip() dans une boucle for:

for x in range(0, FENETRE_LARGEUR + 2*BALLE_RAYON, 5):
    pygame.event.pump()
    balle_position[0] = x
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.display.flip()
    pygame.time.wait(20)

La ligne for x in range(0, FENETRE_LARGEUR + 2*BALLE_RAYON, 5): définit une boucle, qui va itérer sur les éléments d’un ensemble ou d’une séquence. A chaque itération, l’élément en question est stocké dans la variable x.

Les commandes Python qui sont exécutées à chaque itération de la boucle définissent le corps de la boucle, et sont indentées (c’est-à-dire décalées), par rapport à la ligne qui définit la boucle.

Notez que toutes les commandes du corps de la boucle doivent être indentées de la même façon (même décalage). Python est assez borné à ce sujet!

Une fois le dernier élément de l’ensemble ou de la séquence traité, le programme continue avec l’éxécution de la commande qui suit la boucle (c’est-à-dire la première commande qui suit le commande de définition de la boucle et de même indentation que celle-ci). Il est de bonne pratique de laisser une ligne blanche entre la dernière ligne du corps de la boucle et la première ligne suivant cette boucle, afin de rendre le programme plus lisible.

Range

Pour pouvoir comprendre ce que fait notre boucle for, nous devons d’abord nous tourner vers la commande range. Elle prend 1, 2 ou 3 nombres en arguments, et génère une séquence de nombres comme suit:

  • range(b) génère la séquence de tous les nombres entiers de 0 à b-1 inclus.

  • range(a, b) génère la séquence de tous les nombres entiers de a à b-1 inclus.

  • range(a, b, s) génère la séquence des nombres entiers de a au plus grand nombre plus petit que b, par pas de s (c’est-à-dire a, a + s, a + 2s, etc.) [1]

Dans la boucle de notre programme, la variable x va donc prendre, successivement, des valeurs allant de 0 à FENETRE_LARGEUR + 2*BALLE_RAYON, par pas de 5. Comme ces valeurs sont successivement affectées, à chaque itération, à balle_position[0] qui représente la position horizontale du centre du cercle correspondant à la balle, la balle va se déplacer horizontalement le long de l’écran.

En effet, les deux lignes suivantes, comme expliqué précédemment, dessinent et affichent la balle (à sa nouvelle position) à l’écran.

La commande figurant à la dernière ligne de la boucle pause le programme pendant 20 millisecondes: la balle bougera donc 50 fois par seconde (pour un mouvement « coulé »).

Notez aussi que nous pouvons modifier la position horizontale de la balle, stockée dans balle_position, car cette variable stocke les coordonnées dans une liste (c’est-à-dire dans une séquence « mutable »).

Animations

Comme vous avez pu vous en rendre compte, notre programme fait bouger la balle, mais celle-ci laisse une empreinte sur l’écran.

Afin d’améliorer la lisibilité de notre code, nous pouvons d’abord définir les deux constantes H = 0 et V = 1, juste en dessous de la ligne import pygame (par exemple), qui vont représenter les index des coordonnées respectivement horizontale et verticale de notre balle. En effet, la coordonnée horizontale de la balle est stockée dans le premier élément d’une liste, alors que la coordonnée verticale est stockée dans le deuxième élément. N’oubliez pas que la numérotation des indexes commence toujours à 0 en Python!

Nous pouvons dès lors animer notre balle en modifiant notre boucle for comme suit:

for x in range(0, FENETRE_LARGEUR + 2*BALLE_RAYON, 5):
    pygame.event.pump()
    fenetre.fill(BLEU_CLAIR)
    balle_position[H] = x
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.display.flip()
    pygame.time.wait(20)

Les seuls changements sont que:

  1. Nous utilisons la constante H, à la place de 0, pour nous référer à la position horizontale de la balle.

  2. La commande fenetre.fill(BLEU_CLAIR) est ajoutée à la boucle, et donc l’écran est complètement « effacé » (c’est-à-dire, « repeint » en bleu clair) à chaque itération de la boucle.

Un mot sur l’efficacité

Notre programme anime maintenant correctement la balle, mais au prix de devoir « effacer l’écran » à chaque itération de notre animation. Cette opération implique de devoir écrire 800 * 600 = 480000 fois la valeur de la couleur bleu clair à l’écran. L’exécution d’autant d’affectations prend évidemment un certain temps, même pour un ordinateur moderne.

Une façon plus efficace serait de ne repeindre en bleu que la position précédente de la balle, plutôt que de repeindre tout l’écran. En effet, le nombre de points à repeindre est bien moindre pour la balle que pour l’écran.

Nous pouvons donc remplacer la ligne fenetre.fill(BLEU_CLAIR) de la boucle par la commande pygame.draw.circle(fenetre, BLEU_CLAIR, balle_position, BALLE_RAYON).

Cette approche fonctionne très bien dans notre cas, car pour l’instant, seule la balle est affichée à l’écran. Mais lorsque nous aurons d’autres éléments affichés à l’écran (comme un score, par exemple), « effacer la balle » s’avérera plus compliqué que de simplement repeindre celle-ci en bleu. En effet, la balle pourrait alors se trouver au dessus d’un élément qui n’est pas bleu!

Une bonne façon de traiter ce genre de cas est alors de représenter chaque élément de l’image à l’écran par le cadre carré de limitation (« bounding box » ) qui l’entoure. En calculant les cadres de limitation qui s’enchevêtrent, on peut aisément, et efficacement, déterminer les éléments de l’image qui doivent être repeints.

Cependant comme cette méthode requiert plus de code et est moins évidente à comprendre, nous nous limiterons, dans ce premier projet d’informatique, à simplement repeindre tout l’écran. En effet, la vitesse d’affichage obtenue est acceptable pour nos jeux simples.

La notion de vitesse

La position de la balle est essentiellement représentée par un vecteur (au sens géométrique du terme) représentant la position du centre du cercle par rapport à l’origine du système de coordonnées de la fenêtre.

En physique, une vitesse est aussi représentée par un vecteur. Nous pouvons faire de même dans notre jeu en introduisant un variable balle_vitesse dont la valeur est une liste à deux éléments, le premier représentant la composante de la vitesse dans le sens horizontal (de la largeur) et le deuxième représentant la composante de la vitesse dans le sens vertical (de la hauteur): balle_vitesse = [5, 5].

Une fois cette vitesse définie, nous pouvons alors très facilement calculer la nouvelle position de la balle, à chaque itération de notre animation, en ajoutant simplement la vitesse à la position précédente:

balle_position[H] = balle_position[H] + balle_vitesse[H]
balle_position[V] = balle_position[V] + balle_vitesse[V]

Ces deux lignes de code expriment simplement que le changement de position de la balle dans le sens horizontal (resp. vertical) dépend de sa vitesse horizontale (resp. verticale) [2].

A la pacman, avec if

Si nous n’y prenons pas garde, une fois arrivée au bord inférieur de la fenêtre, notre balle disparaîtra. A ce stade, une fois que la balle a quitté l’écran, nous pouvons simplement la faire réapparaître au bord opposé:

if balle_position[H] >= FENETRE_LARGEUR + BALLE_RAYON:
    balle_position[H] = -BALLE_RAYON
if balle_position[V] >= FENETRE_HAUTEUR + BALLE_RAYON:
    balle_position[V] = -BALLE_RAYON

Le mot clé if permet de choisir d’exécuter ou non certaines lignes de code, une seule fois, selon qu’une condition est ou n’est pas remplie.

Dans le premier if ci-dessus, la condition est balle_position[H] >= FENETRE_LARGEUR + BALLE_RAYON (le : indique la fin de l’expression de la condition). Cette condition est vraie quand la position horizontale (du centre) de la balle est plus grande ou égale à la largeur de la fenêtre augmentée du rayon de la balle (c’est-à-dire, quand la balle est complètement sortie du côté droit de la fenêtre). Dans ce cas, les lignes de code “à l’intérieur du if” [3] sont exécutées. Si la condition est fausse, alors ces lignes de codes ne sont pas exécutées.

Quelles sont ces lignes de code dans notre programme ? Et bien, ce sont les lignes de codes qui sont indentées d’un niveau vers la droite par rapport au if. Dans le premier if, il n’y a qu’une seule ligne de code de cette forme, qui replace la balle juste à gauche de la fenêtre, hors de la vue: balle_position[H] = -BALLE_RAYON.

Dans tous les cas, que la condition soit vraie ou fausse, l’exécution du programe continue ensuite avec la première ligne de code qui suit le if. Dans notre exemple ci-dessus, le deuxième if est évalué.

Ce deuxième if teste si la balle sort de la fenêtre par le côté inférieur de celle-ci, et la fait réapparaître par le côté supérieur.

Pour que la balle se déplace, par exemple, 1000 fois, il nous suffit alors de remplacer la boucle for existante par une nouvelle boucle: for n in range(0, 1000):

Notre programme complet est maintenant:

import pygame

H = 0
V = 1

BLEU_CLAIR  = (  0, 191, 200)
JAUNE       = (255, 255,   0)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

pygame.init()

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse  = [5, 5]

for n in range(0, 1000):
    pygame.event.pump()
    fenetre.fill(BLEU_CLAIR)
    
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if balle_position[H] >= FENETRE_LARGEUR + BALLE_RAYON :
        balle_position[H] = -BALLE_RAYON
    if balle_position[V] >= FENETRE_HAUTEUR + BALLE_RAYON:
        balle_position[V] = -BALLE_RAYON

    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.display.flip()
    pygame.time.wait(20)

pygame.time.wait(5000)
pygame.display.quit()
pygame.quit()

Executez ce programme.

Notez que la balle se déplace à une vitesse d’environ 7 points toutes les 20 milisecondes, ce qui équivaut à 350 points par seconde. Puisque la boucle exécute 1000 itérations de 20 milisecondes chacune, elle dure donc 20 secondes et le programme se termine 25 secondes après avoir débuté (car il y a une temporisation de 5 secondes après la boucle).

Notez aussi qu’un if qui est dans le corps de la boucle for doit être indenté par rapport à celui-ci. De même, les lignes de la branche d’un if doivent être indentées par rapport à celui-ci (et donc « doublement » indentées par rapport au for).

Exprimer des conditions

Les instructions de branchement (if) utilisent des conditions.

Une condition est soit vraie (valeur True), soit fausse (valeur False). Une telle expression est une expression booléenne et True et False sont des valeurs booléennes.

Des expressions booléennes peuvent être construites à partir de divers opérateurs, dont les principaux sont repris dans la table ci-dessous:

Opérateur

Signification

a == b

vrai si a est égal à b

a != b

vrai si a est différent de b

a > b

vrai si a est plus grand que b

a >= b

vrai si a est plus grand ou égal à b

a < b

vrai si a est plus petit que b

a <= b

vrai si a est plus petit ou égal à b

not a

vrai si a est faux ; faux si a est vrai

Notez que l’opérateur d’égalité est == et non =, car = est utilisé pour affecter (stocker) une valeur dans une variable.

Les conditions reprises dans la table permettent d’exprimer des conditions “simples”. Nous pouvons exprimer des conditions composées plus complexes en combinant des expressions simples.

Pour exprimer qu’une condition composée est vraie, si au moins une condition simple parmi plusieurs est vraie, on combine ces expressions simples avec des or (“ou” en anglais). Par exemple, si vous avez trois variables a, b et c, chacune avec une valeur numérique, la condition composée suivante sera vraie si soit a est plus grand que b, soit a est plus grand que c, soit les deux: a > b or a > c

L’utilisation d’une telle condition dans un if, par exemple, se ferait comme suit:

if a > b or a > c:
   ...

N’oubliez pas le : qui marque la fin de la condition.

Pour exprimer qu’une condition composée est vraie, seulement si toutes les conditions simples qui la composent sont vraies, on combine ces expressions simples avec des and (“et” en anglais). Par exemple, si vous avez trois variables a, b et c, chacune avec une valeur numérique, la condition composée suivante sera vraie si a est le plus grand des trois nombres: a > b and a > c

L’utilisation d’une telle condition dans un if, par exemple, se ferait comme suit:

if a > b and a > c:
   ...

Exécution indéterminée avec while

Jusqu’à présent, la boucle principale (boucle for) de notre programme a exécuté un nombre déterminé d’itérations, à la suite desquelles le programme se termine.

De telles boucles ne sont pas appropriées pour un jeu, où, souvent, ce sont les actions du joueur qui déterminent la durée de la partie.

Nous pouvons construire une boucle dont l’exécution continue jusqu’à l’invalidation d’une condition en utilisant une boucle while. Par exemple:

fini = False
while not fini:
   ...

Dans cette construction, la variable fini est intitialisée à la valeur False. La boucle while est alors exécutée tant que sa condition est vrai (notez, comme pour le if, la présence du : en fin de condition). Cette condition est évaluée avant le début de chaque nouvelle itération et cette itération ne sera exécutée que si la condition est vraie. Dans notre exemple, la condition est not fini, et sera vraie tant que la valeur de fini est False. Pour que cette boucle se termine, il faudra, evidemment, que la valeur de la variable fini soit changée en True, par la logique de jeu, dans le corps de la boucle while.

Comme pour le for et le if, le corps d’une boucle while correspond simplement au code indenté sous le while (et, de nouveau, l’indentation d’un même niveau doit être identique).

Et, tout comme pour le for et le if, une fois la boucle while terminée, l’exécution du programme se poursuit par l’éxécution de la première instruction qui suit la boucle, c’est-à-dire la première instruction qui suit le corps de la boucle. Cette instruction doit être indentée de la même façon que la ligne qui contient le while lui-même.

Traiter les entrées du joueur

Vous avez peut-être remarqué que, dans tous nos programmes jusqu’à présent, la croix en haut, à droite de la fenêtre, n’a aucun effet lorsque vous cliquez dessus. C’est parce qu” un tel événement d’entrées/sorties est transmis à Pygame, mais que nous n’y réagissons pas dans notre programme.

En effet, Pygame gère une file d’événements dans laquelle il stocke les événements qu’il reçoit. La commande pygame.event.get() renvoie une liste de tous les événements ṕrésents dans la file d’événements, et retire ces événements de la liste. En d’autres termes, pygame.event.get() renvoie une liste de tous les événements qui se sont produits depuis son évocation précédente.

Nous pouvons donc passer ces événements en revue, un par un, à l’aide d’une simple boucle for:

for evenement in pygame.event.get():
    if evenement.type == pygame.QUIT:
        fini = True

A chaque itération de la boucle for, la variable evenement contient un événement. Un événement possède toujours un type, qui indique quel genre d’interaction avec le programme a été effectuée par le joueur. Dans notre exemple, pygame.QUIT est le type de l’événement qui est déclenché lorsque le joueur clique sur la « croix de fermeture » de la fenêtre. Toujours dans notre exemple, lorsque l’événement pygame.QUIT est détecté, la valeur de la variable fini est mise à True. Si cette boucle est placée dans la boucle while décrite plus haut, nous voyons donc qu’à chaque itération du while, le programme testera si le joueur désire fermer la fenêtre de jeu, et obtempérera si tel est le cas: en effet, si la valeur de fini est True lorsque la condition du while sera évaluée la prochaine fois, cette condition (not fini) sera fausse et la boucle sera terminée.

Contrôler le nombre d’images par secondes

Jusqu’à présent, nous avons simplement demandé au programme de s’arrêter 20 millisecondes après avoir affiché chaque image de notre animation. Cependant, cette pause est indépendante du temps qui est effectivement nécessaire à la construction de l’image.

Pour avoir une animation plus régulière, il faudrait pouvoir tenir compte de ce temps de génération et d’affichage des images avant de décider le temps de pause exact, entre deux images.

Heureusement, Pygame fournit une telle fonctionalité, à travers une facilité de mesure du temps, appelée pygame.time.Clock: temps = pygame.time.Clock() stocke dans la variable temps la notion de temps, et en appelant la commande temps.tick(50) après avoir affiché chaque image (c’est-à-dire, à la fin de chaque itération de la boucle while), Pygame garantit que l’affichage se fera avec maximum 50 images par secondes. Si la vitesse d’affichage descend sous ce nombre d’images, alors le programme ne fera aucune pause entre les images, de manière à rester le plus proche possible du taux de 50 images par secondes.

Premier programme interactif

Nous sommes maintenant en mesure d’incorporer ces nouvelles notions à notre programme, adin de le rendre interactif: le programme tournera indéfiniment, jusqu’à ce que le joueur ferme la fenêtre.

import pygame


H = 0
V = 1

BLEU_CLAIR  = (  0, 191, 200)
JAUNE       = (255, 255,   0)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

pygame.init()

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse  = [5, 5]

fini = False
temps = pygame.time.Clock()

#--- Boucle principale
while not fini:
    #--- Traiter entrées joueur
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True

    #--- Logique du jeu
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if balle_position[H] >= FENETRE_LARGEUR + BALLE_RAYON :
        balle_position[H] = -BALLE_RAYON
    if balle_position[V] >= FENETRE_HAUTEUR + BALLE_RAYON:
        balle_position[V] = -BALLE_RAYON

    #--- Dessiner l'écran
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)

    #--- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    #--- 50 images par seconde
    temps.tick(50)


pygame.display.quit()
pygame.quit()

Remarquez que le programme contient des lignes dont le premier caractère non « blanc » est #. Ces lignes sont des commentaires, et sont ignorées par l’interpréteur Python.

Notez que comme on traite la file d’évènements, le pygame.event.pump() n’est plus nécessaire. Et nous ne l’utiliserons plus dans ce tutoriel.

Il est non seulement de bonne pratique de commenter votre code, mais aussi de choisir des commentaires judicieux qui expliquent clairement les intentions du programmeur.

Rebonds et if-else

Nous allons maintenant faire rebondir notre balle sur les côtés du terrain de jeu. Une façon très simple de programmer ces rebonds est la suivante:

  • Si la balle frappe le bord supérieur ou inférieur de la fenêtre, on inverse le sens (signe) de la vitesse verticale. En effet, si la balle se deplace vers le haut et frappe le bord supérieur de la fenêtre, alors la balle doit, dès lors, se déplacer vers le bas. Un raisonnement similaire s’applique à une frappe sur le bord inférieur. Le sens de déplacement gauche-droite ou droite-gauche n’est pas affecté par une frappe sur un bord horizontal.

  • Si la balle frappe le bord gauche ou le bord droit de la fenêtre, on inverse le sens (signe) de la vitesse horizontale.

Une condition telle que « si la balle frappe le bord supérieur ou inférieur de la fenêtre » est relativement simple à coder: en effet, en se rappelant que la position de la balle balle_position represente la position du centre du cercle représentant cette balle, la balle doit nécessairement toucher un bord horizontal de la fenêtre si:

balle_position[V] < BALLE_RAYON or balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR

est vraie.

De même:

balle_position[H] < BALLE_RAYON or balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR

sera vraie si la balle touche un des bords verticaux de la fenêtre.

Nous aurions donc:

if balle_position[V] < BALLE_RAYON or balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
    balle_vitesse[V] = -balle_vitesse[V]
if balle_position[H] < BALLE_RAYON or balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
    balle_vitesse[H] = - balle_vitesse[H]

Nous pourrions coder ces règles simples, et remplacer les deux if que nous avons dans le programme (qui donnent l’effet « pacman » à la balle) par ces deux-ci, mais le résultat ne serait pas satisfaisant. En effet, rappellez-vous que notre balle « saute » d’une certaine distance 50 fois par seconde. Son déplacement n’est donc pas continu, même s’il est suffisemment rapide pour que nos yeux et notre cerveau l’interprètent en tant que tel. Comme illustré sur la figure ci-dessous, la balle pourrait alors « pénétrer » dans le mur, ce qui est clairement un manque d’étiquette pour une balle de squash!

rebond de la balle

Rebond de la balle.

Pour corriger ce défaut, lorsque nous détectons que la balle touche un des murs, nous pouvons nous assurer de corriger la position de la balle de sorte qu’elle reste toujours entièrement dans la fenêtre. Si ce test est effectué avant que la balle ne soit tracée à l’écran, notre problème sera résolu.

Nous proposons ici une méthode très simple pour corriger la position de la balle en cas de « pénétration » dans un mur:

  • Si la balle touche le mur droit, nous la repositionnons horizontallement vers la gauche de sorte que la balle effleure le mur droit.

  • Si la balle touche le mur gauche, nous la repositionnons horizontallement vers la droite de sorte que la balle effleure le mur gauche.

  • Si la balle touche le mur du haut, nous la repositionnons verticallement vers le bas de sorte que la balle effleure le mur du haut.

  • Si la balle touche le mur du bas, nous la repositionnons verticallement vers le haut de sorte que la balle effleure le mur du bas.

Nous avons déjà les conditions (complexes) qui détectent si la balle touche soit un mur horizontal, soit un mur vertical, mais maintenant, comme les actions à prendre sont différentes en fonction du mur touché, nous allons simplement réécrire nos tests en « séparant » ces conditions.

Le code pour détecter et gérer les situations où la balle touche un mur devient donc:

if balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
      balle_position[H] = FENETRE_LARGEUR - BALLE_RAYON
      balle_vitesse[H] = -balle_vitesse[H]
else:
    if balle_position[H] < BALLE_RAYON:
        balle_position[H] = BALLE_RAYON
        balle_vitesse[H] = -balle_vitesse[H]

if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
    balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
    balle_vitesse[V] = -balle_vitesse[V]
else:
    if balle_position[V] < BALLE_RAYON:
        balle_position[V] = BALLE_RAYON
        balle_vitesse[V] = -balle_vitesse[V]

Jusqu’à présent, nous avions utilisé les constructions if pour décider d’exécuter du code conditionnellement (en d’autres termes, le code était exécuté ou pas en fonction de la valeur d’une condition). Nous rencontrons ici une autre construction, le if-else, qui garantit qu’un des choix d’une alternative, sera, quoiqu’il arrive, exécuté: si la condition du if est vraie, alors c’est le code du if qui est exécuté, mais si la condition est fausse, alors c’est le code du else qui est exécuté à la place.

Notre programme est donc maintenant:

import pygame


H = 0
V = 1

BLEU_CLAIR  = (  0, 191, 200)
JAUNE       = (255, 255,   0)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

pygame.init()

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse  = [5, 5]

fini = False
temps = pygame.time.Clock()

#--- Boucle principale
while not fini:
    #--- Traiter entrées joueur
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True

    #--- Logique du jeu
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
        balle_position[H] = FENETRE_LARGEUR - BALLE_RAYON
        balle_vitesse[H] = -balle_vitesse[H]
    else:
        if balle_position[H] < BALLE_RAYON:
            balle_position[H] = BALLE_RAYON
            balle_vitesse[H] = -balle_vitesse[H]

    if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
        balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
        balle_vitesse[V] = -balle_vitesse[V]
    else:
        if balle_position[V] < BALLE_RAYON:
            balle_position[V] = BALLE_RAYON
            balle_vitesse[V] = -balle_vitesse[V]

    #--- Dessiner l'écran
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)

    #--- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    #--- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
pygame.quit()

Notez cependant que notre méthode de « résolution » de collisions avec un mur donne de bons résultats visuellement, mais n’est cependant pas parfaite. En effet, dans certaines circonstances, notre balle peut subir un petit « glissement », comme illustré sur la figure ci-dessous:

anomalie de rebond

Anomalie de rebond.

Cependant, comme ces anomalies de rebond sont assez petites, elles ne sont pas très « dérangeantes », et nous conserverons donc notre méthode pour sa simplicité.

La raquette

Maintenant que nous avons une balle qui rebondit sur nos murs, il est temps de créer notre raquette.

Nous commençons par définir quelques constantes:

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

RAQUETTE_LARGEUR et RAQUETTE_HAUTEUR définissent les dimensions (en points) du rectangle qui représente notre raquette.

RAQUETTE_ESPACE définit la distance au bord inférieur de la fenêtre à laquelle la raquette se trouve toujours. En effet, notre raquette va se déplacer horizontalement dans le bas de la fenêtre. RAQUETTE_DEPLACEMENT définit le déplacement horizontal de la raquette, à chaque commande du joueur.

Au début du jeu, nous voulons que la raquette soit placée à la bonne hauteur, et au milieu de la fenêtre (horizontallement). En Pygame, la commande pygame.draw.rect trace un rectangle. Cette commande prend comme paramètres, entre autres, la position du coin supérieur gauche du rectangle. Nous utiliserons donc ce point supérieur gauche comme position de notre raquette:

raquette_position = [FENETRE_LARGEUR//2 - RAQUETTE_LARGEUR//2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

Dans cette expression, l’opérateur « // » effectue une division entière. Si l’on avait utilisé « / », le résultat de la division aurait été représenté comme un nombre réel, ce qui est problématique pour certaines fonctions de Pygame qui nécessitent des coordonnées entières.

Une fois dans la boucle de jeu, nous affichons notre raquette par la commande:

pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

Le premier paramètre de rect est l’identifiant de la fenêtre où le rectangle doit être affiché, le deuxième est la couleur de notre rectangle (une séquence de trois éléments), et le troisième est une séquence de deux éléments étant chacun une séquence de deux éléments, donnant respectivement la position du coin supérieur gauche du rectangle et ses dimensions.

Nous définissons, en passant, la couleur rouge: ROUGE = (255, 0,   0). Cette ligne de code doit, bien entendu, apparaître dans le code avant son utilisation comme paramètre de pygame.draw.rect.

Une fonction pour déplacer la raquette

Pour déplacer la raquette, il nous suffit de simplement changer sa position horizontale.

Mais, comme un déplacement vers la gauche et un déplacement vers la droite ne diffèrent que par le sens (signe) du déplacement, nous avons ici une opportunité de réutiliser le même code, plutôt que de le dupliquer.

Python supporte la notion de fonction, qui correspond simplement à une série de commandes auxquelles on donne un nom, et que l’on peut invoquer depuis d’autres endroits du programme, en lui passant des variables et/ou des valeurs (ces variables ou valeurs sont communement appelés des paramètres).

Une fonction est définie par le mot clé def, suivi du nom de la fonction, suivi des paramètres (entre parenthèses et séparés par des virgules), suivi d’un :. Les instructions qui la constituent (son corps) sont indentés par rapport à sa définition.

Par exemple, en définissant les constantes:

VERS_DROITE = 1
VERS_GAUCHE = -1

une fonction qui déplace la raquette vers la gauche ou vers la droite peut se définir de la façon suivante:

def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens

L’opérateur a += b est un raccourci pour a = a + b, et notre fonction ajoutera ou soustraira RAQUETTE_DEPLACEMENT de raquette_position[H], selon que sens soit 1 (VERS_DROITE) ou -1 (VERS_GAUCHE).

Pour appeler la fonction, il suffit d’exécuter, par exemple, l’instruction deplacement_raquette(VERS_DROITE).

Notre fonction permet cependant à la raquette de sortir de l’écran.

Comme la position de la raquette est représentée par les coordonnées de son coin supérieur gauche, on peut aisément tester si la raquette sort par la gauche de l’écran en testant si sa coordonnée horizontale devient négative. De même, la coordonnée horizontale du côté droit de la raquette est raquette_position[H] + RAQUETTE_LARGEUR et la raquette sort par le côté droit de l’écran si cette coordonnée devient supérieure à la largeur de la fenêtre. Nous pouvons, dès lors, facilement modifier notre fonction de manière à empècher la raquette de sortir de l’écran:

def deplace_raquette(sens):
  raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
  if raquette_position[H] < 0:
      raquette_position[H] = 0
  elif raquette_position[H] + RAQUETTE_LARGEUR >= FENETRE_LARGEUR:
      raquette_position[H] = FENETRE_LARGEUR - RAQUETTE_LARGEUR

La seule nouveauté dans ce code est l’utilisation du mot clé elif qui est un raccourci pour:

else
    if ...

Entrées clavier

Les entrées clavier sont envoyées à Pygame sous forme d’événements (voir plus haut).

Il ne nous reste plus qu’à traiter ces événements de manière à contôler la raquette.

Nous voulons que le joueur utilise les touche « flèche droite » et « flèche gauche » pour contôler la raquette. Dans Pygame, ces touches possèdent respectivement les codes pygame.K_RIGHT et pygame.K_LEFT. Nous pouvons donc définir les constantes suivantes:

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

et ensuite modifier notre boucle de traitement des événements comme suit:

for evenement in pygame.event.get():
      if evenement.type == pygame.QUIT:
          fini = True
      elif evenement.type == pygame.KEYDOWN:
          if evenement.key == TOUCHE_DROITE:
              deplace_raquette(VERS_DROITE)
          elif evenement.key == TOUCHE_GAUCHE:
              deplace_raquette(VERS_GAUCHE)

Dans cette boucle, nous cherchons, en plus de l’événement pygame.QUIT, un événement pygame.KEYDOWN qui est envoyé chaque fois qu’une touche du clavier est enfoncée. Si une touche est enfoncée, on vérifie si cette touche correspond à une de nos touches de contrôle. Si tel est le cas, nous appelons alors la fonction deplace_raquette avec le sens approprié.

Notre programe, utilisant le raccourci elif partout est maintenant:

import pygame


H = 0
V = 1

BLEU_CLAIR  = (  0, 191, 200)
JAUNE       = (255, 255,   0)
ROUGE       = (255,   0,   0)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

#--- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    if raquette_position[H] < 0:
        raquette_position[H] = 0
    elif raquette_position[H] + RAQUETTE_LARGEUR >= FENETRE_LARGEUR:
        raquette_position[H] = FENETRE_LARGEUR - RAQUETTE_LARGEUR

pygame.init()

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse  = [5, 5]

raquette_position = [FENETRE_LARGEUR//2 - RAQUETTE_LARGEUR//2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

fini = False
temps = pygame.time.Clock()

#--- Boucle principale
while not fini:
    #--- Traiter entrées joueur
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)

    #--- Logique du jeu
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if balle_position[H] + BALLE_RAYON >= FENETRE_LARGEUR:
        balle_position[H] = FENETRE_LARGEUR - BALLE_RAYON
        balle_vitesse[H] = -balle_vitesse[H]
    elif balle_position[H] < BALLE_RAYON:
            balle_position[H] = BALLE_RAYON
            balle_vitesse[H] = -balle_vitesse[H]

    if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
        balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
        balle_vitesse[V] = -balle_vitesse[V]
    elif balle_position[V] < BALLE_RAYON:
            balle_position[V] = BALLE_RAYON
            balle_vitesse[V] = -balle_vitesse[V]

    #--- Dessiner l'écran
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    #--- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    #--- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
pygame.quit()

Utiliser des fonctions pour un code plus lisible

Nous allons maintenant écrire du code pour rendre notre programme plus lisible, et donc plus facile à comprendre et à maintenir, plutôt que d’introduire de nouvelles fonctionalités.

Nous avons, à plusieurs endroits de notre programme, des tests qui détectent si la balle et la raquette sortent de l’écran. Par exemple:

if raquette_position[H] < 0:
    raquette_position[H] = 0
elif raquette_position[H] + RAQUETTE_LARGEUR >= FENETRE_LARGEUR:
    raquette_position[H] = FENETRE_LARGEUR - RAQUETTE_LARGEUR

et:

if balle_position[V] + BALLE_RAYON >= FENETRE_HAUTEUR:
    balle_position[V] = FENETRE_HAUTEUR - BALLE_RAYON
    balle_vitesse[V] = -balle_vitesse[V]
elif balle_position[V] < BALLE_RAYON:
    balle_position[V] = BALLE_RAYON
    balle_vitesse[V] = -balle_vitesse[V]

sont de tels tests.

De plus, dans chacun de nos tests, si l’objet (raquette ou balle) sort de l’écran, sa position est corrigée de sorte que l’objet reste sur l’écran.

Les choses sont compliquées par le fait que la position de la raquette est indiquée par la position de son point supérieur gauche, alors que la position de la balle est indiquée par la position de son centre. Cette « disparité » vient de la manière dont les primitives graphiques utilisées pour représenter ces objets (rectangle et cercle dans Pygame) représentent leur position.

Nous pouvons écrire une fonction qui teste si « le point le plus à droite de l’objet » se trouve « à la droite » d’un point donné et corrige, le cas échéant, la position de l’objet de manière à ce que son point le plus à droite ne dépasse jamais le point donné. Une façon simple de représenter « le point le plus à droite d’un objet » est de partir de la position de l’objet, et de donner la distance horizontale du point le plus à droite, par rapport à la position de l’objet. Notre fonction peut dès lors s’écrire:

def test_touche_droite(objet, largeur_droite, point_droit):
    if objet[H] + largeur_droite >= point_droit:
        objet[H] = point_droit - largeur_droite
        return True
    else:
        return False

En effet, étant donné un objet (réprésenté par sa position), la position horizontale de l’objet est objet[H] et la position horizontale du point le plus à droite de l’objet est donc objet[H] + largeur_droite (où largeur_droite est la distance horizontale entre la position de l’objet et le point le plus à droite de l’objet). La condition du if teste donc si le point le plus à droite de l’objet se trouve à la droite du point donné (représenté pas sa position horizontale point_droit). Si tel est le cas, la position horizontale de l’objet est corrigée de sorte que le point le plus à droite de l’objet coincide avec le point donné et notre fonction retourne le booléen True pour indiquer que l’objet avait bien atteint (ou même dépassé) le point donné. Sinon, la fonction retourne le booléen False pour indiquer que l’objet est toujours à la gauche du point donné.

Notez que lorsqu’une fonction retourne une valeur explicitement (avec une instruction return), la valeur retournée prend la place de l’appel à la fonction dans le code d’appel. Ainsi, les fonctions que nous venons d’écrire pourront simplement être utilisées comme conditions (d’instructions if) et la valeur retournée selectionnera la branche correcte du if.

Par un même raisonnement, nous pouvons écrire une fonction qui teste si un objet a dépassé un point donné vers la gauche:

def test_touche_gauche(objet, largeur_gauche, point_gauche):
    if objet[H] - largeur_gauche <= point_gauche:
        objet[H] = point_gauche + largeur_gauche
        return True
    else:
        return False

En se rappelant que objet[V] est la position verticale d’un objet, nous pouvons écrire des fonctions testant si un objet est plus haut ou plus bas qu’un point donné:

def test_touche_haut(objet, hauteur_haut, point_haut):
    if objet[V] - hauteur_haut <= point_haut:
        objet[V] = point_haut + hauteur_haut
        return True
    else:
        return False

def test_touche_bas(objet, hauteur_bas, point_bas):
    if objet[V] + hauteur_bas >= point_bas:
        objet[V] = point_bas - hauteur_bas
        return True
    else:
        return False

Exploiter des similitudes dans notre code

En y regardant de plus près, on s’apperçoit que le code de la fonction test_touche_droite est similaire à celui de la fonction test_touche_bas: en effet, les deuxième et troisième arguments sont utilisés exactement de la même façon (même s’ils ont des noms différents), dans les mêmes opérations. La seule différence est que test_touche_droite utilise la coordonnée horizontale de l’objet (objet[H]) alors que test_touche_bas utilise la coordonnée verticale (objet[V]).

Comme les arguments d’une fonction sont des variables locales à cette fonction, le choix de leur nom n’a aucune incidence sur la justesse du code [4] et c’est donc bien leur position dans la liste d’arguments qui est importante. Nous changeons donc les noms des deuxième et troisième arguments de test_touche_droite et test_touche_bas pour qu’ils soient (respectivement) les mêmes dans ces deux fonctions et si nous utilisions un quatrième argument pour sélectionner la coordonnée choisie, les corps de ces deux fonctions seraient exactement les mêmes! Dans une telle situation, il serait clairement utile de les fusionner en une seule et même fonction:

def test_touche_db(objet, distance, point, direction):
    if objet[direction] + distance >= point:
        objet[direction] = point - distance
        return True
    else:
        return False

et nous pouvons, dès lors, réécrire nos fonctions test_touche_droite et test_touche_gauche comme de simples « fonctions emballages » (« wrapper » en anglais) qui délèguent leur travail à test_touche_db:

def test_touche_droite(objet, largeur_droite, point_droit):
    return test_touche_db(objet, largeur_droite, point_droit, H)

def test_touche_bas(objet, hauteur_bas, point_bas):
    return test_touche_db(objet, hauteur_bas, point_bas, V)

Nous notons, au passage, qu’il est tout à fait possible, et même normal, qu’une fonction appelle une, ou plusieurs, autres fonctions. Vous verrez même qu’une fonction peut même s’appeler elle-même, mais dans ce cas il faut prendre des précautions particulières pour éviter de tomber dans un cycle d’appels infini.

Le même raisonnement s’applique aux fonctions test_touche_gauche et test_touche_haut, ce qui donne le code suivant:

def test_touche_gh(objet, distance, point, direction):
    if objet[direction] - distance <= point:
        objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_gauche(objet, largeur_gauche, point_gauche):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H)


def test_touche_haut(objet, hauteur_haut, point_haut):
    return test_touche_gh(objet, hauteur_haut, point_haut, V)

Avec ces fonctions, nous pouvons maintenant aisément vérifier, par exemple, si la raquette sort du côté gauche de l’écran par:

test_touche_gauche(raquette_position, 0, 0)

En effet, comme la position de la raquette correspond à son point supérieur gauche, le point « le plus à gauche de la raquette » est à une distance égale à 0 de sa position (deuxième argument), alors que le bord gauche de l’écran est à la coordonnée horizontale 0.

Pour tester, par exemple, si la balle sort par la droite de l’écran:

test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR)

car le point « le plus à droite de la balle » est à une distance d’un rayon moins un point du centre de la balle (et le centre est sa position), alors que le bord droit de l’écran possède une une coordonnée horizontale égale à FENETRE_LARGEUR - 1 (et que comparer BALLE_RAYON - 1 et FENETRE_LARGEUR - 1 revient à comparer BALLE_RAYON et FENETRE_LARGEUR).

En remplaçant tous nos tests de positionnement par des appels à nos fonctions de test, et en gardant à l’esprit que ces fonctions ont pour effet de bord de corriger la position des objets de sorte à ce qu’il n’y ait pas de dépassement, notre programme devient:

import pygame


H = 0
V = 1

BLEU_CLAIR  = (  0, 191, 200)
JAUNE       = (255, 255,   0)
ROUGE       = (255,   0,   0)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

#--- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, 0)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR)

def test_touche_db(objet, distance, point, direction):
    if objet[direction] + distance >= point:
        objet[direction] = point - distance
        return True
    else:
        return False

def test_touche_gh(objet, distance, point, direction):
    if objet[direction] - distance <= point:
        objet[direction] = point + distance
        return True
    else:
        return False

def test_touche_droite(objet, largeur_droite, point_droit):
    return test_touche_db(objet, largeur_droite, point_droit, H)

def test_touche_gauche(objet, largeur_gauche, point_gauche):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H)

def test_touche_haut(objet, hauteur_haut, point_haut):
    return test_touche_gh(objet, hauteur_haut, point_haut, V)

def test_touche_bas(objet, hauteur_bas, point_bas):
    return test_touche_db(objet, hauteur_bas, point_bas, V)


pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse  = [5, 5]

raquette_position = [FENETRE_LARGEUR//2 - RAQUETTE_LARGEUR//2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

fini = False
temps = pygame.time.Clock()

#--- Boucle principale
while not fini:
    #--- Traiter entrées joueur
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)

    #--- Logique du jeu
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR):
        balle_vitesse[H] = -balle_vitesse[H]
    elif test_touche_gauche(balle_position, BALLE_RAYON, 0):
            balle_vitesse[H] = -balle_vitesse[H]

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR):
        balle_vitesse[V] = -balle_vitesse[V]
    elif test_touche_haut(balle_position, BALLE_RAYON, 0):
            balle_vitesse[V] = -balle_vitesse[V]

    #--- Dessiner l'écran
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    #--- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    #--- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
pygame.quit()

Notez que juste après l’initialisation de Pygame, la ligne pygame.key.set_repeat(200, 25) a été ajoutée. Cette ligne permet la répétition automatique des touches du clavier, avec le premier argument donnant le délai avant la première répétition (en milisecondes) et le deuxième argument donnant la période de répétition, toujours en milisecondes. Donc, donc notre cas, il y aura un délai de 200 ms avant la première répétition, et la touche enfoncée sera alors répétée toutes les 25 ms.

Arguments de fonction par défaut

Nos fonctions de test ont comme effet de bord de corriger la position de l’objet testé de manière à ce que celui-ci ne dépasse jamais la position de référence (position de test). C’est le comportement que nous désirons la plupart du temps dans notre programme.

Mais comment pourrions-nous contrôler d’avoir cet effet de bord ou pas?

Nous pourrions simplement passer un argument supplémentaire aux fonctions de test pour indiquer si nous voulons qu’elles effectuent l’effet de bord ou non. Mais vous avez certainement déjà tapé le programme de la section précédente, et ajouter un argument aux fonctions de test conduirait à devoir modifier tous les appels à ces fonctions, ce qui ferait beaucoup de changements!

Peut-on espérer ajouter un argument aux fonctions de test, sans devoir changer tous leurs appels?

La réponse est, fort heureusement, oui!

Il nous suffit de donner à cet argument supplémentaire une valeur par défaut. Une telle valeur est spécifiée en l’affectant à l’argument lorsque celui-ci est déclaré dans la liste des arguments de la fonctions. Dans

def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    ...

separe possède True comme valeur par défaut. En d’autres termes, si un appel à test_touche_droite spécifie explicitement quatre arguments, separe aura comme valeur la valeur de ce quatrième argument; par contre, lorsqu’un appel à test_touche_droite ne spécifie explicitement que trois arguments, alors separe prendra la valeur True, sa valeur par défaut.

Nous pouvons donc adapter de la façon suivante nos fonctions de test, sans devoir modifier leurs appels dans le reste du code:

def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)

Notez que les arguments par défaut doivent obligatoirement être les derniers de la liste des arguments.

Décomposition logique grâce aux fonctions

Nous pouvons maintenant rendre notre programme modulaire et exposer plus explicitement sa logique en décomposant la boucle principale en fonctions:

def traite_entrees():
    global fini
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, 0):
        balle_vitesse[H] = -balle_vitesse[H]

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR) \
       or test_touche_haut(balle_position, BALLE_RAYON, 0):
        balle_vitesse[V] = -balle_vitesse[V]


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

La seule chose nouvelle dans le code ci-dessus est la deuxième ligne de la fonction traite_entree. Une fonction peut lire la valeur de n’importe quelle variable globale (en d’autres termes, d’une variable définie en dehors de toute fonction). Par contre, pour qu’une fonction puisse écrire le contenu d’une variable globale, elle doit explicitement déclarer que c’est bien la variable globale qu’elle désire modifier, en « déclarant » la variable avec le mot clé global dans son corps [5]. En effet, sans cette déclaration, une variable locale du même nom serait créée localement, et la modification ne persisterait pas au delà de la fin de la fonction.

ATTENTION Vous ne devez avoir qu’une seule boucle de traitement des évènements dans votre programme. En effet, cette boucle vide la file d’attente des évènements encore non traités, et avoir plusieurs de ces boucles, s’intéressant à des évènements différents, pourrait faire disparaitre des évènements qui ne seraient alors jamais traités.

Notez aussi que nous avons modifié des instructions de branchement en cascade, où les différentes branches exécutent les mêmes actions, telles que:

if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR):
    balle_vitesse[H] = -balle_vitesse[H]
elif test_touche_gauche(balle_position, BALLE_RAYON, 0):
    balle_vitesse[H] = -balle_vitesse[H]

en instructions de branchement simples, complètement équivalentes, basées sur des conditions composées:

if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR) \
   or test_touche_gauche(balle_position, BALLE_RAYON, 0):
    balle_vitesse[H] = -balle_vitesse[H]

Notre programme est maintenant:

import pygame


H = 0
V = 1

BLEU_CLAIR = (0, 191, 200)
JAUNE = (255, 255, 0)
ROUGE = (255, 0, 0)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1


# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, 0)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, 0):
        balle_vitesse[H] = -balle_vitesse[H]

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR) \
       or test_touche_haut(balle_position, BALLE_RAYON, 0):
        balle_vitesse[V] = -balle_vitesse[V]


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse = [5, 5]

raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

fini = False
temps = pygame.time.Clock()

# --- Boucle principale
while not fini:
    # --- Traiter entrées joueur
    traite_entrees()

    # --- Logique du jeu
    anime()

    # --- Dessiner l'écran
    dessine_court()

    # --- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    # --- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
pygame.quit()

Embellir le terrain de jeu

Nous allons maintenant ajouter des murs blancs sur les côtés gauche, haut et droit de notre terrain, de manière à ce que les limites de notre terrain soient plus explicites.

Pour ce faire, nous définissons les constantes suivantes:

BLANC      = (255, 255, 255)

MUR_EPAISSEUR = 10

Dans la fonction dessine_court, nous traçons ces « murs » qui ne sont, en fait, que des rectangles Pygame:

pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))

Rappelez-vous que le troisième argument de rect représente d’une part le point supérieur gauche du rectangle et d’autre part ses dimensions en largeur et en hauteur. La première ligne ci-dessus trace donc le mur gauche, la deuxième ligne trace celui du haut de l’écran et la troisième trace celui de droite.

Il ne nous reste plus qu`à modifier les points de références dans (certains de) nos tests qui vérifient si la balle ou la raquette sont toujours bien complètement à l’écran, de manière à tenir compte de l’épaisseur de ces murs.

Par exemple, lorsque nous vérifions si la balle touchait le coté droit du terrain avec:

test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR)

il nous suffit de modifier le troisième argument en:

test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR)

Le programme est maintenant:

import pygame


H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10


# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        balle_vitesse[H] = -balle_vitesse[H]

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR) \
       or test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        balle_vitesse[V] = -balle_vitesse[V]


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse = [5, 5]

raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

fini = False
temps = pygame.time.Clock()

# --- Boucle principale
while not fini:
    # --- Traiter entrées joueur
    traite_entrees()

    # --- Logique du jeu
    anime()

    # --- Dessiner l'écran
    dessine_court()

    # --- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    # --- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
pygame.quit()

Collisions

Nous allons maintenant nous intéresser aux interactions entre la balle et la raquette.

Cette interaction va dépendre d’où la balle arrive sur la raquette. Par exemple, si la balle arrive du dessus et frappe le haut de la raquette, sa vitesse verticale va être inversée pour un rebond vers le haut. Par contre, si la balle arrive d’un côté et frappe un des côtés de la raquette, c’est la vitesse horizontale de la balle qui va être inversée.

Nous pouvons donc commencer par déterminer les positions relatives de la balle et de la raquette.

La raquette est un rectangle et détermine neuf zones dans le plan, relativement à elle-même:

zones relatives à la raquette

Zones du plan relatives à la raquette.

Une façon simple de représenter ces zones est de numéroter trois zones horizontales et trois zones verticales (en gris sur la figure ci-dessous), relatives à la raquette, et d’ensuite numéroter les neuf zones définies par la raquette comme étant la somme de leur « coordonnées » horizontale et verticale:

représentation des zones

Représentations des zones relativement à la raquette.

En Python, cette approche se traduit par la définition des constantes suivantes:

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

Nous pouvons maintenant écrire des fonctions qui calculent la position relative d’un point par rapport à la raquette. Comme le point qui nous intéresse est le centre de la balle, nous considérons que le centre de la balle sera toujours le point d’intérêt. Commençons par écrire une fonction qui détermine la position relative de la balle horizontalement à la raquette, représentée par son rectangle:

# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE

L’argument passé à la fonction est un tuple de deux valeurs, chacune étant un tuple de deux valeurs. Donc rect[0] est la première valeur de l’argument, et est donc un tuple. rect[0][H] est la première valeur de ce tuple, censée représenter la coordonnée horizontale du point supérieur gauche du rectangle passé en argument.

Si la position horizontale de la balle est à gauche de ce point, alors nous déclarons notre balle dans la zone GAUCHE relative à la raquette.

De même, si le centre de la balle se situe à droite du côté droit de la raquette, dont la position horizontale est la position horizontale du point supérieur gauche de la raquette, plus la largeur de la raquette (rect[0][H] + rect[1][H]), alors la balle est déclarée dans la zone horizontale droite de la raquette.

Sinon, la balle est « centrée » au dessus de la raquette.

La même logique s’applique pour la position relative verticale de la balle et de la raquette:

# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE

Equipé de ces deux fonctions, nous pouvons aisément combiner leur résultat de manière à obtenir la position relative du centre de la balle et de la raquette:

# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)

Notez bien que nous aurions pu passer le point qui nous intéresse comme argument de ces fonctions, plutôt que de forcer l’utilisation du centre de la balle.

Détection de collisions cercle-rectangle

Une façon classique d’aborder la détection de collisions est de d’abord considérer uniquement les cadres de limitation de chaque objet, c’est-à-dire les plus petits rectangles qui englobent l’objet complètement. Dans notre cas, le cadre de limitation de la raquette, qui est déjà un rectangle, est la raquette elle-même, alors que le cadre de limitation de la balle, qui est un cercle, est un carré, centré sur le centre du cercle, dont le côté est égal au diamètre du cercle.

S’il n’y a aucun enchevêtrement entre ces cadres de limitation, alors il est certain que la balle et la raquette ne sont pas en collision (c’est-à-dire, ne se touchent pas):

pas de collision

Pas de collision entre la balle et la raquette.

Par contre, s’il y a bien enchevêtrement, alors il se peut qu’il y ait collision entre les deux objets:

collision

Collision entre la balle et la raquette.

pas de collision malgré un enchevêtrement

Pas de collision entre la balle et la raquette, malgré l’enchevêtrement de leur cadre de limitation.

Nous pouvons facilement créer un rectangle représentant le cadre de limitation de la balle:

BALLE_DIAM = 2 * BALLE_RAYON

ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))

et, étant donné deux rectangles, Pygame nous fournit un test de leur enchevêtrement, sous la forme rect1.colliderect(rect2), où rect1 et rect2 sont de type Pygame.Rect et colliderect retourne True s’il y a enchevêtrement des deux rectangles, et False sinon.

Nous pouvons commencer à écrire une fonction qui détecte s’il y a collision entre la balle et la raquette, et agit ensuite en conséquence:

# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V]  - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        ...

Dans cette fonction, rect est le cadre de limitation de l’objet avec lequel nous testons la collision de la balle. Ici, ce sera la raquette.

La condition du if ne sera True que si les cadres de limitation s’enchevêtrent, et donc si une collision est possible. C’est cette possibilité que le programme doit ensuite investiguer.

S’il y a une possibilité de collision, cette collision est avérée si la balle est dans une des zones relatives au dessus, à gauche, à droite ou en dessous de la raquette (ce dernier cas en fait impossible dans notre jeu, mais nous en tenons compte pour être complet, et peut-être pouvoir réutiliser notre code dans d’autres circonstances).

Nous évaluons donc la position relative de la balle et de la raquette, et prenons les actions qui s’imposent dans les quatres cas discutés ci-dessus:

...
position = position_relative(rect)
if position == GAUCHE:
    if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
        balle_vitesse[H] = -abs(balle_vitesse[H])
elif position == DROITE:
    if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
        balle_vitesse[H] = abs(balle_vitesse[H])
elif position ==  DESSUS:
    if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
        balle_vitesse[V] = -balle_vitesse[V]
elif position == DESSOUS:
    if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
        balle_vitesse[V] = -balle_vitesse[V]
...

La seule nouvelle chose dans ce code est l’utilisation de la fonction abs, fournie par Python, qui donne la valeur absolue d’un nombre. Nous utilisons la valeur absolue de la vitesse horizontale lorsque nous détectons une collision avec les côtés droit et gauche de la raquette, car cette collision pourrait avoir été causée par un déplacement de la raquette vers la balle, quelle que soit la direction de nouvement de cette balle. Par exemple, la balle pourrait voyager de gauche à droite, et la raquette pourrait bouger de gauche à droite. Dans ce cas, inverser la direction de mouvement horizontal de la balle ne donnerait pas le résultat voulu.

Notez aussi que nous réutilisons les fonctions de test précédemment développées, et que ces fonctions « séparent » les objets enchevêtrés (mais avec un léger glissement potentiel, comme décrit dans la section « Rebonds »).

Nous devons maintenant traiter les cas où la balle touche la raquette « par un coin ». Notez que ces cas sont, précisément, les cas où un enchevêtrement des cadres de limitation ne donnent pas nécessairement lieu à une collision réelle.

Dans tous ces cas, nous devons donc d’abord nous assurer qu’une collision réelle a lieu. Pour ce faire, il nous suffit de « mesurer » la distance entre le centre du cercle et le coin de la raquette: si cette distance est plus petite que le rayon du cercle, il y a, bel et bien, collision.

Nous écrivons donc d’abord une fonction qui, étant donné deux points, calcule leur distance. En fait, le calcul de distance entre deux points implique normalement le calcul d’une racine carrée qui est une opération pouvant être lente. Mais dans notre jeu, nous pouvons travailler avec les carrés des distances, ce qui est plus rapide:

def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v

Dans les cas où la balle touche un coin de la raquette, nous renvoyons la balle avec une direction de 45 degrés dans la zone relative de la balle. Notez que ceci est, de nouveau, une approximation, mais dont l’effet visuel est acceptable:

...
rayon2 = BALLE_RAYON * BALLE_RAYON
...
elif position == HAUT_GAUCHE:
    if distance2(balle_position, rect[0]) <= rayon2:
        collision_coin_haut_gauche(rect)
elif position == HAUT_DROITE:
    if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
        collision_coin_haut_droite(rect)
elif position == DESSOUS_GAUCHE:
    if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
        collision_coin_bas_gauche(rect)
elif position == DESSOUS_DROITE:
    if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
        collision_coin_bas_droite(rect)
...

Les fonctions collision_coin_... appelées dans ce code replacent la balle selon un axe de 45 degrés par rapport au coin de la raquette, et changent la vitesse de la balle de manière appropriée:

def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    balle_vitesse[H] = vitesse_h
    balle_vitesse[V] = vitesse_v


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin(rect[0], -delta, -delta, -abs(balle_vitesse[H]), -abs(balle_vitesse[V]))


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(balle_vitesse[H]),-abs(balle_vitesse[V]))


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(balle_vitesse[H]), abs(balle_vitesse[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(balle_vitesse[H]), abs(balle_vitesse[V]))

Dans les fonction ci-dessus, la valeur 0.707 est une approximation de sin(math.pi / 4) (en d’autres termes, la moitié de la racine carrée de 2), de manière à éviter d’effectuer répétitivement ce calcul plus précis mais potentiellement lent.

Il ne nous reste plus qu’un seul cas à traiter: le cas où le centre de la balle est « dans » la raquette. Pour simplifier, nous faisons ici l’hypothèse que l’amplitude de la vitesse de la balle ne dépasse jamais le rayon, et que donc, si le centre de la balle se retrouve dans la raquette, ce doit être parce que la raquette et la balle se sont déplacées l’une vers l’autre (et que la collision s’est donc faite par un coté de la raquette):

...
else:
    # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la
    # vitesse ne dépasse jamais la taille du rayon.
    # Il faudra s'assurer que cette condition est toujours vraie.
    # Eviter un recouvrement lorsque raquette et balle bougent l'une
    # vers l'autre:
    delta_g = abs(balle_position[H] - rect[0][H])
    delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
    if delta_g < delta_d:
        balle_position[H] = rect[0][H] - BALLE_RAYON
        balle_vitesse[H] = -abs(balle_vitesse[H])
    else:
        balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
        balle_vitesse[H] = abs(balle_vitesse[H])

Notre fonction de test de collision est donc:

# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                balle_vitesse[H] = -abs(balle_vitesse[H])
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                balle_vitesse[H] = abs(balle_vitesse[H])
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                balle_vitesse[V] = -balle_vitesse[V]
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                balle_vitesse[V] = -balle_vitesse[V]
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
                collision_coin_haut_droite(rect)
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                balle_vitesse[H] = -abs(balle_vitesse[H])
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                balle_vitesse[H] = abs(balle_vitesse[H])

Il ne nous reste plus, dans la fonction anime, une fois que la balle a bougé et que l’on a testé les collisions avec les murs, qu’à ajouter un test de collision entre la balle et la raquette et de verifier si la balle est sortie par le bas de l’écran. Si tel est le cas, on termine simplement la partie:

def anime():
  global fini
  balle_position[H] = balle_position[H] + balle_vitesse[H]
  balle_position[V] = balle_position[V] + balle_vitesse[V]

  if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
     or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
      balle_vitesse[H] = -balle_vitesse[H]

  if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
      balle_vitesse[V] = -balle_vitesse[V]

  test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

  if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
      fini = True

Notre programme complet est maintenant:

import pygame


H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    global fini
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        balle_vitesse[H] = -balle_vitesse[H]

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        balle_vitesse[V] = -balle_vitesse[V]

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE



# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE



# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)



# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                balle_vitesse[H] = -abs(balle_vitesse[H])
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                balle_vitesse[H] = abs(balle_vitesse[H])
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                balle_vitesse[V] = -balle_vitesse[V]
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                balle_vitesse[V] = -balle_vitesse[V]
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                balle_vitesse[H] = -abs(balle_vitesse[H])
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                balle_vitesse[H] = abs(balle_vitesse[H])


def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    balle_vitesse[H] = vitesse_h
    balle_vitesse[V] = vitesse_v


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin(rect[0], -delta, -delta, -abs(balle_vitesse[H]), -abs(balle_vitesse[V]))


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(balle_vitesse[H]),-abs(balle_vitesse[V]))


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(balle_vitesse[H]), abs(balle_vitesse[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(balle_vitesse[H]), abs(balle_vitesse[V]))



pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse = [5, 5]

raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

fini = False
temps = pygame.time.Clock()

# --- Boucle principale
while not fini:
    # --- Traiter entrées joueur
    traite_entrees()

    # --- Logique du jeu
    anime()

    # --- Dessiner l'écran
    dessine_court()

    # --- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    # --- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
exit()

Notez que comme fini est une variable globale, la fonction anime doit la déclarer comme telle avant de pouvoir la modifier.

Aussi, notre programme ne gère pas les cas où la balle se fait « coincer » entre un mur et la raquette. Que feriez-vous dans ce cas?

Ajouter un score

Nous allons maintenant ajouter un score, qui augmentera d’un point chaque fois que le joueur frappera la balle avec la raquette.

Pour ce faire, nous initialisons à zéro une variable globale score. Cette variable sera incrémentée, dans test_collision, chaque fois que nous détectons une collision qui garde la balle en jeu.

Pour pouvoir afficher le score à l’écran, nous devons définir la police de caractères à utiliser:

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)

où le premier argument est le nom de cette police de caractères, le deuxième est sa taille, et le troisième indique que nous voulons une version « en gras » de cette police.

Pour afficher le score, dans dessine_court, il nous faut alors construire le message à afficher:

marquoir = police.render(str(score), True, BLEU)

où le premier argument est une chaîne de caractères construite à partir d’un nombre, le deuxième indique que nous souhaitons un « lissage des caractères » pour faire plus joli, et le troisième est la couleur du message (définit par BLEU       = (  0,   0, 255)).

Une fois le message construit, nous donnons l’ordre de l’afficher avec:

fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))

Le deuxième argument de blit représente le coin supérieur gauche de l“« image » représentant le score.

Pour être certain que la balle passe « au dessus » du score, il faut ajouter ces lignes à dessine_court avant la ligne qui dessine la balle.

Notre programme complet est maintenant:

import pygame


H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)
BLEU       = (  0,   0, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    global fini
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        balle_vitesse[H] = -balle_vitesse[H]

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        balle_vitesse[V] = -balle_vitesse[V]

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    marquoir = police.render(str(score), True, BLEU)
    fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)



# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    global score
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                balle_vitesse[H] = -abs(balle_vitesse[H])
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                balle_vitesse[H] = abs(balle_vitesse[H])
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                balle_vitesse[V] = -balle_vitesse[V]
                score += 1
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                balle_vitesse[V] = -balle_vitesse[V]
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
                score += 1
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
               score += 1
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                balle_vitesse[H] = -abs(balle_vitesse[H])
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                balle_vitesse[H] = abs(balle_vitesse[H])



def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    balle_vitesse[H] = vitesse_h
    balle_vitesse[V] = vitesse_v


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin(rect[0], -delta, -delta, -abs(balle_vitesse[H]), -abs(balle_vitesse[V]))


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(balle_vitesse[H]),-abs(balle_vitesse[V]))


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(balle_vitesse[H]), abs(balle_vitesse[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(balle_vitesse[H]), abs(balle_vitesse[V]))



pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
balle_vitesse = [5, 5]

raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)

score = 0

fini = False
temps = pygame.time.Clock()

# --- Boucle principale
while not fini:
    # --- Traiter entrées joueur
    traite_entrees()

    # --- Logique du jeu
    anime()

    # --- Dessiner l'écran
    dessine_court()

    # --- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    # --- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
exit()

Problème 1

Vous allez modifier le code du programme ci-dessus. Tout d’abord, ajoutez les noms et prénoms des membres présents de votre groupe en commentaires au début du code.

Ensuite, vous allez ajouter une brique, où vous voulez, dans l’aire de jeu. Pour simplifier, le bas de la brique doit être plus haut que le dessus de la raquette (pour éviter que la raquette et la brique ne se touchent). La balle doit, bien sûr, rebondir sur cette brique et le score doit augmenter chaque fois que la balle touche la brique.

Une fois votre programme terminé, connectez-vous sur la plateforme de soumission de montefiore, l’un d’entre-vous doit y créer un groupe pour le cours du PPI, et ses partenaires doivent rejoindre ce groupe. Ensuite l’un d’entre-vous doit envoyer votre soumission. Ce tutoriel explique comment soumettre votre problème sur la plateforme de soumission.

Avertissement

Ce problème doit être soumis sur la plateforme de soumission avant le 09/10/2023-23h59. Si la deadline n’est pas respectée, vous aurez 0.

Gérer une vitesse

Jusqu’à présent, dans notre code, la vitesse de la balle été stockée dans une séquence (par exemple, une liste) représentant le vecteur (géométrique) de la vitesse. Cette repésentation est tout à fait acceptable, mais nous pouvons augmenter la flexibilité de notre code en séparant les deux constituants d’une vitesse: la direction et l’amplitude.

La direction de la vitesse est un vecteur, et nous pouvons donc la stocker dans une liste. La seule contrainte que nous imposerons est que ce vecteur soit normalisé (c’est-à-dire de norme [6] unitaire). L’amplitude de la vitesse est alors un nombre. Une telle représentation permettra de changer aisément l’amplitude de la vitesse sans en changer la direction, et vice versa.

Si la norme de la vitesse est stockée dans la variable vitesse_amplitude et que sa direction est stockée dans la liste vitesse_direction, alors la fonction suivante construit un vecteur vitesse, comme utilisé jusqu’à présent:

def vitesse():
    return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))

Remarquez que cette fonction retourne un tuple, et non une liste. Tout deux sont des séquences, et la lecture de leurs éléments se fait de la même manière. Par contre, un tuple est immuable, alors qu’une liste peut être modifiée. Dans notre cas, le choix du tuple est justifiable par le fait que notre programme va manipuler explicitement l’amplitude et la direction de la vitesse, et qu’un nouveau vecteur de vitesse pourra être regénéré, après chaque modification, en appellant la fonction vitesse.

Notez aussi que les composantes de ce tuple sont arrondies à l’entier le plus proche. C’est parce que notre vitesse est utilisée pour calculer la position de la balle sur l’écran, et que les fonctions d’affichage requièrent des coordonnées entières.

Rappelez-vous que notre algorithme de détection de collisions fait l’hypothèse que la grandeur de la vitesse de la balle n’est jamais plus grande que le rayon de la balle. Nous pouvons facilement gérer cette condition en définissant, par exemple, une constante représentant la valeur initiale de l’amplitude de la vitesse, ainsi qu’une constante définissant l’amplitude maximale:

VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5

La vitesse de la balle peut alors être initialisée:

vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()

là où balle_vitesse = [5, 5] apparaissait. math.sqrt calcule la racine carrée de son argument (le module math doit être importé au préalable par import math), et la direction de la vitesse de notre balle est bien un vecteur unitaire.

Nous fournissons aussi une fonction qui permet de changer une des composantes de la direction de la vitesse (quelle que soit cette composante) et d’automatiquement reflèter ce changement dans balle_vitesse:

def change_vitesse(composante, val):
    global balle_vitesse

    vitesse_direction[composante] = val
    balle_vitesse = vitesse()

Dans cette fonction, balle_vitesse doit être déclaré global, car la fonction modifie balle_vitesse en lui assignant directement un nouveau tuple.

Notre programme ne modifie, pour l’instant, que la direction de la vitesse de la balle, et nous pouvons donc utiliser notre nouvelle fonction change_vitesse partout où notre code affectait une valeur directement à balle_vitesse. Par exemple, balle_vitesse[H] = -balle_vitesse[H] devient change_vitesse(H, -vitesse_direction[H]).

De même, tous les appels à la fonction resoudre_collision_coin prennent les composantes horizontale et verticale de la vitesse de la balle comme quatrième et cinquième arguments, mais ces arguments ne sont utilisés que pour des changement de direction. Nous devons donc aussi remplacer balle_vitesse[H] et balle_vitesse[V] par vitesse_direction[H] et vitesse_direction[V] dans ces arguments.

Accélérer la balle en fonction de la durée de jeu

Nous pouvons maintenant aisément implémenter une fonction qui permet d’accélérer la balle après un certain nombre de frappes:

FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7


def augmente_score():
    global score, vitesse_amplitude, balle_vitesse

    score += 1
    if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
        vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
        balle_vitesse = vitesse()

La constante FACTEUR_AUGMENTATION_VITESSE définit que l’amplitude de la vitesse augmentera de 10% toutes les FRAPPES_AUGMENTATION_VITESSE (ici, toutes les 7 frappes).

Dans score % FRAPPES_AUGMENTATION_VITESSE, nous rencontrons l’opérateur % (« modulo ») qui a pour valeur le reste de la division entière de la première opérande par la seconde. Dans notre cas, la condition score % FRAPPES_AUGMENTATION_VITESSE == 0 sera donc True lorsque le reste de la division entière de score par FRAPPES_AUGMENTATION_VITESSE vaut 0, c’est-à-dire quand le score est un multiple de FRAPPES_AUGMENTATION_VITESSE (un multiple de 7 dans notre exemple). Lorsque nous assignons une nouvelle valeur à vitesse_amplitude, nous utilisons la fonction min, fournie par Python, qui retourne le plus petit de ses arguments, de sorte que vitesse_amplitude ne soit jamais plus grande que VITESSE_MAX.

Il suffit alors d’appeler la fonction augmente_score, depuis la fonction test_collision, à chaque fois qu’un rebond est détecté, et ce, à la place du simple score += 1.

Notez que comme test_collision ne modifie plus directement score (qui est maintenant modifié par augmente_score), la ligne global score peut être effacé de cette fonction.

Notre programme complet est maintenant:

import pygame
import math

H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)
BLEU       = (  0,   0, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    global fini
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(H, -vitesse_direction[H])

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(V, -vitesse_direction[V])

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    marquoir = police.render(str(score), True, BLEU)
    fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                change_vitesse(H, -abs(vitesse_direction[H]))
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                change_vitesse(H, abs(vitesse_direction[H]))
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                change_vitesse(V, -vitesse_direction[V])
                augmente_score()
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                change_vitesse(V, -vitesse_direction[V])
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
                augmente_score()
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
               augmente_score()
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                change_vitesse(H, -abs(vitesse_direction[H]))
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                change_vitesse(H, abs(vitesse_direction[H]))


def augmente_score():
    global score, vitesse_amplitude, balle_vitesse

    score += 1
    if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
        vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
        balle_vitesse = vitesse()


def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    change_vitesse(H, vitesse_h)
    change_vitesse(V, vitesse_v)


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin(rect[0], -delta, -delta, -abs(vitesse_direction[H]), -abs(vitesse_direction[V]))


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(vitesse_direction[H]),-abs(vitesse_direction[V]))


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))

def change_vitesse(composante, val):
    global balle_vitesse

    vitesse_direction[composante] = val
    balle_vitesse = vitesse()

def vitesse():
    return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))



pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()


raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)

score = 0

fini = False
temps = pygame.time.Clock()

# --- Boucle principale
while not fini:
    # --- Traiter entrées joueur
    traite_entrees()

    # --- Logique du jeu
    anime()

    # --- Dessiner l'écran
    dessine_court()

    # --- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    # --- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
exit()

Ajouter des « effets » dans la frappe

Pour rendre notre jeu plus attrayant en donnant plus de contrôle au joueur, nous pouvons ajouter des « effets » lorsque la balle frappe la raquette.

Une technique très simple, mais efficace, pour implémenter ce genre de fonctionnalité, est de définir plusieurs zones sur la raquette, et de choisir l’angle de rebond de la balle en fonction de la zone sur laquelle la balle rebondit. Notez que dans ce cas, l’angle d’incidence de la balle est complètement ignoré.

Comme nous représentons maintenant la direction de la vitesse de la balle par un vecteur unitaire, nous allons écrire une fonction qui, étant donné un angle en degrés, génère le tuple représentant le vecteur unitaire dans cette direction. Les coordonnées d’un tel vecteur sont aisément calculées, grâce aux formules bien connues des triangles rectangles, et sont illustrées dans la figure suivante:

vecteur unitaire

Coordonnées d’un vecteur unitaire.

Notre fonction est donc:

def vecteur_unitaire(angle):
    angle_radian = math.radians(angle)
    return (math.cos(angle_radian), math.sin(angle_radian))

Nous souhaitons exprimer l’argument angle en degrés, mais les fonctions math.cos et math.sin, fournies par le module math de Python, doivent recevoir un angle exprimé en radians. La fonction math.radians effectue cette conversion de degrés en radians.

Considérons que nous souhaitons avoir quatre directions de rebond différentes sur notre raquette. Nous pouvons facilement générer un tuple contenant les vecteurs unitaires correspondants:

RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))

Beaucoup de choses se passent dans cette ligne de code. D’abord, la fonction tuple, qui prend une séquence de valeurs comme seul argument, construit un tuple dont les éléments sont ceux de la séquence donnée.

La commande vecteur_unitaire(a) for a in (-120, -100, -80, -60) est du sucre syntaxique qui génère une séquence de valeurs et les stocke dans un générateur. Sans entrer dans les détails, un générateur est une séquence de valeurs qui ne peuvent être lues qu’une seule fois.

Notez que les angles choisis sont négatifs car nous désirons que la balle rebondisse vers le haut.

Il ne nous reste plus qu’à diviser notre raquette en quatre zones, et à determiner laquelle de ces zones est touchée par la balle lorsque celle-ci frappe le haut de la raquette:

RAQUETTE_ZONES = 4

def zone_raquette(position_horizontale):
    x_relatif = position_horizontale - raquette_position[H]
    if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
        return -1
    return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)

Cette fonction, qui prend une position horizontale comme argument, détermine si cette position est au dessus de la raquette. Si c’est le cas, elle retourne le numéro de la zone, sinon elle retourne -1. Le numéro de zone servira d’index dans le tuple des vitesses de rebond, afin de sélectionner la direction du rebond.

Il ne nous reste plus qu’à fixer la vitesse de rebond, à la place de simplement « retourner » la vitesse de la balle verticalement, lorsqu’une collision est détectée « au dessus de la raquette » (dans test_collision):

zone = zone_raquette(balle_position[H])
change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])

Notre programme complet est maintenant:

import pygame
import math

H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)
BLEU       = (  0,   0, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    global fini
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(H, -vitesse_direction[H])

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(V, -vitesse_direction[V])

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    marquoir = police.render(str(score), True, BLEU)
    fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                change_vitesse(H, -abs(vitesse_direction[H]))
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                change_vitesse(H, abs(vitesse_direction[H]))
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                zone = zone_raquette(balle_position[H])
                change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
                change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
                augmente_score()
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                change_vitesse(V, -vitesse_direction[V])
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
                augmente_score()
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
               augmente_score()
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                change_vitesse(H, -abs(vitesse_direction[H]))
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                change_vitesse(H, abs(vitesse_direction[H]))


def augmente_score():
    global score, vitesse_amplitude, balle_vitesse

    score += 1
    if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
        vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
        balle_vitesse = vitesse()


def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    change_vitesse(H, vitesse_h)
    change_vitesse(V, vitesse_v)


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin(rect[0], -delta, -delta, -abs(vitesse_direction[H]), -abs(vitesse_direction[V]))


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, abs(vitesse_direction[H]),-abs(vitesse_direction[V]))


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))

def change_vitesse(composante, val):
    global balle_vitesse

    vitesse_direction[composante] = val
    balle_vitesse = vitesse()

def vitesse():
    return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))


def vecteur_unitaire(angle):
    angle_radian = math.radians(angle)
    return (math.cos(angle_radian), math.sin(angle_radian))


def zone_raquette(position_horizontale):
    x_relatif = position_horizontale - raquette_position[H]
    if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
        return -1
    return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)


pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [10, 300]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()

RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))

raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)

score = 0

fini = False
temps = pygame.time.Clock()

# --- Boucle principale
while not fini:
    # --- Traiter entrées joueur
    traite_entrees()

    # --- Logique du jeu
    anime()

    # --- Dessiner l'écran
    dessine_court()

    # --- Afficher (rafraîchir) l'écran
    pygame.display.flip()

    # --- 50 images par seconde
    temps.tick(50)

pygame.display.quit()
exit()

Un peu de suspens

Jusqu’à présent, notre jeu est tout à fait déterministe: pour une même séquence de touches frappées au clavier, aux mêmes moments, le jeu se comporte exactement de la même manière. Nous pouvons changer tout cela en décidant que lorsque la balle frappe un coin de la raquette, elle a autant de chance de « bien » rebondir (vers le haut) que de « mal » rebondir (vers le bas).

Nous avons donc maintenant quatre façons de rebondir sur les coins de notre raquette:

RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))

et nous écrivons une fonction qui sélectionne une vitesse de rebond de manière aléatoire:

def vitesse_coin(cote):
    if cote == VERS_DROITE:
        v = RAQUETTE_REBOND_COIN[2:]
    else:
        v = RAQUETTE_REBOND_COIN[:2]
    return random.choice(v)

Dans cette fonction, v est une slice (c’est-à-dire une partie) du tuple RAQUETTE_REBOND_COIN. La notation [a:b] utilisée comme indice, sélectionne tous les éléments de la sous-séquence commençant à l’indice a et se terminant à l’indice b-1 (l’élément à l’indice b est exclus). La notation [2:] signifie la sous-séquence commençant à l’indice 2 jusqu’à la fin de la séquence, et [:2] signifie la sous-séquence commençant au début de la séquence, et se terminant à l’élément situé avant celui d’indice 2.

Donc, si l’argument cote vaut VERS_DROITE, alors v est un tuple qui contient deux vecteurs unitaires, dont les angles sont respectivement -45 et 45 degrés. Sinon, v contient des vecteurs dont les angles sont respectivement -135 et 135.

La commande random.choice(v), du module random qui doit être importé, renvoie alors, au hasard, un élément de la séquence passée en argument. Comme chacune des séquences v construites contient un « bon » et un « mauvais » rebond pour le coin correspondant de la raquette, les rebonds sur les coins de la raquette sont maintenant aléatoires.

La séquence de valeurs tirées aléatoirement est cependant prédéterminée, c’est-à-dire qu’elle se répétera à l’identique à chaque exécution du programme. Pour un choix plus aléatoire, dépendant de l’heure (à la milliseconde près) à laquelle le programme est lancé, il suffit d’exécuter random.seed() une seule fois avant le début de la boucle de jeu.

Il ne nous reste plus, dans les fonctions collision_coin_haut_gauche et collision_coin_haut_droit, qu’à sélectionner une vitesse de rebond avec vitesse_coin:

def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_GAUCHE)
    resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_DROITE)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])

Plusieurs vies

Nous introduisons maintenant plusieurs vies pour le joueur:

MAX_VIES = 3

Le principe de la modification du jeu est maintenant très simple: il suffit de perdre une vie lorsque le joueur manque la balle, et d’englober la boucle de jeu actuelle dans une autre boucle qui ne se termine que lorsque le nombre de vies restantes est 0. Bien évidemment, il faut replacer la balle à l’écran au début de chaque nouvelle vie.

Nous désirons aussi instaurer une pause de deux secondes au début de chaque vie, de manière à donner au joueur le temps de se préparer: la balle ne doit pas bouger pendant deux secondes, mais le joueur doit pouvoir déplacer sa raquette. Python, dans le module time (qui doit être importé), offre une fonction time() qui mesure le temps écoulé, en secondes, depuis un moment de référence. Nous utilisons cette facilité pour mesurer nos pauses.

Finalement, nous affichons le nombre de vies restantes dans le mur supérieur du terrain de jeu.

Les changements requis sont sans difficultés:

import pygame
import math
import random
import time

H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)
BLEU       = (  0,   0, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7

MAX_VIES = 3

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini, vies_restantes
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
            vies_restantes = 0
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE:
                deplace_raquette(VERS_GAUCHE)


def anime():
    global fini, vies_restantes
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(H, -vitesse_direction[H])

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(V, -vitesse_direction[V])

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True
        vies_restantes -= 1


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    marquoir = police.render(str(score), True, BLEU)
    fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    for vies in range(vies_restantes - 1):
        pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                change_vitesse(H, -abs(vitesse_direction[H]))
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                change_vitesse(H, abs(vitesse_direction[H]))
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                zone = zone_raquette(balle_position[H])
                change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
                change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
                augmente_score()
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                change_vitesse(V, -vitesse_direction[V])
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
                augmente_score()
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
               augmente_score()
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                change_vitesse(H, -abs(vitesse_direction[H]))
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                change_vitesse(H, abs(vitesse_direction[H]))


def augmente_score():
    global score, vitesse_amplitude, balle_vitesse

    score += 1
    if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
        vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
        balle_vitesse = vitesse()


def vitesse_coin(cote):
    if cote == VERS_DROITE:
        v = RAQUETTE_REBOND_COIN[2:]
    else:
        v = RAQUETTE_REBOND_COIN[:2]
    return random.choice(v)


def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    change_vitesse(H, vitesse_h)
    change_vitesse(V, vitesse_v)


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_GAUCHE)
    resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_DROITE)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))

def change_vitesse(composante, val):
    global balle_vitesse

    vitesse_direction[composante] = val
    balle_vitesse = vitesse()

def vitesse():
    return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))


def vecteur_unitaire(angle):
    angle_radian = math.radians(angle)
    return (math.cos(angle_radian), math.sin(angle_radian))


def zone_raquette(position_horizontale):
    x_relatif = position_horizontale - raquette_position[H]
    if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
        return -1
    return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)


random.seed()

pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()

RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))

raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)

score = 0

temps = pygame.time.Clock()

vies_restantes = MAX_VIES

# --- Boucle principale
while vies_restantes > 0:

    balle_position[V] = FENETRE_HAUTEUR // 3
    start = time.time()
    auto_pause = True
    fini = False

    while not fini:
        # --- Traiter entrées joueur
        traite_entrees()

        # --- Logique du jeu
        if not auto_pause:
            anime()
        else:
            auto_pause = time.time() - start < 2.0

        # --- Dessiner l'écran
        dessine_court()

        # --- Afficher (rafraîchir) l'écran
        pygame.display.flip()

        # --- 50 images par seconde
        temps.tick(50)

pygame.display.quit()
exit()

Mettre le jeu sur pause

Implémenter une touche « pause » est très simple: il nous suffit de choisir une touche clavier (par exemple la touche « p ») pour changer la valeur logique (True/False) d’une variable booléenne globale représentant l’état de pause du jeu. Il nous suffit alors d’utiliser la valeur de cette variable globale pour inhiber les animations et les mouvements de la raquette (car nous ne voulons pas que le joueur utilise la facilité de pauser le jeu pour tricher):

import pygame
import math
import random
import time

H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)
BLEU       = (  0,   0, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
TOUCHE_PAUSE  = pygame.K_p

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7

MAX_VIES = 3

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini, vies_restantes, pause
    for evenement in pygame.event.get():
        if evenement.type == pygame.QUIT:
            fini = True
            vies_restantes = 0
        elif evenement.type == pygame.KEYDOWN:
            if evenement.key == TOUCHE_DROITE and not pause:
                deplace_raquette(VERS_DROITE)
            elif evenement.key == TOUCHE_GAUCHE and not pause:
                deplace_raquette(VERS_GAUCHE)
            elif evenement.key == TOUCHE_PAUSE:
                pause = not pause


def anime():
    global fini, vies_restantes
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(H, -vitesse_direction[H])

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(V, -vitesse_direction[V])

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True
        vies_restantes -= 1


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    marquoir = police.render(str(score), True, BLEU)
    fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    for vies in range(vies_restantes - 1):
        pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))


def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                change_vitesse(H, -abs(vitesse_direction[H]))
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                change_vitesse(H, abs(vitesse_direction[H]))
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                zone = zone_raquette(balle_position[H])
                change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
                change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
                augmente_score()
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                change_vitesse(V, -vitesse_direction[V])
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
                augmente_score()
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
               augmente_score()
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                change_vitesse(H, -abs(vitesse_direction[H]))
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                change_vitesse(H, abs(vitesse_direction[H]))


def augmente_score():
    global score, vitesse_amplitude, balle_vitesse

    score += 1
    if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
        vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
        balle_vitesse = vitesse()


def vitesse_coin(cote):
    if cote == VERS_DROITE:
        v = RAQUETTE_REBOND_COIN[2:]
    else:
        v = RAQUETTE_REBOND_COIN[:2]
    return random.choice(v)


def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    change_vitesse(H, vitesse_h)
    change_vitesse(V, vitesse_v)


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_GAUCHE)
    resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_DROITE)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))

def change_vitesse(composante, val):
    global balle_vitesse

    vitesse_direction[composante] = val
    balle_vitesse = vitesse()

def vitesse():
    return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))


def vecteur_unitaire(angle):
    angle_radian = math.radians(angle)
    return (math.cos(angle_radian), math.sin(angle_radian))


def zone_raquette(position_horizontale):
    x_relatif = position_horizontale - raquette_position[H]
    if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
        return -1
    return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)


random.seed()

pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
vitesse_amplitude = AMPLI_VITESSE_INIT
balle_vitesse = vitesse()

RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))

raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2, FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//12, True)

score = 0

temps = pygame.time.Clock()

pause = False

vies_restantes = MAX_VIES

# --- Boucle principale
while vies_restantes > 0:

    balle_position[V] = FENETRE_HAUTEUR // 3
    start = time.time()
    auto_pause = True
    fini = False

    while not fini:
        # --- Traiter entrées joueur
        traite_entrees()

        # --- Logique du jeu
        if not auto_pause and not pause:
            anime()
        else:
            auto_pause = time.time() - start < 2.0

        # --- Dessiner l'écran
        dessine_court()

        # --- Afficher (rafraîchir) l'écran
        pygame.display.flip()

        # --- 50 images par seconde
        temps.tick(50)

pygame.display.quit()
exit()

Ecran d’accueil

Pour avoir un écran d’accueil, vers lequel le jeu retourne après une partie, il suffit d’englober la boucle de jeu dans une autre boucle. Cette dernière ne se terminera que lorsque la touche “q” sera pressée alors que l’écran d’accueil est affiché.

Cette boucle initialise le jeu et doit traiter les entrées clavier, de sorte que le joueur puisse interagir avec notre écran d’accueil. Lorsque l’écran d’accueil est affiché, nous ralentissons donc la fréquence de rafraîchissement de l’écran, et de traitement d’entrées du joueur, à quatre fois par seconde (il ne sert strictement à rien d’aller plus vite), et nous jouons sur le fait que le jeu ne commencera que lorsque le joueur recevra des vies:

import pygame
import math
import random
import time

H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)
BLEU       = (  0,   0, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
TOUCHE_PAUSE  = pygame.K_p
TOUCHE_QUITTE = pygame.K_q

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7

MAX_VIES = 3

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini, vies_restantes, pause, joue

    for evenement in pygame.event.get():
        if delai:
            return
        if evenement.type == pygame.QUIT:
            fini = True
            vies_restantes = 0
            joue = False
        elif evenement.type == pygame.KEYDOWN:
            if vies_restantes == 0: # Ecran intro
                if evenement.key == TOUCHE_QUITTE:
                    joue = False
                    fini = True
                else: # Toute touche (sauf Q) commence le jeu
                    vies_restantes = MAX_VIES
            else:
                if evenement.key == TOUCHE_DROITE and not pause:
                    deplace_raquette(VERS_DROITE)
                elif evenement.key == TOUCHE_GAUCHE and not pause:
                    deplace_raquette(VERS_GAUCHE)
                elif evenement.key == TOUCHE_PAUSE:
                    pause = not pause
                elif evenement.key == TOUCHE_QUITTE:
                    fini = True
                    vies_restantes = 0



def anime():
    global fini, vies_restantes
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(H, -vitesse_direction[H])

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(V, -vitesse_direction[V])

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True
        vies_restantes -= 1


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    marquoir = police.render(str(score), True, BLEU)
    fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    for vies in range(vies_restantes - 1):
        pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
    if vies_restantes == 0:
        message = police.render("Au revoir...", True, JAUNE)
        message_largeur, message_hauteur = police.size("Au revoir...",)
        fenetre.blit(message, ((FENETRE_LARGEUR - message_largeur) // 2, 4 * FENETRE_HAUTEUR // 5))
    if pause:
        pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 - 35, FENETRE_HAUTEUR//2 - 25), (30, 50)))
        pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 + 5, FENETRE_HAUTEUR//2 - 25), (30, 50)))


def dessine_intro():
    fenetre.fill(BLEU_CLAIR)
    titre = police_titre.render('Squash!', True, JAUNE)
    titre_largeur, titre_hauteur = police_titre.size('Squash!')
    fenetre.blit(titre, ((FENETRE_LARGEUR - titre_largeur) // 2, (FENETRE_HAUTEUR - titre_hauteur) // 4))
    message1 = police.render("[Q]uitter", True, BLEU)
    message1_largeur, message1_hauteur = police.size("[Q]quitter")
    fenetre.blit(message1, ((FENETRE_LARGEUR - message1_largeur) // 2, 4 * FENETRE_HAUTEUR  // 5))
    message2 = police.render("N'importe quelle touche pour commencer...", True, BLEU)
    message2_largeur, message2_hauteur = police.size("N'importe quelle touche pour commencer...")
    fenetre.blit(message2, ((FENETRE_LARGEUR - message2_largeur) // 2, 4 * FENETRE_HAUTEUR // 5 + 1.2 * message1_hauteur))

def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                change_vitesse(H, -abs(vitesse_direction[H]))
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                change_vitesse(H, abs(vitesse_direction[H]))
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                zone = zone_raquette(balle_position[H])
                change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
                change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
                augmente_score()
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                change_vitesse(V, -vitesse_direction[V])
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
                augmente_score()
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
               augmente_score()
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                change_vitesse(H, -abs(vitesse_direction[H]))
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                change_vitesse(H, abs(vitesse_direction[H]))


def augmente_score():
    global score, vitesse_amplitude, balle_vitesse

    score += 1
    if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
        vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
        balle_vitesse = vitesse()


def vitesse_coin(cote):
    if cote == VERS_DROITE:
        v = RAQUETTE_REBOND_COIN[2:]
    else:
        v = RAQUETTE_REBOND_COIN[:2]
    return random.choice(v)


def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    change_vitesse(H, vitesse_h)
    change_vitesse(V, vitesse_v)


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_GAUCHE)
    resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_DROITE)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))

def change_vitesse(composante, val):
    global balle_vitesse

    vitesse_direction[composante] = val
    balle_vitesse = vitesse()

def vitesse():
    return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))


def vecteur_unitaire(angle):
    angle_radian = math.radians(angle)
    return (math.cos(angle_radian), math.sin(angle_radian))


def zone_raquette(position_horizontale):
    x_relatif = position_horizontale - raquette_position[H]
    if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
        return -1
    return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)


random.seed()

pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//20, True)
police_titre = pygame.font.SysFont('monospace', 80, True)

temps = pygame.time.Clock()

vies_restantes = 0
pause = False
joue   = True

# --- Boucle principale
while joue:
    delai = False
    balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
    vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
    vitesse_amplitude = AMPLI_VITESSE_INIT
    balle_vitesse = vitesse()
    raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2,
                         FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
    score = 0

    traite_entrees()
    dessine_intro()
    pygame.display.flip()
    temps.tick(4)

    # --- Boucle partie
    while vies_restantes > 0:

        balle_position[V] = FENETRE_HAUTEUR // 3
        start = time.time()
        auto_pause = True
        fini = False
        delai = False

        while not fini:
            # --- Traiter entrées joueur
            traite_entrees()

            # --- Logique du jeu
            if not auto_pause and not pause and not delai:
                anime()
                if fini and vies_restantes == 0:
                    delai = True
                    fini = False
                    start = time.time()
            else:
                auto_pause = time.time() - start < 2.0
                if delai and not auto_pause:
                    fini = True

            # --- Dessiner l'écran
            dessine_court()

            # --- Afficher (rafraîchir) l'écran
            pygame.display.flip()

            # --- 50 images par seconde
            temps.tick(50)

pygame.display.quit()
exit()

Interaction avec la souris

Pygame définit des événements d’entrée/sortie pour gérer la souris (voir la documentation de Pygame). On peut détecter un clic par un événement de type pygame.MOUSEBUTTONDOWN. Tout événement evenement de ce type possède une variable evenement.button qui indique quel bouton a été cliqué et une variable evenement.pos qui contient la position du clic (dans une séquence de deux éléments).

Nous modifions donc notre boucle de traitement des événements d’entrée pour non seulement réagir à un clic du bouton gauche de la souris, mais aussi pour donner l’alternative au joueur de déplacer la raquette avec les touches “a” et “s”.

Lorsque le joueur clique sur la balle, son score est doublé. Par contre, si le joueur clique en dehors de la balle, alors son score est divisé de moitié:

import pygame
import math
import random
import time

H = 0
V = 1

BLEU_CLAIR = (  0, 191, 200)
JAUNE      = (255, 255,   0)
ROUGE      = (255,   0,   0)
BLANC      = (255, 255, 255)
BLEU       = (  0,   0, 255)

FENETRE_LARGEUR = 800
FENETRE_HAUTEUR = 600

BALLE_RAYON = 10
BALLE_DIAM = 2 * BALLE_RAYON

RAQUETTE_LARGEUR = 70
RAQUETTE_HAUTEUR = 10
RAQUETTE_ESPACE = 10
RAQUETTE_DEPLACEMENT = 10
RAQUETTE_ZONES = 4

TOUCHE_DROITE = pygame.K_RIGHT
TOUCHE_GAUCHE = pygame.K_LEFT
TOUCHE_DROITE2 = pygame.K_s
TOUCHE_GAUCHE2 = pygame.K_a
TOUCHE_PAUSE  = pygame.K_p
TOUCHE_QUITTE = pygame.K_q

BOUTON_SOURIS_GAUCHE = 1

VERS_DROITE = 1
VERS_GAUCHE = -1

MUR_EPAISSEUR = 10

CENTRE         = 0
GAUCHE         = 1
DROITE         = 2
DESSUS         = 4
HAUT_GAUCHE    = 5
HAUT_DROITE    = 6
DESSOUS        = 8
DESSOUS_GAUCHE = 9
DESSOUS_DROITE = 10

VITESSE_MAX = BALLE_RAYON
AMPLI_VITESSE_INIT = 5
FACTEUR_AUGMENTATION_VITESSE = 1.1
FRAPPES_AUGMENTATION_VITESSE = 7

MAX_VIES = 3

# --- Définitions de fonctions
def deplace_raquette(sens):
    raquette_position[H] += RAQUETTE_DEPLACEMENT * sens
    test_touche_gauche(raquette_position, 0, MUR_EPAISSEUR)
    test_touche_droite(raquette_position, RAQUETTE_LARGEUR, FENETRE_LARGEUR - MUR_EPAISSEUR)


def souris_cliquee(pos):
    global score
    if distance2(pos, balle_position) > BALLE_RAYON * BALLE_RAYON:
        score = score // 2
    else:
        score = score * 2


def test_touche_db(objet, distance, point, direction, separe):
    if objet[direction] + distance >= point:
        if separe:
            objet[direction] = point - distance
        return True
    else:
        return False


def test_touche_gh(objet, distance, point, direction, separe):
    if objet[direction] - distance <= point:
        if separe:
            objet[direction] = point + distance
        return True
    else:
        return False


def test_touche_droite(objet, largeur_droite, point_droit, separe=True):
    return test_touche_db(objet, largeur_droite, point_droit, H, separe)


def test_touche_gauche(objet, largeur_gauche, point_gauche, separe=True):
    return test_touche_gh(objet, largeur_gauche, point_gauche, H, separe)


def test_touche_haut(objet, hauteur_haut, point_haut, separe=True):
    return test_touche_gh(objet, hauteur_haut, point_haut, V, separe)


def test_touche_bas(objet, hauteur_bas, point_bas, separe=True):
    return test_touche_db(objet, hauteur_bas, point_bas, V, separe)


def traite_entrees():
    global fini, vies_restantes, pause, joue

    for evenement in pygame.event.get():
        if delai:
            return
        if evenement.type == pygame.QUIT:
            fini = True
            vies_restantes = 0
            joue = False
        elif evenement.type == pygame.KEYDOWN:
            if vies_restantes == 0: # Ecran intro
                if evenement.key == TOUCHE_QUITTE:
                    joue = False
                    fini = True
                else: # Toute touche (sauf Q) commence le jeu
                    vies_restantes = MAX_VIES
            else:
                touche = evenement.key
                if (touche == TOUCHE_DROITE or touche == TOUCHE_DROITE2) and not pause:
                    deplace_raquette(VERS_DROITE)
                elif (touche == TOUCHE_GAUCHE or touche == TOUCHE_GAUCHE2) and not pause:
                    deplace_raquette(VERS_GAUCHE)
                elif touche == TOUCHE_PAUSE:
                    pause = not pause
                elif touche == TOUCHE_QUITTE:
                    fini = True
                    vies_restantes = 0
        elif evenement.type == pygame.MOUSEBUTTONDOWN and evenement.button == BOUTON_SOURIS_GAUCHE and not (pause or auto_pause):
            souris_cliquee(evenement.pos)


def anime():
    global fini, vies_restantes
    balle_position[H] = balle_position[H] + balle_vitesse[H]
    balle_position[V] = balle_position[V] + balle_vitesse[V]

    if test_touche_droite(balle_position, BALLE_RAYON, FENETRE_LARGEUR - MUR_EPAISSEUR) \
       or test_touche_gauche(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(H, -vitesse_direction[H])

    if test_touche_haut(balle_position, BALLE_RAYON, MUR_EPAISSEUR):
        change_vitesse(V, -vitesse_direction[V])

    test_collision((raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))

    if test_touche_bas(balle_position, BALLE_RAYON, FENETRE_HAUTEUR + BALLE_DIAM):
        fini = True
        vies_restantes -= 1


def dessine_court():
    fenetre.fill(BLEU_CLAIR)
    marquoir = police.render(str(score), True, BLEU)
    fenetre.blit(marquoir, (5 * FENETRE_LARGEUR // 8, FENETRE_HAUTEUR // 10))
    pygame.draw.rect(fenetre, BLANC, ((0, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    pygame.draw.rect(fenetre, BLANC, ((MUR_EPAISSEUR, 0), (FENETRE_LARGEUR - 2 * MUR_EPAISSEUR, MUR_EPAISSEUR)))
    pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR - MUR_EPAISSEUR, 0), (MUR_EPAISSEUR, FENETRE_HAUTEUR)))
    for vies in range(vies_restantes - 1):
        pygame.draw.circle(fenetre, BLEU, (3 * FENETRE_LARGEUR // 8 + vies * MUR_EPAISSEUR + 2, MUR_EPAISSEUR // 2 ), (MUR_EPAISSEUR - 2) // 2)
    pygame.draw.circle(fenetre, JAUNE, balle_position, BALLE_RAYON)
    pygame.draw.rect(fenetre, ROUGE, (raquette_position, (RAQUETTE_LARGEUR, RAQUETTE_HAUTEUR)))
    if vies_restantes == 0:
        message = police.render("Au revoir...", True, JAUNE)
        message_largeur, message_hauteur = police.size("Au revoir...",)
        fenetre.blit(message, ((FENETRE_LARGEUR - message_largeur) // 2, 4 * FENETRE_HAUTEUR // 5))
    if pause:
        pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 - 35, FENETRE_HAUTEUR//2 - 25), (30, 50)))
        pygame.draw.rect(fenetre, BLANC, ((FENETRE_LARGEUR//2 + 5, FENETRE_HAUTEUR//2 - 25), (30, 50)))


def dessine_intro():
    fenetre.fill(BLEU_CLAIR)
    titre = police_titre.render('Squash!', True, JAUNE)
    titre_largeur, titre_hauteur = police_titre.size('Squash!')
    fenetre.blit(titre, ((FENETRE_LARGEUR - titre_largeur) // 2, (FENETRE_HAUTEUR - titre_hauteur) // 4))
    message1 = police.render("[Q]uitter", True, BLEU)
    message1_largeur, message1_hauteur = police.size("[Q]quitter")
    fenetre.blit(message1, ((FENETRE_LARGEUR - message1_largeur) // 2, 4 * FENETRE_HAUTEUR  // 5))
    message2 = police.render("N'importe quelle touche pour commencer...", True, BLEU)
    message2_largeur, message2_hauteur = police.size("N'importe quelle touche pour commencer...")
    fenetre.blit(message2, ((FENETRE_LARGEUR - message2_largeur) // 2, 4 * FENETRE_HAUTEUR // 5 + 1.2 * message1_hauteur))

def distance2(pt1, pt2):
    delta_h = pt1[H] - pt2[H]
    delta_v = pt1[V] - pt2[V]
    return delta_h * delta_h + delta_v * delta_v


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_horizontale_rel(rect):
    if balle_position[H] < rect[0][H]:
        return GAUCHE
    elif balle_position[H] > rect[0][H] + rect[1][H]:
        return DROITE
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_verticale_rel(rect):
    if balle_position[V] < rect[0][V]:
        return DESSUS
    elif balle_position[V] > rect[0][V] + rect[1][V]:
        return DESSOUS
    else:
        return CENTRE


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def position_relative(rect):
    return position_horizontale_rel(rect) + position_verticale_rel(rect)


# rect represente un rectangle ((gauche, haut), (largeur, hauteur))
def test_collision(rect):
    ball_rect = pygame.Rect((balle_position[H] - BALLE_RAYON, balle_position[V] - BALLE_RAYON), (BALLE_DIAM, BALLE_DIAM))
    if ball_rect.colliderect(rect):
        rayon2 = BALLE_RAYON * BALLE_RAYON
        position = position_relative(rect)
        if position == GAUCHE:
            if test_touche_droite(balle_position, BALLE_RAYON, rect[0][H]):
                change_vitesse(H, -abs(vitesse_direction[H]))
        elif position == DROITE:
            if test_touche_gauche(balle_position, BALLE_RAYON, rect[0][H] + rect[1][H]):
                change_vitesse(H, abs(vitesse_direction[H]))
        elif position ==  DESSUS:
            if test_touche_bas(balle_position, BALLE_RAYON, rect[0][V]):
                zone = zone_raquette(balle_position[H])
                change_vitesse(H, RAQUETTE_VITESSE_REBOND[zone][H])
                change_vitesse(V, RAQUETTE_VITESSE_REBOND[zone][V])
                augmente_score()
        elif position == DESSOUS:
            if test_touche_haut(balle_position, BALLE_RAYON, rect[0][V] + rect[1][V]):
                change_vitesse(V, -vitesse_direction[V])
        elif position == HAUT_GAUCHE:
            if distance2(balle_position, rect[0]) <= rayon2:
                collision_coin_haut_gauche(rect)
                augmente_score()
        elif position == HAUT_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V])) <= rayon2:
               collision_coin_haut_droite(rect)
               augmente_score()
        elif position == DESSOUS_GAUCHE:
            if distance2(balle_position, (rect[0][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_gauche(rect)
        elif position == DESSOUS_DROITE:
            if distance2(balle_position, (rect[0][H] + rect[1][H], rect[0][V] + rect[1][V])) <= rayon2:
                collision_coin_bas_droite(rect)
        else:
            # Cas final: CENTRE. On fait l'hypothèse que l'amplitude de la vitesse ne dépasse jamais la taille du rayon.
            # Il faudra s'assurer que cette condition est toujours vraie.
            # Eviter un recouvrement lorsque raquette et balle bougent l'une vers l'autre:
            delta_g = abs(balle_position[H] - rect[0][H])
            delta_d = abs(balle_position[H] - rect[0][H] - rect[1][H])
            if delta_g < delta_d:
                balle_position[H] = rect[0][H] - BALLE_RAYON
                change_vitesse(H, -abs(vitesse_direction[H]))
            else:
                balle_position[H] = rect[0][H] + rect[1][H] + BALLE_RAYON
                change_vitesse(H, abs(vitesse_direction[H]))


def augmente_score():
    global score, vitesse_amplitude, balle_vitesse

    score += 1
    if vitesse_amplitude < VITESSE_MAX and score % FRAPPES_AUGMENTATION_VITESSE == 0:
        vitesse_amplitude = min(vitesse_amplitude * FACTEUR_AUGMENTATION_VITESSE, VITESSE_MAX)
        balle_vitesse = vitesse()


def vitesse_coin(cote):
    if cote == VERS_DROITE:
        v = RAQUETTE_REBOND_COIN[2:]
    else:
        v = RAQUETTE_REBOND_COIN[:2]
    return random.choice(v)


def resoudre_collision_coin(coin, delta_h, delta_v, vitesse_h, vitesse_v):
    balle_position[H] = coin[H] + delta_h
    balle_position[V] = coin[V] + delta_v
    change_vitesse(H, vitesse_h)
    change_vitesse(V, vitesse_v)


def collision_coin_haut_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_GAUCHE)
    resoudre_collision_coin(rect[0], -delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_haut_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    vitesse_rebond = vitesse_coin(VERS_DROITE)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V]), delta, -delta, vitesse_rebond[H], vitesse_rebond[V])


def collision_coin_bas_gauche(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H], rect[0][V] + rect[1][V]), -delta, delta, -abs(vitesse_direction[H]), abs(vitesse_direction[V]))


def collision_coin_bas_droite(rect):
    delta = round(BALLE_RAYON * 0.707)
    resoudre_collision_coin((rect[0][H] + rect[1][H], rect[0][V] + rect[1][V]), delta, delta, abs(vitesse_direction[H]), abs(vitesse_direction[V]))

def change_vitesse(composante, val):
    global balle_vitesse

    vitesse_direction[composante] = val
    balle_vitesse = vitesse()

def vitesse():
    return (round(vitesse_amplitude * vitesse_direction[H]), round(vitesse_amplitude * vitesse_direction[V]))


def vecteur_unitaire(angle):
    angle_radian = math.radians(angle)
    return (math.cos(angle_radian), math.sin(angle_radian))


def zone_raquette(position_horizontale):
    x_relatif = position_horizontale - raquette_position[H]
    if x_relatif < 0 or x_relatif >= RAQUETTE_LARGEUR:
        return -1
    return int(x_relatif / RAQUETTE_LARGEUR * RAQUETTE_ZONES)


random.seed()

pygame.init()
pygame.key.set_repeat(200, 25)

fenetre_taille = (FENETRE_LARGEUR, FENETRE_HAUTEUR)
fenetre = pygame.display.set_mode(fenetre_taille)

fenetre.fill(BLEU_CLAIR)

RAQUETTE_VITESSE_REBOND = tuple(vecteur_unitaire(a) for a in (-120, -100, -80, -60))
RAQUETTE_REBOND_COIN = tuple(vecteur_unitaire(a) for a in (-135, 135, -45, 45))

police = pygame.font.SysFont('monospace', FENETRE_HAUTEUR//20, True)
police_titre = pygame.font.SysFont('monospace', 80, True)

temps = pygame.time.Clock()

vies_restantes = 0
pause = False
auto_pause = True
joue   = True

# --- Boucle principale
while joue:
    delai = False
    balle_position = [FENETRE_LARGEUR // 2, FENETRE_HAUTEUR // 3]
    vitesse_direction = [math.sqrt(0.5), math.sqrt(0.5)]
    vitesse_amplitude = AMPLI_VITESSE_INIT
    balle_vitesse = vitesse()
    raquette_position = [FENETRE_LARGEUR // 2 - RAQUETTE_LARGEUR // 2,
                         FENETRE_HAUTEUR - RAQUETTE_ESPACE - RAQUETTE_HAUTEUR]
    score = 0

    traite_entrees()
    dessine_intro()
    pygame.display.flip()
    temps.tick(4)

    # --- Boucle partie
    while vies_restantes > 0:

        balle_position[V] = FENETRE_HAUTEUR // 3
        start = time.time()
        auto_pause = True
        fini = False
        delai = False

        while not fini:
            # --- Traiter entrées joueur
            traite_entrees()

            # --- Logique du jeu
            if not auto_pause and not pause and not delai:
                anime()
                if fini and vies_restantes == 0:
                    delai = True
                    fini = False
                    start = time.time()
            else:
                auto_pause = time.time() - start < 2.0
                if delai and not auto_pause:
                    fini = True

            # --- Dessiner l'écran
            dessine_court()

            # --- Afficher (rafraîchir) l'écran
            pygame.display.flip()

            # --- 50 images par seconde
            temps.tick(50)

pygame.display.quit()
exit()

Problème 2

Vous allez ajouter un « power-up » à notre jeu: si, au cours d’une même vie, le joueur frappe la balle un certain nombre (prédéfini) de fois, la largeur de la raquette augmente de 50%. Ce « power-up » ne peut se produire qu’une seule fois par vie, et ne doit pas avoir d’effet d’une vie à l’autre.

Une fois votre programme terminé, connectez-vous sur la plateforme de soumission de montefiore, l’un d’entre-vous doit y créer un groupe pour le cours du PPI, et ses partenaires doivent rejoindre ce groupe. Ensuite l’un d’entre-vous doit envoyer votre soumission. Ce tutoriel explique comment soumettre votre problème sur la plateforme de soumission.

Avertissement

Ce problème doit être soumis sur la plateforme de soumission avant le 16/10/2022-23h59. Si la deadline n’est pas respectée, vous aurez 0.

Fonctionnalités supplémentaires

En utilisant les mécanismes de programmation que nous avons appris, vous avez maintenant tous les outils en main pour implémenter des fonctionnalités supplémentaires. Voici quelques suggestions:

  • Ajouter automatiquement une vie dès qu’un certain nombre de points sont atteints.

  • De temps à autre, faire apparaître brièvement des objets qui offrent un bonus (des points, une vie ou un « power-up ») s’ils sont touchés par la balle.

  • Placer des obstacles supplémentaires.

Amusez-vous!