python pendu console

Identification

Infoforall

21 - Interface du pendu


La suite de l'activité précédente.

La dernière fois, nous avons réalisé les fonctions gérant les données. Nous allons aujourd'hui faire l'interface avec la console pour obtenir un petit jeu autonome : pour l'instant, quelqu'un désirant joueur doit savoir coder en Python puisqu'il doit faire les appels aux fonctions lui-même !

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

Evaluation ✎ : rien, tous les exercices sont corrigés. Par contre, il y a le DS qui va bien derrière :o)

1 - Interface Console

Rappel : les deux fonctions natives d'interface liées à la console

Les deux fonctions natives que propose Python pour gérer une interface texte :

  1. print pour afficher sur l'écran et
  2. input pour récupérer le texte tapé sur le clavier (sous forme d'un string).

Attention : pas de print ou de print en spécialité math pour ceux qui font à la fois NSI et Maths : en maths, vous n'aurez jamais à gérer l'interface : vous aurez à créer les fonctions et vous enverrez juste les informations via les paramètres.

2 - Affichage du pendu

Pour afficher le pendu, il faut déterminer le nombre de tentatives possibles.

Partons sur 6 :

L'affichage initial sera par exemple (0 faute)

1 ╔═══════╦═══ 2 ║ 3 ║ 4 ║ 5 ║ 6 ║ ███

L'avant-dernier affichage sera par exemple (5 fautes):

1 ╔═══════╦═══ 2 ║ | 3 ║ O 4 ║ -|- 5 ║ / \ 6 ║ ███

Le dernier affichage sera par exemple (6 fautes, fin de partie):

1 ╔═══════╦═══ 2 ║ | 3 ║ O 4 ║ -|- 5 ║ / \ 6 ║ perdu !

01° Réaliser les autres tableaux permettant à la fonction d'interface de fonctionner quelque soit l'étape.

A quoi servent les caracères \n ?

>>> recuperer_pendu(5) '1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║ / \\ \n6 ║ perdu !' >>> afficher_pendu(5) 1 ╔═══════╦═══ 2 ║ | 3 ║ O 4 ║ -|- 5 ║ / \ 6 ║ perdu !
1 2 3 4 5 6 7 8
def recuperer_pendu(etape:int) -> str : pendu = [None for x in range(6)] pendu[0] = "1 ╔═══════╦═══\n2 ║\n3 ║\n4 ║\n5 ║\n6 ║ ███" pendu[5] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║ / \ \n6 ║ perdu !" return pendu[etape] def afficher_pendu(etape) : print(recuperer_pendu(etape))

...CORRECTION...

Le caracère d'échappement suivi de n (\n) signale qu'on veut passer à la ligne. Le n est là pour dire new line.

Sinon, il suffit de définir les autres index à afficher.

3 - Récupération de la lettre voulue

02° Réaliser la fonction d'interface récupérant la réponse de l'utilisateur depuis le clavier.

Voici son prototype :

1
def recuperer_reponse() -> str :

Voici un exemple de ce qu'elle doit faire :

>>> lettre_voulue = recuperer_reponse() A quelle lettre pensez-vous ? a >>> lettre_voulue 'a'

...CORRECTION...

1 2
def recuperer_reponse() -> str : return input("A quelle lettre pensez-vous ? ")

4 - Les autres affichages

04° Pour afficher le mot déjà découvert au joueur, quelqu'un réalise la fonction ci-dessous :

1 2 3
def afficher_mot(mot:list) -> None : for element in mot : print(element, end=" ")

Pourquoi a-t-il effectivement le droit d'utliser cette façon d'utiliser le FOR (for 'nominatif') ?

...CORRECTION...

On peut utiliser cette façon de faire car on veut juste LIRE les cases.

04° Aurait-on pu l'utiliser si on avait voulu MODIFIER les cases du tableau ?

...CORRECTION...

Non : dans ce cas, il faut taper un code du type tableau[numero]. Il faut donc utiliser un FOR pour obtenir les valeurs numériques des index possibles.

05° Transformer la fonction précédente en utilisant cette fois plutôt un FOR sous la forme for index in range (len(mot)).

...CORRECTION...

1 2 3
def afficher_mot(mot:list) -> None : for index in range(len(mot)) : print(mot[index], end=" ")

06° Réaliser la dernière fonction d'affichage dont nous avons besoin : voici son prototype.

1
def affichage_final(reussite:bool, vrai_mot:str) -> None :

Elle doit afficher un message de victoire si le paramètre booléen reussite est VRAI. Dans le cas contraire, elle doit afficher un message signalant que c'est fini et afficher le mot qu'il fallait trouver. On l'obtient via le paramètre vrai_mot.

...CORRECTION...

1 2 3 4 5
def affichage_final(reussite:bool, vrai_mot:str) -> None : if reussite : print('Bravo !') else : print(f'Et non, perdu. Le mot était {vrai_mot}')

5 - Le jeu

Nous avons maintenant réalisé toutes nos fonctions d'interface et de gestion de données. Il ne reste que l'assemblage final. Voici une possibilté d'utilisation :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# Déclaration des fonctions de liaison def jouer(mot_mystere:str) -> None : vrai_mot = transformer(mot_mystere) mot = dissimuler(vrai_mot) etape = 0 afficher_pendu(etape) while etape < 5 and not verifier(mot, vrai_mot) : afficher_mot(mot) lettre_voulue = recuperer_reponse() exact = modifier_mot(mot, vrai_mot, lettre_voulue) if not exact : etape = etape + 1 afficher_pendu(etape) affichage_final(etape < 5, vrai_mot) # programme mot_choisi = 'informatique' jouer(mot_choisi)

On constate que le programme ne fait que deux lignes (19-20 ici) !

Il consiste à choisir un mot (pour l'instant, on l'impose à la main) et à lancer le jeu en transmettant notre mot.

Et que fait la fonction jouer ? C'est une fonction de liaison qui utilise toutes nos fonctions de gestion de données et nos fonctions d'interface. Elle contrôle le jeu. La seule nouveauté pour vous est le rajout d'une variable etape permettant de compter le nombre d'échec et d'ainsi savoir où en est le joueur.

07° Mettre le jeu en mémoire et faire une partie ou deux.

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
'''Liste des fonctions INTERFACE : ---------- + recuperer_pendu(etape:int) -> str + afficher_pendu(etape:int) -> None + affichage_final(reussite:bool, vrai_mot:str) -> None + afficher_mot(mot:list) -> None + recuperer_reponse() -> str GESTION DES DONNEES : --------------------- + dissimuler(reponse:list) -> list + transformer(mot:str) -> list + valider(mot:str) -> bool + modifier_mot(mot:list, vrai_mot:list, caractere:str) -> bool + verifier(mot, vrai_mot) -> bool LIAISON : --------- + jouer(mot_mystere:str) -> None ''' # Déclaration des fonctions d'interface def recuperer_pendu(etape:int) -> str : pendu = [None for x in range(6)] pendu[0] = "1 ╔═══════╦═══\n2 ║\n3 ║\n4 ║\n5 ║\n6 ║ ███" pendu[1] = "1 ╔═══════╦═══\n2 ║ |\n3 ║\n4 ║\n5 ║\n6 ║ ███" pendu[2] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║\n5 ║\n6 ║ ███" pendu[3] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║\n6 ║ ███" pendu[4] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║ / \ \n6 ║ ███" pendu[5] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║ / \ \n6 ║ perdu !" return pendu[etape] def afficher_pendu(etape) : print(recuperer_pendu(etape)) def affichage_final(reussite:bool, vrai_mot:str) -> None : if reussite : print('Bravo !') else : print(f'Et non, perdu. Le mot était {vrai_mot}') def afficher_mot(mot:list) -> None : for index in range(len(mot)) : print(mot[index], end=" ") def recuperer_reponse() -> str : return input("A quelle lettre pensez-vous ? ") # Déclaration des fonctions de gestion des données def verifier(tableau1:list, tableau2:list) -> bool : '''Fonction booléenne : True si les deux contenus des tableaux sont identiques''' for index in range(len(tableau1)) : if not tableau1[index] == tableau2[index] : return False return True def modifier_mot(mot:list, vrai_mot:list, caractere:str) -> bool : '''Fonction booléenne : True si caractére est dans vrai_mot. Modifie mot par effet de bord''' existe = False for index in range(len(vrai_mot)) : if vrai_mot[index] == caractere : mot[index] = vrai_mot[index] if not existe : existe = True return existe def dissimuler(reponse:list) -> list : '''Renvoie une 'copie' ne contenant que des étoiles''' return ['*' for carac in reponse] def transformer(mot:str) -> list : '''Renvoie le tableau regroupant les caractères du mot reçu :: param mot(str) :: un string VALIDE dans le cadre du jeu :: return (list) :: un tableau des caractères du mot :: exemple .. >>> transformer('bonjour') ['b', 'o', 'n', 'j', 'o', 'u', 'r'] ''' return list(mot) def valider(mot:str) -> bool : acceptables = "abcdefghijklmnopqrstuvwxyz" for index in range (len(mot)) : if not mot[index] in acceptables : return False return True # Déclaration des fonctions de liaison def jouer(mot_mystere:str) -> None : vrai_mot = transformer(mot_mystere) mot = dissimuler(vrai_mot) etape = 0 afficher_pendu(etape) while etape < 5 and not verifier(mot, vrai_mot) : afficher_mot(mot) lettre_voulue = recuperer_reponse() exact = modifier_mot(mot, vrai_mot, lettre_voulue) if not exact : etape = etape + 1 afficher_pendu(etape) affichage_final(etape < 5, vrai_mot) # programme mot_choisi = 'informatique' jouer(mot_choisi)

09° Ligne 104 : quelles sont les deux conditions à réunir pour continuer à poser la question au joueur ? Expliquer pourquoi on rentre dans la boucle au moins une fois au départ.

Pour rendre le jeu plus intéressant, il faudrait par exemple que le mot choisi soit issu d'une liste présente dans un fichier-texte et qu'on tire le mot au hasard.

Nous verrons la gestion des fichiers-texte un peu plus tard dans l'année mais voici la fonction qu'il suffit de rajouter pour le moment pour rendre cela utilisable.

Cela vous permettra de voir que ce n'est pas très compliqué à faire : il suffit de connaître la syntaxe.

6 - Récupérer les mots dans un fichier-texte

Pas trop d'explications sur cette partie. Nous verrons comment comprendre ce code plus tard. L'important est que vous comprenniez le principe.

10° Créer un nouveau fichier avec Thonny. N'y mettre que des mots, un par ligne. Enregister (dans un dossier que vous saurez localiser bien) le tout sous le nom "desmots.txt".

Par exemple :

informatique ordinateur boucle fonction condition sciences

11° Ouvrir une nouvelle page dans Thonny et rajouter cette fonction en mémoire en sauvegardant au même endroit que le fichier-texte précédent.

1 2 3 4 5 6 7 8
def recuperer_des_mots(f:str) -> list : '''Renvoie un tableau des mots contenu dans le fichier f, un mot par ligne''' objet_fichier = open(f, "r") # On ouvre le fichier f en lecture ("r") tableau_mots = objet_fichier.readlines() # On récupère un tableau (une case par ligne de texte finissant par "\n" objet_fichier.close() # Important : on ferme le fichier for index in range(len(tableau_mots)) : tableau_mots[index] = tableau_mots[index].replace('\n','') # On crée un nouveau string en remplaçant "\n" par rien... return tableau_mots
>>> recuperer_des_mots("desmots.txt") ['informatique', 'ordinateur', 'boucle', 'fonction', 'condition', 'sciences']

12° D'après le code, quel est le point important à ne pas oublier une fois le fichier ouvert ?

...CORRECTION...

Il ne faut pas oublier de fermer le fichier.

Encore une fois : une tâche par fonction uniquement.

Maintenant, il faudrait tirer l'un des mots au hasard avec une autre fonction.

13° Rajouter cette nouvelle fonction en mémoire : elle permet de tirer un mot au hasard parmi les mots présents. En réalité, on tire au hasard un numéro d'index valide. Question : pourquoi marquer qu'on prend la longeur - 1 sur la ligne 14 ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import random def recuperer_des_mots(f:str) -> list : '''Renvoie un tableau des mots contenu dans le fichier f, un mot par ligne''' objet_fichier = open(f, "r") # On ouvre le fichier f en lecture ("r") tableau_mots = objet_fichier.readlines() # On récupère un tableau (une case par ligne de texte finissant par "\n" objet_fichier.close() # Important : on ferme le fichier for index in range(len(tableau_mots)) : tableau_mots[index] = tableau_mots[index].replace('\n','') # On crée un nouveau string en remplaçant "\n" par rien... return tableau_mots def mot_aleatoire(mots:list) -> str : '''Renvoie aléatoirement l'un des mots du tableau mots qui contient des mots''' index = random.randint(0, len(mots)-1) return mots[index]

...CORRECTION...

Contrairement à range, la fonction randint prend réellement la valeur finale. Or si le tableau comporte x éléments, l'index final sera x-1 puisqu'on commence à 0.

Nous avons maintenant deux fonctions basiques : l'une récupére les données dans un fichier-texte et les transforme en tableau. L'autre tire aléatoirement un élément d'un tableau. Vous devinez ce qu'on va pouvoir faire : les associer dans une fonction plus "complexe" qui va utiliser les deux fonctions "basiques" précédentes.

14° Mettre les trois fonctions en mémoire.

Vérifiez ensuite que cela fonctionne :

>>> recuperer_un_mot("desmots.txt") 'boucle'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import random def recuperer_des_mots(f:str) -> list : '''Renvoie un tableau des mots contenu dans le fichier f, un mot par ligne''' objet_fichier = open(f, "r") # On ouvre le fichier f en lecture ("r") tableau_mots = objet_fichier.readlines() # On récupère un tableau (une case par ligne de texte finissant par "\n" objet_fichier.close() # Important : on ferme le fichier for index in range(len(tableau_mots)) : tableau_mots[index] = tableau_mots[index].replace('\n','') # On crée un nouveau string en remplaçant "\n" par rien... return tableau_mots def mot_aleatoire(mots:list) -> str : '''Renvoie aléatoirement l'un des mots du tableau mots qui contient des mots''' index = random.randint(0, len(mots)-1) return mots[index] def recuperer_un_mot(fichier:str) -> str : '''Fonction de 'niveau 2' qui utilise les fonctions de 'niveau 1' : renvoi un mot du fichier texte''' disponibles = recuperer_des_mots(fichier) return mot_aleatoire(disponibles)

Et voilà : on réalise une tâche complexe en la décomposant en tâches plus simples. Du coup, on peut maintenant intégrer ce qu'on vient de faire et demander au jeu de tirer un mot aléatoirement plutôt que de n'utiliser que le mot mystère qu'on avait noté à la main.

15° Mettre le code suivant en mémoire. Observer bien que le code est décomposé en plusieurs parties :

  1. Une documentation présentant les prototypes des fonctions du fichier
  2. Ensuite la partie Importation
  3. Ensuite les déclarations de fonctions triées : fonctions d'interface, fonctions gestion de données et fonctions de liaison.
  4. Le programme lui-même si besoin
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
'''Liste des fonctions INTERFACE : ---------- + recuperer_pendu(etape:int) -> str + afficher_pendu(etape:int) -> None + affichage_final(reussite:bool, vrai_mot:str) -> None + afficher_mot(mot:list) -> None + recuperer_reponse() -> str GESTION DES DONNEES : --------------------- + dissimuler(reponse:list) -> list + transformer(mot:str) -> list + valider(mot:str) -> bool + modifier_mot(mot:list, vrai_mot:list, caractere:str) -> bool + verifier(mot, vrai_mot) -> bool + recuperer_des_mots(fichier:str) -> list + mot_aleatoire(mots:list) -> str + recuperer_un_mot(fichier:str) -> str LIAISON : --------- + jouer(mot_mystere:str) -> None ''' # Phase d'importation import random # Déclaration des fonctions d'interface def recuperer_pendu(etape:int) -> str : pendu = [None for x in range(6)] pendu[0] = "1 ╔═══════╦═══\n2 ║\n3 ║\n4 ║\n5 ║\n6 ║ ███" pendu[1] = "1 ╔═══════╦═══\n2 ║ |\n3 ║\n4 ║\n5 ║\n6 ║ ███" pendu[2] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║\n5 ║\n6 ║ ███" pendu[3] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║\n6 ║ ███" pendu[4] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║ / \ \n6 ║ ███" pendu[5] = "1 ╔═══════╦═══\n2 ║ |\n3 ║ O\n4 ║ -|-\n5 ║ / \ \n6 ║ perdu !" return pendu[etape] def afficher_pendu(etape) : print(recuperer_pendu(etape)) def affichage_final(reussite:bool, vrai_mot:str) -> None : if reussite : print('Bravo !') else : print(f'Et non, perdu. Le mot était {vrai_mot}') def afficher_mot(mot:list) -> None : for index in range(len(mot)) : print(mot[index], end=" ") def recuperer_reponse() -> str : return input("A quelle lettre pensez-vous ? ") # Déclaration des fonctions basiques de gestion des données def verifier(tableau1:list, tableau2:list) -> bool : '''Fonction booléenne : True si les deux contenus des tableaux sont identiques''' for index in range(len(tableau1)) : if not tableau1[index] == tableau2[index] : return False return True def modifier_mot(mot:list, vrai_mot:list, caractere:str) -> bool : '''Fonction booléenne : True si caractére est dans vrai_mot. Modifie mot par effet de bord''' existe = False for index in range(len(vrai_mot)) : if vrai_mot[index] == caractere : mot[index] = vrai_mot[index] if not existe : existe = True return existe def dissimuler(reponse:list) -> list : '''Renvoie une 'copie' ne contenant que des étoiles''' return ['*' for carac in reponse] def transformer(mot:str) -> list : '''Renvoie le tableau regroupant les caractères du mot reçu :: param mot(str) :: un string VALIDE dans le cadre du jeu :: return (list) :: un tableau des caractères du mot :: exemple .. >>> transformer('bonjour') ['b', 'o', 'n', 'j', 'o', 'u', 'r'] ''' return list(mot) def valider(mot:str) -> bool : acceptables = "abcdefghijklmnopqrstuvwxyz" for index in range (len(mot)) : if not mot[index] in acceptables : return False return True def recuperer_des_mots(f:str) -> list : '''Renvoie un tableau des mots contenu dans le fichier f, un mot par ligne''' objet_fichier = open(f, "r") # On ouvre le fichier f en lecture ("r") tableau_mots = objet_fichier.readlines() # On récupère un tableau (une case par ligne de texte finissant par "\n" objet_fichier.close() # Important : on ferme le fichier for index in range(len(tableau_mots)) : tableau_mots[index] = tableau_mots[index].replace('\n','') # On crée un nouveau string en remplaçant "\n" par rien... return tableau_mots def mot_aleatoire(mots:list) -> str : '''Renvoie aléatoirement l'un des mots du tableau mots qui contient des mots''' index = random.randint(0, len(mots)-1) return mots[index] def recuperer_un_mot(fichier:str) -> str : '''Fonction de 'niveau 2' qui utilise les fonctions de 'niveau 1' : renvoi un mot du fichier texte''' disponibles = recuperer_des_mots(fichier) return mot_aleatoire(disponibles) # Déclaration des fonctions de liaison def jouer(mot_mystere:str) -> None : vrai_mot = transformer(mot_mystere) mot = dissimuler(vrai_mot) etape = 0 afficher_pendu(etape) while etape < 5 and not verifier(mot, vrai_mot) : afficher_mot(mot) lettre_voulue = recuperer_reponse() exact = modifier_mot(mot, vrai_mot, lettre_voulue) if not exact : etape = etape + 1 afficher_pendu(etape) affichage_final(etape < 5, vrai_mot) # programme mot_choisi = recuperer_un_mot("desmots.txt") jouer(mot_choisi)

Voilà, c'est la fin de cette grande séquence permettant de stabiliser vos connaissances.

La prochaine fois que vous aurez un projet à faire, vous serez beaucoup plus autonome.

Vous appliquez la méthode suivante :

  1. Réflexion globale sur les petites fonctions qu'il faudra créer
  2. Créer les prototypes de fonctions, leurs documentations et quelques exemples d'utilisation qu'on placera directement dans la documentation.
  3. Se lancer dans la conception des premières fonctions, en envoyant directement les arguments depuis la console en mode interactif
  4. Une fois qu'une majorité des petites fonctions sont réalisées, passer à la phase conception des fonctions les utilisant
  5. Réaliser quelques fonctions d'interface basiques
  6. Réaliser votre premier prototype utilisant une fonction de liaison au moins
  7. Affiner le projet une fois qu'un premier prototype fonctionne même s'il manque des fonctionnalités : inutile d'affiner AVANT qu'un prototype même basique ne soit fonctionnel...

En conclusion :

  1. Pour lire le contenu d'un tableau depuis une fonction : deux méthodes possibles
    • L'accès via l'index
    • 1 2 3 4 5 6
      def afficher(tableau) : for index in range( len(tableau) ) : print(tableau[index]) notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] afficher(notes)
    • L'accès en lisant directement le contenu des 'cases'
    • 1 2 3 4 5 6
      def afficher(tableau) : for element in tableau : print(element) notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] afficher(notes)
  2. Pour modifier un tableau depuis une fonction : une seule méthode utilisable : l'accès via l'index.
  3. 1 2 3 4 5 6
    def modifier(tableau) : for index in range( len(tableau) ) : tableau[index] = tableau[index] ** 2 notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] modifier(notes)
  4. Pour renvoyer un nouveau tableau basé sur un tableau-paramètre depuis une fonction : une seule méthode utilisable :
    • Créer une copie du tableau initial
    • Modifier la copie en utilisant les index
    • Renvoyer la copie (et la stocker dans une variable !)
    1 2 3 4 5 6 7 8
    def copier_et_modifier_la_copie(tableau) : copie = [valeur for valeur in tableau] for index in range( len(copie) ) : copie[index] = copie[index] * 2 return copie notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] nouvelles_notes = copier_et_modifier_la_copie(notes)

Activité publiée le 22 11 2020
Dernière modification : 22 11 2020
Auteur : ows. h.