Script et programme

Identification

Infoforall

8 - Créer un programme en Python 2


Deuxième partie de l'activité.

Elle est consacrée à quelques interfaces graphiques. Nous ne rentrerons pas dans les détails de leurs utilisations, hors programme en NSI car il s'agit de choses trop spécifiques. Rien de difficile par contre : si vous savez vous documenter, vous pourrez facilement les prendre en main avec un peu de bonne volonté. Je ne fais que les survoler ici.

On en profitera pour revoir les types structurés.

Voici ce que vous allez réaliser 

Réaliser des courbes automatiquement.

Récupérer une image, la transformer plusieurs fois et coller l'ensemble pour faire une belle affiche :

Réaliser un jeu vidéo dans l'esprit Retro avec Pyxel :

Logo Pyxel
Logo Pyxel (https://github.com/kitao/pyxel#)

Logiciel nécessaire pour l'activité : Thonny

Evaluation : 2 questions

  questions 09

  questions 10

DM 🏠 : Non

Documents de cours PDF : .PDF

Sources latex : .TEX et entete.tex et licence.tex

1 - Cours : structure et exécution d'un programme

Attention aux noms des programmes Python

Vous allez enregistrer votre programme en lui donnant un nom. N'utilisez JAMAIS le nom d'un des modules Python qui existent déjà : turtle, random... Sinon, vous ne pourriez plus utiliser turtle.

(Rappel) 1.1 Différence entre console interactive et programme

Cas de la console

Lorsqu'une ligne contient uniquement une expression, l'interpréteur l'évalue et affiche le résultat.

>>> a = 10 >>> b = a + 2 >>> b 12

On visualise bien que 12 s'affiche sur la console lorsqu'on demande juste d'évaluer b.

Cas du programme

Lorsqu'une ligne contient uniquement une expression, l'interpréteur l'évalue et... c'est tout. Pas d'affichage.

1 2 3
a = 10 b = a + 2 b

Ce programme n'affiche rien alors que la ligne 3 ne contient que la demande d'évaluation d'une expresssion.

Dans un programme, si vous voulez voir quelque chose s'afficher, il faut le demander explicitement :

1 2 3
a = 10 b = a + 2 print(b)
(Rappel) 1.2 COMMENTAIRES : expliquer le fonctionnement

Les commentaires sont destinés à un lecteur humain et ils visent à rendre le code interne facile à comprendre.

Cela doit permettre de modifier un code même plusieurs années après sa création initiale.

Pour rajouter un commentaire en Python, on utilise le caractère dièse (#) de façon adaptée. Trois exemples à parfaitement comprendre :

  • Commentaire sur toute une ligne (ligne 1 ci-dessous)
  • 1
    # Toute cette ligne est un commentaire.
  • Commentaire en fin de ligne (ligne 2 ci-dessous)
  • 2
    print("Bonjour tout le monde") # Ceci est également un commentaire
  • Notez bien que la ligne 3 ne comporte aucun commentaire puisque le # fait juste partie d'un string.
  • 3
    print("Cette ligne ne contient pas de # commentaire")

Voici le résultat de ce programme L1 à L3 dans la console : il n'affiche pas les commentaires.

1 2 3
# Toute cette ligne est un commentaire. print("Bonjour tout le monde") # Ceci est également un commentaire print("Cette ligne ne contient pas de # commentaire")
>>> %Run progcommentaires.py Bonjour tout le monde Cette ligne ne contient pas de # commentaire >>>

L'interpréteur Python ne tentera pas d'exécuter les commentaires.

(Rappel) 1.3 Structure d'un programme

Voici la structure attendue d'un programme Python :

  1. Importation des modules nécessaires au programme.
  2. Déclarations des CONSTANTES : par convention, le nom des CONSTANTES est constitué uniquement de majuscules.
  3. Déclaration des variables globales destinées à être lues depuis des fonctions : à utiliser avec modération. Elles sont sources de nombreux disfonctionnements lorsqu'elles sont mal gérées.
  4. Déclaration des fonctions.
  5. Les instructions du programme en lui-même : on nomme cette partie "programme principal" parfois. On y place également les variables globales qu'on envoie simplement en tant que paramètres.

Convention

Habituellement, on sépare les parties ci-dessous par au moins deux lignes vides.

(Rappel) 1.4 Noms des variables globales et locales

Deux utilisations des variables globales :

A - Lues directement par les fonctions

Cela semble pratique mais si vous décidez de changer le nom dans le programme, c'est l'ensemble des lignes de vos fonctions qu'il va falloir modifier. C'est donc une source d'erreurs.

Exemple avec une fonction qui déplace le crayon pour le déplacer à la position voulue :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# Importation des modules supplémentaires import turtle as trt # CONSTANTES et variables globales lues par les fonctions feutre = trt.Turtle() # feutre va être lue par les fonctions # Déclaration des fonctions def deplacer(x, y): feutre.penup() # On lève la pointe feutre.goto(x, y) # On déplace le crayon feutre.pendown() # On abaisse la pointe # Programme feutre.color("red") feutre.fillcolor("orange") feutre.pensize(4) feutre.speed(5) feutre.forward(150) deplacer(0, 100) feutre.forward(150) deplacer(0, -100) feutre.forward(150)
B - Transmises aux fonctions lors de l'appel

Avantage : le nom de la variable globale n'a aucune importance depus la fonction puisqu'on la stocke temporairement dans une variable locale. Changer le nom de la variable d'un côté ou de l'autre n'aura donc aucune incidence.

Désavantage : le code semble plus lourd.

D'ailleurs, on peut même garder le même nom si on veut, Python ne sera pas perdu entre variable locale et globale.

Pour ne pas perdre le lecteur humain, le mieux est d'avoir

  • un nom pour la variable globale (feutre) et
  • un nom proche dans toutes les fonctions qui vont le récupérer en entrée (ftr).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# Importation des modules supplémentaires import turtle as trt # CONSTANTES et variables globales lues par les fonctions # Déclaration des fonctions def deplacer(ftr, x, y): ftr.penup() # On lève la pointe ftr.goto(x, y) # On déplace le crayon ftr.pendown() # On abaisse la pointe # Programme feutre = trt.Turtle() # transmise en entrée aux fonctions feutre.color("red") feutre.fillcolor("orange") feutre.pensize(4) feutre.speed(5) feutre.forward(150) deplacer(feutre, 0, 100) feutre.forward(150) deplacer(feutre, 0, -100) feutre.forward(150)

Cette activité (en deux parties) consiste en une présentation de programmes qu'on peut réaliser en réalisant des importations de modules divers et variés. Je donne quelques exemples mais le nombre de modules réalisés pour Python est énorme.

Aujourd'hui, quelques interfaces graphiques.

2 - pyplot de matplotlib : gestion de graphique

Nous allons travailler ici avec des tableaux (qu'on déclare avec des crochets []) pour réaliser des courbes avec le module pyplot.

01 ✔° Si cela n'est pas fait, installer la bibliothèque matplotlib dans Thonny :

  • Allez dans l'onglet "Outils"
  • Cherchez "Gérer les paquets"
  • Lancez la recherche sur matplotlib puis activer l'installation
  • Attendez que l'installation soit totalement effectuée
  • Validez l'installation en utisant le bouton Fermer

02 ✔° Utiliser ce programme (en le nommant graphique.py qui va vous permettre de tracer un graphique affichant 3 points :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Importation import matplotlib.pyplot as plt # Constante TAILLE = 50 # Programme principal plt.scatter(x=5, y=10, c="blue", s=TAILLE*4) plt.scatter(x=10, y=20, c="green", s=TAILLE) plt.scatter(x=15, y=6, c="red", s=TAILLE*2) plt.grid() plt.show() plt.savefig("graphique.png")

Que fait ce programme ?

  • Ligne 2 : il importe le module permettant de tracer des courbes sous le nom de plt
  • Ligne 5 : on génère une constante valant 50.
  • Lignes 9-10-11 : on rajoute, avec la fonction scatter(), trois points dont on fournit les coordonnées x et y, ainsi que leur couleur c et leur taille s (pour size). La taille du point est gérée à partir de la constante TAILLE (to scatter veut dire éparpiller en anglais).
  • Ligne 13 : on rajoute la grille avec la fonction grid() (qui veut dire grille en anglais)
  • Ligne 14 : affiche le graphique à l'écran avec la fonction show()
  • Ligne 15 : sauvegarde l'image à l'aide de la fonction save()

On fait trois fois la même chose pour placer les points avec scatter(). Or, la fonction scatter() accepte également de recevoir des arguments de type tableau. On peut donc lui envoyer un tableau contenant les trois abscisses, un autre tableau contenant les trois ordonnées correspondantes, un tableau contenant les couleurs et un tableau contenant la taille du point.

03° Utiliser ce nouveau programme pour vérifier qu'il crée bien un graphique identique au précédent en utilisant uniquement la ligne 12 pour le générer :

12
plt.scatter(x=tx, y=ty, c=tc, s=ts)

Voici le programme dans sa totalité :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Importation import matplotlib.pyplot as plt # Constantes TAILLE = 50 TX = [5, 10, 15] TY = [10, 15, 5] TC = ["blue", "green", "red"] TS = [TAILLE*4, TAILLE, TAILLE*2] # Programme principal plt.scatter(x=TX, y=TY, c=TC, s=TS) plt.grid() plt.show() plt.savefig("graphique-avec-tableaux.png")

Questions

  • Que reçoit le paramètre x de la fonction scatter() cette fois (type et contenu) ?
  • Quelles vont être les informations utilisées pour tracer le premier point ?
  • Quelles vont être les informations transmises pour tracer le deuxième point ?

...CORRECTION...

  • Que reçoit le paramètre x de la fonction scatter() cette fois ?
  • 12
    plt.scatter(x=TX, y=TY, c=TC, s=TS)

    On voit que le paramètre x reçoit l'argument nommé TX. En allant voir en ligne 6, on voit que cette variable fait référence à un tableau.

    6
    TX = [5, 10, 15]

  • Quelles vont être les informations utilisées pour tracer le premier point ?
  • Pour répondre, il faut comprendre la ligne 12 : le paramètre x des abscisses reçoit le tableau TX, le paramètre y des ordonnées reçoit le tableau TY, le paramètre c des couleurs reçoit le tableau TC, le paramètre s des tailles (sizes en anglais) reçoit le tableau TS.

    Ensuite, la fonction utilise d'abord les éléments d'indice 0, puis ceux d'indice 1...

    Lors du premier tracé, on utilise donc les valeurs surlignées en jaune, celles correspondant aux élements d'indice 0.

    6 7 8 9
    TX = [5, 10, 15] TY = [10, 15, 5] TC = ["blue", "green", "red"] TS = [TAILLE*4, TAILLE, TAILLE*2]
  • Quelles vont être les informations transmises pour tracer le deuxième point ?
  • On utilise donc les valeurs surlignées en jaune, celles correspondant aux élements d'indice 1.

    6 7 8 9
    TX = [5, 10, 15] TY = [10, 15, 5] TC = ["blue", "green", "red"] TS = [TAILLE*4, TAILLE, TAILLE*2]

On peut aussi demander de tracer le graphique correspondant à une suite de droite reliant les points qu'on lui transmet. Il faut dans ce cas utiliser la fonction plot().

04 ✔° Utiliser ce nouveau programme où la seule différence se trouve en ligne 12 :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Importation import matplotlib.pyplot as plt # Constantes TAILLE = 50 TX = [5, 10, 15] TY = [10, 15, 5] TC = ["blue", "green", "red"] TS = [TAILLE*4, TAILLE, TAILLE*2] # Programme principal plt.plot(TX, TY) plt.grid() plt.show() plt.savefig("graphique-avec-tableaux-2.png")

Cette fois, on obtient un graphique où les points sont reliés entre eux par des droites.

En anglais,

  • to scatter veut dire éparpiller ou disperser.
  • to plot veut dire tracer un graphique.
2 Introduction au module pyplot

2..1 Installer puis importer

Le module pyplot appartient à la bibliothèque matplotlib.

Par y avoir accès, il faut

  1. Avoir téléchargé sur votre ordinateur la bibliothèque matplotlib contenant ce module pyplot: elle n'est pas présente de base dans Python Pour cela, deux solutions :
    • Avec Thonny : utiliser le menu Outils puis Gérer les paquets et rechercher puis installer matplotlib. C'est fini.
    • Avec Python en ayant bien coché la case PATH lors de l'installation : ouvrir un terminal (bash sous Linux, invite de commande ou powershell sous Windows puis taper ceci :
    • $ python3 -m pip install matplotlib

      La bibliothèque s'installe toute seule. C'est fini. Sous Windows, le 3 n'est pas obligatoire.

  2. Importer le module dans votre programme. Pour vérifier que l'installation s'est bien passée, le mieux est de tenter d'importer le module depuis la console Python.
  3. >>> from matplotlib import pyplot

    D'ailleurs, comme pyplot est long à taper, on fait plutôt ceci :

    >>> from matplotlib import pyplot as plt

2..2 Fonctions basiques

On notera qu'on rajoute toujours ici plt. pour indiquer d'aller chercher cette fonction dans le module qui porte ce nom.

  • plt.scatter(x=5, y=10, c="green", s=3) permet de demander la création d'un point en fournissant les coordonnées x, y et la couleur c et la taille s (size) du point.
  • plt.scatter(x=[5, 10, 15], y=[10, 20, 6], c=["blue", "green", "red"], s=[1, 2, 4]) permet de demander la création de plusieurs points en fournissant 4 tableaux : un par les x, un pour les y, un pour les couleurs et un pour les tailles de ces points.
  • Le premier point sera donc en (x=5, y=10) et aura l'apparence d'un point bleu de taille 1 pixel.

  • plt.plot([5, 10, 15], [10, 15, 5]) permet de placer les points puis trace des traits entrechaque point. On lui transmet ici d'abord le tableau des x puis le tableau des y.
  • plt.grid() permet de demander l'affichage d'une grille facilitant la lecture des valeurs.
  • plt.show() permet de demander l'affichage des données fournies. Sans son appel, rien ne s'affiche : plot() et scatter() générent la courbe en mémoire mais c'est bien show() qui autorise sa visualisation.
  • plt.savefig("graphique.png") permet d'enregistrer le résultat visuel dans un fichier image nommé ici "graphique.png". Puisqu'on ne donne qu'une adresse relative, le fichier-image apparaitra dans le répertoire où se trouve votre fichier-python.

3 - Pillow : gestion des images

Dans cette partie, nous allons travailler notamment avec les tuples (qu'on déclare avec des parenthèses ()) et le module de gestion d'images Pillow mais qui se nomme PIL dans les scipts Python...

PIL ou Pillow ? Un peu d'histoire...

Il existait un premier module permettant de gérer les images en Python. Ce module se nommait PIL.

Après quelques années, une partie de l'équipe des créateurs a décidé de faire un 'fork', c'est à dire une version alternative, différente mais basée sur le même code initial. Cette autre version se nomme Pillow.

Aujourd'hui, c'est la version Pillow qui perdure mais pour des raisons de retrocompatibilité des programmes, elle s'appelle PIL dans les programmes...

C'est pourquoi :

  • Nous allons installé depuis Thonny un module nommé Pillow, mais
  • on l'importera depuis notre programme Python sous le nom PIL.

05 ✔° Si cela n'est pas fait, installer la bibliothèque Pillow dans Thonny :

  • Allez dans l'onglet "Outils"
  • Cherchez "Gérer les paquets"
  • Lancez la recherche sur Pillow puis activer l'installation
  • Attendez que l'installation soit totalement effectuée
  • Validez l'installation en utisant le bouton Fermer
  • Vérifiez que cela c'est bien passé en tentant d'importer le module depuis la console
  • >>> import PIL >>> _

    Vous devriez juste retrouver la main une fois le module importé.

06 ✔° Télécharger l'image suivante en la plaçant dans un répertoire nommé par exemple Couleurs. Vous nommerez l'image Tux.png.

Tux est le nom de ce pingouin, la mascotte de Linux.

Sur votre ordinateur, vous aurez donc quelque chose semblable à cela :

📁 Couleurs

📄 Tux.png

4 types d'images
Les images PNG

ce type compresse les images sans les dégrader (leurs fichiers nécessitent moins d'octets qu'avec un enregistrement basique)

Ce type PNG accepte

  • les images où les pixels sont définis par trois valeurs (dans l'ordre RGB ou RVB : le rouge, le vert, le bleu) mais également
  • les images RGBA possèdant une capacité de transparence et où les pixels sont définis par quatre valeurs : le rouge, le vert, le bleu et la valeur ALPHA qui désigne à quel point ce pixel doit être transparent.
Les images JPEG ou JPG

Ce type d'images compressent encore plus les données et les fichiers image prennent donc encore moins de place.

Leurs désavantages ?

  1. Ces fichiers JPEG n'acceptent pas la transparence ALPHA
  2. Les images enregistrées sont dégradées lors de la compression : elles perdent un peu en qualité. Pas assez pour le voir réellement sur un écran, mais assez pour le voir si on zoome.
Les images BMP

Ces images ne sont pas compressées : on ne perd pas en qualité mais les fichiers sont énormes...

Les images GIF

Ces images permettent pas un grand nombre de couleurs mais ont l'avantage d'être de petits fichiers, d'avoir la capacité de transparence si on le veut et on peut les associer à la suite pour réaliser une animation GIF.

07 ✔° Créer un nouveau programme Python nommé gestion_images.py dans un répertoire Couleurs.

1 2 3 4 5 6 7 8 9 10
# Importations from PIL import Image as Img # Constantes NOM_IMAGE = "Tux.png" NOM_SAUVEGARDE = "Tux2.png" # Programme principal ref_image = Img.open(NOM_IMAGE) ref_image.show()

📁 Couleurs

📄 Tux.png

📄 gestion_images.py

Si vous lancez le programme, vous devriez voir et comprendre :

  • Qu'il crée un objet-python de votre image en L9. A partir de là, la variable ref_image contient l'identifiant mémoire de l'image que Python pourra manipuler. Mais attention, l'image stockée en mémoire de Python est juste une copie créée à partir du fichier-image. Les deux sont indépendants.
  • En L10, on demande à Python de demander au programme d'affichage d'images de votre ordinateur de vous l'afficher à l'écran.

Nous allons maintenant utiliser Python pour créer des images à la Andy Warhol.

08° Télécharger l'image suivante en la plaçant dans Couleurs. Vous nommerez l'image mm.jpg.

Il s'agit d'une image de Marilyn Monroe.

📁 Couleurs

📄 Tux.png

📄 gestion_image.py

📄 mm.jpg

Modifier le script gestion_image.py pour qu'il parvienne à afficher cette nouvelle image. Bien entendu, modifier les choses correctement en agissant uniquement sur les variables et constantes.

Dans cette partie, nous allons voir comment modifier facilement les couleurs d'une image RBG. Nous allons :

  1. Créer d'un objet-image dans Python à partir d'un fichier-image (avec la fonction open()).
  2. Récupérer dans trois variables les informations des couches R, G, et B avec la méthode split().
    On notera qu'en anglais, to split veut dire séparer.
  3. Créer un nouvel objet-image avec la méthode merge() mais en inversant les couches.
    On notera qu'en anglais, to merge veut dire fusionner.
  4. Demander l'affichage à l'écran du nouveau objet-image avec show().
  5. Enregistrer le nouvel objet-image dans un vrai fichier-image save().

09 ✎° Utiliser cette version de gestion_images.py. Ce nouveau programme réalise les opérations 1 à 5 signalées ci-dessus.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
# Importations from PIL import Image as Img # Constantes NOM_IMAGE = "mm.jpg" NOM_SAUVEGARDE = "mm_modif.jpg" # Programme principal ref_image = Img.open(NOM_IMAGE) # Création d'un objet-image couches = ref_image.split() # Récupération des intensités des différentes couches rouge = couches[0] # contient uniquement les valeurs rouges des pixels vert = couches[1] # contient uniquement les valeurs vertes des pixels bleu = couches[2] # contient uniquement les valeurs bleues des pixels nouvelles_couches = (bleu, rouge, vert) # RGB : bleu prend la place de rouge, rouge celle de vert et vert celle de bleu nouvelle_image = Img.merge("RGB", nouvelles_couches) # On fusionne (merge) les 3 couches dans un autre sens ! nouvelle_image.show() nouvelle_image.save(NOM_SAUVEGARDE)

Lancer le programme. Vous devriez constater l'apparition d'un nouveau fichier image dans le répertoire :

📁 Couleurs

📄 Tux.png

📄 gestion_image.py

📄 mm.jpg

📄 mm2.jpg

Répondre aux questions suivantes sur votre copie numérique (et faire appel à moi si vous ne comprennez pas une question ou ce qu'il faut répondre).

  1. Sur quelle ligne est déclaré le nom du fichier-image à lire ?
  2. Sur quelle ligne est déclaré l'objet-image ref_image de base ?
  3. Lignes 12 à 15 : pourquoi peut-on dire que la variable couches (créée par split() pour récupérer les parties rouge-vert-bleu de l'image de base) fait référence à un type construit ?
  4. Lignes 17 : pourquoi peut-on dire que la variable nouvelles_couches fait référence à un tuple ?
  5. Comment se nomme la méthode qui permet de créer une nouvelle image en lui fournissant trois couches jouant le rôle des couches R, G et B ?
  6. Quelle est la couche qu'on désigne comme devant jouer le rôle du rouge dans notre nouvelle image ?
  7. Comment se nomme la méthode qui permet de sauvegarder notre nouvel objet-image dans un fichier ?
  8. Sur quelle ligne est déclaré le nom du fichier-image qu'on veut créer ?

Nouvelle compétence testée  savoir analyser un programme d'une trentaine de lignes.

Le programme ci-dessous utilise une nouvelle méthode paste() dont voici le prototype :

image_sur_laquelle_on_veut_coller.paste(image_à_coller, (x,y))
où le couple (x, y) désigne les coordonnées du point où on veut coller l'image.

10 ✌° Utiliser cette dernière version du programme :

  1. Il lit l'image de base et en tire les informations dont on a besoin (largeur, hauteur, valeurs des couches R, G et B)
  2. Il génère 3 nouvelles images aux mêmes dimensions mais en permutant les couches.
  3. Il génère une nouvelle image nommée affiche_artistique, deux fois plus large et plus haute que les autres
  4. Il place une à une les 4 autres images sur l'affiche de façon à faire un panneau.
  5. Il montre l'affiche à l'écran puis la sauvegarde dans un fichier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
# Importations from PIL import Image as Img # Constantes NOM_IMAGE = "mm.jpg" NOM_AFFICHE = "affiche_Andy_Warhol.jpg" # Programme principal # étape 1 : on récupère l'image de base et les informations dont on aura besoin ref_image = Img.open(NOM_IMAGE) # Création d'un objet-image x = ref_image.width # on récupère la largeur en pixels de l'image de base y = ref_image.height # on récupère la hauteur en pixels de l'image de base couches = ref_image.split() # Récupération des intensités des différentes couches rouge = couches[0] # contient uniquement les valeurs rouges des pixels vert = couches[1] # contient uniquement les valeurs vertes des pixels bleu = couches[2] # contient uniquement les valeurs bleues des pixels # étape 2 : on génère trois images en permutant les couleurs nouvelle_image1 = Img.merge("RGB", (bleu, vert, rouge)) # On fusionne (merge) les 3 couches dans un autre sens ! nouvelle_image2 = Img.merge("RGB", (bleu, rouge, vert)) # On fusionne (merge) les 3 couches dans un autre sens ! nouvelle_image3 = Img.merge("RGB", (rouge, bleu, vert)) # On fusionne (merge) les 3 couches dans un autre sens ! # étape 3 : on génère une image RBB complétement noir et on rajoute les 3 autres dessus. affiche_artistique = Img.new("RGB", (x*2, y*2), (0, 0, 0)) affiche_artistique.paste(ref_image, (0, 0)) # on la colle en haut à gauche affiche_artistique.paste(nouvelle_image1, (x, 0)) # on la colle en haut à droite affiche_artistique.paste(nouvelle_image2, (0, y)) # on la colle en bas à gauche affiche_artistique.paste(nouvelle_image3, (x, y)) # on la colle en bas à gauche affiche_artistique.show() affiche_artistique.save(NOM_AFFICHE)

Après avoir visualisé que cela fonctionne, modifier le programme pour qu'il réalise une affiche des 4 images en largeur (4*1) plutôt qu'en (2*2). La nouvelle image devra se nommer affiche_Andy_Warhol4x1.jpg".

4 - Pyxel

Attention, cette partie est difficile puisqu'il s'agit d'un exercice de synthèse. Il faudra tout connaître en même temps.

Si vous bloquez, ne restez pas seul : demandez de l'aide à votre voisine, votre voisin ou l'enseignant.

Nous allons devoir lire le contenu de dictionnaires et parfois supprimer certaines clés. Avant de réaliser le programme en lui-même, voyons comment réaliser cela.

(Rappel) 4.1 DICTIONNAIRE : accéder à toutes les cases

On utilise une boucle for .. in mais on ne peut pas la coupler à la fonction len puisqu'un dictionnaire possède des clés, pas des indices.

Voici la manière usuelle d'obtenir les clés une par une.

1 2 3 4
ds = {"Alice": 13, "Bob": 8, "Charlie": 12} for cle in ds.keys(): # Pour chaque cle possible dans ds print(ds[cle]) # Affiche la valeur associée à cette clé

Ce programme est équivalent à ceci :

1 2 3 4 5
ds = {"Alice": 13, "Bob": 8, "Charlie": 12} print(ds["Alice"]) print(ds["Bob"]) print(ds["Charlie"])

Ils affichent l'un et l'autre ceci dans la console :

13 8 12

11° Dans notre programme, nous allons générer des ennemis. Nous allons vouloir les stocker dans une variable ennemis faisant référence à un dictionnaire.

Chaque ennemi sera encodé via un tableau dont les indices 0,1 et 2 sont [x, y, couleur].

  • La clé est le numéro de l'ennemi
  • La valeur associée est un tableau [x, y, couleur].

Compléter le programme ci-dessous pour qu'on parvienne à afficher le contenu de la case couleur de tous les ennemis stockés dans le dictionnaire ennemis qui ont une ordonnée supérieure à 80.

1 2 3 4 5 6 7 8 9 10 11 12
ennemis = { 30: [100, 50, 3], 60: [120, 80, 5], 90: [80, 110, 8], 120: [50, 90, 2], 150: [40, 20, 14] } for cle in ennemis.keys(): # Pour chaque clé dans ennemis ennemi = ... # récupère le tableau-ennemi associé à cette clé if ...: # si cet ennemi a une ordonnée supérieure à 80 print(...) # affiche sa couleur

Sur l'exemple donné, le programme doit donc afficher 8 et 2.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12
ennemis = { 30: [100, 50, 3], 60: [120, 80, 5], 90: [80, 110, 8], 120: [50, 90, 2], 150: [40, 20, 14] } for cle in ennemis.keys(): # Pour chaque clé dans ennemis ennemi = ennemis[cle] # récupère le tableau-ennemi associé à cette clé if ennemi[1] > 80: # si cet ennemi a une ordonnée supérieure à 80 print(ennemi[2]) # affiche sa couleur

12° Nous voudrions maintenant supprimer les ennemis qui ont une ordonnée supérieure à 80.

Nous avions vu que pour supprimer un couple (clé, valeur) dans un dictionnaire, il suffit d'utliser le mot-clé del.

Utiliser les deux programmes ci-dessous.

Vous devriez constater que le premier fonctionne mais que Python refuse d'exécuter le deuxième.

Question

Pourquoi ? De quoi se plaint-il ?

Programme 1

1 2 3 4 5 6 7 8 9 10 11 12
ennemis = { 30: [100, 50, 3], 60: [120, 80, 5], 90: [80, 110, 8], 120: [50, 90, 2], 150: [40, 20, 14] } del ennemis[90] del ennemis[120] print(ennemis)
{30: [100, 50, 3], 60: [120, 80, 5], 150: [40, 20, 14]}

Programme 2

1 2 3 4 5 6 7 8 9 10 11 12 13 14
ennemis = { 30: [100, 50, 3], 60: [120, 80, 5], 90: [80, 110, 8], 120: [50, 90, 2], 150: [40, 20, 14] } for cle in ennemis.keys(): # Pour chaque clé dans ennemis ennemi = ennemis[cle] # récupère le tableau-ennemi associé à cette clé if ennemi[1] > 80: # si cet ennemi a une ordonnée supérieure à 80 del ennemis[cle] # supprime la clé de cet ennemi dans ennemis print(ennemis)
Traceback (most recent call last): .. line 9, in ... RuntimeError: dictionary changed size during iteration

...CORRECTION...

Python vous signale que vous tentez de modifier les entrées du dictionnaire alors qu'il est train de les lire une par une. Il vous dit clairement qu'il ne veut pas le faire car c'est potentiellement une erreur. Est-ce vraiment volontaire ?

13° On peut résoudre le problème de plusieurs façons. L'une des façons les plus rapides à taper est de simplement transformer l'ensemble des clés qu'on consulte en une structure immuable sans lien avec les clés réelles actuelles : on fait une photographie à un instant t0 et on travaille avec cette image, même si certaines clés n'existent plus.

Quelle structure immuable connaissez-vous ? Les n-uplets, le type tuple de Python.

Programme 3

1 2 3 4 5 6 7 8 9 10 11 12 13 14
ennemis = { 30: [100, 50, 3], 60: [120, 80, 5], 90: [80, 110, 8], 120: [50, 90, 2], 150: [40, 20, 14] } for cle in tuple(ennemis.keys()): # Pour chaque clé INITIALEMENT dans ennemis ennemi = ennemis[cle] # récupère le tableau-ennemi associé à cette clé if ennemi[1] > 80: # si cet ennemi a une ordonnée supérieure à 80 del ennemis[cle] # supprime la clé de cet ennemi dans ennemis print(ennemis)
{30: [100, 50, 3], 60: [120, 80, 5], 150: [40, 20, 14]}

Questions

  • Pourquoi est-ce que cela fonctionne maintenant :?
  • Pourquoi ne pas faire cela à chaque fois ?

...CORRECTION...

  • Pourquoi est-ce que cela fonctionne maintenant :?
  • Python itère maintenant sur un contenu qui est le tuple qu'on voit de créer : il n'est absolument pas en lien en mémoire avec le dictionnaire lui-même. Il est basé sur les clés qu'il y avait initialement dans le dictionnaire mais il est créé une fois pour toutes, même si on modifie le dictionnaire.

  • Pourquoi ne pas faire cela à chaque fois ?
  • Pour des questions d'optimisation de mémoire. Ne faites cela que si vous comptez supprimer des clés en itérant sur les clés une à une. Si vous ne voulez pas supprimer de clés, autant garder la formulation de base qui va optimiser l'exécution de la boucle.

(Bilan) 4.2 DICTIONNAIRE : supprimer des clés en boucle

Lorsqu'on veut supprimer des clés, on ne peut pas utiliser simplement la méthode keys() : l'interpréteur Python n'accepterait pas de supprimer des clés sur l'ensemble des clés qu'il est en train de parcourir.

Pour contrer cela, il existe deux façons de faire.

Méthode 1 : ne pas travailler sur l'ensemble des clés en temps réel
1 2 3 4 5 6 7 8 9 10 11 12 13 14
ennemis = { 30: [100, 50, 3], 60: [120, 80, 5], 90: [80, 110, 8], 120: [50, 90, 2], 150: [40, 20, 14] } for cle in tuple(ennemis.keys()): # Pour chaque clé INITIALEMENT dans ennemis ennemi = ennemis[cle] # récupère le tableau-ennemi associé à cette clé if ennemi[1] > 80: # si cet ennemi a une ordonnée supérieure à 80 del ennemis[cle] # supprime la clé de cet ennemi dans ennemis print(ennemis)
Méthode 2 : mémoriser les clés à supprimer puis supprimer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
ennemis = { 30: [100, 50, 3], 60: [120, 80, 5], 90: [80, 110, 8], 120: [50, 90, 2], 150: [40, 20, 14] } a_supprimer = {} # va servir à stocker les clés à supprimer for cle in ennemis.keys(): # Pour chaque clé dans ennemis ennemi = ennemis[cle] # récupère le tableau-ennemi associé à cette clé if ennemi[1] > 80: # si cet ennemi a une ordonnée supérieure à 80 a_supprimer[cle] = ennemi # on crée une entrée avec cette clé dans a_supprimer for cle in a_supprimer.keys(): # Pour chaque clé dans a_supprimer del ennemis[cle] # supprime l'entrée de ennemi qui a cette clé également print(ennemis)
(Rappel) 4.3 Principe d'un programme pyxel

Logo Pyxel
Logo Pyxel

Le principe de fonctionnement d'un programme utilisant le module pyxel est basé sur une séquence qui inclut une boucle infinie.

Déroulé fondamental

On peut résumer le déroulé d'un programme Pyxel de cette façon 

  • Séquence de réglage : init() puis run()
  • 30 fois par seconde : controle() puis vue()
Déroulé plus détaillé

    1 - La fonction init() crée la fenêtre graphique.

    2 - La fonction run() active l'alternance infinie ci dessous :

    3 - TANT QUE le programme est actif :

      On mémorise les actions de l'utilisateur

      SI la dernière activation date de plus de 1/30e de seconde

        On active la fonction controle() qui va

        faire appel à différentes fonctions pour mettre à jour les données

        On active la fonction vue() qui va

        faire appel à cls() pour effacer l'écran

        faire appel à des fonctions afficher_...() pour afficher des formes à l'écran

      Fin du SI

    Fin TANT QUE

Système d'axes

Le point (0,0) est le bord haut gauche.

L'abscisse va bien vers la gauche.

Par contre, l'ordonnée est orientée vers le bas, attention.

Les axes dans Pyxel

14° Tester si le module pyxel est importé sur votre ordinateur.

>>> import pyxel

Si cela déclenche une erreur, Outils > Gérer les paquets dans Thonny et chercher à installer pyxel.

Vous avez maintenant vu plusieurs programmes d'une cinquantaine de lignes. Nous allons être plus ambitieux ici : votre programme final va atteindre environ 200 lignes. Il sera construit progressivement à partir d'un squelette que je fournis. Pas de panique !

15° Placer ce (long) programme en mémoire. Lancer pour visualiser le résultat. Vous pouvez déplacer l'un des carrés (le "vaisseau") à l'aide des flèches du clavier. Répondre ensuite aux questions proposées.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
# 1 - Importations ============================================================ import pyxel import random # 2 - CONSTANTES et variables globales lues directement ========================= vaisseau = {'x': 100, # la clé 'x' correspond à l'abscisse du vaisseau 'y': 100} # la clé 'y' correspond à l'ordonnée du vaisseau points = {'score':0} # +1 par ennemi détruit, -5 par ennemi qui passe tirs = {} # dict des tirs contenant des tableaux [x, y] explosions = {} # dict des explosions contenant des tableaux [x, y, rayon] ennemis = {} # dict des ennemis contenant des tableaux [x, y, couleur] # 3 - FONCTIONS =============================================================== def controle(): """Fonction qui récupère l'action, modifie les données en conséquence""" if pyxel.btn(pyxel.KEY_LEFT): # Si on maintient appuyé la flèche GAUCHE deplacer_vaisseau(-1, 0) # --> Mettre à jour les coordonnées du vaisseau if pyxel.btn(pyxel.KEY_RIGHT): # Si on maintient appuyé la flèche DROITE deplacer_vaisseau(1, 0) # --> Mettre à jour les coordonnées du vaisseau if pyxel.btnp(pyxel.KEY_SPACE): # Si on a appuyé sur la touche ESPACE x = vaisseau['x'] # --> récupère x y = vaisseau['y'] # --> récupère y creer_tir(x, y) # --> rajoute un tir dans le dictionnaire tirs if pyxel.frame_count % 30 == 0: # S'il s'est écoulé 30 secondes creer_ennemi() # insère un nouvel ennemi gerer_explosions() # Mettre les explosions à jour en les amplifiant/supprimant gerer_tirs() # Mettre les tirs à jour (déplacement et collision) gerer_ennemis() # Mettre les ennemis à jour def deplacer_vaisseau(dx, dy): """Modifie les données du vaisseau pour intégrer le déplacement dx et dy voulu""" vaisseau['x'] = vaisseau['x'] + dx # Modifie l'abscisse du vaisseau if vaisseau['x'] > 120: # Si on va trop loin à droite vaisseau['x'] = 120 # --> on bloque l'abscisse à 120 if vaisseau['x'] < 0: # Si on va trop loin à gauche vaisseau['x'] = 0 # --> on bloque l'abscisse à 0 def creer_tir(x, y): """Rajoute ce tir tiré depuis un vaisseau en (x, y) dans le tableau des tirs""" pass def gerer_explosions(): """Modifie ou supprime les données liées aux explosions""" pass def gerer_tirs(): """Modifie les coordonnées des tirs, en les supprimant au besoin""" pass def tir_touche(x, y, ennemi): """Prédicat qui renvoie True si le tir (x, y) touche l'ennemi""" pass def creer_ennemi(): """Création et ajout d'un ennemi dans le dictionnaire ennemis""" pass def gerer_ennemis(): """Modifie les coordonnées des ennemis en les supprimant au besoin""" pass def vue(): """Création de l'affichage (30 fois par seconde)""" pyxel.cls(0) # vide la fenêtre afficher_vaisseau() # vaisseau (carre 8x8) afficher_tirs() # tous les tirs (rectangle 1x4) afficher_ennemis() # tous les ennemis (carré) afficher_explosions() # toutes les explosions (cercle) afficher_informations() # les textes def afficher_vaisseau(): """Dessine le vaisseau""" x = vaisseau['x'] # on récupère l'abscisse y = vaisseau['y'] # on récupère l'ordonnée pyxel.rect(x, y, 8, 8, 1) # on trace un carré 8*8 de couleur 1 def afficher_tirs(): """Dessine les tirs""" pass def afficher_explosions(): """Dessine les explosions""" pass def afficher_ennemis(): """Dessine les ennemis""" pass def afficher_informations(): """Affiche les informations voulues à l'écran""" texte = str(pyxel.frame_count) # on génère un string à partir du nombre de frames pyxel.text(1, 1, texte, 7) # affiche le texte def lancer_jeu(): """Cette fonction lance la surveillance des événements""" pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controle, vue) # Lancement de l'alternance controle / vue # 4 - PROGRAMME PRINCIPAL ==================================================== lancer_jeu()

Questions

  1. Vous trouvez sans doute ce programme compliqué, mais quelle est la seule ligne du programme principal ?
  2. En effectuant ce appel, où voit-on apparaître les noms des deux fonctions qui vont être appelées 30 fois par seconde ?
  3. En allant voir controle(), dire quelles sont les trois interactions que peut réaliser le joueur.
  4. En regardant les lignes 31-32, la variable vaisseau fait-elle référence à un tableau, un tuple, un string ou un dictionnaire ?
  5. vaisseau est-il un paramètre d'entrée de la fonction ? A-t-il été déclaré dans la fonction ? S'agit-il d'une variable locale ou d'une variable globale ?
  6. Pourquoi le vaisseau ne peut-il jamais quitter l'écran ?
  7. Sur quelle ligne le vaisseau est-il déclaré d'ailleurs ?
  8. En allant voir la fonction afficher_vaisseau(), donner le nom de la fonction qui permet de tracer des rectangles à l'écran.
  9. En allant voir la fonction afficher_informations(), donner le nom de la fonction qui permet d'afficher un texte à l'écran.
  10. Lignes 109-110. pyxel.frame_count permet de récupèrer le nombre de frames qui ont été générées depuis le lancement du jeu. Connaissant le principe du déroulé de pyxel, de combien augmente ce compteur toutes les secondes ?
  11. Dernière question. Retour à controle() lignes 35-36. Que veut-dire la ligne 35 ? Quel est le rythme de création des ennemis ?

...CORRECTION...

  1. Quelle est la seule ligne du programme principal ?
  2. La ligne 121, lancer_jeu().


  3. En effectuant ce appel, où voit-on apparaître les noms des deux fonctions qui vont être appelées 30 fois par seconde ?
  4. Ligne 116 : pyxel.run(controle, vue)


  5. En allant voir controle(), dire quelles sont les trois interactions que peut réaliser le joueur.
  6. Lignes 24-27-30, on voit qu'on peut appuyer sur certaines touches.

    • L24 : appuyer sur la flèche LEFT semble faire aller le vaisseau à gauche.
    • L27 : appuyer sur la flèche RIGHT semble faire aller le vaisseau à droite.
    • L27 : appuyer sur la barre ESPACE semble faire tirer le vaisseau.

  7. En regardant les lignes 31-32, la variable vaisseau fait-elle référence à un tableau, un tuple, un string ou un dictionnaire ?
  8. x = vaisseau['x']

    On voit qu'il s'agit d'un type structuré dans laquelle les cases portent des noms. Il s'agit donc d'un dictionnaire.


  9. vaisseau est-il un paramètre d'entrée de la fonction ? A-t-il été déclaré dans la fonction ? S'agit-il d'une variable locale ou d'une variable globale ?
  10. vaisseau n'est pas un paramètre et n'a pas de déclaration DANS la fonction. Il s'agit donc d'un dictionnaire global.


  11. Pourquoi le vaisseau ne peut-il jamais quitter l'écran ?
  12. Il faut aller voir la fonction deplacer_vaisseau().

    On voit qu'on ne laisse jamais la valeur associée à la clé 'x' sortir de l'intervalle [0, 120].


  13. Sur quelle ligne le vaisseau est-il déclaré d'ailleurs ?
  14. Lignes 9-10 : on voit qu'il possède deux clés. 'x' pour l'abscisse et 'y' pour l'ordonnée du vaisseau.

    Nous aurions pu le déclarer en une seule ligne d'ailleurs :

    vaisseau = {'x': 100, 'y': 100}


  15. En allant voir la fonction afficher_vaisseau(), donner le nom de la fonction qui permet de tracer des rectangles à l'écran.
  16. On voit L92 qu'il s'agit de rect().

    pyxel.rect(x, y, 8, 8, 1)


  17. En allant voir la fonction afficher_informations(), donner le nom de la fonction qui permet d'afficher un texte à l'écran.
  18. On voit L110 qu'il s'agit de text().

    pyxel.text(1, 1, texte, 7)


  19. Lignes 109-110. pyxel.frame_count permet de récupèrer le nombre de frames qui ont été générées depuis le lancement du jeu. Connaissant le principe du déroulé de pyxel, de combien augmente ce compteur toutes les secondes ?
  20. On sait que Pyxel génère 30 frames par seconde. Le compteur augmente donc de 30 toutes les secondes : 0-30-60-90-120...


  21. Dernière question. Retour à controle() lignes 35-36. Que veut-dire la ligne 35 ? Quel est le rythme de création des ennemis ?
  22. La ligne 35 consiste à réaliser la division euclidienne du nombre de frames par 30. Si le reste vaut bien 0, c'est qu'on vient de finir une seconde de jeu. La condition testée est donc vraie une fois par seconde.

    On génère donc un monstre par seconde.

4.4 Configuration initiale

Documentation officielle : https://github.com/kitao/pyxel/blob/main/docs/README.fr.md

Fonction init()

Voici le prototype de cette fonction :

def init(width, height, [title], [fps], [quit_key], [display_scale], [capture_scale], [capture_sec])

Initialise l’application Pyxel avec un écran de taillede largeur width et de hauteur height.

Il est possible de passer comme [options] :

  • le titre de la fenêtre avec title,
  • le nombre d’images par seconde avec fps,
  • la touche pour quitter l’application avec quit_key,
  • l'échelle de l'affichage avec display_scale,
  • l’échelle des captures d’écran avec capture_scale,
  • et le temps maximum d’enregistrement vidéo avec capture_sec.

Deux exemples :

1 2 3
import pyxel pyxel.init(160, 120, title=My Pyxel App")
1 2 3 4 5
import pyxel pyxel.init(160, 120, title=My Pyxel App", fps=60, quit_key=pyxel.KEY_NONE, capture_scale=3, capture_sec=0)

Ici, on modifie à 60 le nombre d'images par seconde.

Fonction run()

Cette fonction permet d'indiquer les noms des deux fonctions qui seront appellées en boucle.

On la trouve souvent sous la forme pyxel.run(update, draw)

Ces deux fonctions seront alors appellées automatiquement plusieurs fois par seconde (le réglage par défaut est 30 fois).

La première fonction update sert habituellement à définir les actions à réaliser après les événements utilisateur. On pourrait également la nommer controle par exemple, pour indiquer qu'elle fait partie du controleur en logique Modèle Vue Controleur.

La seconde fonction draw sert habituellement à définir l'affichage. On pourrait également la nommer vue par exemple, pour indiquer qu'elle fait partie de la Vue en logique Modèle Vue Controleur.

Attention

Il convient de ne pas mettre les parenthèses lors de la transmission des fonctions : uniquement leurs noms, on ne désire pas les activer à ce stade, juste transmettre les adresses.

Compteur des frames frame_count et autres informations

On peut récupérer le nombre de frames depuis le lancement du jeu avec pyxel.frame_count.

Si vous voulez réaliser une action par seconde et qu'il y a 30 frames par seconde, il suffit donc de placer une instruction de ce type :

1 2
if pyxel.frame_count % 30 == 0: # S'il s'est écoulé 30 secondes faire_un_truc()

Sur le même principe, on peut récupérer la taille de l'écran avec pyxel.width et pyxel.height.

Les 16 couleurs disponibles

16 couleurs disponibles sont disponibles par défaut, chacune identifiée par un simple numéro.

L'image ci-dessous fournit les numéros, les codes RGB en hexadécimal précédés d'un # et les valeurs RGB décimales.

Les couleurs dans Pyxel
Les couleurs dans Pyxel
Effacer l'écran avec cls()

Le principe standard de l'affichage d'un jeu est d'effacer l'écran à chaque appel de la vue. Pour cela, on utilise un appel pyxel.cls(col)col est l'une des couleurs du jeu.

Un exemple qui ne sert à rien mais où le fond de l'écran change toutes les secondes.

1 2 3 4 5 6 7 8 9 10 11 12
import pyxel import random def controle(): pass def vue(): if pyxel.frame_count % 30 == 0: pyxel.cls( random.randint(0, 15) ) pyxel.init(128,128) pyxel.run(controle, vue)
4.5 Gestion des touches avec btn()

Fonction btn()

def btn(key:int) -> bool

Fonction-Prédicat qui renvoie True si la touche key (codée par un entier, voir ci-dessous) est appuyée.

Fonction btbp()

def btnp(key:int) -> bool

Fonction-Prédicat qui renvoie True si la touche key est appuyée à cette frame.

def btnp(key:int, [hold:int], [repeat:int]) -> bool

Quand hold et repeat sont spécifiés, True sera renvoyé à l’intervalle de frame repeat quand la touche key est appuyée pendant plus de hold frames.

Encodage des touches

Disponibles ici : https://github.com/kitao/pyxel/blob/main/python/pyxel/__init__.pyi

Chaque touche est encodée à l'interne pour un entier stocké dans une CONSTANTE du module.

Pour les plus utiles :

  • pyxel.KEY_RETURN pour la touche ENTREE.
  • pyxel.KEY_SPACE pour la barre ESPACE.
  • pyxel.KEY_0 à pyxel.KEY_9 pour les touches de chiffres.
  • pyxel.KEY_A à pyxel.KEY_Z pour les touches de lettres.
  • Pour les touches de flèches :
    • pyxel.DOWN
    • pyxel.UP
    • pyxel.LEFT
    • pyxel.RIGHT
4.6 Rectangle avec rect()

Fonction rect()

def rect(x:int, y:int, w:int, h:int, col:int) -> None

Dessine un rectangle de largeur w, de hauteur h et de couleur col à partir de (x, y).

Les coordonnées (x,y) désignent le coin en haut à gauche du rectange.

Exemple

Voici un visuel et le code correspondant.

Exemples de rectangles
Les rectangles dans Pyxel
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import pyxel pyxel.init(128,128) # Rectange 20x40 bleu en (x=0, y=0) pyxel.rect(0, 0, 20, 40, 5) # Rectange 20x50 bleu ciel en (x=50, y=0) pyxel.rect(50, 0, 20, 50, 6) # Rectange 20x30 rouge en (x=0, y=50) pyxel.rect(0, 50, 20, 30, 4) # Afficher l'écran sans passer par run() pyxel.show()
Fonction rectb()

def rectb(x:int, y:int, w:int, h:int, col:int) -> None

Comme la précédente mais uniquement les contours.

Exemple

Voici un visuel et le code correspondant.

Exemples de contours de rectangles
Les rectangles dans Pyxel
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import pyxel pyxel.init(128,128) # Rectange 20x40 bleu en (x=0, y=0) pyxel.rectb(0, 0, 20, 40, 5) # Rectange 20x50 bleu ciel en (x=50, y=0) pyxel.rectb(50, 0, 20, 50, 6) # Rectange 20x30 rouge en (x=0, y=50) pyxel.rectb(0, 50, 20, 30, 4) # Afficher l'écran sans passer par run() pyxel.show()

Le programme que nous allons concevoir est un peu plus long et complexe que les précédents. Voici comment s'organisent les appels de fonctions. Si pendant ce TP vous ne savez plus qui appelle qui, revenez à ce schéma.

flowchart TB J([lancer_jeu]) --> I([init]) J ---> R([run]) R --> C([controle]) R --> V([vue]) C --> DV([deplacer_vaisseau]) C --> NT([creer_tir]) C --> NE([creer_ennemi]) C --> GE([gerer_explosions]) C --> DT([gerer_tirs]) --> TT([tir_touche]) C --> DE([gerer_ennemis]) V --> AV([afficher_vaisseau]) V --> AT([afficher_tirs]) V --> AEN([afficher_ennemis]) V --> AI([afficher_informations])

16° Vous allez devoir créer les tirs, les explosions et l'interaction de tout cela avec les ennemis. Regardons comment créer les ennemis. Lancer d'abord le programme pour visualiser le résultat puis répondre aux questions.

  1. Quels sont les 5 dictionaires globaux de ce programme ?
  2. CREATION. Nous avons vu lignes 35-36 qu'on demande de créer un ennemi par seconde (c'est à dire toutes les 30 frames). Aller en ligne 110 et expliquer :
    • Ce que contient chaque tableau-ennemi
    • Comment on rajoute un ennemi dans le dictionnaire ennemis : quelle clé ? quelle valeur associée ?
  3. GESTION. On voit en ligne 40 qu'on lance un appel à gerer_ennemis(). Aller en ligne 118 et analyser chaque ligne pour être certain de bien la comprendre (c'est important car vous allez devoir adapter ces lignes pour savoir gérer les tirs et les explosions).
  4. AFFICHAGE. On voit en ligne 134 qu'on lance un appel à afficher_ennemis(). Aller en ligne 165 et analyser chaque ligne pour être certain de bien la comprendre (c'est important car vous allez devoir adapter ces lignes pour savoir gérer les tirs et les explosions).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
# 1 - Importations ============================================================ import pyxel import random # 2 - CONSTANTES et variables globales ======================================== vaisseau = {'x': 100, # la clé 'x' correspond à l'abscisse du vaisseau 'y': 100} # la clé 'y' correspond à l'ordonnée du vaisseau points = {'score':0} # +1 par ennemi détruit, -5 par ennemi qui passe tirs = {} # dict des tirs contenant des tableaux [x, y] explosions = {} # dict des explosions contenant des tableaux [x, y, rayon] ennemis = {} # dict des ennemis contenant des tableaux [x, y, couleur] # 3 - FONCTIONS =============================================================== def controle(): """Fonction qui récupère l'action, modifie les données en conséquence""" if pyxel.btn(pyxel.KEY_LEFT): # Si on maintient appuyé la flèche GAUCHE deplacer_vaisseau(-1, 0) # --> Mettre à jour les coordonnées du vaisseau if pyxel.btn(pyxel.KEY_RIGHT): # Si on maintient appuyé la flèche DROITE deplacer_vaisseau(1, 0) # --> Mettre à jour les coordonnées du vaisseau if pyxel.btnp(pyxel.KEY_SPACE): # Si on a appuyé sur la touche ESPACE x = vaisseau['x'] # --> récupère x y = vaisseau['y'] # --> récupère y creer_tir(x, y) # --> rajoute un tir dans le dictionnaire tirs if pyxel.frame_count % 30 == 0: # S'il s'est écoulé 30 secondes creer_ennemi() # insère un nouvel ennemi gerer_explosions() # Mettre les explosions à jour en les amplifiant/supprimant gerer_tirs() # Mettre les tirs à jour (déplacement et collision) gerer_ennemis() # Mettre les ennemis à jour def deplacer_vaisseau(dx, dy): """Modifie les données du vaisseau pour intégrer le déplacement dx et dy voulu""" vaisseau['x'] = vaisseau['x'] + dx # Modifie l'abscisse du vaisseau if vaisseau['x'] > 120: # Si on va trop loin à droite vaisseau['x'] = 120 # --> on bloque l'abscisse à 120 if vaisseau['x'] < 0: # Si on va trop loin à gauche vaisseau['x'] = 0 # --> on bloque l'abscisse à 0 def creer_tir(x, y): """Rajoute ce tir tiré depuis un vaisseau en (x, y) dans le tableau des tirs""" pass # crée le tableau contenant les coordonnées du tir # stocke le tir dans tirs avec cle = pyxel.frame_count def gerer_explosions(): """Modifie ou supprime les données liées aux explosions""" pass # Pour chaque clé du dictionnaire explosions # récupère l'une des explosions (un tableau) # augmente le rayon de 2 pixels # Si le rayon est supérieur à 8 # --> supprime cette explosion du dictionnaire def gerer_tirs(): """Modifie les coordonnées des tirs, en les supprimant au besoin""" pass # Pour chaque cle du dictionnaire tirs # --> récupère l'adresse d'un tableau contenant un tir # --> fait "monter" le tir # --> récupère son abscisse # --> récupère son ordonnée # --> Si le tir percute le bord du haut # --> --> génère une explosion de rayon 1 dans le dico explosions # --> --> supprime ce tir du dico tirs # --> Sinon # --> --> pour chaque cle du dictionnaire ennemis # --> --> --> on récupère cet ennemi # --> --> --> si cet ennemi est touché par ce tir # --> --> --> --> rajoute l'explosion dans le dico explosions # --> --> --> --> supprime cet ennemi du dico ennemis # --> --> --> --> supprime ce tir du dico tirs def tir_touche(x, y, ennemi): """Prédicat qui renvoie True si le tir (x, y) touche l'ennemi""" xg = ennemi[0] # on récupère son abscisse à gauche xd = ennemi[0] + 8 # on récupère son abscisse à droite yh = ennemi[1] # on récupère son abscisse en haut yb = ennemi[1] + 8 # on récupère son abscisse en bas if x >= xg: # si l'abscisse du tir dépasse le côté gauche de l'ennemi if x <= xd: # si l'abscisse du tir précède le côté droite de l'ennemi if y <= yb: # si le tir est (à l'écran) au dessus du côté bas (Oy vers le bas) if y >= yh: # si le tir est (à l'écran) en dessous du côté haut return True # --> si on arrive ici, c'est que cet ennemi est touché return False # --> si on arrive ici, c'est qu'on a pas rencontré le True ! def creer_ennemi(): """Création et ajout d'un ennemi dans le dictionnaire ennemis""" x = random.randint(0, 120) # on génère une abscisse aléatoire entre 0 et 120 couleur = random.randint(2, 15) # on génère une couleur aléatoire entre 2 et 15 ennemi = [x, 0, couleur] # on génère le tableau [x, y, couleur] ennemis[pyxel.frame_count] = ennemi # on rajoute cet ennemi dans le dictionnaire def gerer_ennemis(): """Modifie les coordonnées des ennemis en les supprimant au besoin""" for cle in tuple(ennemis.keys()): # Pour chaque cle du dictionnaire ennemis ennemi = ennemis[cle] # On récupère l'adresse d'un tableau contenant un ennemi ennemi[1] = ennemi[1] + 1 # On fait "descendre" l'ennemi if ennemi[1] > 120: # Si l'ennemi percute le bord du bas del ennemis[cle] # --> on supprime cet ennemi du dictionnaire def vue(): """création des objets (30 fois par seconde)""" pyxel.cls(0) # vide la fenêtre afficher_vaisseau() # vaisseau (carre 8x8) afficher_tirs() # tous les tirs (rectangle 1x4) afficher_ennemis() # tous les ennemis (carré) afficher_explosions() # toutes les explosions (cercle) afficher_informations() # les textes def afficher_vaisseau(): """Dessine le vaisseau""" x = vaisseau['x'] # on récupère l'abscisse y = vaisseau['y'] # on récupère l'ordonnée pyxel.rect(x, y, 8, 8, 1) # on trace un carré 8*8 de couleur 1 def afficher_tirs(): """Dessine les tirs""" pass # pour chaque clé du dictionnaire tirs # on récupère dans tir le tir associé à cette clé # on récupère l'abscisse # on récupère l'ordonnée # on trace un rectangle 1*3 de couleur 10 def afficher_explosions(): """Dessine les explosions""" pass # pour chaque clé du dictionnaire explosions # on récupère l'une des explosions : [x, y, rayon] # on récupère son abscisse # on récupère son ordonnée # on récupère son rayon # on trace un cercle de couleur 10 # on trace un cercle plus petit de couleur 0 def afficher_ennemis(): """Dessine les ennemis""" for cle in ennemis.keys(): # pour chaque cle du dictionnaire ennemis ennemi = ennemis[cle] # on récupère l'un des ennemis : [x, y, couleur] x = ennemi[0] # on récupère sa abscisse y = ennemi[1] # on récupère son ordonnée c = ennemi[2] # on récupère sa couleur pyxel.rect(x, y, 8, 8, c) # on trace un carré 8*8 def afficher_informations(): """Affiche les informations voulues à l'écran""" texte = str(pyxel.frame_count) # on génère un string à partir du nombre de frames pyxel.text(1, 1, texte, 7) # affiche le texte def lancer_jeu(): """Cette fonction lance la surveillance des événements""" pyxel.init(128, 128, title="Mon premier jeu") # Initialisation de la fenêtre pyxel.run(controle, vue) # Lancement de l'alternance controle / vue # 4 - PROGRAMME PRINCIPAL ==================================================== lancer_jeu()

17° Utiliser maintenant les commentaires placés dans le programme pour créer les fonctions suivantes une par une. Pensez à lancer le programme régulièrement pour vérifier que cela fonctionne peu à peu : attendre d'avoir tapé 30 lignes avant de tester est une très mauvaise pratique !

  1. Aller voir en ligne 14 pour voir les tableaux que sont censés contenir le dictionnaire tirs et les tableaux que sont censés contenir le dictionnaire explosions.
  2. CREATION : ligne 53 et plus, compléter la fonction creer_tir() pour qu'elle parvienne à créer un nouveau tir au centre du côté supérieur du vaisseau. Il faudra traduire en python les commentaires fournis en français.
  3. GESTION : ligne 70 et plus, compléter la fonction gerer_tirs() pour qu'elle parvienne à déplacer les tirs. Il faudra traduire en python les commentaires fournis en français. Dès qu'un tir déclenche une explosion, on rajoutera dans explosions une explosion dans une case qui aura la même clé que le tir. Vous aurez besoin d'utiliser la fonction tir_touche() qui est déclarée juste en dessous.
  4. AFFICHAGE : ligne 145 et plus, compléter la fonction afficher_tirs().
4.7 Cercle avec circ()

Fonction circ()

def circ(x:int, y:int, r:int, col:int) -> None

Dessine un cercle (fortement pixelisé !) de rayon r et de couleur col en (x, y).

Les coordonnées (x,y) désignent le centre du cercle.

Exemple

Voici un visuel et le code correspondant.

Exemples de cercles
Les cercles dans Pyxel
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import pyxel pyxel.init(128,128) # Cercle bleu en (x=0, y=0) pyxel.circ(0, 0, 10, 5) # Cercle bleu ciel en (x=50, y=0) pyxel.circ(50, 0, 20, 6) # Cercle rouge en (x=0, y=50) pyxel.circ(0, 50, 30, 4) # Afficher l'écran sans passer par run() pyxel.show()
Fonction circb()

def circb(x:int, y:int, r:int, col:int) -> None

Idem, mais juste la bordure.

Exemple

Voici un visuel et le code correspondant.

Exemples de cercles
Les bordures de cercles dans Pyxel
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import pyxel pyxel.init(128,128) # Bordure de cercle bleu en (x=0, y=0) pyxel.circb(0, 0, 10, 5) # Bordure de cercle bleu ciel en (x=50, y=0) pyxel.circb(50, 0, 20, 6) # Bordure de cercle rouge en (x=0, y=50) pyxel.circb(0, 50, 30, 4) # Afficher l'écran sans passer par run() pyxel.show()

18° Utiliser les commentaires placés dans le programme pour créer les fonctions suivantes une par une. Pensez à lancer le programme régulièrement pour vérifier que cela fonctionne peu à peu : attendre d'avoir tapé 30 lignes avant de tester est une très mauvaise pratique !

  1. GESTION : ligne 60 et plus, compléter la fonction gerer_explosions() pour qu'elle parvienne à faire grossir puis disparaître les explosions.
  2. AFFICHAGE : ligne 154 et plus, compléter la fonction afficher_explosions().

19° Dans la fonction afficher_informations(), modifier l'affichage pour qu'on affiche plutôt le score stocké dans le dictionnaire points.

20° Finaliser le jeu en gérant correctement les points tels qu'ils sont documentés sur la ligne 12.

Ce bout de cours vous permettra de retrouver facilement le nom des couleurs et fonctions disponibles dans Pyxel. Juste un pense-bête.

4.8 Mémo pyxel

Cet encart condense les informations utiles pour Pyxel.

Les 16 couleurs disponibles
Les couleurs dans Pyxel
Les couleurs dans Pyxel
Les fonctions disponibles
Fiche
Logo Pyxel (https://github.com/kitao/pyxel#)

5 - Pour les plus rapides : Tkinter

Vous avez vu plusieurs interfaces homme-machine (IHM) : la console (pour le texte), turtle (pour apprendre à programmer), pyplot de matplotlib (pour tracer des courbes) et pyxel (pour faire des jeux retro). Mais il existe également des IHM généralistes, permettant d'afficher des choses, sans but prédéfini. Le module turtle est basé lui-même sur le module d'interface graphique intégré de base à Python : tkinter.

Sans rentrer dans les détails, vous allez maintenant manipuler un programme permettant de créer une interface graphique Tkinter, et voir l'importance des variables dans un tel programme plutôt que de tout définir à la main.

Regardons à quoi ressemble le code de cette petite interface graphique.

21 ✔° Placer le programme en mémoire en le nommant interface.py par exemple. Attention, n'utilisez surtout par un nom comme turtle, random, tkinter... Jamais l'un des noms d'un module Python déjà existant. Lancer.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
# Importations import tkinter as tk # Déclaration des constantes COU1 = "#FF8844" # Première couleur (une variante de rouge) COU2 = "#4488FF" # Deuxième couleur (une variante de bleu) FOND = "#000000" # Couleur du fond (noir) # Programme # ----- création de la fenêtre de l'application graphique fenetre = tk.Tk() fenetre.geometry("600x300") fenetre.title("Ma première interface graphique") fenetre.configure(bg=FOND) # ----- création et affichage des Labels zone1 = tk.Label(fenetre, text="A", fg="white", bg=COU1, width=10, height=5) zone1.place(x=25, y=25) zone2 = tk.Label(fenetre, text="B", fg="white", bg=COU2, width=10, height=5) zone2.place(x=125, y=25)

Vous devriez voir la fenêtre graphique s'ouvrir.

22 ✔° Lire (et comprendre, en demandant de l'aide au besoin) les explications sur son fonctionnement.

D'abord l'importation des modules utiles pour notre programme

1 2 3
# 1 - Importation de modules import tkinter as tk

On importe le module tkinter sous l'alias tk.

Constantes

5 6 7 8 9
# Déclaration des constantes COU1 = "#FF8844" # Première couleur (variante de rouge) COU2 = "#4488FF" # Deuxième couleur (variante de bleu) FOND = "#000000" # Couleur du fond (noir)

On déclare 3 constantes COU1 COU2 et FOND contenant deux couleurs pour les cases et la couleur de fond de l'interface. Elles sont fournies en mode RGB hexadécimal.

COU1 = "#FF8844" # Première couleur (variante de rouge)
Valeur du rouge : FF (le maximum)
Valeur du vert(green) : 88
Valeur du bleu : 44

Le programme en lui-même

Il se décompose en deux sous-parties :

  1. la création de la fenêtre graphique en elle-même
  2. 11 12 13 14 15 16 17 18
    # Programme # ----- création de la fenêtre de l'application graphique fenetre = tk.Tk() fenetre.geometry("600x300") fenetre.title("Ma première interface graphique") fenetre.configure(bg=FOND)

    On retrouve la notation avec le point pour indiquer qu'on va agir sur l'objet devant le point.

    • Ligne 15 : on crée la fenêtre en utilisant tk.Tk() et on stocke la référence dans la variable fenetre.
    • Ligne 16 : on définit les dimensions (600 pixels de large et 300 pixels de haut) en utilisant la notation habituelle utilisant un point : objet.methode().
    • Ligne 17 : on définit le titre de l'application (elle apparaît en haut de la fenêtre)
    • Ligne 18 : on utilise la méthode configure() pour modifier la couleur du fond (background, bg)
  3. la création des widgets (gadgets visuels, contraction de window gadget) qui vont remplir la fenêtre
  4. 20 21 22 23 24 25 26
    # ----- création et affichage des Labels zone1 = tk.Label(fenetre, text="A", fg="white", bg=COU1, width=10, height=5) zone1.place(x=25, y=25) zone2 = tk.Label(fenetre, text="B", fg="white", bg=COU2, width=10, height=5) zone2.place(x=125, y=25)

    Ligne 22 : on crée une zone d'affichage (un Label) en lui fournissant quelques informations :

    • d'abord, on précise qu'il est lié à fenetre
    • ensuite, on dit qu'on veut y afficher "A"
    • on précise que la couleur d'écriture est "white" (fg pour foreground, avant-plan)
    • on précise que la couleur d'arrière plan (background, bg) est COU1
    • on impose une largeur (width) de 10 caractères.
    • on impose un hauteur (height) de 5 lignes.

    Ligne 23, on l'affiche à l'écran en le plaçant à la position correspondant aux coordonnées x et y fournies en pixels.

    Ligne 25 et ligne 26, on fait la même chose avec un autre widget. C'est quasiment du copier/coller, seul la couleur du carré est différente puisque qu'on a choisi COU2 en tant que couleur de background (bg).

ATTENTION

Le module que nous allons utiliser se nomme tkinter. Il est donc important de ne pas nommer votre programme tkinter.py. Sinon, l'interpréteur Python ne parviendra pas à retrouver le vrai fichier tkinter.py !

23° Compléter le programme de façon à obtenir cet affichage :

Il faudrait donc créer les widgets zone3 et zone4.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
# Importations import tkinter as tk # Déclaration des constantes et des variables COU1 = "#FF8844" # Première couleur (variante de rouge initialement) COU2 = "#4488FF" # Deuxième couleur (variante de bleu initialement) FOND = "#000000" # Couleur du fond (noir initialement) # Programme # ----- création de la fenêtre de l'application graphique fenetre = tk.Tk() fenetre.geometry("600x300") fenetre.title("Ma première interface graphique") fenetre.configure(bg=FOND) # ----- création et affichage des Labels zone1 = tk.Label(fenetre, text="A", fg="white", bg=COU1, width=10, height=5) zone1.place(x=25, y=25) zone2 = tk.Label(fenetre, text="B", fg="white", bg=COU2, width=10, height=5) zone2.place(x=125, y=25) zone3 = tk.Label(fenetre, text="C", fg="white", bg=COU1, width=10, height=5) zone3.place(x=225, y=25) zone4 = tk.Label(fenetre, text="D", fg="white", bg=COU2, width=10, height=5) zone4.place(x=325, y=25)

✎ 24° Quelles sont les deux seules lignes à modifier si on désire modifier les couleurs des widgets ?

Faire quelques essais de modification.

Mais on peut aussi gérer les tailles avec des variables, de façon à avoir des widgets de plus en plus haut par exemple.

25 ✔° Utiliser ce nouveau programme. Les modifications importantes par rapport au précédent sont surlignées et feront l'objet de la dernière question.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
# Importations import tkinter as tk # Déclaration des constantes et des variables COU1 = "#88BB44" # Première couleur (variante de rouge initialement) COU2 = "#44BB88" # Deuxième couleur (variante de bleu initialement) FOND = "#000000" # Couleur du fond (noir initialement) taille = 5 # taille du premier widget # Programme # ----- création de la fenêtre de l'application graphique fenetre = tk.Tk() fenetre.geometry("600x300") fenetre.title("Ma première interface graphique") fenetre.configure(bg=FOND) # ----- création et affichage des Labels zone1 = tk.Label(fenetre, text="A", fg="white", bg=COU1, width=10, height=taille) zone1.place(x=25, y=25) taille = taille + 1 zone2 = tk.Label(fenetre, text="B", fg="white", bg=COU2, width=10, height=taille) zone2.place(x=125, y=25) taille = taille + 1 zone3 = tk.Label(fenetre, text="C", fg="white", bg=COU1, width=10, height=taille) zone3.place(x=225, y=25) taille = taille + 1 zone4 = tk.Label(fenetre, text="D", fg="white", bg=COU2, width=10, height=taille) zone4.place(x=325, y=25)

✎ 26° Analyser le programme précédent pour répondre aux questions suivantes :

  1. Ligne 10 : Que vaut la variable taille ?
  2. Ligne 23 : Quelle va alors être la hauteur du widget zone1 ?
  3. Ligne 26 : Que vaut la variable taille ?
  4. Ligne 27 : Quelle va alors être la hauteur du widget zone2 ?
  5. Ligne 30 : Que vaut la variable taille ?
  6. Ligne 31 : Quelle va alors être la hauteur du widget zone3 ?
  7. Ligne 34 : Que vaut la variable taille ?
  8. Ligne 35 : Quelle va alors être la hauteur du widget zone4 ?
  9. Pourquoi ne peut avoir utiliser les majuscules sur le nom taille ?

Maintenant que nous avons vu comment créer un programme, nous allons compléter les notions de fonctions, de boucles et d'instructions conditionnelles dans le but de réaliser des programmes de plus en plus complexes.

Activité publiée le 11 11 2023
Dernière modification : 07 12 2023
Auteur : ows. h.