Script et programme

Identification

Infoforall

13 - Créer un programme en Python 3


Troisième partie de l'activité "Programme".

Elle est consacrée à deux choses :

  1. Les tableaux dynamiques list de Python et leurs méthodes append(), pop() et remove();
  2. Le module pyxel qui permet de créer assez "facilement" des interfaces graphiques ressemblant aux jeux vidéos rétro : des gros pixels, des musiques basiques et du jeu plutôt que du graphisme.
Logo Pyxel
Logo Pyxel (https://github.com/kitao/pyxel#)

Evaluation : 2 questions

  questions 09

  questions 10

DM 🏠 : Non

Documents de cours PDF : .PDF

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

1 - Tableaux dynamiques

1.1 TABLEAU DYNAMIQUE : présentation rapide

Tableau statique avec le type list

Dans un tableau statique :

  1. toutes les cases contiennent le même type de données et
  2. le nombre de cases est constant : après création, on ne peut pas en supprimer ou en rajouter.

Les tableaux statiques s'implémentent en Python à l'aide du type list. D'ailleurs Python accepte des contenus de types différents dans les différentes cases contrairement à un tableau statique standard.

Type list : tableau dynamique

Le list Python est encore plus flexible que cela : il s'agit d'un tableau dynamique (on peut ajouter ou supprimer des cases).

On pourrait donc créer une list Python contenant trois cases comme ceci :

exemple = [1, 2, 5]

Et plus tard dans le programme, le tableau pourrait faire référence à 6 cases :

[1, 2, 5, 10, 20, -10]

Ou même simplement 1 case :

[5]

1.2 TABLEAU DYNAMIQUE : list.append()

Principe et syntaxe

La méthode append() rajoute une case contenant la valeur voulue à la fin du tableau. Toujours à la fin.

La syntaxe est la suivante :

nom_du_tableau.append(contenu_de_la_nouvelle_case)

Insertion en fin (list) : coût CONSTANT, c'est donc une opération efficace qu'on peut utiliser sans dégrader les performances du programme.

Exemple
>>> tab = [] >>> tab.append(10) >>> tab [10] >>> tab.append(20) >>> tab [10, 20] >>> tab.append(30) >>> tab [10, 20, 30] >>> tab.append(0) >>> tab [10, 20, 30, 0]

Notez bien que l'élément est toujours rajouté à la fin du tableau.

Modifie en place, aucune réponse renvoyée

Notez bien que la méthode ne renvoie aucune réponse : elle modifie en place le tableau sur lequel elle doit agir.

Une erreur très courante, qu'il ne faut donc jamais faire : réaliser une affectation de ce type :

>>> tab = [10, 20, 30] >>> tab = tab.append(100) >>> tab

Ici, on place dans tab le résultat renvoyé par append(), à savoir... rien. On a donc involontairement vidé tab de tout contenu. Notez que cela ne provoque pas d'erreur, c'est une erreur de sémantique.

⚙ 01° Réaliser ceci :

  1. Exécuter mentalement ce programme suivant puis donner l'affichage provoqué.
  2. 1 2 3 4 5 6 7 8 9
    couleurs = [] couleurs.append("red") couleurs.append("green") couleurs.append("blue") couleurs.append("yellow") couleurs.append("cyan") couleurs.append("magenta") print(couleurs)
  3. Lancer le programme pour vérifier.

...CORRECTION...

Ligne 1 : couleurs = [] Ligne 2 : couleurs = ['red'] Ligne 3 : couleurs = ['red', 'green'] Ligne 4 : couleurs = ['red', 'green', 'blue'] Ligne 5 : couleurs = ['red', 'green', 'blue', 'yellow'] ...

On agit ainsi en rajoutant toujours le nouvel élément à la fin du tableau.

⚙ 02° Réaliser ceci :

  1. Déterminer quelles sont les 4 premières valeurs qui seront intégrées au tableau.
  2. 1 2 3 4 5 6 7
    multiples7 = [] for x in range(1000000): if x % 7 == 0: multiples7.append(x) print(multiples7)
  3. Lancer le programme pour vérifier.
  4. Que peut-on dire du coût de cette création en fonction du nombre n de nombres à tester ? Coût constant, logarithmique, linéaire ou quadratique ?
  5. Vérifier que ce programme permet d'expliquer comment réaliser cette création par compréhension :
  6. >>> tab = [x for x in range(1000000) if x % 7 == 0]

...CORRECTION...

⚙ 03° Réaliser ceci :

  1. Compléter ce programme pour qu'il parvienne à concaténer ces deux tableaux (créer un nouveau tableau qui contient les éléments du tableau A puis les éléments du tableau B).
  2. 1 2 3 4 5 6 7 8
    animaux = ["Chat", "Chien", "Poisson"] animaux_sauvages = ["Lion", "Tigre", "Éléphant"] for i in ... : # Pour chaque indice possible du tableau animaux_sauvages nouveau = ... # Récupère ce nouvel animal ... # Ajoute cet animal aux autres animaux print(animaux)
  3. Que peut-on dire du coût du rajout en fonction du nombre d'éléments ns d'animaux sauvages ? Coût constant, logarithmique, linéaire ou quadratique ?
  4. Vérifier que ce programme permet d'expliquer comment Python réaliser ceci :
  5. 1 2 3 4
    animaux = ["Chat", "Chien", "Poisson"] animaux_sauvages = ["Lion", "Tigre", "Éléphant"] animaux = animaux + animaux_sauvages

...CORRECTION...

1 2 3 4 5 6 7 8
animaux = ["Chat", "Chien", "Poisson"] animaux_sauvages = ["Lion", "Tigre", "Éléphant"] for i in range(len(animaux_sauvages)): # Pour chaque indice possible du tableau animaux_sauvages nouveau = animaux_sauvages[i] # Récupère ce nouvel animal animaux.append(nouveau) # Ajoute cet animal aux autres animaux print(animaux)

Le rajout n'intervient que sur les lignes 4-5-6.

L4 : nous allons réaliser la boucle ns fois : autant de fois que d'animaux sauvages.

Pendant la boucle :

  • L5 : accès à une case de tableau donc CONSTANT
  • L6 : ajout en fin de tableau donc CONSTANT

On réalise ns fois des actions à coût constant, le coût est donc LINEAIRE par rapport à nd :

𝓞(nd*(1+1)) = 𝓞(2*nd) = 𝓞(nd).

1.3 TABLEAU DYNAMIQUE : list.pop() sans argument

Principe et syntaxe, version sans argument

La méthode pop() supprime la dernière case d'un tableau ET renvoie la valeur supprimée : cela permet de la stocker si on le désire.

La syntaxe est la suivante :

nom_du_tableau.pop()

Suppression en fin (list) : coût CONSTANT, c'est donc une opération efficace qu'on peut utiliser sans dégrader les performances du programme.

Exemple
>>> tab = [10, 20, 30] >>> tab.pop() # supprimer sans mémoriser >>> tab [10, 20] >>> a = tab.pop() # on stocke le 20 qu'on va supprimer >>> tab [10] >>> tab.pop() # supprimer sans mémoriser >>> tab [] >>> a 20 >>> tab.pop() # supprimer une case d'un tableau vide ? IndexError: pop from empty list

Notez bien que vouloir supprimer une case d'un tableau vide lève une exception.

Gérer le cas problématique

Si vous n'êtes pas certain qu'il reste une case à supprimer, il faut donc tester avant d'agir :

>>> tab = [10] >>> tab.pop() # supprimer la case de fin >>> tab [] >>> if tab: tab.pop() # si tab n'est pas vide >>> if len(tab) > 0: tab.pop() # si tab n'est pas vide

Il n'y aura d'activation de la méthode que si le tableau contient un élément au moins.

Modifie en place et renvoie la valeur supprimée

La méthode renvoie la valeur supprimée. C'est le développeur qui décidera ce qu'il veut en faire. Ne pas réaliser d'affectation permet donc de simplement supprimer la valeur définitevemnt.

⚙ 04-A° On veut supprimer et afficher un par un les éléments en fin de tableau.

  1. Compléter ce programme pour qu'il réalise cela, jusqu'à ce qu'il soit entièrement vidé.
  2. 1 2 3 4 5 6 7 8
    memoire1 = ["Alice", "Bob", "Charlie", "David" ,"Elodie", "Fabien"] while len(...) > ...: # Tant que memoire1 n'est pas vide prenom = ... # Supprime et mémorise le dernier prénom de memoire1 ... # Affiche l'élément supprimé print(memoire1)
  3. Que peut-on dire du coût de cette suppression ? Coût constant, logarithmique, linéaire ou quadratique ?

...CORRECTION...

1 2 3 4 5 6 7 8
memoire1 = ["Alice", "Bob", "Charlie", "David" ,"Elodie", "Fabien"] while len(memoire1) > 0: # Tant que memoire1 n'est pas vide prenom = memoire1.pop() # Supprime et mémorise le dernier prénom de memoire1 print(prenom) # Affiche l'élément supprimé print(memoire1)

La suppression n'intervient que sur les lignes 4-5.

On réalise la boucle n fois si n est le nombre d'éléments dans le tableau.

La ligne 5 est une action à coôt CONSTANT.

Le coût est donc LINEAIRE par rapport à n :

𝓞(n*1) = 𝓞(n).

On remarquera que le print() est une action qui demande beaucoup de temps. On évitera de placer des print() dans des boucles si le but est d'aller vite.

⚙ 04-B° On veut maintenant supprimer un à un les éléments de fin d'un tableau et les placer à la fin d'un deuxième tableau.

  1. Compléter ce programme pour qu'il parvienne à supprimer une par une les dernières cases du tableau jusqu'à ce qu'il soit entièrement vidé.
  2. 1 2 3 4 5 6 7 8 9
    memoire1 = ["Alice", "Bob", "Charlie", "David" ,"Elodie", "Fabien"] memoire2 = [] while len(...) > ...: # Tant que memoire1 n'est pas vide prenom = ... # Supprime et mémorise le dernier prénom de memoire1 ... # Place ce prénom en fin de memoire2 print(memoire1) print(memoire2)
  3. Quelle est la propriété liée à l'ordre que possède le deuxième tableau par rapport au premiert ?
  4. Que peut-on dire du coût de cette action ? Coût constant, logarithmique, linéaire ou quadratique ?
  5. Au vu du programme réalisé, de son action et de son coût, que pouvez-vous dire du coût de la méthode reverse() qu'on présente ici mais qui n'est pas à connaître ou à utiliser en NSI&nbp;:
  6. >>> tab = [10, 20, 30, 40, 50] >>> tab.reverse() >>> tab [50, 40, 30, 20, 10]

...CORRECTION...

1 2 3 4 5 6 7 8 9
memoire1 = ["Alice", "Bob", "Charlie", "David" ,"Elodie", "Fabien"] memoire2 = [] while len(memoire1) > 0: # Tant que memoire1 n'est pas vide prenom = memoire1.pop() # Supprime et mémorise le dernier prénom de memoire1 memoire2.append(prenom) # Place ce prénom en fin de memoire2 print(memoire1) print(memoire2)

On voit que memoire2 contient les prénoms mais dans l'ordre inverse.

L'inversion intervient sur les lignes 4-5-6.

L4 : nous allons réaliser la boucle n fois : autant de fois que de prénoms dans la mémoire 1.

Pendant la boucle :

  • L5 : suppression d'une case de fin : CONSTANT
  • L6 : ajout en fin : CONSTANT

On réalise n fois des actions à coût constant, le coût est donc LINEAIRE par rapport à n :

𝓞(n*(1+1)) = 𝓞(2*n) = 𝓞(n).

On peut donc supposer que la méthode reverse() agit également à coût linéaire.

1.4 TABLEAU DYNAMIQUE : list.pop(i) avec argument

Principe et syntaxe, version avec argument

La méthode pop(i) supprime la case d'indice i du tableau ET renvoie la valeur supprimée : cela permet de la stocker si on le désire.

La syntaxe est la suivante :

nom_du_tableau.pop(i)

Suppression sur une position quelconque (list) : coût LINEAIRE, c'est donc une opération à utiliser avec précaution car elle peut dégrader les performances du programme si le tableau possède beaucoup de cases.

Exemple
>>> tab = [10, 20, 30] >>> tab.pop(1) # supprimer la case 1 sans mémoriser >>> tab [10, 30] >>> a = tab.pop(0) # on stocke le 10 qu'on va supprimer >>> tab [30] >>> a 10

⚙ 05° On désire supprimer systématiquement l'élément du mileu d'un tableau totalement aléatoire.

  1. Quel est le plus petit nombre de cases possibles ? Le plus grand nombre ?
  2. Que peut contenir chaque case ?
  3. Compléter le programme pour qu'il parvienne bien à supprimer et afficher l'élément central ?
  4. Quel est le coût de la ligne qui supprime cette case du milieu ?
  5. 1 2 3 4 5 6 7 8 9 10 11 12 13 14
    import random nc = random.randint(10, 50) t = [random.randint(10, 99) for _ in range(nc)] print(t) g = 0 # indice de gauche d = len(t) - 1 # indice de droite m = ... # indice du milieu valeur = ... # supprime et récupère la valeur du milieu print(valeur) # affiche cette valeur print(t)

...CORRECTION...

L3 : il y aura entre 10 et 50 cases dans le tableau.

L5 : on crée le tableau par compréhension et on voit qu'on crée des cases dont le contenu est une valeur aléatoire entre 10 et 99.

1 2 3 4 5 6 7 8 9 10 11 12 13 14
import random nc = random.randint(10, 50) t = [random.randint(10, 99) for _ in range(nc)] print(t) g = 0 # indice de gauche d = len(t) - 1 # indice de droite m = (g+d) // 2 # indice du milieu valeur = t.pop(m) # supprime et récupère la valeur du milieu print(valeur) # affiche cette valeur print(t)

La ligne 12 supprime la case avec t.pop(m). Cette action est à coût linéaire.

Il y a beaucoup d'autres méthodes disponibles sur les tableaux dynamiques. La plupart des fonctionnalités ont un coût linéaire, comme le pop(x). Il faut donc les utiliser en connaissance de cause et ne pas croire qu'il s'agit d'opérations magiques à coût constant.

D'autres leçons Python vous montreront parfois quelques fonctionnalités supplémentaires mais il faut bien comprendre que le principe de la NSI est de n'utiliser que les boucles, les accès et éventuellement pop() et append() pour reconstruire toutes ces fonctionnalités. Sur vos projets personnels, ces méthodes supplémentaires permettent néanmoins de considérablement réduire le nombre de lignes à taper !

2 - 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 utiliser des tableaux mais ils ne seront plus STATIQUES. Nous allons découvrir la notion de tableaux DYNAMIQUES.

(Rappel) 2.1 DICTIONNAIRE : accés à une valeur avec [cle]

Accès (avec des crochets)

Pour accéder à la valeur d'une case, il faut connaître sa clé. On accède à une valeur en tapant le nom du dictionnaire suivi de la clé entre crochets.

Attention, on place des crochets pour accéder, les accolades c'est pour la déclaration.

>>> ds = {"Alice": 13, "Bob": 8, "Charlie": 12} >>> ds["Alice"] 13 >>> ds["Charlie"] 12 >>> ds["Bob"] 8 >>> ds["Bob l'éponge"] KeyError: "Bob l'éponge"

Comme vous le voyez, demander un accès avec une clé inconnue provoque une erreur.

Accès (dict) : coût CONSTANT par rapport au nombre n d'éléments.

Résumé
  • Si d est un dictionnaire,
  • Si c est une clé valide dans le dictionnaire,
  • Alors d[c] est le contenu associé à la clé c du dictionnaire d,
(Rappel) 2.2 DICTIONNAIRE : muable en Python (modification possible)

En Python, les dictionnaires sont muables (ou mutables en anglais) : on peut modifier le contenu d'une case après la création du dictionnaire.

Modification d'un couple existant

Imaginons qu'on ai oublié des points à Bob : il n'a pas 8 mais 11 finalement. Voici comment nous pourrions modifier le tableau APRES création

>>> {"Alice": 13, "Bob": 8, "Charlie": 12} >>> notes["Bob"] 8 >>> notes["Bob"] = 11 >>> notes["Bob"] 11 >>> notes {'Alice': 13, 'Bob': 11, 'Charlie': 12}

Notez bien que ce n'est pas une affectation sur le dictionnaire : l'affectation est faite sur l'un des contenus des associations clé-valeur du dictionnaire, pas sur le dictionnaire lui-même.

Accès en modification (dict) : coût CONSTANT par rapport au nombre n d'éléments.

Rajout d'un nouveau couple

On peut rajouter de la même façon un couple qui n'existe pas encore.

Imaginons un nouvel élève nommé David qui a eu 15.

>>> {"Alice": 13, "Bob": 11, "Charlie": 12} >>> notes["David"] = 15 >>> notes {'Alice': 13, 'Bob': 11, 'Charlie': 12, 'David': 15}

⚙ 10° Dans notre programme, un ennemi sera un vaisseau alien ennemi. Nous allons vouloir stocker ses données dans une variable ennemi faisant référence à un dictionnaire, le type dict de Python.

  1. La clé 'x' fera référence à son abscisse, sa colonne à l'écran;
  2. La clé 'y' fera référence à son ordonnée, sa ligne à l'écran;
  3. La clé 'c' fera référence à son couleur, un nombre compris entre 1 et 15 dans Pyxel.
>>> ennemi = {'x':100, 'y':30, 'c':6}

Questions

  1. Comment récupérer dans une variable x la valeur associée à la clé 'x' ?
  2. Comment incrémenter la valeur associée à la clé 'x' ?

...CORRECTION...

>>> ennemi = {'x':100, 'y':30, 'c':6} >>> x = ennemi['x'] # question A >>> x 100 >>> ennemi['x'] = ennemi['x'] + 1 # question B >>> ennemi {'x':101, 'y':30, 'c':6}

⚙ 11° Dans notre programme, il y aura plusieurs ennemis. Nous allons les stocker dans une variable ennemis faisant référence à un tableau, le type list de Python.

Compléter le programme ci-dessous pour qu'on parvienne à afficher la couleur de tous les ennemis qui ont une ordonnée y supérieure à 85.

1 2 3 4 5 6 7 8 9 10 11 12
ennemis = [ {'x': 100, 'y': 50, 'c': 3}, {'x': 120, 'y': 80, 'c': 5}, {'x': 80, 'y': 110, 'c': 8}, {'x': 50, 'y': 90, 'c': 2}, {'x': 40, 'y': 20, 'c': 14}, ] for i in range(len(ennemis)): # Pour chaque indice i de ennemis ennemi = ... # récupère l'ennemi ayant cet indice if ...: # si cet ennemi a une ordonnée y 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 = [ {'x': 100, 'y': 50, 'c': 3}, {'x': 120, 'y': 80, 'c': 5}, {'x': 80, 'y': 110, 'c': 8}, {'x': 50, 'y': 90, 'c': 2}, {'x': 40, 'y': 20, 'c': 14}, ] for i in range(len(ennemis)): # Pour chaque indice i de ennemis ennemi = ennemis[i] # récupère l'ennemi ayant cet indice if ennemi['y'] > 80: # si cet ennemi a une ordonnée supérieure à 80 print(ennemi['c']) # affiche sa couleur

✔ 12-A° Nous voudrions maintenant supprimer avec pop(i) les ennemis qui ont une ordonnée supérieure à 80.

Utiliser ce premier programme  : vous devriez constater qu'il fonctionne bien. Par contre, il est "manuel" dans le sens où les demandes de suppressions sont tapées manuellement par le développeur.

1 2 3 4 5 6 7 8 9 10 11 12
ennemis = [ {'x': 100, 'y': 50, 'c': 3}, {'x': 120, 'y': 80, 'c': 5}, {'x': 80, 'y': 110, 'c': 8}, {'x': 50, 'y': 90, 'c': 2}, {'x': 40, 'y': 20, 'c': 14}, ] ennemis.pop(2) ennemis.pop(3) print(ennemis)
[{'x': 100, 'y': 50, 'c': 3}, {'x': 120, 'y': 80, 'c': 5}, {'x': 40, 'y': 20, 'c': 14}]

On constate bien que les ennemis qui avaient les indices 2 et 3 ne sont plus dans le tableau.

⚙ 12-B° Nous voudrions faire la même chose mais en automatisant l'ensemble : pour chaque ennemi, on lit son ordonnée et on le supprime si l'ordonnée est supérieure à 80.

Utiliser le deuxième programme : vous devriez constater qu'il refuse de fonctionner.

Question : pourquoi ? De quoi se plaint-il ?

Programme 2

1 2 3 4 5 6 7 8 9 10 11 12 13 14
ennemis = { 1: [100, 50, 3], 2: [120, 80, 5], 3: [80, 110, 8], 4: [50, 90, 2], 5: [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 qu'il refuse de supprimer une clé alors que vous êtes en train de les lire une par une en utilisant keys().

✔ 12-C° L'une des façons les plus rapides de régler le problème est de travailler directement à partir ennemis.keys() mais à partir d'une copie immuable des clés : tuple(ennemis.keys()).

Programme 3

1 2 3 4 5 6 7 8 9 10 11 12 13 14
ennemis = { 1: [100, 50, 3], 2: [120, 80, 5], 3: [80, 110, 8], 4: [50, 90, 2], 5: [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)
{1: [100, 50, 3], 2: [120, 80, 5], 5: [40, 20, 14]}
(Bilan) 2.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) 2.3 Principe d'un programme pyxel

Logo Pyxel
Logo Pyxel

Un programme utilisant le module pyxel est basé sur une boucle infinie décrite ci-dessous.

Principe de fonctionnement

    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 1/30e de seconde

        On active la fonction controleur() qui mettra les données à jour en fonction des événements détéctés.

        On active la fonction vue() qui effacera l'écran avec cls() puis générera le nouvel affichage.

      Fin du SI

    Fin TANT QUE

La structure usuelle du code Python pour ce module
1 2 3 4 5 6 7 8 9 10 11 12 13
import pyxel def controleur(): """Récupère l'événement et le gère les données (30 fois par seconde)""" ... # A modifier def vue(): """création des objets (30 fois par seconde)""" pyxel.cls(0) # vide la fenetre ... # A modifier pyxel.init(128, 128, title="Mon premier jeu") pyxel.run(controleur, vue)
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, chercher et installer pyxel.

Nous allons être ambitieux : le programme final va atteindre environ 200 lignes. Il sera construit progressivement à partir d'un squelette que je fournis. Pas de panique !

✔ 15-A° 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.

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 numeros = {"tir": 1, "explosion": 1, "ennemi": 1} 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 controleur(): """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é 1 seconde 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 le tir [x,y] du vaisseau dans le tableau 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(controleur, vue) # Lancement de l'alternance controleur / vue # 4 - PROGRAMME PRINCIPAL ==================================================== lancer_jeu()

✔ 15-B° Répondre aux questions suivantes :

Questions

  1. Quel est le type des 6 variables globales destinées à être lues depuis les fonctions : vaisseau points ennemis tirs explosions ? S'agit-il d'un type MUABLE ou IMMUABLE ?
  2. Le programme a l'air compliqué, le programme principal ne comporte qu'une ligne : l'appel à lancer_jeu() sur la ligne 121. Suivre le code à partir de cet appel et trouver les noms des deux fonctions qui vont être appelées 30 fois par seconde.
  3. Regarder la fonction controleur() :
    1. Dire quelles sont les trois interactions que peut réaliser le joueur.
    2. 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 ?
    3. Lignes 35-36. pyxel.frame_count permet de récupèrer le nombre de frames/images qui ont été générées depuis le lancement du jeu. Pourquoi la condition ligne 35 permet-elle de réaliser une création par seconde ?
  4. Regarder la fonction deplacer_vaisseau(). Pourquoi le vaisseau ne peut-il jamais quitter l'écran ?
  5. Regarder la fonction afficher_vaisseau(), donner le nom de la fonction qui permet de tracer des rectangles à l'écran.
  6. Regarder la fonction la fonction afficher_informations(). Donner le nom de la fonction qui permet d'afficher un texte à l'écran.

...CORRECTION...

  1. Les 6 variables globales sont des dictionnaires et sont donc muables.
  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. Ligne 116 : pyxel.run(controleur, vue)

  4. En allant voir controleur()
    1. Les trois interactions que peut réaliser le joueur :
    2. 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.
    3. vaisseau est-il un paramètre d'entrée de la fonction controleur() ? A-t-il été déclaré dans la fonction ? En conclure s'il s'agit-il d'une variable locale ou d'une variable globale.
    4. 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.

    5. La ligne 35 consiste à réaliser la division euclidienne du nombre de frames par 30. Or, il y a 30 images par seconde. 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.
    6. On génère donc un monstre par seconde.

  5. Pourquoi le vaisseau ne peut-il jamais quitter l'écran ?
  6. On voit qu'on ne laisse jamais la valeur associée à la clé 'x' sortir de l'intervalle [0, 120].

  7. Fonction afficher_vaisseau() : on voit L92 qu'il s'agit de rect().

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

  8. Fonction afficher_informations() : on voit L110 qu'il s'agit de text().
  9. pyxel.text(1, 1, texte, 7)

2.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)
2.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
2.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([controleur]) 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° Regardons comment créer les ennemis. Lancer d'abord le programme pour visualiser le résultat puis répondre aux questions.

  1. On y fait appel une fois par seconde. Voir creer_ennemi() en ligne 108 et plus. On y fait appel une fois par seconde. Expliquer :
    • Ce que contient chaque tableau-ennemi
    • Comment on rajoute un ennemi dans le dictionnaire ennemis : quelle clé ? quelle valeur associée ?
  2. GESTION. On lance un appel à gerer_ennemis() en ligne 40. Aller en ligne 119 et analyser chaque ligne pour être certain de bien la comprendre (important car il faudra s'inspirer de ces lignes pour gérer tirs et explosions).
  3. AFFICHAGE. On lance un appel à afficher_ennemis() en ligne 135. Aller en ligne 166 et analyser chaque ligne pour être certain de bien la comprendre.
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 191
# 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 numeros = {"tir": 1, "explosion": 1, "ennemi": 1} 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 controleur(): """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é 1 seconde 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 le tir [x,y] du vaisseau dans le tableau tirs""" return 0 # A SUPPRIMER lorsque vous compléterez la fonction tir = ... # crée le tableau contenant les coordonnées du tir n = ... # récupère depuis numeros le prochain numéro pour les tirs ... # stocke le tir dans tirs avec n comme clé ... # modifie dans numeros le prochain numéro de tir 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] n = numeros["ennemi"] # on récupère le numéro du futur ennemi ennemis[n] = ennemi # on rajoute cet ennemi dans le dictionnaire numeros["ennemi"] = numeros["ennemi"] + 1 # incrémentation du prochain numéro 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] # tableau [x, y, couleur] 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(controleur, vue) # Lancement de l'alternance controleur / 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().
2.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.

2.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#)

3 - 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 ?

4 - FAQ

Et si on veut faire plus de choses avec Tkinter ?

Il existe beaucoup de documentation sur le Web.

Les informations risquent néanmoins d'être encore difficiles à comprendre pour le moment.

Sinon, vous trouverez ci-dessous un petit tuto pour faire quelques petites manipulations :

FICHE Tkinter

5 -

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 02 07 2024
Dernière modification : 02 07 2024
Auteur : ows. h.