Vincent Maille CRÉATIONS NUMÉRIQUES Apprendre la programmation par le jeu RESSOURCES TÉLÉCHARGEABLES Découvrir Pygame avec de nouveaux jeux en Python Vincent Maille Graphismes de David Beauget CRÉATIONS NUMÉRIQUES Apprendre la programmation par le jeu Apprendre la programmation par le jeu Découvrir Pygame avec de nouveaux jeux Découvrir Pygame avec de nouveaux jeux en Python Vincent Maille Graphismes de David Beauget Collection CRÉ ATIONS NUMÉRIQUES dirigée par Vincent Maille http://creations-numeriques.fr/ Retrouvez tous les titres de la collection et des extraits sur www.editions-ellipses.fr ISBN 9782340-042100 © Ellipses Édition Marketing S.A., 2020 32, rue Bargue 75740 Paris cedex 15 Petite intro Pourquoi ce livre ? Je vais vous parler d’un temps que les moins de 40 ans ne peuvent pas connaı̂tre (ni peut-être comprendre l’allusion à cette chanson !), mais j’ai eu mon premier ordinateur à mon entrée en sixième (il faut dire que je travaillais au corps mes parents depuis déjà quelques années ! ! !). C’était un MO5 de Thomson et les jeux étaient enregistrés sur des cassettes magnétiques... Avec lui, j’ai fait mes premiers pas dans la programmation avec mon grand frère. Notre plus grand projet de jeu fut ≪ La capture de Joe ≫ ! Grâce au droit à l’oubli beaucoup plus facile à l’époque, il doit finir ses jours à 6 pieds sous terre au fond d’une ancienne décharge à ciel ouvert comme cela existait malheureusement à l’époque ! Quelques années plus tard, alors que j’étais en seconde et première, nous avons eu un ATARI STE, le top du moment, on pouvait jouer, programmer et même faire de la MAO (Musique Assistée par Ordinateur !) avec ce petit bijou. J’avais plus de connaissances en mathématiques pour aborder des choses plus complexes et nous avons passé pas mal de temps avec mon frère à coder, des heures durant, des démos pour notre groupe de programmation ( LEGEND ). Un retour de 20 ans en arrière est proposé d’ailleurs au chapitre F du livre. A cette époque, pas d’Internet pour apprendre, ni de forum pour poser des questions... Nous avions juste une BAL (Boı̂te Aux Lettres) sur Minitel, que nous relevions une fois par semaine (vu les tarifs !). Heureusement, nous avions à notre disposition un énorme livre de 400 pages : notre bible du GFA Basic, mais surtout, tous les mois, nous attendions avec impatience la sortie du nouveau numéro de ST Magazine. Cette revue parlait de jeux, mais aussi de programmation avec des cours, des astuces et bien sûr les GFA Punchs, dont il fallait relever le défi de réaliser un programme – utile ou joli – en moins de 20 lignes ! (Sur la capture de la page suivante, un Tétris en 20 lignes) 1 Le livre que vous lisez en ce moment est donc un peu celui que j’aurais aimé avoir entre les mains il y a 30 ans... La démarche de projet On parle souvent d’enseigner par compétence ou d’enseigner par projet pour changer les façons de faire... Pourtant la plupart des manuels ou des ouvrages sont encore souvent très linéaires. Ce livre propose enfin une démarche d’apprentissage par projet. Ainsi chaque chapitre présente la réalisation d’un petit jeu ; les connaissances nécessaires à la réalisation de celui-ci sont apportées au fur et à mesure du projet. Dans la plupart des cas, le chapitre se termine par une résolution automatique du niveau ou une intelligence artificielle simple servant de deuxième joueur. Bien entendu, dans ce livre, les chapitres sont assez guidés pour suivre le cheminement que j’ai pu imaginer au cours de l’activité. J’aurais opté pour une version bien plus ouverte pour un projet en classe, mais cela donnera, je l’espère en tout cas, des idées au lecteur pour chercher à approfondir certaines notions. Le choix de Pygame Il n’est pas nécessaire d’avoir lu le premier tome pour entamer ce second opus. Si vous l’avez acheté et lu, merci, cela vous aidera grandement à comprendre les notions qui sont un peu plus poussées dans cet ouvrage. Sinon, il est quand même conseillé d’avoir des notions de base de Python avant de s’atteler à la découverte du livre. Dans tous les cas, un premier chapitre reprenant les types de base de Python est proposé pour faire le point ensemble. Pour ce second tome, j’ai choisi de vous présenter l’interface Pygame, plus puissante que Tkinter pour réaliser des jeux, mais moins adaptée si vous souhaitez réaliser des applications. Si vous utilisez EduPython, c’est un bon choix et vous n’aurez rien à installer pour utiliser Pygame ; sinon, il vous suffit d’installer Pygame avec pip : pip install pygame et le tour est joué. Sous EduPython, lorsque votre programme plantera (ne vous inquiétez pas, ça arrivera !), ne tentez pas de fermer la fenêtre mais le fait de relancer le programme (avec l’erreur corrigée) relancera la machine. Dans le pire des cas, l’appui sur CTRL + F2 fera tout rentrer dans l’ordre. Comme pour tous les livres de la collection créations numériques, vous aurez la possibilité d’utiliser le complément en ligne sur le site : http://creations-numeriques.fr/ En saisissant les CODES signalés sur fond gris ou les numéros des exercices, vous pourrez télécharger les différents codes sources des programmes proposés ainsi que les images et autres ressources. Tous les exercices y sont corrigés. À ce propos, n’hésitez pas à lire la correction des différents exercices que vous aurez réalisés, même si vous les avez réussis. Les corrections donnent souvent différentes pistes de résolution et apportent parfois des compléments précieux pour la suite de l’aventure. Merci, merci, merci J’aimerais commencer par remercier ici les élèves du lycée Louis Thuillier d’Amiens, ceux qui, comme Guillaume, n’ont jamais manqué un rendez-vous le mercredi midi au club info, mes étudiants de TSI en particulier Théo, Alexandre et Lucas, mais aussi de MPSI, MP ou BCPST qui nous obligent à régulièrement faire des recherches, imaginer de nouvelles stratégies d’enseignement et m’ont souvent inspiré par les idées qu’ils pouvaient avoir au travers de leurs projets ou TIPE. Merci à ceux qui ont pris le temps de relire ce livre pour en proposer une version qui contienne moins de fautes que dans sa version originale ! Mes parents, sans qui tout cela n’aurait pu commencer (je ne parle pas de ma naissance, mais du MO5 !). Mes collègues et amis avec qui j’ai la chance de travailler : Guillaume Miannay et Paul Stienne, deux traqueurs de coquilles d’une efficacité redoutable. Enfin, un livre sur les jeux n’aurait pas le même cachet sans les superbes graphismes de David Beauget, mon complice de toujours qui en plus d’être un informaticien hors norme, possède comme vous le verrez un réel talent de dessinateur ! Table des matières Table des matières A Révisions 1 - Variables et fonctions . . . . . . . . . . A Révisions 2 - Tests et boucles . . . . . . . . . . . . . . 1 - Variables et fonctions . . . . . . . . . . 3 - Chaı̂nes de caractères . . . . . . . . . . 2 - Tests et boucles . . . . . . . . . . . . . . 4 - Les listes et matrices . . . . . . . . . . . 3 - Chaı̂nes de caractères . . . . . . . . . . 5 - Aide pour les exercices . . . . . . . . . 4 - Les listes et matrices . . . . . . . . . . . 6 - Solutions des exercices . . . . . . . . . . 5 - Aide pour les exercices . . . . . . . . . 6 - Solutions des exercices . . . . . . . . . . B Jeu de Pong 1 - Interface et variables . . . . . . . . . . . B Jeu de Pong 2 - Utilisation des dictionnaires . . . . . . 1 - Interface et variables . . . . . . . . . . . 3 - Retour à notre projet . . . . . . . . . . . 2 - Utilisation des dictionnaires . . . . . . 4 - Animation et gestion des rebonds . . . 3 - Retour à notre projet . . . . . . . . . . . 5 - Déplacement des raquettes . . . . . . . 4 - Animation et gestion des rebonds . . . 6 - Buts et scores . . . . . . . . . . . . . . . 5 - Déplacement des raquettes . . . . . . . 7 - Amélioration des rebonds . . . . . . . 6 - Buts et scores . . . . . . . . . . . . . . . 8 - Un peu d’intelligence artificielle... . . . 7 - Amélioration des rebonds . . . . . . . 9 - Aide pour les exercices . . . . . . . . . 8 - Un peu d’intelligence artificielle... . . . 10 - Solutions des exercices . . . . . . . . . . 9 - Aide pour les exercices . . . . . . . . . 10 - Solutions des exercices . . . . . . . . . . C Minos 1 - Fenêtre et surfaces dans Pygame . . . C Minos 2 - Dessiner un labyrinthe . . . . . . . . . 1 - Fenêtre et surfaces dans Pygame . . . 3 - Générer un labyrinthe . . . . . . . . . . 2 - Dessiner un labyrinthe . . . . . . . . . 4 - Les événements clavier, retour au jeu ! 3 - Générer un labyrinthe . . . . . . . . . . 5 - Pour aller plus loin : sortir . . . . . . . 4 - Les événements clavier, retour au jeu ! 6 - Aide pour les exercices . . . . . . . . . 5 - Pour aller plus loin : sortir . . . . . . . 7 - Solutions des exercices . . . . . . . . . . 6 - Aide pour les exercices . . . . . . . . . 7 - Solutions des exercices . . . . . . . . . . D SELEM-STOM 1 - Quelques fonctions pour commencer . 2 - Remplissage et expressions régulières 3 - Dessiner la grille de jeu . . . . . . . . . 4 - Gestion de la souris . . . . . . . . . . . 5 - Une grille à thème ! . . . . . . . . . . . . 6 - Aide pour les exercices . . . . . . . . . 7 - Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 11 18 11 24 18 26 24 31 26 32 31 32 45 45 45 48 45 54 48 54 54 55 54 57 55 57 57 60 57 60 60 62 60 62 73 74 73 83 74 86 83 88 86 92 88 96 92 98 96 98 113 114 116 122 125 127 137 139 E Cupidon 157 1 - L’object Rect de Pygame . . . . . . . . . . . . . . . . . . . . . . . . . . 158 34567- Dessiner la grille de jeu Gestion de la souris . . Une grille à thème ! . . . Aide pour les exercices Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 125 127 137 139 E Cupidon 1 - L’object Rect de Pygame . . . . . . . . . 2 - Mise en place de Cupidon . . . . . . . . 3 - La flèche . . . . . . . . . . . . . . . . . . . 4 - Les ennemis . . . . . . . . . . . . . . . . . 5 - Introduction à la programmation objet . 6 - Aide pour les exercices . . . . . . . . . . 7 - Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 158 161 163 164 168 175 176 F SELEM-STOM Front-end D Manipulerfonctions les fichiers de commencer configuration 11 -- Quelques pour . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. La bande de film . . . . . . régulières . . . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 22 -- Remplissage et expressions Unlimitedlasprites . .jeu . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 33 -- Dessiner grille de 3D Starfield . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 44 -- Gestion de la souris 5 Aide pour les exercices 5 - Une grille à thème ! . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 6 Solutions des exercices 6 - Aide pour les exercices . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 7 - Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . G EHPAD-RUN 1 - Manipulation d’images . . . . . . . . . . . . . . . . . . . . . . . . . . . E Cupidon L’interface du de jeuPygame . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 12 -- L’object Rect Les personnages . . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 23 -- Mise en place de Cupidon Aide pour .les 34 -- La flèche . .exercices . . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. Solutions des .exercices 45 -- Les ennemis . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 5 - Introduction à la programmation objet . . . . . . . . . . . . . . . . . . H 6Les données - bases Aide de pour les exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Présentation . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 7 - Solutions des .exercices 2 - Utilité des tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 - Premières requêtes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . F Front-end 41 -- Compléments sur les requêtes . . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. Manipuler les fichiers de configuration 52 -- Les jointures . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. La bande de film 63 -- Ajouter, modifier, Unlimited sprites supprimer . . . . . . . des . . .enregistrements . . . . . . . . . . .. .. .. .. .. .. .. .. .. .. .. 7 Compléments sur les dates et chaı̂nes .. .. 4 3D Starfield . . . . . . . . . . . . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 113 D SELEM-STOM D SELEM-STOM 8 Les groupements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aide pour les exercices .commencer . . . . . . . . . . . . . . . . . . . . . . . . 114 1 - 15Quelques - Quelques fonctions fonctions pourpour commencer .. .. D SELEM-STOM D SELEM-STOM 113 9 Aides pour les exercices . . . . . . . . . . . . . . . . . . . . . . . . . Solutionsetdes exercices . . . régulières . . . . . . . . . . . . . . . . . . . . . . . 116 2 - 26Remplissage - Remplissage expressions et expressions régulières .. .. 10 Solutions des exercices . . . 1 - 3Quelques 1 Quelques fonctions fonctions pour commencer pour commencer . . . . . . . . . . . . . . . . . . . . 114 . . . .. .. - 3Dessiner - Dessiner la grille la grille de jeu de .jeu . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 G 2 - 4Remplissage -- Remplissage et expressions expressions .. .. .. 125 .. .. - 24EHPAD-RUN Gestion Gestion de lade souris la et souris . régulières . . . . . . régulières . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 116 I O-But 1 Manipulation d’images 3 - 5A Dessiner 3 la Dessiner grille de la jeu grille . . de . . jeu . . . . . . . . . . . . . . . . . . . . . . . . 122 . . . . . Révisions - 15Une Une grillegrille à thème à thème ! . niveaux . .! . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 127 --- de Chargement des ... ... 2 L’interface du jeu 4 - 6Gestion 4 Gestion la souris de la . souris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 . . . - 126Aide Aide pourpour lesetexercices les exercices . .. .. -.. Propriétés .. .. .. .. .. .. .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 137 - -- Variables fonctions Déplacement des boules ... .. A Les personnages 5 - 7A Une 573Révisions grille -- Une à thème grille !exercices à. thème . .exercices . . ..! .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 127 . .. .. 139 . . - 2Révisions Solutions Solutions des des . et bouclesdu. personnage . . . . . . . . -.La . .classe . . . .Mask . . . . .. .. .. .. .. .. .. .. .. .. .. ... .. 3- - Tests Déplacement 6 - Aide - Variables Aide les exercices pour exercices . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 137 .. .. .. .. . 1641-pour Variables etetles fonctions fonctions Ce n’estde qu’un au revoir... . . . . . . . . . . . . . . . . . . . . . .. .. .. .. . 345- -- Chaı̂nes caractères 7 E- Cupidon Solutions Solutions des et exercices des .exercices .. ... ... ... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... .... 139 . . . ... .. E 27Cupidon Tests etboucles boucles 52- -- Tests Aide pour les exercices . . . . . . . . . . . . . . . . . . . . . . ... ... ... 157 . . 4 Les listes et matrices . . . . . . . . . . . . . . . . . . . . . . . . . 1 - 3613L’object L’object Rect Rect de Pygame de Pygame .. ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... .... .... .... 158 .... .. Solutions des exercices - -- Chaı̂nes Chaı̂nes de de caractères caractères H bases de E Cupidon - - Aide pour les Cupidon exercices 2E - 5Cupidon 2Les Mise Mise en place en données place de de Cupidon .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 157 .. .. .. 161 .. . 414- -- Rect Les Leslistes listes etetmatrices matrices . ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 158 .. .. .. ... .. Présentation . . 1 - 3L’object L’object de Pygame Rect de . Pygame . . - - flèche Solutions - 6Glossaire 3La La flèche . . des . . .exercices . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ... ... ... 163 .. . 525-en - Aide Aidepour pour les lesexercices exercices . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 161 .. .. .. .. . Utilité des tables 2 - 4Mise place Mise de en Cupidon place de Cupidon . . . . - 4Les - ennemis Les ennemis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 . . Premières requêtes 6356Introduction - -- de Solutions Solutions des exercices . .programmation . . .. .. .. .. ..objet .. .. ..objet ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 163 3 - 5B La La flèch àdes - flèche Introduction la programmation à.exercices la ... ... ... 168 ... .. Jeu Pong Compléments sur . . . .. .. 4 - 6Les . . les . .. requêtes . . . . . . . . . . . . . . . . . . . . . . . . 164 46Aide - Les enn - 1enne Aide pour pour lesetexercices les .exercices - - Interface variables . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 175 . Les jointures . . . . . . . . . . 5 - 7B Introduction à la programmation objet . . . . . . . . . . . . . . . . . . 168 5 Introduction à la programmation objet . . .. .. B Jeu Jeu de de Pong Pong - 27Solutions - Utilisation Solutions des exercices des exercices . . . . . . .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ... ... 176 des dictionnaires Ajouter, modifier, 6 - Aide6pour les exercices . . supprimer . . . . . . . des . . .enregistrements . . . . . . . . . . . . . . . . 175 - Aide pour les exercices . . . .. . Table des matières Table Table des des matières matières 191 113 192 114 197 116 200 122 204 125 206 127 207 137 139 215 216 157 225 158 230 161 238 163 240 164 168 261 175 261 176 262 265 191 267 192 270 197 274 200 277 204 113 282 206 114 113 283 207 116 283 114 122 215 116 125 293 216 122 11 127 294 225 125 137 11 301 11 11 230 127 139 18 304 238 137 1111 309 24 240 139 157 1818 310 26 158 311 2424 261 157 31 161 26 26 261 158 32 163 3131 262 161 164 168 45 267 175 45 270 168 45 45 176 48 274 175 Chapitre A Révisions Dans ce premier chapitre, nous n’allons pas programmer de jeu, mais revoir à la vitesse d’un grand 8 quelques exercices de base de programmation en Python. Si vous manipulez déjà avec aisance les tests, les boucles et les manipulations de listes, vous pouvez directement passer au chapitre suivant. 1 - Variables et fonctions La console Dans la zone console de Python, on peut saisir des instructions qui seront exécutées immédiatement. >>> Cette saisie se fait après les >>> que l’on appelle des 4>>> chevrons. Cela est très pratique pour tester en di- 6 rect les fonctions que vous aurez programmées. Voici >>> 9 un rappel de quelques opérations possibles sur les nombres : Opérateur +, -, * a/b 3+1 len("Python") (15%4) ** 2 Effet Respectivement la somme, la différence et le produit Quotient décimal de a par b a // b a%b Quotient entier de a par b Reste entier dans la division de a par b a ** b Résultat de ab ( et non ˆ) 11 C HAPITRE A ✍ Souvenez-vous que dans la console, vous pouvez utiliser les touches ↑ et ↓ pour vous déplacer dans l’historique de ce que vous avez déjà tapé au fur et à mesure. Python respecte les priorités mathématiques des opérations. En informatique, ce concept porte le nom de précédence des opérateurs. Dans le cas du langage Python, les précédences sont définies ainsi (dans l’ordre croissant) : +, *, /, //, % +x, -x ** Addition et soustraction Multiplication, division, quotient et reste Positif, négatif Exponentiation Les variables Pour programmer nos jeux, il faudra conserver un certain nombre d’informations en mémoire ; on utilise alors des variables pour stocker les valeurs. Le fait de donner un nom à un calcul, un texte ou un autre objet s’appelle l’affectation. En Algorithmique a←3 On lit ... a reçoit la valeur 3 En Python a = 3 Dans la console Python, lorsque l’on affecte une valeur à une variable, le contenu de celle-ci n’est pas affiché. Il faut taper le nom de la variable pour afficher sa valeur. Lors d’une affectation, la quantité à droite du signe = est évaluée et stockée dans la variable de gauche. L’écriture a = a + 1 a donc un sens en informatique. Cela signifie simplement que la variable a est augmentée de 1. Le symbole = joue donc un rôle d’affectation et non d’égalité. D’ailleurs, l’égalité b = a * 2 n’est plus vérifiée à la fin de notre exemple. 12 >>> >>> >>> 6 >>> 4 >>> 3 >>> >>> 4 >>> 6 a = 3 b = a * 2 b a + 1 a a = a + 1 a b R ÉVISIONS Voici donc un petit exercice permettant de jouer avec les nombres et les opérations avec Python... E X A1 Si n est un entier positif, relier les expressions Python aux expressions françaises correspondantes : ● Le chiffre des unités de n ● n + 1 ● n // 10 % 10 ● Le premier nombre impair qui suit n ● n // 2 * 2 + 1 ● n ** 2 ● Le nombre de dizaines de n ● n // 10 ● n % 10 ● n * 2 ● Le chiffre des dizaines de n ● Le carré de n ● Le double de n ● L’entier qui suit n Affectations simultanées Commençons par un premier exercice pour nous faire une idée. E X A2 On considère la suite d’instructions ci-contre : 1. Que contiennent les variables x et y à la fin ? 2. Constate-t-on le même phénomène pour des valeurs quelconques de x et y ? >>> >>> >>> >>> >>> x y x y x = = = = = 17 32 x + y x - y x - y 3. Écrire une suite d’instructions produisant le même effet, mais sans effectuer de calcul (on pourra utiliser une troisième variable temporaire). Python propose une manière simple d’affecter simultanément des variables en utilisant une virgule : les deux premières lignes de l’exercice précédent peuvent être réduites en x, y = 17, 32. Comme pour l’affectation classique, lorsque l’on procède à une affectation simultanée, les quantités de droite sont évaluées dans un premier temps, puis dans un second temps elles sont stockées dans les variables de gauche. Ainsi les 3 dernières lignes de l’exercice précédent peuvent tout simplement être transformées en : x, y = y, x. 13 C HAPITRE A E X A3 La carte vitale. En France, le numéro de sécurité sociale présent sur la carte vitale est un numéro ”signifiant” (c’est-à-dire non aléatoire) composé de 13 chiffres permettant d’identifier une personne, suivi d’une clé de contrôle de 2 chiffres pour déceler d’éventuelles erreurs de saisie. Image par Giesesamvitale sur Wikipédia. Par exemple, le premier chiffre indique le sexe (1 pour un homme, 2 pour une femme), les deux suivants l’année de naissance, suivis de deux chiffres pour indiquer le mois et ainsi de suite. Pour la carte fictive ci-dessus, le code est donc 2690549588157 et la clé 80. Cette clé correspond à la différence entre 97 et le reste de la division du code par 97. Réaliser une suite d’instructions qui, connaissant le code N à 13 chiffres, calcule la clé de sécurité. Les types d’objets Il existe de nombreux types d’objets en Python, nous apprendrons même dans ce livre à créer nos propres types ! En voici quelques-uns que nous allons régulièrement utiliser. Dans la console (ou plus tard dans un programme), on peut demander de quel type est une variable à l’aide de la fonction type comme le montre l’exemple de droite. >>> n = 5 >>> type(n) <class 'int'> >>> type(2 ** 2020) <class 'int'> >>> type(1/2) <class 'float'> >>> A=(2,3) >>> type(A) <class 'tuple'> >>> n < 1 False >>> type(n < 1) <class 'bool'> ● Les entiers : en Python, les entiers ont la particularité de ne pas être limités en taille (contrairement à la majorité des autres langages). ● Les flottants : pour simplifier, on peut considérer que ce sont les nombres décimaux ou même réels. En réalité ce n’est pas si simple, comme l’illustre le 3e exemple. >> type(3) <class 'int'> >>> type(3.0) <class 'float'> 14 Pour indiquer à Python qu’un nombre entier est un flottant, on ajoute .0 ou juste un point. R ÉVISIONS >>> a = 45 / 9 >>> a 5.0 >>> type(a) <class 'float'> Une division décimale renvoie toujours un flottant même si le résultat est entier. Enfin, il faut se méfier des ”erreurs d’arrondis” avec les nombres flottants même simples. Ici le résultat affiché est environ −5,6.10−17. Nous n’irons pas plus loin dans les explications mais il faut retenir qu’il peut se produire des erreurs d’arrondis sur les nombres flottants et qu’il est préférable d’utiliser des entiers, dans la mesure du possible bien entendu. >>> 1 - 0.8 - 0.2 -5.551115123125783e-17 ● Les tuples : ils représentent des couples ou triplets ou n-uplets. On peut récupérer chacune des composantes via leurs indices (ces derniers commencent à 0) ou directement par une affectation simultanée : >>> >>> >>> >>> 4 >>> 2 A = (4, 2, 5) x = A[0] y = A[1] x ou y >>> >>> >>> 4 >>> 2 A = (4, 2, 5) x, y, z = A x y ● Les booléens : c’est le dernier type dont nous parlerons rapidement. Ces variables ne peuvent prendre que deux valeurs : True ou False (Vrai ou Faux pour les non-anglophones !). Vous aurez sûrement remarqué la majuscule devant ces deux mots, pensez-y. >>> b = 2 < 3 >>> b True >>> type(b) <class 'bool'> Les fonctions Python propose déjà un certain nombre de fonctions déjà programmées (on les appelle les fonctions built-in ou fonctions natives en français). Nous avons déjà rencontré par exemple la fonction type : elle reçoit une variable en entrée (un objet) et renvoie son type (sa classe). Dans nos projets, nous utiliserons très souvent des fonctions : elles permettent de séparer une tâche à réaliser en tâches plus simples et donnent ainsi de la clarté au code. 15 C HAPITRE A ✍ L’exemple suivant calcule et renvoie la distance entre un personnage P(xP ; yP ) et un ennemi E(x √ E ; yE ) à l’aide de la formule issue du théorème de Pythagore, PE = (xE − xP )2 + (yE − yP )2 : À noter que l’on a dû ici importer en première ligne la fonction racine carrée (sqrt) depuis le module math : il s’agit d’une bibliothèque contenant un grand nombre de fonctions mathématiques. La déclaration d’une fonction est ainsi structurée : ● La déclaration commence par le mot clef def suivi du nom de la fonction (ici distance), puis des paramètres (ici P et E) et enfin deux points pour déclarer le bloc correspondant à la fonction. ● Juste en dessous, un texte placé entre des triples guillemets qui donne la description de la fonction. Ce texte appelé docstring est facultatif. S’il est renseigné, il s’affiche en info-bulle lorsque l’on tape le nom de la fonction, comme sur la capture précédente dans la console. Ceci est très pratique pour retrouver l’ordre des paramètres dès lors que l’on a créé un grand nombre de fonctions. ● L’ensemble des instructions à réaliser dans la fonction est indenté, c’est à dire décalé vers la droite pour structurer le programme. ● Enfin, dès que le programme rencontre l’instruction return, il quitte la fonction et renvoie la valeur indiquée. 16 R ÉVISIONS Quelques remarques supplémentaires sur les fonctions : ● Comme dans l’exemple précédent, une fonction peut dépendre de plusieurs paramètres, il suffit de les lister en les séparant par une virgule. ● Si vous définissez une fonction dont le nom existe déjà, seule la dernière définition est prise en compte en écrasant la précédente. ● Une fois le programme exécuté, les fonctions et les variables en cours sont accessibles dans la console comme on peut le voir sur la capture d’écran précédente. Cependant, gardez en tête que les paramètres de la fonction sont des variables muettes : les variables P et E ne sont connues qu’à l’intérieur même de la fonction et vous pouvez très bien appeler distance(A, (-1,4)) si A a été défini auparavant. ● De la même manière, toutes les nouvelles variables créées à l’intérieur de la fonction sont appelées variables locales et n’existent qu’à l’intérieur de la fonction. En particulier, elles n’interfèrent heureusement pas avec d’autres variables qui pourraient porter le même nom dans le reste du programme. Voici ce qui se passe si on demande la valeur de xP : elle n’est pas connue. ● Une fonction peut aussi renvoyer plusieurs valeurs, il suffit de les séparer par une virgule au moment du return. ● Enfin, une fonction peut à son tour en appeler une autre. L’exemple suivant propose de détecter une collision et renvoie un booléen indiquant si la distance PE est inférieure à 10. def touche(P,E) : return (distance(P,E) < 10) >>> touche((10,2),(12,1)) True 17 C HAPITRE A E X A4 On dispose à l’écran d’une grille de 8 × 8 cases, dont chaque case mesure 10 × 10 pixels. 1. Écrire une fonction xy2lc qui reçoit deux entiers x et y chacun entre 0 et 79 (correspondant par exemple aux coordonnées d’un clic de souris) et retourne un couple indiquant l’indice de la ligne et de la colonne correspondant. 2. Utiliser la fonction précédente pour écrire à nouveau une fonction xy2num qui retourne cette fois-ci le numéro de la case (entre 0 et 63). x = 32 0 1 2 3 4 5 6 7 0 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 0 1 y = 18 × 2 3 4 5 6 7 2 - Tests et boucles Les tests Jusque-là nous nous sommes contentés de réaliser des programmes très linéaires : les instructions s’enchaı̂nent toujours à l’identique. Nous allons donc ajouter des tests à nos programmes de manière à ne pas effectuer systématiquement toutes les instructions. 18 R ÉVISIONS Partons d’un exemple où l’on souhaite afficher un message en fonction de l’âge du joueur placé dans une variable age. C’est d’ailleurs l’occasion de présenter la fonction print qui affiche un texte dans la console (celui-ci sera placé entre guillemets). ● SI... ALORS age = ... if age == 0 : print("Peux-tu être sérieux 2 minutes ?") print("Tout est pardonné...") print("C'est parti !") ✍ Remarquez que le test d’égalité de deux variables se réalise avec un double égal == (le simple = étant réservé à l’affectation). Remarquez aussi que toute la partie indenté (décalée) est exécutée lorsque le test est validé. La dernière ligne quant à elle est exécutée dans tous les cas. Bien évidemment, remplacez les ... par différentes valeurs pour faire les tests. ● SI... ALORS...SINON : age = ... if age >= 18 : print("Vous êtes majeur") print("Bienvenue") else : print("Désolé, vous ne pouvez jouer à ce jeu") print("Fin du programme") ● SI... ALORS...SINON SI ... : Si on souhaite distinguer 3 messages différents selon que l’utilisateur est majeur, qu’il a entre 13 et 18 ans ou moins de 13 ans, on peut imbriquer les if dans les if ainsi : age = ... if age >= 18 : print("Vous êtes majeur") print("Bienvenue") else : if age >= 13 : print("Vos parents doivent valider votre inscription") else : print("Désolé, vous ne pouvez jouer à ce jeu") print("Fin du programme") 19 C HAPITRE A Cependant, il est plus efficace d’utiliser l’instruction elif (contraction de else if) pour effectuer une disjonction de cas : age = ... if age >= 18 : print("Vous êtes majeur") print("Bienvenue") elif age >= 13 : print("Vos parents doivent valider votre inscription") else : print("Désolé, vous ne pouvez jouer à ce jeu") print("Fin du programme") ✍ On peut enchaı̂ner autant d’instructions elif que nécessaire. L’utilisation du else final n’est pas obligatoire si les autres cas ne nécessitent aucune action supplémentaire. Dès qu’une des conditions est validée, les autres ne sont pas testées et le programme continue la suite des instructions. Pour les tests, on pourra utiliser les connecteurs logiques suivants : Opérateur A and B Signification Renvoie VRAI si les conditions A et B sont vérifiées. A or B Renvoie VRAI si au moins l’une des conditions A ou B est vérifiée. AˆB Renvoie VRAI si exactement une des conditions A ou B est vérifiée (mais pas les deux, on dit qu’il s’agit d’un "ou exclusif"). not A Renvoie le contraire de la véracité de la condition A. E X A5 Écrire une fonction qui reçoit le numéro du mois et renvoie le nombre de jours de celui-ci. 20 ÉVISIONS RRÉVISIONS sur les les tests tests :: Python Python ininDernière remarque sur ... les tests tests de de doubles doubles xx == ... terprète correctement les if 22 <= <= xx << 44 :: if cas, ilil traduit traduit A A << BB << C C inégalités. Dans ce cas, print("Oui") print("Oui") else :: C). Par Par exemple exemple le le propro- else par (A < B) et (B < C). print("Non") print("Non") affiche "Oui" "Oui" si si et et seulement seulement gramme suivant affiche si x ∈ [2; 4[. E X A6 Un jeu d’échecs contient contient 64 64 cases cases (8 (8××8) 8);; celles-ci celles-ci sont sont repérées repérées par pardes des couples d’entiers (l,c) représentant représentant respectivement respectivement la la ligne ligne etet lala colonne colonne(de (de00àà 7). On suppose d’autre part qu’il qu’il n’y n’y aa qu’une qu’une pièce pièce sur sur lele plateau. plateau. 1. Écrire une fonction tour tour qui qui renvoie renvoie un unbooléen booléenindiquant indiquantsisileledéplacement déplacement une autre autre case case A Aest est possible possible(les (lestours toursse sedéplacent déplacent d’une tour de la case D àà une verticalement ou ou horizontalement). horizontalement). uniquement verticalement 2. Même question pour un un fou. fou. 3. Même question pour un un cavalier. cavalier. Les boucles Commençons par présenter présenter les les boucles boucles conditionnelles conditionnellescar carce cesont sontcelcelles qui se comprennent le plus plus facilement facilement en en français français;; ce ce sont sontles lesboucles boucles TANT QUE .... FAIRE : ... Cette Cette instruction instruction sera sera traduite traduite par par while while en en Python. while cond : Exécute Exécute une une instruction instructionou ouun unbloc blocd’instructions d’instructionstant tant que la condition cond est est vérifiée. vérifiée. ● Comme pour le if, l’ensemble l’ensemble des desinstructions instructionsààexécuter exécuterest estindenté. indenté. ● La boucle peut donc ne ne jamais jamais être être exécutée exécutée si si d’entrée d’entréela lacondition condition n’est pas remplie. E X A7 Qu’affiche cet algorithme algorithme?? Le Le programmer programmer en en Python. Python. n ← 11 TANT TANT QUE QUE nn ⩽⩽ 100 100 FAIRE FAIRE :: nn ← ← 22××nn FIN TANT TANT QUE QUE AFFICHER AFFICHER n. n. 21 21 C HAPITRE A E X A8 ”Un homme met un couple de lapins dans un lieu isolé de tous les côtés par un mur. Combien de couples obtient-on en un an si chaque couple engendre tous les mois un nouveau couple à compter du troisième mois de son existence ?” Tel est le problème proposé par Fibonacci dans un problème récréatif publié en 1202 dans son ouvrage ”Liber Abaci”. Image wikipédia Pour tout l’exercice, on suppose qu’aucun lapin ne meurt. 1. Combien y a-t-il de couples le 3e et le 4e mois ? 2. Réaliser une fonction qui renvoie le nombre de couples au nième mois. 3. Écrire un programme qui trouve à partir de quel mois il y a plus de 100 couples de lapins. Boucle inconditionnelle POUR Parfois, on connaı̂t à l’avance combien de fois on souhaite répéter la boucle (par exemple tirer 3 cartes). Dans ce cas, on peut utiliser la commande for pour gagner du temps. Boucle TANT QUE : Boucle POUR : n = 0 while n < 5: ...instructions... n = n + 1 22 for n in range(5): ...instructions... R ÉVISIONS for var in range(debut,f in,pas): Les paramètres debut et pas sont optionnels. Cela permet de réaliser une boucle en faisant parcourir à la variable var tous les entiers : ● de l’intervalle [0; f in[ si un seul paramètre est renseigné ; ● de l’intervalle [debut; f in[ si 2 paramètres sont renseignés ; ● dans l’intervalle [debut; f in[ mais avec un pas de pas si les 3 paramètres sont renseignés. À noter que : ● Comme pour le while, l’instruction se termine par deux points et les instructions à répéter doivent être décalées. ● L’intervalle parcouru est semi-ouvert : for i in range (1,5), la variable i prend successivement les valeurs 1, 2, 3, 4. ● En cas d’impossibilité, la boucle n’est pas exécutée. Par exemple dans for n in range (100,110,-2): ● Contrairement à la boucle TANT QUE, la variable du compteur parcourt, quoiqu’il arrive, les valeurs demandées, on ne peut pas la modifier en cours de boucle. Le programme ci-dessous affiche bien les 10 premiers entiers de 0 à 9 : for i in range(10): print(i) i = i * 2 Avec la boucle POUR, on peut parcourir d’autres choses que des suites arithmétiques de nombres : for i in (1, 4, 5, 0, 9, 1): print(i) for i in ("a", "e", "i", "o", "u", "y"): print(i) for lettre in "Python": print(lettre) ● Des listes de nombres ou d’autres objets. On les met alors entre parenthèses ou entre cro chets. ● Une à une, les lettres d’un mot... 23 C HAPITRE A E X A9 Code ISBN - L’International Standard Book Number (ISBN) à 13 chiffres est un numéro international qui permet d’identifier, de manière unique, chaque édition de chaque livre publié, que son support soit numérique ou papier. Les 3 premiers chiffres sont 978, suivis de 9 chiffres. Le dernier chiffre est une clé obtenue par le programme de calcul suivant : ● Additionner les 12 chiffres du code ● Y ajouter le double de la somme des chiffres de rang pairs (2e , 4e , 6e , 8e , 10e et 12e ) ● Prendre le dernier chiffre de cette somme ● Calculer la différence entre 10 et le résultat obtenu. ● La clé est le chiffre des unités de ce dernier résultat. 1. Vérifier sans programme la clé sur l’exemple. 2. Écrire une fonction qui vérifie qu’un code ISBN est correct. 3 - Chaı̂nes de caractères Même si vous n’avez pas dans l’idée de programmer un jeu de mots croisés, vous verrez qu’il est parfois utile d’avoir quelques notions sur les chaı̂nes de caractères. Une chaı̂ne de caractères est donc, comme son nom peut le laisser penser, une succession de caractères. Un caractère pouvant être une lettre, mais aussi un chiffre, <, @, *, §, ... Pour simplifier on peut considérer qu’un caractère est un symbole. Dans une chaı̂ne de caractères, chaque caractère est indicé, Python commence la numérotation à 0. Une chaı̂ne de caractères U n caractère U d’indice 0 e x e m p l e . caractère espace d’indice 2 caractère . d’indice 10 Dans un programme, il faut donc différencier le nom de la variable qui contient une chaı̂ne de son contenu. Pour cela en Python, pour signifier qu’il s’agit d’une chaı̂ne de caractères, ou d’un caractère, on met le texte entre guillemets ’ainsi’ ou "ainsi". 24 R ÉVISIONS Fonctions sur les chaı̂nes Il existe de très nombreuses fonctions pour travailler sur les chaı̂nes de caractères, nous n’en donnerons que quelques-unes : Commande len(ch) Effet Renvoie la longueur de la chaı̂ne ch. ch[i] Renvoie le caractère situé au rang i dans la chaı̂ne ch. Attention : ● La première lettre est au rang 0. ● On ne peut pas modifier cette lettre (ch[2]=’A’ renvoie une erreur). mots[debut:f in] Renvoie la partie de chaı̂ne mots comprise entre le caractère à la position debut (inclus) et celui à la position f in (exclu). mots1+mots2 Colle deux chaı̂nes mots1 et mots2. Le verbe utilisé est ≪ concaténer ≫. Recopie nb fois la chaı̂ne mots. mots*nb str(nb) int(ch) for l in mot : Renvoie une chaı̂ne de caractères contenant l’écriture décimale du nombre nb. Transforme la chaı̂ne ch en entier. Réalise une boucle dans laquelle la variable l va prendre tour à tour chaque lettre de mot. E X A10 Un palindrome est un mot qui peut se lire aussi bien à l’endroit qu’à l’envers comme ”kayak”. Écrire une fonction qui reçoit en entrée une chaı̂ne de caractères et renvoie un booléen affichant si le texte est un palindrome. E X A11 Écrire une fonction qui affiche les initiales d’une personne. Par exemple : ● initiales("Vincent Maille") → ’VM’ ● initiales("Jean-Jacques Goldman") → ’JJG’ ● initiales("Jean-Claude Van Damme") → ’JCVD’ E X A12 Écrire une fonction qui compte le nombre de mots dans une phrase simple (on ne tient pas compte des mots composés, des apostrophes...). 25 C HAPITRE A E X A13 Écrire une fonction qui renvoie la lettre qui apparaı̂t le plus souvent dans un texte donné. E X A14 Temps passé sur un jeu... 1. Écrire une fonction str2min qui transforme une chaı̂ne de caractères type h:mm ou hh:mm en nombre de minutes que cela représente. Par exemple str2min(’2:12’) renvoie 132. 2. Écrire une fonction min2str ayant l’effet inverse. 3. Créer une troisième qui calcule le temps de jeu entre deux instants t1 et t2 (maximum 24H !) donnés sous forme hh:mm 4 - Les listes et matrices Les listes Une liste peut être vue comme un tableau dont le nombre de cases n’est pas limité. Le terme illimité est ici exagéré dans la mesure où cela dépend néanmoins de votre machine. Sur mon ordinateur portable, j’ai pu entrer dans une liste tous les entiers de 1 à un million (mais pas jusqu’à un milliard) ce qui devrait largement nous suffire ! Comme pour les chaı̂nes de caractères, dans une liste, le premier élément a pour indice 0. Création et manipulation d’une liste : Instruction Effet L=[] Crée une liste vide L. L=[e1,e2,...] Crée une liste L contenant les éléments e1, e2, ... L[i] L[i]=elem len(L) Renvoie le nombre d’éléments de la liste L. e in L Teste si l’élément e est dans la liste L et renvoie True ou False (VRAI ou FAUX). for e in L 26 Renvoie l’élément d’indice i de la liste (indice 0 pour le premier). Stocke elem dans la liste L au rang i. Réalise une boucle dans laquelle la variable e prend tour à tour les valeurs des éléments de la liste L. R ÉVISIONS Opérations qui agissent en modifiant une liste : Méthode Effet L.append(e) Ajoute l’élément e à la fin de la liste L. L.insert(j ,e) L.remove(e) L.pop(i) Insère l’élément e au rang j de la liste L. Supprime la première occurrence de l’élément e dans la liste L. Attention, si e n’est pas présent dans la liste, une erreur se produit. Pensez donc à vérifier que e est présent avec l’instruction in. Supprime l’élément d’indice i de la liste L. Fonctions qui agissent sur les listes sans les modifier : Fonction Effet min(L), max(L) Renvoie le plus petit (le plus grand) élément de la liste L. sorted(L) Renvoie une nouvelle liste contenant les éléments ordonnés de la liste L. choice(L) Choisit au hasard un élément de la liste L (nécessite le module random). E X A15 Dire ce qu’affichent les programmes suivants (ou s’ils déclenchent une erreur) avant de les taper pour vérifier... 1. 2. 3. P = [2, 4, 6, 8] print(P[2]) P.append(3) print(P[2]) P.remove(3) print(P) print(len(P)) f = [] for x in range(1, 6): f.append(3 - (x-3) ** 2) print(min(f), max(f)) L = [] for i in range(6): L.append(i**2) if i in L: L.remove(i) print(L) 4. L = [1, 1] for i in range(6): print(L[0], end=' ; ') L.append(L[0] + L[1]) L.pop(0) 27 C HAPITRE A E X A16 Une vache qui pisse dans un tonneau... On cherche à simuler ce jeu d’enfant. Écrire une fonction qui reçoit une liste de prénoms, l’indice du joueur qui commence et le nombre de syllabes de la ritournelle. Elle affiche les personnes qui perdent une à une et renvoie la personne qui reste à la fin. Par exemple avec les prénoms Hugo, Lily, Mathilda, Pierre et Léa. Si Pierre commence et que la ritournelle comporte 16 syllabes, on obtient le tableau suivant où l’on a indiqué en gras celui par lequel on commence et souligné la personne éliminée : ● Hugo Lily Mathilda Pierre ● Hugo Lily Mathilda Léa Mathilda sort ● Hugo Lily Léa Léa sort ● Hugo Lily Lily sort Léa Pierre sort ● Hugo a gagné ! E X A17 On cherche à simuler le choix d’une main dans un jeu de poker simplifié en piochant successivement, et sans remise, 5 cartes dans un jeu de 52 cartes. On veut alors déterminer si on a un full, c’est-à-dire une main comportant 3 cartes de même valeur et deux autres cartes de même valeur (mais distinctes de la première valeur, sinon, vous avez triché !). 1. Créer une fonction qui génère et renvoie une liste jeu dont les éléments représentent les 52 cartes du jeu. Pour notre problème, inutile de coder l’information sur la couleur de la carte. On créera juste une liste contenant 4 cartes de chaque valeur ’As’, ’2’, ’3’, ..., ’10’, ’Valet’, ’Dame’, ’Roi’. 2. Écrire une fonction main qui reçoit une liste correspondant au jeu de cartes et retourne une main de 5 cartes choisies au hasard. 3. Écrire une fonction full qui indique par un booléen si la main, donnée sous forme de liste en argument, est un full ou non. Les listes sont des alias Les listes présentent l’avantage de pouvoir stocker un très grand nombre de données, mais cela a un prix...Lorsque l’on écrit L=[1,2,3,4], la variable L ne contient pas le contenu de la liste comme c’est le cas pour un nombre, mais l’emplacement mémoire où se trouve la liste. On dit que la variable L est un pointeur ou plus exactement un alias en Python. 28 R ÉVISIONS L’exemple qui suit illustre le problème : lorsque l’on saisit en Python L2=L1, on ne crée pas une copie de la liste, mais une copie des adresses mémoires, ainsi lorsque l’on modifie L2, la liste L1 est aussi modifiée, puisqu’il s’agit d’une seule et même liste. Pour éviter ce problème, on peut créer un double de la liste en tapant L2=L1[:] qui recopie le contenu de la liste L1 dans L2, lui affectant ainsi une adresse mémoire différente de L1. L1 = ["Bleu", "Blanc", "Rouge"] L2 = L1 L2[1] = "Vert" print("L1=", L1) print("L2=", L2) L1 = ["Bleu", "Blanc", "Rouge"] L2 = L1[:] L2[1] = "Vert" print("L1=", L1) print("L2=", L2) Affiche : Affiche : L1= [’Bleu’,’Vert’,’Rouge’] L2= [’Bleu’,’Vert’,’Rouge’] L1= [’Bleu’,’Blanc’,’Rouge’] L2= [’Bleu’,’Vert’,’Rouge’] Capture réalisée via le site http ://pythontutor.com/ Autre subtilité liée aux alias : si vous avez utilisé le code ci-contre dans l’exercice précédent, bien que la liste j ne soit pas renvoyée, celle-ci est modifiée dans le reste du programme ! def main(j) : m = [] for i in range(5) : m.append(choice(j)) j.remove(m[-1]) return m E X A18 [Mastermind] Le jeu de Mastermind est un jeu dans lequel il faut trouver une combinaison de 5 pions, chaque pion pouvant être choisi parmi 8 couleurs (ici représentées par les nombres 0 à 7). Pour chaque proposition du joueur, le maı̂tre du jeu lui répond le nombre de jetons bien placés et le nombre de jetons mal placés. Écrire une fonction qui reçoit 2 listes : la proposition et la réponse et renvoie sous forme d’un couple le nombre de jetons bien placés et mal placés. Par exemple test([1,1,2,1,3],[1,7,3,2,1]) renvoie (1,3). 29 C HAPITRE A Matrices Terminons ces petits rappels avec les listes de listes appelées parfois liste2D ou matrices : La liste M définie ci-contre comporte 3 éléments : M=[[13, M[0] : [13, 5, 1, 7], M[1] : [11, 9, [11, 4, 7] et M[2] : [12, 4, 5, 7]. Chaque [12, élément étant à son tour une liste. 5, 9, 4, 1, 4, 5, 7], 7], 7]] On peut ainsi : ● Connaı̂tre le nombre de lignes : len(M) ● Connaı̂tre le nombre de colonnes : len(M[0]) ● Accéder à l’élément de la ligne l et colonne c : M[l][c]. Ce dernier étant accessible en lecture / écriture (il peut être modifié). Voici quelques exercices, ceux qui en veulent davantage sur ce thème pourront se reporter au chapitre N du tome 1. E X A19 Le but du jeu Roller Splat est de repeindre un plateau de jeu à l’aide d’une bille de peinture. Lorsque l’on choisit une direction, la bille avance jusqu’à toucher un mur. Dans cet exercice, on modélise le plateau de jeu par une matrice avec la convention suivante : 0 case vide, 1 : mur, 2 : case peinte. Par exemple, le plateau ci-contre est modélisé par la déclaration : plateau = [[1, [1, [1, [1, [1, [1, [1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 0, 0, 2, 0, 0, 1, 1, 0, 1, 2, 1, 0, 1, 1, 0, 1, 2, 0, 0, 1, 1], 1], 1], 1], 1], 1], 1]] À noter enfin que l’on peut repasser sur une case peinte et que le plateau de jeu est toujours entouré de murs. 1. Écrire une fonction gagne qui reçoit un plateau et renvoie un booléen indiquant si la partie est gagnée ou non. 2. Écrire une fonction haut qui reçoit un couple (l, c) indiquant la position de la bille et le plateau et renvoie la nouvelle position de la bille, une fois que l’on a appuyé sur le flèche du haut. Le plateau est colorié en conséquence. 30 ÉVISIONS RRÉVISIONS E XX A20 [Puissance 4] Pour ce jeu bien connu, le but est d’aligner 4 pions pions d’une même couleur dans une des 8 directions pospossibles. Comme pour le jeu précédent, on modélise modélise le le plateau par une matrice composée de 0 (la case case est est vide), de 1 (pion du joueur 1) et de 2. 1. Écrire une fonction gagne dir qui reçoit reçoit 33 paramètres paramètres :: lele plateau, plateau, une une case sous la forme d’un couple (l, c) et et une une direction direction ooùù regarder regarder sous sous lala forme d’un couple (Δl, Δc). Elle renvoie renvoie 11 si si le le joueur joueur 11 gagne gagne àà partir partir de cette case dans cette direction, 2 si c’est c’est le le joueur joueur 22 et et 00 dans dans les les autres autres cas. Par exemple gagne dir(p, (2,6),(1,-1)) (2,6),(1,-1))va va tester testersisiles lescases cases (2,6), (3,5), (4,4) et (5,3) sont de la même même couleur couleur sur sur le le plateau plateau p. p. 2. Écrire une fonction gagne qui reçoit le le plateau plateau et et renvoie renvoie qui qui aa gagné gagné (ou (ou 0 si personne n’a encore gagné). 5 - Aide pour les exercices Aide pour l’exercice A1 Petit rappel sur un un exemple exemple :: pour pour le le nombre nombre123, 123, le chiffre des dizaines est 2 alors que le nombre nombre de de dizaines dizaines est est 12. 12. Aide pour l’exercice A4 Prenez des exemples exemples et et pensez pensez àà la la division divisioneuclieuclidienne. Aide pour l’exercice A7 Il suffit d’exécuter d’exécuter ce ce programme programme àà la la main main pour pour avoir la réponse. Aide pour l’exercice A8 Pour la deuxième deuxième question, question, pensez pensezqu’il qu’il faut fautconconserver en mémoire le nombre de couples des des deux deux mois mois précédents précédents pour pour trouver le nombre de couples du mois en cours. cours. Aide pour l’exercice A9 On rappelle que pour pour un un nombre nombre xxdonné, donné,on onpeut peut trouver le chiffre des unités par le calcul du du reste reste de de la la division division par par 10. 10. Aide pour l’exercice A11 On peut parcourir parcourir le le mot mot et et garder garder en en mémoire mémoire toutes les lettres qui se trouvent au début, début, après après un un trait trait d’union d’union ou ou après après un espace. 31 31 C HAPITRE A Aide pour l’exercice A12 Le nombre d’espaces donne une bonne information sur le nombre de mots... Aide pour l’exercice A13 Avec l’exercice précédent, on a vu comment compter le nombre d’espaces. On peut facilement généraliser cela à n’importe quel caractère et créer une fonction à cet effet. La difficulté ensuite est de conserver en mémoire le caractère qui apparaı̂t le plus fréquemment. Il faudra 2 variables, l’une pour garder en mémoire la lettre, une autre pour garder le nombre de fois où celle-ci apparaı̂t afin de pouvoir comparer avec les nombre d’apparitions des suivantes. Aide pour l’exercice A16 L’idée la plus simple est d’apprendre à l’ordinateur à jouer réellement : on avance de case en case et lorsque l’on arrive au bout de la liste, on revient au début... Attention, quand un joueur est retiré, pensez à reculer d’une case, sinon cela aura le même effet qu’avancer de 2 cases au tour suivant... Aide pour l’exercice A17 2. Pensez à ôter du jeu la carte tirée... 3. Pour tester si on a un full ou non, il peut être intéressant d’ordonner la liste pour ensuite tester si elle est de la forme AAABB ou AABBB. Aide pour l’exercice A18 On peut par exemple chercher ceux qui sont bien placés et les supprimer des listes pour ne pas les recompter dans les mal placés. 6 - Solutions des exercices Retour sur l’exercice A1 ● Le chiffre des unités de n : n % 10 ● Le premier nombre impair qui suit n : n // 2 * 2 + 1 ● Le chiffre des dizaines n : n // 10 % 10 ● Le nombre de dizaines de n : n // 10 ● Le carré de n : n ** 2 ● Le double de n : n * 2 ● L’entier qui suit n : n + 1 32 R ÉVISIONS R ÉVISIONS Retour sur l’exercice A2 Retour sur l’exercice A2 1. À la fin, c’est x qui vaut 32 et y 17. 1. la fin, c’estvaleurs x qui vaut 17.départ, le programme inverse ces 2. À Pour toutes de x32etetyyde deux valeurs. En effet, notons x y0 leslevaleurs de départ de xces et 0 et 2. Pour toutes valeurs de x et y de départ, programme inverse y : valeurs. En effet, notons x et y les valeurs de départ de x et deux Ligne 3Ligne 43 54 Instruction x = x + y Instruction y x = = x x + y y x = x y y = x - y 0 x x x0 + y0 x x00 + + yy00 x x0 + + yy0 − x0 = y0 0 0 y y0 y x y00 + y0 − y0 = x0 x x0 + y − y = x 0 0 0 0 5 x = x - y x0 + y0 − x0 = y0 x0 3. Autre alternative pour inverser deux variables : t = x alternative pour inverser deux variables : 3. Autre x t y x y = = = = y x t y t Retour sur l’exercice A3 Il suffit de reprendre les opérations indiquées : Retour l’exercice A3 Il suffit de reprendre les opérations indiquées : >>> N = sur 2690549588157 >>> >>> >>> 80 >>> 80 cle = 97 - N % 97 N = 2690549588157 cle cle = 97 - N % 97 cle Retour sur l’exercice A4 Retour sur l’exercice A4 1. Pour commencer, on peut remarquer que l’abscisse x va déterminer numéro de la colonne l’ordonnée celui de la ligne. D’autre 1. le Pour commencer, on peutet remarquer quey l’abscisse x va déterminer part, la colonne 0 est composée des abscisses de 0 à 9, la colonne 1 le numéro de la colonne et l’ordonnée y celui de la ligne. D’autre des de 0 10est à 19 etc. Ainsides uneabscisses simple division par 10 part,abscisses la colonne composée de 0 à 9,entière la colonne 1 permet de répondre à la question. Voici le code : des abscisses de 10 à 19 etc. Ainsi une simple division entière par 10 permet de répondre à la question. Voici le code : def xy2lc(x, y) : def return y // 10, x // 10 xy2lc(x, y) : return y // 10, x // 10 2. Pour récupérer le numéro de la case, puisqu’une ligne comporte 8 il suffit d’effectuer le de calcul : ligne × 8 + colonne : comporte 8 2. cases, Pour récupérer le numéro la case, puisqu’une ligne def xy2num(x,y) : cases, il suffit d’effectuer le calcul : ligne × 8 + colonne : l, c = xy2lc(x, y) def xy2num(x,y) return l*8 +:c l, c = xy2lc(x, y) return l*8 + c 33 33 Solutions des exercices 0 y: C HAPITRE A Retour sur l’exercice A5 Les mois qui comportent 31 jours sont : 1-Janvier, 3-Mars, 5-Mai, 7-Juillet, 8-Août, 10-Octobre, 12-Décembre. Voici donc une première idée : def nbjours(n) : if n==1 or n==3 or n==5 or n==7 or n==8 or n==10 or n==12: return 31 elif n == 2 : return 28 # Oui, ça peut être bissextile, je sais... else : return 30 On pourrait simplifier le premier test à l’aide de l’instruction in en : if n in (1,3,5,7,8,10,12) : ou, si vous aimez les maths : if (n + n//8) % 2 == 1 : Retour sur l’exercice A6 1. Il suffit que les cases A et D se situent à la même ligne ou à la même colonne : def tour(D, A) : lD, cD = D lA, cA = A return (lD == lA) or (cD == cA) 2. Pour le fou, c’est la même idée, il faut que le décalage de ligne (Δl) soit égal au décalage de colonne (Δc) éventuellement avec un changement de signe. Avec la fonction valeur absolue abs, on peut réaliser la fonction : def fou(D, lD, cD lA, cA return A) : = D = A abs(lD-lA) == abs(cD-cA) 3. Pour le cavalier, c’est un peu plus compliqué. Il faut que le déplacement absolu en ligne soit égal à 2 et que celui en colonne fasse 1 ou le contraire. Commençons par calculer ces quantités : def cavalier(D, A) : Dl = abs(A[0]-D[0]) Dc = abs(A[1]-D[1]) 34 ÉVISIONS RRÉVISIONS tester :: Ensuite on peut tester 2) and and (Dl (Dl ==1)) ==1)) or or ((Dc ((Dc == == 1) 1) and and (Dl (Dl ==2)) ==2)) return ((Dc == 2) simplifier ce ce test, test, on on peut peututiliser utiliserun uncouple couplede decouples couples: : Si on souhaite simplifier in ((2,1), ((2,1), (1,2)) (1,2)) return (Dc, Dl) in tenté de de tester tester si si la la somme sommeΔΔll+Δc +Δcvaut vaut3,3,mais maisalors alors On pourrait être tenté les déplacements déplacementsde de33cases casesen enligne lignedroite... droite...Une Une on accepterait aussi les fonctionne, est est de de regarder regarder sisiΔl Δl22++Δc autre idée qui, elle, fonctionne, Δc22==55: :cette cette fois, pour que la somme somme de de deux deux carrés carrés d’entiers d’entiers naturels naturels fasse fasse 5,5, l’un doit faire 2 et l’autre l’autre 11 :: Dl** == 55 return Dc**2 + Dl **22 == Retour sur l’exercice A7 n prend prend successivement successivement les les valeurs valeurs::11--22--44--88-16 - 32 - 64 - 128 (qui est la première première àà dépasser dépasser 100). 100). Ainsi Ainsile leprogramme programme affiche 128. En Python, on obtient obtient :: n = 1 while n <= 100: n = 2 * n print(n) Retour sur l’exercice A8 1. ● Le premier mois, mois, ilil yy aa un un couple couple de de jeunes jeuneslapins. lapins. ● Le deuxième mois, mois, ilil yy aa un un couple couple reproducteur. reproducteur. ● Le troisième mois, mois, ilil yy aa un un couple couple de dejeunes jeuneset etun uncouple couplereproreproducteur, soit 2 couples. couples. ● Le quatrième mois, mois, ilil yy aa 22 couples couples reproducteurs reproducteurset et11couple couplede de jeunes, soit 3 couples. couples. 2. En gardant la même idée idée que que dans dans la lapremière premièrequestion, question,on ons’aperçoit s’aperçoit qu’il faut 2 variables, la la première première pour pour dénombrer dénombrerles lesjeunes jeunescouples couples de lapins, la seconde pour pour dénombrer dénombrer les les couples couples de de lapins lapins en enâge âge de se reproduire. 35 35 Solutions des exercices C C HAPITRE HAPITRE A A def couples(n) : def couples(n) : jeunes = 1 jeunes = 1 reproducteurs = 0 reproducteurs = 0 mois = 1 mois = 1 while mois < n: while mois < n: jeunes, reproducteurs = reproducteurs, reproducteurs + jeunes jeunes, reproducteurs = reproducteurs, reproducteurs + jeunes mois = mois + 1 mois = mois + 1 return jeunes+reproducteurs return jeunes+reproducteurs 3. 3. Pour Pour déterminer déterminer le le nombre nombre nécessaire nécessaire de de mois mois permettant permettant de de dépasser 100 couples, on peut utiliser la fonction précédente dépasser 100 couples, on peut utiliser la fonction précédente :: mois = 1 mois = 1 while couples(mois) < 100 : while couples(mois) < 100 : mois = mois + 1 mois = mois + 1 print(mois) print(mois) En En réalité réalité il il aurait aurait été été plus plus efficace efficace de de modifier modifier la la fonction fonction précédente précédente pour ne pas avoir à recalculer tous les termes à chaque pour ne pas avoir à recalculer tous les termes à chaque mois mois :: jeunes = 1 jeunes = 1 reproducteurs = 0 reproducteurs = 0 mois = 1 mois = 1 while jeunes + reproducteurs < 100: while jeunes + reproducteurs < 100: jeunes, reproducteurs = reproducteurs, reproducteurs + jeunes jeunes, reproducteurs = reproducteurs, reproducteurs + jeunes mois = mois + 1 mois = mois + 1 print("A partir du", mois, "ième mois, il y a plus de 100 couples") print("A partir du", mois, "ième mois, il y a plus de 100 couples") Retour Retour sur sur l’exercice l’exercice A9 A9 1. 1. Pour Pour l’exemple l’exemple donné, donné, on on peut peut en en effet effet vérifier vérifier :: 9 9+ +7 7+ +8 8+ +2 2+ +7 7+ +2 2+ + 9+8+7+7+8+2+2×(7+2+2+8+7+2) = 132, le dernier chiffre étant 9+8+7+7+8+2+2×(7+2+2+8+7+2) = 132, le dernier chiffre étant 2, 2, on on calcule calcule 10 10 − −2 2= =8 8 qui qui est est la la clé clé puisque puisque le le résultat résultat ne ne comporte comporte qu’un chiffre. qu’un chiffre. 2. 2. On On peut peut commencer commencer par par remarquer remarquer qu’en qu’en réalité, réalité, les les chiffres chiffres placés placés àà un rang pair sont comptés 3 fois alors que ceux situés un rang pair sont comptés 3 fois alors que ceux situés àà un un rang rang impair ne le sont qu’une fois. On va donc ajouter les chiffres impair ne le sont qu’une fois. On va donc ajouter les chiffres des des unités unités un un àà un un en en les les comptant comptant une une fois fois ou ou trois trois selon selon leur leur place place dans dans le le code. code. À À chaque chaque passage passage dans dans la la boucle, boucle, on on divisera divisera par par 10 10 (quotient entier) le code de manière à retirer le dernier chiffre. (quotient entier) le code de manière à retirer le dernier chiffre. Sur Sur notre notre exemple, exemple, cela cela donnerait donnerait :: 36 36 R ÉVISIONS ÉVISIONS R rang rang %2 %2 00 11 00 11 00 11 00 11 00 11 00 11 chiffres chiffres 978272987782 978272987782 97827298778 97827298778 9782729877 9782729877 978272987 978272987 97827298 97827298 9782729 9782729 978272 978272 97827 97827 9782 9782 978 978 97 97 99 cle2 cle2 0 + 0+3 3× ×2 2= =6 6 6 + 8 = 14 6 + 8 = 14 14 14 + +3 3× ×7 7= = 35 35 35 + +7 7= = 42 42 35 42 42 + +3 3× ×8 8= = 66 66 66 + 9 = 66 + 9 = 75 75 75 + +3 3× ×2 2= = 81 81 75 81 + 7 = 88 81 + 7 = 88 88 + +3 3× ×2 2= = 94 94 88 94 + 8 = 102 94 + 8 = 102 102 102 + +3 3× ×7 7= = 123 123 123 + +9 9= = 132 132 123 Solutions des exercices rang rang 12 12 11 11 10 10 99 88 77 66 55 44 33 22 11 Que Que l’on l’on peut peut programmer programmer ainsi ainsi :: def def ISBN(n) ISBN(n) : : """ En En entrée entrée le le code code est est donné donné sous sous forme forme d'un d'un entier entier """ """ """ chiffres chiffres = = n n // // 10 10 cle1 cle1 = = n n % % 10 10 cle2 = = 0 0 cle2 for for rang rang in in range(12): range(12): if if rang rang % % 2 2 == == 0: 0: # # Rang Rang pair pair ? ? cle2 = cle2 + 3 (chiffres * cle2 = cle2 + 3 * (chiffres % % 10) 10) else: else: cle2 = = cle2 cle2 + + chiffres chiffres % % 10 10 cle2 chiffres chiffres = = chiffres chiffres // // 10 10 cle2 cle2 = = (10 (10 - (cle2 (cle2 % % 10) 10) % % 10) 10) return cle1 cle1 == == cle2 cle2 return Retour Retour sur sur l’exercice l’exercice A10 A10 Le Le plus plus simple simple est est ssûrement ûrement de de parcourir parcourir le le mot mot jusqu’au milieu milieu et et de de regarder regarder si si le le caractère caractère présent présent est est le le même même que que le le cacajusqu’au ractère miroir. On utilise alors à bon escient le fait que dès que l’instruction ractère miroir. On utilise alors à bon escient le fait que dès que l’instruction return return est est rencontrée, rencontrée, on on sort sort de de la la fonction fonction :: def palindrome(mot) : def palindrome(mot) : L L = = len(mot) len(mot) for for i i in in range(L//2) range(L//2) : : if mot[i] if mot[i] != != mot[L-1-i] mot[L-1-i] : : return return False False return True True return Retour Retour sur sur l’exercice l’exercice A11 A11 Voici Voici une une solution solution qui qui parcourt parcourt le le nom nom àà partir partir de la la lettre lettre d’indice d’indice 11 et et regarde regarde àà chaque chaque fois fois si si le le caractère caractère précédent précédent est est de un espace ou un trait d’union. un espace ou un trait d’union. 37 37 C HAPITRE A def initiales(nom) : reponse = nom[0] # Il faut au moins prendre la première lettre for i in range(1, len(nom)): if nom[i-1] == ' ' or nom[i-1] == '-': reponse = reponse + nom[i] return reponse Retour sur l’exercice A12 Voici une solution qui compte le nombre d’espaces (+1), ce qui doit être assez proche du nombre de mots : def nb_mots(phrase) : nbM = 1 for lettre in phrase: if lettre == ' ': nbM = nbM + 1 return nbM Retour sur l’exercice A13 En s’appuyant sur la remarque donnée en indication, on utilise une variable fois qui compte combien de fois le caractère lettre apparaı̂t dans la phrase. À chaque fois que l’on obtient un meilleur score, on mémorise ce nombre dans foisMaxi ainsi que la lettre en question dans lettreMaxi : def nb_fois(phrase, car) : nbM = 1 for lettre in phrase: if lettre == car : nbM = nbM + 1 return nbM def lettre_maxi(phrase) : foisMaxi = 0 for lettre in phrase: if lettre != " " : # On ne compte pas les espaces fois = nb_fois(phrase, lettre) if fois > foisMaxi: lettreMaxi = lettre foisMaxi = fois return lettreMaxi 38 R ÉVISIONS Retour sur l’exercice A14 1. Pour cette fonction, il n’y a pas de difficulté, il faut simplement gérer les cas où l’heure comporte 1 ou 2 chiffres : 2. Pour le problème inverse, pas de difficulté non plus, il faut juste faire attention à ajouter un ”0” si le nombre de minutes ne comporte qu’un chiffre : def min2str(minutes): h = minutes // 60 m = minutes % 60 if m < 10: return str(h)+":0"+str(m) else: return str(h)+":"+str(m) Remarque : au lieu d’écrire en deux lignes h = minutes // 60 et m = minutes % 60, on peut écrire en une ligne à l’aide de la fonction divmod de cette manière : h, m = divmod(minutes, 60). 3. Pour la dernière fonction, on utilise les deux fonctions précédemment créées. Si l’heure d’arrivée est inférieure à l’heure de départ, il faut ajouter une journée : def duree(t1, t2) : temps = str2min(t2) - str2min(t1) if temps <= 0: temps = temps + 24*60 return min2str(temps) Retour sur l’exercice A15 1. instruction P = [2, 4, 6, 8] print(P[2]) P.append(3) print(P[2]) P.remove(3) print(P) print(len(P)) affichage 6 6 [2,4,6,8] 4 P [2,4,6,8] 6 [2,4, ,8] [2,4,6,8,2] 6 [2,4, ,8,3] [2,4,6,8] [2,4,6,8] [2,4,6,8] 39 Solutions des exercices def str2min(txt): if txt[1] == ':': # Si c'est au format h:mm return int(txt[0])*60 + int(txt[2]+txt[3]) else: return int(txt[0]+txt[1])*60 + int(txt[3]+txt[4]) C HAPITRE A C HAPITRE A x 2. 2. 3. 3. 4. 4. 40 40 1 x 2 3 1 4 2 5 3 4 5 f [] [-1] f [-1,2] [] [-1,2,3] [-1] [-1,2,3,2] [-1,2] [-1,2,3,2,-1] [-1,2,3] [-1,2,3,2] [-1,2,3,2,-1] 3 2 3 1 2 −1 1 −1 1 2 3 4 5 1 2 3 4 5 −2 −1 −1 Finalement, le programme affiche -1 (pour le minimum) et 3 pour le −2 maximum. Remarquons qu’ici ces 2 valeurs sont bien le maximum et le minimum de f sur l’intervalle [1,5] car les extrémums sont atFinalement, le programme affiche -1 (pour le minimum) et 3 pour le teints pour des valeurs entières ce qui n’est évidemment pas toujours maximum. Remarquons qu’ici ces 2 valeurs sont bien le maximum le cas ... et le minimum de f sur l’intervalle [1,5] car les extrémums sont ati Lpour des valeurs i ∈ L? entières L teints ce qui n’est évidemment pas toujours 0 [0] OUI [] le cas ... 1i [1] OUI L i ∈ L? [] L 2 [4] NON [4] 0 [0] OUI [] 3 [4, 9] NON [4,9] 1 [1] OUI [] 4 [4, 9, 16] OUI [9, 16] 2 [4] NON [4] 5 [9, 16, 25] NON [9, 16, 25] 3 [4, 9] NON [4,9] À4la fin la boucle, affiche [9,16,25]. [4, de 9, 16] OUIle programme [9, 16] 5 [9, 16, 25] NON [9, 16, 25] print... L.append... L.pop...affiche [9,16,25]. Ài la fin de la boucle, le programme 1 0 1 [,1,2] [1,2] 1 1i print... 1 [,2,3] [2,3] L.append... L.pop... 2 2 2 [,3,5] [3,5] 1 0 1 [,1,2] [1,2] 3 3 3 [,5,8] [5,8] 1 1 1 [,2,3] [2,3] 5 4 5 [,8,13] [8,13] 2 2 2 [,3,5] [3,5] 8 5 8 [,13,21] [13,21] 3 3 3 [,5,8] [5,8] Le affiche ”1 ; 1 ; [8,13] 2 ; 3 ; 5 ; 8 ;” qui sont en réalité les pre5 4 programme 5 [,8,13] miers termes de[,13,21] 8la suite de Fibonacci 5 8 [13,21] définie par u0 = u1 = 1 et pour n ∈ N, un+2 = un+1 +un que l’on retrouve dans de nombreux problèmes Le programme affiche ”1 ; 1 ; 2 ; 3 ; 5 ; 8 ;” qui sont en réalité les pred’évolution (souvenez-vous des lapins !) miers termes de la suite de Fibonacci définie par u0 = u1 = 1 et pour n ∈ N, un+2 = un+1 +un que l’on retrouve dans de nombreux problèmes d’évolution (souvenez-vous des lapins !) R ÉVISIONS Retour sur l’exercice A16 Cette fonction convient (attention aux indices !) : def vache(L, pos, syllabe) : pos = pos - 1 n = len(L) - 1 for i in range(n): for avance in range(syllabe): pos = pos + 1 if pos == len(L): pos = 0 print(L[pos], "sort du jeu") L.pop(pos) pos = pos - 1 return L[0] Pour optimiser le programme on peut remarquer que si on avance de 16 et s’il y a 5 personnes, cela revient finalement à avancer de 1. En effet 16 = 5×3+1, on a donc fait 3 tours et encore une personne. 1 n’est autre que le reste de la division de 16 par 5. Ainsi, la fonction devient plus simple : def vache(L, pos, syllabe) : pos = pos - 1 n = len(L) - 1 for i in range(n): pos = (pos + syllabe) % len(L) print(L[pos], "sort du jeu") L.pop(pos) pos = pos - 1 return L[0] Pour information, ce problème mathématique connu sous le nom de Tragédie de Flavius Josèphe n’a pas de solution explicite connue. C’està-dire que, connaissant une collection (x1 ,...,xn ) de valeurs, un indice i0 de départ et un nombre T de syllabes, il n’existe pas à ce jour de formule donnant l’indice du gagnant en fonction de n,i0 et T. Retour sur l’exercice A17 1. Le plus rapide est sûrement d’utiliser *4 pour recopier 4 fois la liste. Pour la liste initiale, on peut soit la saisir à la main, soit la saisir en compréhension ainsi : def jeu() : liste = [str(i) for i in range(2,11)]+['Valet','Dame','Roi','As'] return liste*4 2. Pour tirer une main, on va répéter 5 fois : tirer une carte, l’ajouter à la liste m, la retirer de la liste du jeu : 41 Solutions des exercices C HAPITRE A from random import choice def main(j) : m = [] for i in range(5) : m.append(choice(j)) j.remove(m[-1]) return m Ici vous remarquerez l’utilisation de m[-1] pour signifier le dernier élément de m. Enfin, notons que l’on aurait pu utiliser la fonction sample(L,k) du module random qui a le même effet : choisir k valeurs d’une liste L sans remise : from random import * m = sample(jeu, 5) 3. Pour tester s’il s’agit d’un full, nous allons appliquer l’idée proposée en aide : def full(m) : L = sorted(m) return L[0]==L[1] and L[3]==L[4] and (L[2]==L[1] or L[2]==L[3]) Retour sur l’exercice A18 Pour compter les pions bien placés, on va mettre en place la solution proposée en aide. Pour cette boucle, on ne peut pas utiliser un for puisque la taille de la liste évolue au fur et à mesure des suppressions. Pour les pions mal placés, même principe, mais on peut cette fois utiliser une boucle for pour parcourir la proposition et on supprime dans la réponse donnée au fur et à mesure qu’un pion a été identifié comme mal placé pour ne pas le compter encore une fois : def test(prop,rep) : p, r = prop[:], rep[:] # On recopie # les bien placés bp, i = 0, 0 while i < len(p) : if p[i] == r[i] : r.pop(i) p.pop(i) bp= bp + 1 else : i = i + 1 # Les mal placés : mp = 0 for j in p : if j in r : r.remove(j) mp = mp + 1 return bp, mp 42 R ÉVISIONS Retour sur l’exercice A19 1. Pour tester si l’on a gagné, on peut parcourir le tableau et dès que l’on trouve un 0, on renvoie False. Si à la fin du parcours, on est encore dans la fonction, c’est qu’il n’y avait pas de 0 ; on renvoie alors True. Voici une solution où on fait la boucle sur les indices : une seconde, un peu plus légère, où on travaille sur les éléments : def gagne(p) : for ligne in p : for case in ligne : if case == 0 : return False return True 2. Pas de difficulté pour cette deuxième question, à partir de la case donnée, on remonte jusqu’à atteindre une case marquée 1 : def haut(case, p) : l, c = case while p[l-1][c] != 1 : l = l - 1 p[l][c] = 2 return l, c Noter qu’ici on peut regarder la case p[l-1][c] sans risque de débordement puisque le terrain est toujours entouré de murs. Retour sur l’exercice A20 1. Il suffit de se déplacer 3 fois dans la direction demandée, si on sort du plateau ou si on rencontre un pion d’une autre couleur, on quitte la fonction en renvoyant 0 : 43 Solutions des exercices def gagne(p) : nbl, nbc = len(p), len(p[0]) for l in range(nbl) : for c in range(nbc) : if p[l][c] == 0 : return False return True C HAPITRE A def gagne_dir(p, case, direction) : l, c = case Dl, Dc = direction j = p[l][c] for i in range(1,4) : # 3 cases suivantes l, c = l + Dl, c + Dc if l < 0 or l >= 6 or c < 0 or c >= 7 : # On déborde return 0 if p[l][c] != j : # Si c'est une autre couleur return 0 return j On aurait pu regrouper les deux if en un, mais cela ne change pas grand chose et c’est peut-être plus clair ainsi. 2. On lance un parcours ”brutal” sur toutes les cases et dans toutes les directions. Dès que l’on rencontre une situation gagnante, on s’arrête : def gagne(p) : for l in range(6) : for c in range(7) : for Dl in range(-1,2) : for Dc in range(-1,2) : if (Dl, Dc) != (0,0) : # La direction g = gagne_dir(p, (l,c), (Dl,Dc)) # if g != 0 : return g return 0 # Personne n'a gagné (0,0) n'existe pas Cela fait 6 × 7 × 8 = 336 appels à la fonction gagne dir ce qui est raisonnable (pour cet exercice). 44 Chapitre B Objectif Programmer un jeu de pong Programmation Découvrir l’interface pygame Utiliser un dictionnaire informatique Algorithmique Réaliser une intelligence artificielle simple Mathématiques Trigonométrie Jeu de Pong Votre première mission – si vous l’acceptez – va être de programmer un jeu de Pong. Pour ceux, trop jeunes, qui ne connaissent pas ce jeu sorti dans les années 70, il s’agit d’un jeu vidéo inspiré du tennis de table développé par Ralph Baer et son équipe chez Sanders Associates. Le jeu sera rapidement repris par la société Atari pour en faire un gros succès populaire. 1 - Interface et variables Pour ce premier projet, l’interface graphique du jeu a déjà été programmée avec le module pygame que nous allons découvrir tout au long de ce livre. Vous pouvez télécharger cette interface en tapant sur le site 45 C HAPITRE B du livre le code PONG et ainsi découvrir une vidéo du projet final. La création de l’interface graphique et la gestion du clavier seront repris au chapitre suivant et nous allons pour le moment nous concentrer sur la partie algorithmique. centre.png droite.png gauche.png haut.png Pour vous aider dans vos calculs durant le projet, vous pouvez vous appuyer sur ce plan de l’écran (disponible au format PDF pour impression sur le site) : 0 100 130 570 600 0 46 100 900 1000 J EU DE P ONG Le terrain de jeu est découpé en 4 images. Les différents éléments graphiques seront affichés dans cet ordre : haut, centre, la balle, les deux raquettes et en dernier les buts (gauche et droit). Ainsi lorsque la balle entrera sur l’un des cotés, elle ”disparaı̂tra” dans le but. Voici un extrait du fichier pong.py que vous pouvez exécuter : import pygame from pygame.locals import * def afficheScore(s1, s2): .... def enfoncee(key): .... def relachee(key): .... pygame.init() fenetre = pygame.display.set_mode((1000,600)) # Déclaration des variables globales continuer = True Touches = [] # Chargement des images haut = pygame.image.load("haut.png").convert() fenetre.blit(haut, (0,0)) gauche = pygame.image.load("gauche.png").convert() fenetre.blit(gauche, (0,100)) terrain = pygame.image.load("centre.png").convert() fenetre.blit(terrain, (100,100)) droite = pygame.image.load("droite.png").convert() fenetre.blit(droite, (900,100)) balle = pygame.image.load("balle.png").convert_alpha() P1 = pygame.image.load("R1.png").convert_alpha() P2 = pygame.image.load("R2.png").convert_alpha() clock = pygame.time.Clock() while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False elif event.type == KEYDOWN : enfoncee(event.key) elif event.type == KEYUP : relachee(event.key) # ici on réalise l'animation # Mise à jour de l'écran pygame.display.flip() print('Fin') pygame.quit() 47 C HAPITRE B 2 - Utilisation des dictionnaires Avant de se lancer de but en blanc dans le projet, prenons le temps de découvrir une nouvelle structure de données. Au niveau des variables, nous avons besoin : ● pour la balle : ◇ de son abscisse, de son ordonnée ◇ de sa vitesse (composantes horizontale et verticale) ● pour le joueur 1 (et pour le joueur 2) : ◇ de la hauteur (ordonnée) de la raquette ◇ de son score Une première idée pourrait donc être d’utiliser des variables distinctes pour toutes ces quantités. Nous allons plutôt ici utiliser des dictionnaires pour regrouper ces informations. Cela nous servira aussi de première approche de la programmation objet. Pour la balle, une liste [100, 50, -2, 3] pourrait permettre de définir en une seule variable la position et la vitesse de la balle, mais rapidement la programmation va être illisible, il faudra se souvenir de ce que représente la variable à chaque indice. Un dictionnaire est une collection d’objets pouvant être de natures différentes. Il permet de repérer les éléments par des noms (on parle de clés) plutôt que par leurs indices, ce qui permet de se repérer plus simplement. La déclaration de la balle pourra alors se faire ainsi : balle = {'X': 100, 'Y':50, 'VX':-2, 'VY':3 } dé VY pla ce m en t VX 48 (X, Y) 16 J EU DE P ONG Accès aux éléments La déclaration d’un dictionnaire (on parle aussi en informatique de tableau associatif) ressemble donc beaucoup à celle d’une liste : on déclare les éléments entre accolades et on précise cette fois le nom de la clé à laquelle chacun est associé. Voici un tableau donnant quelques commandes pour manipuler les dictionnaires : Commande dict={} Effet Crée un dictionnaire vide et le nomme dict. dict[cle] Renvoie l’élément associé à la clé cle du dictionnaire dict. Si cette clé n’existe pas, une erreur est déclenchée. dict[cle]=v del(dict[cle]) v=d.pop(cle) d2=d1.copy() len(dict) cle in dict Modifie l’élément du dictionnaire dict associé à la clé cle en lui assignant la valeur v. Si la clé n’existe pas encore, elle est créée à ce moment. Efface la clé cle (et sa valeur) du dictionnaire dict. Même effet que del, mais renvoie la valeur avant de l’effacer. Crée une copie (indépendante) du dictionnaire d1 et la stocke dans le dictionnaire d2. Renvoie le nombre d’enregistrements dans le dictionnaire dict. Renvoie un booléen indiquant si cle est une clé du dictionnaire dict. E X B1 Écrire une fonction lettres qui reçoit en entrée une chaı̂ne de caractères et renvoie un dictionnaire dont les clés sont les lettres du mot et les valeurs sont le nombre d’occurrences. >>> lettres('Bonjour') {'B': 1, 'j': 1, 'n': 1, 'o': 2, 'r': 1, 'u': 1} 49 C HAPITRE B ✍ ● À noter qu’il n’y a pas de notion d’ordre dans un dictionnaire. ● Vous trouverez peut-être des exemples sur des sites web utilisant la méthode has key() pour tester l’existence d’une clé. Cette méthode n’existe plus en Python 3. ● Comme pour les listes, saisir d2 = dict ne crée pas une copie du dictionnaire dict, mais donne simplement un nouveau nom au même objet, comme l’illustre l’exemple ci-dessous : >>> scores = {'Hugo':1, 'Lily':3} >>> sc2 = scores >>> sc2['Mathilda'] = 2 >>> sc2 {'Hugo': 1, 'Lily': 3, 'Mathilda': 2} >>> scores {'Hugo': 1, 'Lily': 3, 'Mathilda': 2} >>> scores = {'Hugo':1, 'Lily':3} >>> sc2 = scores.copy() >>> sc2['Mathilda'] = 2 >>> sc2 {'Hugo': 1, 'Lily': 3, 'Mathilda': 2} >>> scores {'Hugo': 1, 'Lily': 3} Clés d’un dictionnaire Nous avons présenté des dictionnaires dans lesquels les clés étaient des textes, en réalité cela peut être tout type d’objets non mutables, c’est le cas par exemple pour des nombres, des chaı̂nes et des tuples formés de nombres et de chaı̂nes. Par exemple : ● Des couples : G = { ('A',4):"Porte-Avion", \ ('B',4):"Porte-Avion", \ ('C',4):"Porte-Avion", \ ('D',4):"Porte-Avion", \ ('E',4):"Porte-Avion" } G['D',4] = 'Touché' for k in G : if G[k] == 'Porte-Avion' : print('Il reste du porte-avion en',k) 50 J EU DE P ONG ● Des nombres (attention cependant le comportement n’est pas celui d’une liste, si on supprime l’élément 2, les indices passent de 1 à 3) : >>> >>> {0: >>> >>> {0: D = { 0 : 'zéro', 1:'un', 2 : 'deux', 3: 'trois'} D 'zéro', 1: 'un', 2: 'deux', 3: 'trois'} del D[2] D 'zéro', 1: 'un', 3: 'trois'} E X B2 En mathématiques, on a le théorème suivant : Tout entier naturel n ⩾ 2 se décompose de manière unique sous la forme n = p1α1 ×p2α2 ×...×pkαk où les pi sont des nombres premiers distincts classés par ordre croissant et les αi des entiers naturels non nuls. Par exemple 12 = 22 ×31 et 98000 = 24 ×53 ×72. Écrire une fonction decompose qui renvoie cette décomposition sous forme d’un dictionnaire : >>> decompose(12) {2: 2, 3: 1} >>> decompose(98000) {2: 4, 5: 3, 7: 2} Parcours d’un dictionnaire Pour parcourir les éléments d’un dictionnaire, on dispose de trois méthodes pour générer des itérateurs qui peuvent être ensuite appelés dans une boucle for : Commande d.keys() Effet Renvoie la liste des clés du dictionnaire d. d.values() Renvoie la liste des valeurs du dictionnaire d. d.items() Renvoie la liste des couples (clés, valeurs) de d. Par exemple FRUITS : decoupe = {'pasteques':3, 'ananas':2, 'oranges':4} for fruit in decoupe.keys() : print('Vous avez coupé des', fruit) print('--------') total = 0 for quantite in decoupe.values() : total = total + quantite print('Vous avez coupé', total, 'fruits.') print('--------') for (fruit,quantite) in decoupe.items() : print('Vous avez coupé', quantite, fruit) 51 C HAPITRE B Vous avez Vous avez Vous avez -------Vous avez -------Vous avez Vous avez Vous avez coupé des oranges coupé des ananas coupé des pasteques coupé 9 fruits. coupé 4 oranges coupé 2 ananas coupé 3 pasteques ✍ La première boucle for peut aussi être simplement remplacée par for fruit in decoupe : E X B3 Renseigner un dictionnaire dont les clés sont les numéros des 12 mois de l’année et les valeurs le nombre de jours qu’ils comportent. Réaliser alors une fonction jour(date) qui reçoit une date sous forme de texte au format ′ j j /mm′ et renvoie le numéro du jour de l’année. Par exemple jour(’14/03’) doit renvoyer 73. E X B4 En tapant sur le site du livre le code de l’exercice, on peut télécharger un début de programme contenant un dictionnaire où les clés sont des noms de joueurs et les valeurs leurs scores à un jeu (il n’y a pas d’ex-æquo). scores = {'Léo':2441, 'Gabriel':2991, 'Adam':3993, 'Timéo':5554, \ 'Raphaël':2012, 'Lucas':4617, 'Arthur':8709, 'Nathan':3601, \ ... 'Adèle':9844, 'Rose':8778, 'Mila':7709} Écrire un programme permettant d’afficher : 1. le meilleur score, 2. le prénom du joueur ayant le meilleur score, 3. la liste ordonnée des prénoms des 5 meilleurs joueurs. Comme une liste, un dictionnaire peut contenir un très grand nombre d’enregistrements. La taille n’est pas limitée par Python mais seulement par la mémoire de votre ordinateur. L’exercice qui suit en est un exemple. 52 J EU DE P ONG E X B5 À partir des données sur le site https://www.data.gouv.fr, on a récupéré dans un fichier texte (disponible sur le site du livre) la liste de toutes les communes de France ainsi que leurs codes postaux séparés par un point-virgule. 1. Écrire un programme qui lit le fichier texte et stocke ces informations dans un dictionnaire. La clé sera le nom de la ville et la valeur sera le code postal de celle-ci. 2. Utiliser ce programme pour connaı̂tre a) le nombre de villes en France, b) les villes dont le code postal est 80540, c) le nombre de villes de la Somme (département 80). E X B6 On a représenté ci-contre une situation de jeu d’échecs où il n’y a que des pièces noires (tour ou fou) sous forme d’un dictionnaire. Les clés sont des couples sous la forme (ligne,colonne) et les valeurs le type de pièce sur cette case. plateau = {(1,5):'fou', (4,2):'tour', (4,8):'tour', (8,5):'fou'} On rappelle que la tour ne peut se déplacer que horizontalement ou verticalement et le fou qu’en diagonale et d’autant de cases qu’ils le souhaitent. 1. Écrire une fonction deplacement(c1,c2) qui reçoit deux cases sous forme d’un couple et renvoie un booléen indiquant si ce déplacement est autorisé ou non. S’il n’y a pas de pièce sur la case départ, la fonction renverra False, sinon, on tiendra compte du type de pièce sur la case c1. On ne tient pas compte des autres pièces pour le moment. >>> deplacement((1,4), (2,5)) False >>> deplacement((4,2),(3,1)) False >>> deplacement((4,2),(4,6)) True 2. Écrire une fonction piece(c1,c2) qui, pour un déplacement autorisé de la case c1 vers la case c2, renvoie un booléen indiquant la présence d’une autre pièce sur le chemin. >>> piece((4,2),(4,7)) False >>> piece((4,2),(4,8)) True 53 C HAPITRE B 3 - Retour à notre projet Nous allons donc définir notre balle comme un dictionnaire contenant : ● les coordonnées de son centre (’X’, ’Y’) ● la largeur et la hauteur (’L’, ’H’) ● les coordonnées de son vecteur vitesse (’VX’, ’VY’) dé VY pla ce m en t ● l’image associée que l’on attachera à la clé ’sprite’ VX 16 Ainsi, toutes les informations concernant la balle seront placées dans une seule variable balle, on va donc remplacer la ligne : balle = pygame.image.load("balle.png").convert_alpha() par : balle = { 'X':300, 'Y':300, 'VX':0, 'VY':0, 'L':32, 'H':32} balle['sprite'] = pygame.image.load("balle.png").convert_alpha() E X B7 Faire de même en initialisant deux dictionnaires J1 et J2 pour représenter les joueurs 1 (à gauche) et 2 (à droite). On indiquera les clés X, Y, L et H pour les coordonnées du centre, la largeur et la hauteur ainsi que sprite pour l’image de la raquette. 100 4 - Animation et gestion des rebonds 16 Dans le code du projet commencé, le programme réalise une boucle à l’aide du while continuer de laquelle on sort lorsque l’on ferme la fenêtre. L’intégralité de l’animation se fera donc au niveau de la ligne 32 entre les deux commentaires : # ici on réalise l'animation # Mise à jour de l'écran pygame.display.flip() Ces lignes réalisent donc la boucle principale du jeu, vous aller les compléter au fur et à mesure des exercices. 54 J EU DE P ONG E X B8 Écrire une fonction place(D) qui place le sprite du dictionnaire D aux coordonnées indiquées par les clés X et Y de ce dictionnaire. Pour dessiner une image sur le terrain, on utilisera la méthode blit qui sera présentée en détail dans le chapitre suivant : fenetre.blit(image, (x,y)) Cette dernière permet de déposer une image au point de coordonnées (x,y). Attention, ce point représente le coin en haut à gauche de l’image, il faudra donc tenir compte de sa largeur et de sa hauteur pour la recentrer. E X B9 Ajouter quelques lignes à la boucle principale pour afficher la balle et les raquettes et pour qu’à chaque tour, les coordonnées de la balle soient augmentées de celles du vecteur vitesse (on ne s’intéresse pas aux rebonds pour le moment). Si vous avez réalisé l’exercice précédent et que vous étiez habitués à Tkinter, vous serez sûrement surpris par le fait que la balle ne soit pas déplacée à proprement parler mais laisse derrière elle une traı̂née. Il faut donc à chaque tour de boucle que l’on dessine à nouveau l’intégralité du fond via la ligne (on pourra améliorer par la suite) fenetre.blit(terrain, (100,100)) E X B10 À présent, gérer les rebonds : 1. sur les bords haut et bas, la balle rebondit ; 2. lorsqu’elle frappe un but, la balle revient au centre et part dans la direction opposée. 5 - Déplacement des raquettes Dans le projet téléchargé, les événements clavier sont déjà surveillés grâce aux lignes suivantes (tout ceci sera détaillé au prochain chapitre) : for event in pygame.event.get(): if event.type == QUIT : continuer = False elif event.type == KEYDOWN : enfoncee(event.key) elif event.type == KEYUP : relachee(event.key) 55 C HAPITRE B Ainsi, lorsqu’une touche est enfoncée, la fonction suivante ajoute à la liste Touches le code de la touche en question : def enfoncee(key): """ Fonction qui est déclenchée lorsqu'on appuie sur une touche. On met à jour la liste Touches """ if not key in Touches: Touches.append(key) De même, lorsque la touche est relâchée, la fonction suivante retire le code correspondant : def relachee(key): """ Fonction qui est déclenchée lorsqu'on relâche une touche. On met à jour la liste Touches """ if key in Touches: Touches.remove(key) Ainsi, on peut tester si la touche X est enfoncée via l’instruction : if K_x in Touches : ... Comme les déplacements des deux joueurs vont être assez similaires et que les noms des clés sont identiques, on peut éviter de refaire deux fois la même chose. E X B11 Créer deux fonctions raquetteUP(J) et raquetteDOWN(J) qui auront pour effet de baisser ou augmenter les valeurs de la clé Y du dictionnaire J représentant l’un des joueurs. E X B12 Utiliser les deux fonctions de l’exercice précédent pour déplacer la ra- quette de gauche avec les touches S et X et celle de droite avec les touches directionnelles ↑ et ↓ . Les codes des touches X, S, haut et bas sont respectivement K x, K s, K UP et K DOWN. Pour le moment, les raquettes peuvent sortir du terrain, mais l’intérêt d’avoir programmé les déplacements des deux joueurs dans une fonction est que la modification peut se faire en une seule fois. E X B13 Modifier les fonctions raquetteUP et raquetteDOWN pour maı̂triser les raquettes à l’intérieur du terrain. 56 J EU DE P ONG 6 - Buts et scores Il nous reste encore à gérer les rebonds lorsque la balle touche une raquette. E X B14 Réaliser cette mission (un petit schéma sera peut-être le bienvenu). Dans le projet commencé, vous disposez déjà d’une fonction nommée afficheScore recevant en paramètres deux entiers représentant les scores des deux joueurs et qui affiche ces scores à l’écran. E X B15 Dans notre jeu, chaque but marqué contre l’adversaire rapporte 1 point. Ajouter les lignes nécessaires pour que les scores apparaissent à l’écran. 7 - Amélioration des rebonds À présent le projet est jouable, cependant après quelques parties, on reste sur sa faim car il n’est pas possible d’avoir de stratégie pour gagner. On va donc améliorer la gestion des rebonds en tenant compte de la position de l’impact sur la raquette. Pour cette partie, nous allons avoir besoin de quelques notions de trigonométrie et du module math dont voici quelques fonctions : fonction sqrt(x) Description √ Renvoie x. cos(a) sin(a) tan(a) Représentent les 3 fonctions trigonométriques cosinus, sinus et tangente. Attention, le nombre a représente une mesure d’angle en radians. degrees(x) radians(x) Permet de convertir en degrés un angle en radians, et inversement. 57 C HAPITRE B fonction asin(x) Description π π Renvoie l’unique nombre de l’intervalle [− ; ] 2 2 dont le sinus vaut x. 1 √ x = 23 −1 π 3 1 −1 Pour que ce soit plus simple, nous travaillerons en degrés, et nous convertirons en radians pour les calculs. Partons du cas le plus simple où les composantes VX et VY de la vitesse sont positives. Nous noterons α l’angle d’arrivée sur la normale à la raquette et β l’angle après rebond. Jusque là nous avions α = β. L’idée va être d’ajouter à α un angle en fonction de la position de la balle sur la raquette. Ici on choisit d’ajouter (en degrés) la distance (en pixels) séparant le centre de la raquette et celui de la balle. Les 3 cas qui suivent illustrent 1 cette idée où la vitesse est v⃗ ( ). 2 58 J EU cas 1 cas 2 DE P ONG cas 3 α B J2 β α β J2 B B α β BY = J2Y β = α ≈ 63○ BY = J2Y + 15 β = α + 15○ ≈ 78○ BY = J2Y − 30 β = α − 30○ ≈ 33○ E X B16 Programmer une fonction rebond(x,y,e) qui reçoit les composantes (x, y) (positives pour le moment) du vecteur vitesse et l’écart e entre l’impact et le centre de la raquette et renvoie le nouveau vecteur vitesse après rebond. Pour vérification sur les 3 cas présentés : >>> rebond(1,2,0) (-1.0000000000000002, 2.0) >>> rebond(1,2,15) (-0.44828773608402667, 2.1906706976806576) >>> rebond(1,2,-30) (-1.8660254037844388, 1.2320508075688772) E X B17 Adapter la fonction précédente pour qu’elle fonctionne avec une vitesse x v⃗ ( ) où x > 0 et y < 0 (collision avec la raquette 2). Par symétrie, on aura : y >>> rebond(1,-2,0) (-1.0000000000000002, -2.0) >>> rebond(1,-2,-15) (-0.44828773608402667, -2.1906706976806576) >>> rebond(1,-2,30) (-1.8660254037844388, -1.2320508075688772) E X B18 Réaliser une version finale où la fonction rebond fonctionne avec les deux raquettes et l’incorporer au jeu. 59 C HAPITRE B E X B19 Pour éviter les échanges interminables, améliorer une dernière fois le projet, pour qu’à chaque rebond sur la raquette, la vitesse de la balle augmente de 10%. 8 - Un peu d’intelligence artificielle... Pour ce premier projet, nous allons utiliser une intelligence artificielle assez basique sur le joueur 2 afin de pouvoir jouer à un seul joueur : ● Lorsque la balle part vers le joueur 1, la machine replace la raquette au milieu du terrain puisqu’elle ne sait pas où l’adversaire va la renvoyer. ● Lorsque la balle arrive vers lui, l’ordinateur tente de se rapprocher au mieux de l’ordonnée de la balle. E X B20 Programmer cette première solution. Après quelques parties, on s’aperçoit que l’ordinateur est un piètre adversaire, en particulier lorsque la balle accélère car il suit la balle et, contrairement à nous, n’anticipe pas la position d’impact. E X B21 Écrire une fonction arrivee(b) qui reçoit un dictionnaire représentant la balle sur le terrain et retourne l’ordonnée du point de collision prévu avec la raquette 2. L’incorporer au projet pour programmer un adversaire plus redoutable. Cette fois-ci on tombe dans l’excès inverse : la machine est invincible. Une solution souvent utilisée est alors de limiter le nombre de coups anticipés, ce qui permet de régler le niveau du joueur (ici un coup représente un pas de balle). E X B22 Réaliser cette dernière amélioration et faire en sorte qu’à chaque point marqué par l’humain, le nombre de coups anticipés par la machine augmente de 2 et, dans le cas inverse, diminue de 2 pour équilibrer la partie. N’hésitez pas à aller voir sur le site le résultat final du projet. 9 - Aide pour les exercices Aide pour l’exercice B4 Les deux premières questions ne devraient pas poser trop de problèmes. Pour la dernière, on pourra utiliser une liste des 5 meilleurs scores et utiliser la méthode sort pour l’ordonner. 60 J EU DE P ONG Aide pour l’exercice B5 Si vous avez oublié comment lire un fichier en Python, consultez le tomme 1, chapitre E. Aide pour l’exercice B14 Xballe = X J2 − 24 Xballe = X J2 − 8 Yballe = YJ2 − 50 Parfois une figure vaut mieux qu’un long discours. On a représenté par 4 points noirs les positions limites des centres de la balle lorsqu’elle entre en contact avec la raquette et avec une croix le centre de la raquette. On peut donc déterminer les encadrements à tester. Ainsi dès que le centre de la balle entre dans la zone hachurée, elle doit rebondir. × 100 Yballe = YJ2 + 50 16 Aide pour l’exercice B15 On peut : ● soit découper le test d’origine if balle['X'] > 900 + 16 or balle['X'] < 100 - 16 : en deux tests pour donner le point au joueur 1 ou 2 ● soit conserver ce test et regarder le signe de l’abscisse du vecteur vitesse de la balle pour savoir qui a marqué. Aide pour l’exercice B21 Inutile de chercher à faire compliqué, une boucle TANT QUE simulant l’avancée de la balle fera l’affaire. 61 C HAPITRE B 10 - Solutions des exercices Retour sur l’exercice B1 Pour ce premier exercice, on va créer un dictionnaire vide puis parcourir la chaı̂ne donnée lettre par lettre. La première fois que l’on rencontre une lettre, on crée la clé correspondante et on initialise la valeur à 1. Si on a déjà rencontré cette lettre, on augmente le compteur de 1 : def lettres(mot) : reponse = {} for l in mot : if l in reponse : reponse[l] += 1 else : reponse[l] = 1 return reponse La ligne reponse[l]+=1 est équivalente à reponse[l]=reponse[l]+1 dans notre cas. Personnellement, je préfère de loin la seconde notation Retour sur l’exercice B2 On peut s’inspirer de l’exercice précédent. On va commencer par diviser par 2 jusqu’à ne plus pouvoir, puis par 3, et ainsi de suite... def decompose(n) : decomp = {} diviseur = 2 while n > 1 : if n % diviseur == 0 : decomp[diviseur] = 1 n = n // diviseur while n % diviseur == 0 : n = n // diviseur decomp[diviseur] += 1 diviseur = diviseur + 1 return decomp Remarquons quand même que l’on teste aussi des diviseurs non premiers, ce qui n’est pas gênant dans la mesure où ceux-ci ne peuvent plus être diviseurs (si on ne peut plus diviser par 2, ni par 3, on ne pourra pas diviser par 6). Si vous trouvez cet exercice trop mathématiques, ne vous inquiétez pas, nous n’en aurons pas besoin pour la suite. 62 J EU DE P ONG Retour sur l’exercice B3 La solution présentée ne devrait pas poser de difficulté, d’ailleurs un dictionnaire n’est pas vraiment nécessaire ici : mois = {1:31, 2:28, 3:31, 4:30, 5:31, 6: 30, \ 7:31, 8:31, 9:30, 10:31, 11:30, 12:31} def jour(date) : jj, mm = int(date[:2]), int(date[3:]) nbJours = 0 for i in range(1,mm) : nbJours += mois[i] return nbJours + jj 1. On va utiliser une variable best pour enregistrer le meilleur score au fur et à mesure du parcours du dictionnaire : best = 0 for s in scores.values() : if s > best : best = s print('Le meilleur score est', best) 2. Il faut une seconde variable pour mémoriser le prénom du joueur à chaque fois que le meilleur score est mis à jour : best = 0 for nom, s in scores.items() : if s > best : best = s bestJoueur = nom print("C'est", bestJoueur, "qui a le meilleur score (", best, ")") 3. En utilisant une liste pour mémoriser les 5 meilleurs scores, on peut procéder ainsi : listeBest = [0] * 5 for s in scores.values() : if s > min(listeBest) : listeBest.remove(min(listeBest)) listeBest.append(s) listeBest.sort() print('Classement des 5 meilleurs') for s in listeBest : for nom in scores : if scores[nom] == s : print(nom, 'avec' ,s ,'points') 63 Solutions des exercices Retour sur l’exercice B4 C HAPITRE B On peut améliorer le programme précédent en conservant dans la liste le couple (score,nom). Attention l’ordre du couple est important car lors du tri de la liste, ce sont les scores qui seront comparés et à score égal (ce qui n’arrive pas ici), les noms : listeBest = [(0,0)] * 5 for nom, s in scores.items() : if s > listeBest[0][0] : listeBest.pop(0) listeBest.append((s,nom)) listeBest.sort() print('Classement des 5 meilleurs') for s, nom in listeBest : print(nom,'avec', s, 'points') Avec la méthode proposée, on trie à nouveau la liste à chaque insertion. On pourrait profiter du fait que la liste est déjà ordonnée pour insérer le nouveau score au bon endroit pour être plus efficace. Retour sur l’exercice B5 1. Pour lire les informations, une première solution est de lire une à une les lignes du fichier, de repérer la position du point-virgule et de découper la chaı̂ne en deux : f = open('codes_postaux.txt','r') d = {} for ligne in f : i = ligne.index(';') ville, code = ligne[:i], ligne[i+1:-1] d[ville] = int(code) f.close() Remarque : si on met code=ligne[i+1:] à la place de code= ligne[i+1:-1], il subsiste un \n de fin de chaı̂ne sous Windows (voir page 42 du tome 1). Une autre solution plus rapide est d’utiliser la méthode split d’une chaı̂ne de caractères : ● ch.split(sep) : renvoie une liste dont les éléments sont les morceaux de la chaı̂ne de caractères ch, la coupure ayant effet sur le séparateur sep. ● sep.join(liste) : Renvoie une chaı̂ne où les éléments de la liste (de textes) sont concaténés par le séparateur sep. 64 J EU DE P ONG On obtient une solution plus lisible : f = open('codes_postaux.txt','r') d = {} for ligne in f : liste = ligne.split(';') ville, code = liste[0], liste[1] d[ville] = int(code[:-1]) f.close() f = open('codes_postaux.txt','r') d = {ligne[:ligne.index(';')]:int(ligne[ligne.index(';')+1:-1]) ⤦ � for ligne in f} f.close() Vous pouvez encore utiliser cette proposition de wiztricks sur le forum du site de programmation www.developpez.net : d = { nom:int(code) for nom, code in (l.split(';') for l in f ) } 2. a) Pour obtenir le nombre de villes : print("le nombre de villes est", len(d)) b) Pour obtenir les villes dont le code postal est 80540 avec une boucle : for nom,code in d.items() : if code == 80540 : print(nom) ou en une seule ligne : print(" / ".join([n for n,c in d.items() if c == 80540])) c) Pour connaı̂tre les villes de la Somme, on peut convertir le code postal en chaı̂ne de caractères puis supprimer les 3 derniers caractères pour tester s’il reste "80" : for nom,code in d.items() : if str(code)[:-3] == '80' : print(nom) Où regarder si le quotient entier du code postal par 1000 est 80, ce qui évite les conversions de types. print(" / ".join([n for n,c in d.items() if c//1000 == 80])) 65 Solutions des exercices Encore plus court : on réalise une déclaration du dictionnaire en compréhension : C HAPITRE B Retour sur l’exercice B6 1. Voici une première solution : si c’est une tour, on s’assure que la case de départ et celle d’arrivée ont même abscisse ou même ordonnée. ���→ Si c’est un fou, on peut calculer les coordonnées du vecteur c2c1 et vérifier que ses deux composantes sont égales en valeur absolue : def deplacement(c1, c2) : if c1 not in plateau : return False if plateau[c1] == 'tour' : if c1[0] == c2[0] or c1[1] == c2[1] : return True else : return False if plateau[c1] == 'fou' : if abs(c1[0]-c2[0]) == abs(c1[1]-c2[1]) : return True else : return False On peut, comme souvent, raccourcir, puisque l’expression c1[0] == c2[0] or c1[1] == c2[1] est déjà un booléen. def deplacement(c1, c2) : if c1 not in plateau : return False if plateau[c1] == 'tour' : return (c1[0] == c2[0] or c1[1] == c2[1]) if plateau[c1] == 'fou' : return (abs(c1[0]-c2[0]) == abs(c1[1]-c2[1]) ) 2. La seconde question est un peu plus ardue. Il faut parcourir toutes les cases depuis celle de départ jusqu’à celle d’arrivée et regarder si une pièce se trouve dessus. Une idée peut être de s’appuyer sur la constatation illustrée ci-dessous : si A et B sont deux points du plan, ��→ �→ le point M défini par AM = t AB parcourt l’intégralité du segment [AB] lorsque t parcourt l’intervalle [0,1]. A M t = 0.3 B x −x x = xA + t (xB − xA ) xM − xA ) = t ( B A ) soit { M yM = yA + t (yB − yA ) yM − yA yB − yA Par analogie, si c1 est la case de départ et c2 celle d’arrivée, on va parcourir toutes les cases entre c1 et c2 en calculant les coordonnées c1 + t× (c2-c1) avec t ∈ { n1 , 2n ,..., nn } et n le nombre de cases que l’on souhaite parcourir. Ainsi, on a ( 66 J EU DE P ONG def piece(c1,c2): x1, y1 = c1 x2, y2 = c2 if plateau[c1] == 'tour' : n = abs((x2-x1) + (y2-y1)) #l'un des deux vaut 0 else : n = abs(x2-x1) for i in range(1, n+1) : if (x1 + i*(x2-x1)//n, y1 + i*(y2-y1)//n) in plateau : return True return False J1 = { 'X':110, 'Y':350, 'L':16, 'H':100, 'score':0} J1['sprite'] = pygame.image.load("R1.png").convert_alpha() J2 = { 'X':890, 'Y':350, 'L':16, 'H':100, 'score':0} J2['sprite'] = pygame.image.load("R2.png").convert_alpha() Retour sur l’exercice B8 Avec les clés ainsi définies, on obtient le code : def place(D) : """ Cette fonction place le 'sprite' du dictionnaire D aux coordonnées (X,Y). """ fenetre.blit(D['sprite'], (D['X']-D['L']//2,D['Y']-D['H']//2)) Vous remarquerez l’intérêt d’avoir utilisé les mêmes clés pour la balle et les raquettes : la même fonction permet de dessiner n’importe lequel des 3 sprites du jeu. Retour sur l’exercice B9 Les deux dernières lignes permettent d’effectuer VX une translation de vecteur v⃗ ( ), il faut aussi afficher la balle : VY place(balle) place(J1) place(J2) # Mise à jour des coordonnées balle['X'] = balle['X'] + balle['VX'] balle['Y'] = balle['Y'] + balle['VY'] ...évidemment, la balle ne bougera pas si sa vitesse est initialisée à 0 ... Retour sur l’exercice B10 Il est préférable de tester les collisions sur les bords avec des inégalités plutôt que des égalités, dans la mesure où rien n’assure que les coordonnées resteront entières. 67 Solutions des exercices Retour sur l’exercice B7 On peut faire ainsi : C HAPITRE B 1. Pour les rebonds haut/bas, on peut gérer ainsi : # rebond du haut : if balle['Y'] < 130 + 16 : # limite du terrain + rayon de la balle balle['VY'] = -balle['VY'] # rebond du bas : if balle['Y'] > 570 - 16 : # limite du terrain - rayon de la balle balle['VY'] = -balle['VY'] 2. Et pour les entrées dans le but (on peut gérer les deux de la même manière pour le moment) : # but gauche / droit : if balle['X'] > 900 + 16 or balle['X'] < 100 - 16: balle['VX'] = -balle['VX'] balle['VY'] = -balle['VY'] balle['X'] = 500 balle['Y'] = 350 Retour sur l’exercice B11 On peut augmenter ou diminuer la hauteur de la raquette de un pixel ou plus si on veut une raquette plus rapide : def raquetteUP(J) : J['Y'] = J['Y'] - 1 def raquetteDOWN(J) : J['Y'] = J['Y'] + 1 Retour sur l’exercice B12 Tout est déjà programmé, il ne reste plus qu’à assembler les morceaux : if if if if K_s in Touches : raquetteUP(J1) K_x in Touches : raquetteDOWN(J1) K_UP in Touches : raquetteUP(J2) K_DOWN in Touches : raquetteDOWN(J2) Retour sur l’exercice B13 On ne va ajouter ou retirer 1 que lorsque les ordonnées sont dans l’intervalle correspondant au terrain : def raquetteUP(J) : if J['Y'] > 130 + J['H']//2 : J['Y'] = J['Y'] - 1 # Limite haute du terrain + 1/2 de la # hauteur de la raquette def raquetteDOWN(J) : if J['Y'] < 570 - J['H']//2 : J['Y'] = J['Y'] + 1 # Limite basse du terrain - 1/2 de la # hauteur de la raquette 68 J EU DE P ONG J EU DE P ONG Retour sur l’exercice B14 En s’appuyant sur la figure donnée il J EUen DEaide, P ONG suffit, une fois les encadrements déterminés, de prendre l’opposé de l’absRetour sur l’exercice B14 En s’appuyant sur la figure donnée en aide, il cisse du vecteur vitesse pour faire rebondir la balle : suffit, une les encadrements déterminés, l’opposé l’absRetour sur fois l’exercice B14 En s’appuyant surde la prendre figure donnée endeaide, il cisse du vecteur vitesse pour faire rebondir la balle : suffit, unela fois les encadrements déterminés, de prendre l’opposé de l’abs# Lorsque balle touche la raquette 1 : if J1['X']+8 <= balle['X'] <= faire J1['X']+24 andla\ balle : cisse du vecteur vitesse pour rebondir J1['Y']+50 : J1['Y']+50 : J1['Y']+50 : J2['Y']+50 : J2['Y']+50 : J2['Y']+50 : Retour sur l’exercice B15 En appliquant la deuxième idée proposée dans l’aide : Retour sur l’exercice B15 En appliquant la deuxième idée proposée dans l’aide : sur l’exercice Retour En appliquant la deuxième idée proposée dans # but gauche / droit B15 : if balle['X'] > 900 + 16 or balle['X'] < 100 - 16 : l’aide : ifgauche balle['VX'] > : 0 : # but / droit J1['score'] 1 or balle['X'] < 100 - 16 : if balle['X'] > 900 += + 16 # but gauche / droit : else : if balle['VX'] > 0 : if balle['X'] > 900 += + 16 J2['score'] 1 or balle['X'] < 100 - 16 : J1['score'] += 1 if balle['VX'] > 0 : balle['VX'] = -balle['VX'] else : J1['score'] += 1 balle['VY'] = -balle['VY'] J2['score'] += 1 else : balle['X'] = 500 balle['VX'] = -balle['VX'] J2['score'] += 1 balle['Y'] ==350 balle['VY'] -balle['VY'] balle['VX'] = -balle['VX']J2['score']) afficheScore(J1['score'], = 500 balle['X'] balle['VY'] = -balle['VY'] balle['Y'] = 350 balle['X'] = 500 afficheScore(J1['score'], J2['score']) balle['Y'] = 350 afficheScore(J1['score'], J2['score']) x Retour sur l’exercice B16 La norme de v⃗ ( ) est conservée lors du choc. y x√ 2 2 Retour surthéorème l’exercicede B16 La norme conservée lors du choc. ⃗v⃗ (= ) est Grâce au Pythagore, on de a ∥v∥ yx x + y et les relations trigo√ ⃗ Retour sur l’exercice B16 La norme xde v ( ) est conservée lors du choc. = � cos α ⃗ relations trigo⃗ =y √x2avec nométriques du triangle donnent { ∥v∥. Grâce au théorème de Pythagore, on a ∥v∥ + y 2�et= les y = � sinα 2 2 ⃗cos Grâce au théorème de Pythagore, onxa=∥�v∥ = α x + y et les relations trigo⃗ nométriques du triangle donnent { avec � = ∥v∥. yx = � sqrt, sinα cos α sin, cos from math import asin, degrees, radians, ⃗ nométriques du triangle donnent { avec � = ∥v∥. y = � sinα def : fromrebond(x,y,e) math import asin, degrees, radians, sqrt, sin, cos l = sqrt(x**2 + y**2) fromalpha math = import asin, degrees, radians, sqrt, sin, cos degrees( def rebond(x,y,e) : asin(y/l) ) beta = alpha + e l = sqrt(x**2 + y**2) def return rebond(x,y,e) : *cos(radians(beta)), =-l degrees( asin(y/l) ) l*sin(radians(beta)) alpha l = sqrt(x**2 + y**2) beta = alpha + e alpha = degrees( asin(y/l) ) return -l*cos(radians(beta)), l*sin(radians(beta)) beta = alpha + e return -l*cos(radians(beta)), l*sin(radians(beta)) 69 69 69 Solutions des exercices J1['Y']-50 <= balle['Y'] <= # Lorsque la balle touche la raquette 1 : balle['VX'] = -balle['VX'] if J1['X']+8 <= balle['X'] <= J1['X']+24 and \ # Lorsque la balle touche la raquette 1 : J1['Y']-50 <= balle['Y'] <= if J1['X']+8 balle['X'] <=raquette J1['X']+24 # Lorsque la <= balle touche la 2 :and \ balle['VX'] = -balle['VX'] J1['Y']-50 balle['Y'] if J2['X']-24 <= balle['X'] <=<= J2['X']-8 and<= \ balle['VX'] = -balle['VX'] J2['Y']-50 <= balle['Y'] <= # Lorsque la balle touche la raquette 2 : balle['VX'] -balle['VX'] if J2['X']-24 <==balle['X'] <= J2['X']-8 and \ # Lorsque la balle touche la raquette 2 : J2['Y']-50 <= balle['Y'] <= if J2['X']-24 <= balle['X'] <= J2['X']-8 and \ balle['VX'] = -balle['VX'] J2['Y']-50 <= balle['Y'] <= balle['VX'] = -balle['VX'] C HAPITRE B Retour sur l’exercice B17 La première idée est sûrement d’adapter le code précédent par disjonction de cas : def rebond(x,y,e) : l = sqrt(x**2+y**2) if y > 0 : alpha = degrees(asin(y/l)) beta = alpha + e return -l*cos(radians(beta)), l*sin(radians(beta)) else : alpha = degrees(asin(-y/l)) beta = alpha - e return -l*cos(radians(beta)), -l*sin(radians(beta)) Si vous êtes habitué à manipuler les angles orientés, en jouant sur la parité des fonctions trigonométriques circulaires (cos(−x) = cos x ; sin(−x) = − sinx et arcsin(−x) = − arcsinx) vous vous apercevrez alors que la fonction de l’exercice précédent fonctionne sans ajout de test ! Retour sur l’exercice B18 Encore une fois, il n’y a pas grand-chose à changer, puisque la valeur de x n’entre pas dans le calcul de l’angle α. Il faut juste faire attention à ce que, cette fois-ci, la première composante de la vitesse renvoyée soit positive. Voici une proposition où l’on a ajouté aussi une ligne afin de limiter les grandes valeurs de β pour une meilleure jouabilité : def rebond(x,y,e) : l = sqrt(x**2+y**2) alpha = degrees(asin(y/l)) beta = alpha + e beta = min(max(-70,beta),70) # Pour ne pas avoir d'angles trop grands if x > 0 : return -l*cos(radians(beta)), l*sin(radians(beta)) else : return l*cos(radians(beta)), l*sin(radians(beta)) On peut éviter ce dernier test en utilisant la fonction copysign du module math . copysign(A,x) : renvoie la valeur (absolue) de A précédée du signe de x. En particulier copysign(1,x) renvoie 1 si x ⩾ 0 et −1 sinon. def rebond(x,y,e) : l = sqrt(x**2+y**2) alpha = degrees(asin(y/l)) 70 beta = alpha + e beta = min(max(-70,beta),70) # Pour ne pas avoir d'angles trop grands return -copysign(1,x)*l*cos(radians(beta)), l*sin(radians(beta)) J EU DE P ONG Enfin, pour incorporer ce nouveau type de rebond, on modifiera les lignes correspondantes dans la boucle principale. Par exemple pour la raquette numéro 1 : # Lorsque la balle touche la raquette 1 : if J1['X']+8 <= balle['X'] <= J1['X']+24 and \ J1['Y']-50 <= balle['Y'] <= J1['Y']+50 : balle['VX'], balle['VY'] = rebond(balle['VX'], balle['VY'], ⤦ � balle['Y']-J1['Y']) balle['X'] = J1['X']+25 Retour sur l’exercice B19 Augmenter de 10% revient à multiplier la quantité par 1,1. On va donc, dans la fonction rebond, multiplier les nouvelles composantes du vecteur vitesse avant le renvoi. def rebond(x,y,e) : l = sqrt(x**2+y**2) alpha = degrees(asin(y/l)) beta = alpha + e beta = min(max(-70,beta),70) # Pour ne pas avoir d'angles trop grands return -copysign(1.1,x)*l*cos(radians(beta)), 1.1*l*sin(radians(beta)) Pensez aussi à ré-initialiser la vitesse lorsqu’un point est marqué ! Retour sur l’exercice B20 En suivant la proposition d’intelligence artificielle, on va calculer la hauteur cible à atteindre : ● si la balle vient vers la machine, la hauteur cible est celle de la balle ; ● 350 (milieu du terrain) si la balle repart. On va donc remplacer les deux lignes : if 'UP' in Touches : raquetteUP(J2) if 'DOWN' in Touches : raquetteDOWN(J2) par : # Déplacement du joueur 2 par IA if balle['VX'] > 0 : # La balle vient vers le joueur 2 cible = balle['Y'] else : cible = 350 if J2['Y'] < cible : raquetteDOWN(J2) elif J2['Y'] > cible : raquetteUP(J2) 71 Solutions des exercices Remarquer que l’on ré-initialise ici l’abscisse de la balle au bord de la raquette pour s’assurer que la balle n’est plus en collision avec elle. C HAPITRE B Retour sur l’exercice B21 En s’inspirant de ce qui a précédemment été fait pour gérer les déplacements de la balle, on peut proposer cette solution : def arrivee(b) : x, y, vx, vy = b['X'], b['Y'], b['VX'], b['VY'] while x < 890 - 24 : x, y = x + vx, y+ vy if y < 130 + 16 or y > 570 - 16 : vy = -vy return y Il faut ensuite mettre le calcul de cible à jour dans l’I.A : cible = arrivee(balle) Retour sur l’exercice B22 D’une part, il faut adapter la fonction arrivee : def arrivee(b, coup) : x, y, vx, vy = b['X'], b['Y'], b['VX'], b['VY'] c = 0 while x < 890 - 24 and c < coup : x, y = x + vx, y+ vy c = c + 1 if y < 130 + 16 or y > 570 - 16 : vy = -vy return y D’autre part, il faut créer une variable globale coup qui sera initialisée arbitrairement à 10 et mise à jour lorsqu’un but sera marqué. 72 Chapitre C Objectif Programmer un jeu de labyrinthe Programmation Listes chaı̂nées et tuples Principe d’une file Algorithmique Générer un labyrinthe parfait Sortir d’un labyrinthe Pygame Gestion de la fenêtre Gestion des surfaces Gestion des événements clavier Minos Bravo ! Si vous êtes là, c’est que le jeu de Pong a été réalisé. Ce premier projet avait pour objectif de vous faire découvrir quelques fonctions de Pygame, mais comme vous l’aurez remarqué, un certain nombre de fonctions étaient déjà présentes pour nous simplifier la tâche. Pour autant, vous pouvez être fier du travail accompli. Il arrive souvent en informatique que nous ayons recours à des fonctions dont nous ne connaissons que les entrées et sorties sans pour autant connaitre en détail l’algorithme qui se cache derrière. À présent, nous allons réaliser nos projets à partir d’une feuille blanche ! Commençons par un jeu de labyrinthe : MINOS 73 C HAPITRE C 1 - Fenêtre et surfaces dans Pygame Voici le script classique et minimal pour afficher une fenêtre avec le module Pygame : 1 2 import pygame from pygame.locals import * 3 4 5 pygame.init() fenetre = pygame.display.set_mode((1000,600)) 6 7 8 9 10 11 12 13 continuer = True clock = pygame.time.Clock() while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False 14 15 pygame.display.flip() 16 17 pygame.quit() Expliquons en détail les actions de chaque ligne : 1. On importe le module pygame. Avec ce type de syntaxe, toutes les fonctions de ce module appelées seront précédées de pygame. 2. On importe les constantes de pygame, comme QUIT utilisé à la ligne 12 ou encore les codes clavier comme cela sera détaillé plus loin. 4. On initialise le module (obligatoire). 5. On crée une surface qui sera de dimensions 1000 × 600 pixels et qui représentera la fenêtre. Sous Pygame, une surface peut être vue comme une feuille blanche sur laquelle on pourra exécuter des actions. 7. La variable continuer est un booléen, c’est-à-dire une variable qui ne peut valoir que VRAI (True) ou FAUX (False) . Ici continuer sera la condition pour entrer dans la boucle principale du jeu et en sortir. 8. La variable clock est un objet de type Clock , qui permet de gérer la vitesse du jeu, en particulier pour que le jeu ne soit pas trop rapide sur une machine très puissante. La ligne 10 indique que le jeu va se jouer au mieux à 100 FPS, c’est à dire à 100 images par seconde (100 Frames Per Second). Si la boucle revient à la ligne 10 avant 1/100ème de seconde, le programme attend. 74 M INOS 11. Si vous avez lu le tome 1, vous connaissez la notion d’événement : ce sont toutes les actions extérieures qui se produisent : la souris qui bouge, une touche pressée, la fenêtre déplacée ou fermée... Ici avec la boucle for, on va lire tous les événements qui se sont produits depuis le tour précédent, chaque événement est alors placé dans la variable event pour être traité ensuite. 12. Si l’événement est de type QUIT (on cherche à fermer la fenêtre)... 13. ...on met la variable continuer à FAUX de manière à quitter la boucle. 15. Cette ligne ne sert à rien pour le moment puisque rien n’est affiché mais sera nécessaire par la suite. Lorsque l’on dessine sur la fenêtre, tout l’affichage se passe sur une surface ”virtuelle”. Lors du flip , la surface est entièrement recopiée sur l’écran ”réel”. Cette technique qui a pour but d’éviter de voir les étapes de dessin à l’écran ainsi que les problèmes de scintillement s’appelle le principe de double buffering . 17. On quitte proprement le mode Pygame, cette dernière ligne est importante, surtout pour les utilisateurs de Windows, sinon la fenêtre se gèle et on ne peut plus reprendre la main. ✍ À propos de la fenêtre qui gèle lorsqu’une erreur est rencontrée : si vous utilisez EduPython, inutile de chercher à la fermer. Corrigez le problème et relancez votre programme, l’autre fenêtre disparaitra alors. Vous pouvez aussi revenir à l’éditeur Python en appuyant sur les touches CTRL + F2 pour tout réinitialiser. La fenêtre Les fonctions qui permettent d’agir sur la fenêtre se trouvent dans le module display du package Pygame. Si vous commencez votre programme par import pygame il faudra faire précéder le nom de la fonction de pygame.display., par exemple : pygame.display.flip() 75 C HAPITRE C Fonction set mode(s,opt) Effet Renvoie une surface qui représentera la fenêtre présente à l’écran. La taille (intérieure) de la fenêtre est donnée par le couple s. On peut aussi préciser des options opt, celles-ci sont facultatives. On peut choisir (entre autres) parmi FULLSCREEN (plein écran), RESIZABLE (fenêtre redimensionnable, elle ne l’est pas par défaut) ou encore NOFRAME (fenêtre sans bordure). set caption(txt) Affiche le texte txt dans la barre du haut de la fenêtre ; cette fonction ne renvoie rien. flip() set icon(s) Recopie la surface virtuelle à l’écran. Associe à la fenêtre l’icône définie par la surface s. Cette image peut être de n’importe quelle taille et contenir de la transparence ; souvent on utilise des images 32 × 32. ✍ ● Si vous utilisez le mode FULLSCREEN ou NOFRAME, pensez à prévoir une sortie (par la touche ECHAP par exemple). ● Si vous utilisez le mode FULLSCREEN, choisissez de préférence une résolution supportée par votre carte graphique, sinon vous aurez des cadres noirs pour compléter l’écran. Vous avez accès à ces informations en affichant les ”paramètres d’affichage” sous Windows. 76 Paramètres d’affichage sous Windows 10 M INOS Les surfaces et les images Le module image de Pygame, permet de lire et enregistrer des images. Encore une fois, il faut penser à faire précéder les noms des fonctions de pygame.image. selon le mode d’importation. Les images sont composées de pixels, qui peuvent être codés de différentes façons en mémoire : Fonction load(f ich) save(surf ,f ich) Un dessin de Mathilda pour l’anniversaire de son papa Effet Renvoie une surface représentant l’image contenue dans le fichier f ich. Sauvegarde la surface surf sous forme d’un fichier image nommé f ich. Le format de l’image est déterminé par son extension. ✍ Au niveau des formats d’images, Pygame sait lire les principaux fichiers d’images (PNG, JPG et GIF non animé). À noter que : ● Les fichiers JPG sont souvent au format 16 millions de couleurs : chaque pixel est codé sur 3 octets (donc 3 nombres entre 0 et 255) : le premier représente la dose de rouge, le second le vert et le dernier le bleu. On parle d’image au format RGB (Red-Green-Blue) ● Les fichiers PNG peuvent contenir une information supplémentaire de transparence, chaque pixel est alors codé sur 4 octets : 3 octets pour le rouge, vert, bleu et un quatrième appelé canal alpha. Ce nombre est compris entre 0 (pixel totalement transparent) et 255 (pixel totalement opaque). On parlera dans ce livre d’images (ou de surface) RGBA (Red-Green-Blue-Alpha) ● Les fichiers GIF (une fois pour toute, on prononce ≪ jif ≫ selon l’inventeur même Steve Wilhite dans une interview accordée au New York Times) ne seront pas beaucoup utilisés dans cet ouvrage car ils ne peuvent contenir que 256 couleurs (dont éventuellement une transparente) données sous forme de palette. Ainsi chaque pixel est codé sur 1 octet indiquant le numéro de la couleur dans la palette. 77 C HAPITRE C On peut alors manipuler les pixels de ces surfaces : Méthode surf =Surface(s,opt) Effet Crée et renvoie une surface de dimension s donnée sous forme de couple. En option opt, on peut préciser SRCALPHA si on veut une image au format RGBA. Attention à la majuscule (on verra d’où vient le S majuscule plus loin dans le livre). surf .fill(coul) Remplit la surface surf de la couleur coul. Cette dernière peut être donnée par un triplet ou un quadruplet selon le format de la surface. surf .set at(p,coul) Colorie le pixel de coordonnées p = (x,y) de la couleur coul. Comme d’habitude le point de coordonnées (0,0) est le coin en haut à gauche de l’image. surf .get at(p) Renvoie la couleur du pixel de coordonnées p sous la forme d’un quadruplet (R, G, B, A). Si la surface n’a pas d’information de transparence, le canal alpha est à 255. E X C1 Dessiner un soleil dans une image 128 × 128. L’extérieur du soleil étant transparent et l’intérieur jaune avec une transparence proportionnelle à la distance à son centre. E X C2 Dans ce second exercice, nous allons tenter de dessiner ce motif de trois couleurs sur une image de dimension 128 × 64. Nous enregistrerons le résultat sous le nom motif.png pour le réutiliser plus tard. 78 M INOS En pratique, il est assez rare de recopier une image pixel par pixel, on copie le plus souvent des zones entières. Méthode surf .copy() surf .blit(s1,p,r) Effet Renvoie une copie de la surface surf . Copie la surface s1 sur la surface surf à partir du point de coordonnées p. Si un quadruplet r = (x0 ,y0 ,l,h) est précisé, la surface s1 est limitée aux pixels de coordonnées (x,y) vérifiant : x0 ⩽ x < x0 + l et y0 ⩽ y < y0 + h. Si r n’est pas précisé, l’intégralité de la surface s1 est recopiée. Voici un exemple qui illustre l’utilisation de ces fonctions (on peut obtenir le code et les images en tapant BLIT sur le site du livre) : image : nombres.png import pygame fond = pygame.image.load('fond.jpg') nombres = pygame.image.load('nombres.png') nouveau = fond.copy() nouveau.blit(nombres,(100,50),(57,82,50,80)) pygame.image.save(nouveau,'nouveau.jpg') 50 (100, 50) fond.jpg nouveau.jpg E X C3 À partir du motif réalisé motif.png dans l’exercice précédent réaliser un pavage à la mode q*bert de taille 640 × 640 et l’afficher dans une fenêtre dont le titre est ”Pavages” et l’icône le motif de base. 79 C HAPITRE C ✍ À propos du chargement d’images : il existe deux méthodes sur les surfaces permettant de les convertir dans le même format que la fenêtre d’affichage, il s’agit de convert et convert alpha , la seconde étant utilisée pour conserver l’information RGBA lors du chargement d’une image PNG par exemple. Il est conseillé de convertir ces images dès le chargement pour gagner du temps par la suite. Typiquement, on charge ainsi les images : import pygame imageRGB = pygame.image.load('fond.jpg').convert() imageRGBA = pygame.image.load('nombres.png').convert_alpha() ... Pour les coordonnées des points ou les codes couleur, nous utilisons souvent des tuple. Prenons un petit moment pour faire la différence avec les listes : Les listes sont des structures de taille dynamique (on peut ajouter ou supprimer des éléments) et mutables (on peut modifier des éléments). Derrière se cache une structure de liste chaı̂née : quand on tape L = [5, 8 10], on mémorise l’adresse du début de la liste plutôt que son contenu, c’est-à-dire que, pour chaque valeur de la liste, la ma- 80 M INOS chine conserve deux informations : la valeur et l’adresse de la valeur suivante. ● Création d’une liste : L = [5,8,10] L 5 8 10 ● Suppression d’un élément par son indice : x = L.pop(1). La valeur de la case d’indice 1 est stockée dans la variable x. La machine garde en mémoire l’adresse de la case suivante, détruit la cellule d’indice 1 pour libérer la mémoire et on ré-adresse la case précédente : L 5 8 10 ● Même principe pour l’ajout ou l’insertion : L.append(3). La machine se place à l’endroit voulu, crée une nouvelle case et corrige les liens des maillons de la chaı̂ne : L 5 10 3 Tout cela est totalement transparent pour le programmeur, c’est pour cela que l’on dit que Python est un langage de haut niveau , car on laisse à Python le soin de gérer cela. D’ailleurs la gestion des listes dans ce langage est bien plus complexe que cela de manière à optimiser l’accès aux éléments de celles-ci. Néanmoins, garder à l’esprit le principe de fonctionnement d’une liste en mémoire permet de comprendre certaines subtilités du langage comme le montre l’exemple suivant : 81 C HAPITRE C >>> France = ['Bleu', 'Blanc', 'Rouge'] >>> Roumanie = France >>> Roumanie[1] = 'Jaune' >>> Roumanie ['Bleu', 'Jaune', 'Rouge'] >>> France ['Bleu', 'Jaune', 'Rouge'] >>> id(France), id(Roumanie) (109725216, 109725216) >>> a = 10 >>> b = a >>> b = 3 >>> b 3 >>> a 10 >>> id(a) 1881531232 >>> a = a + 1 >>> id(a) 1881531248 En effet, lorsque l’on tape Roumanie = France, on ne copie pas le contenu de la liste, mais l’adresse (on parle d’alias ). Ainsi lorsque l’on fait la modification sur l’une, on la fait sur l’autre puisque ce sont les mêmes objets. Avec les nombres, on ne rencontre pas ce problème, car les nombres en Python sont des données non mutables : si on change sa valeur, on change d’emplacement mémoire comme le montre l’exemple avec la fonction id qui renvoie l’adresse mémoire de l’objet. Les tuples ressemblent aux listes, on sait déjà qu’on les déclare avec des parenthèses au lieu de crochets, mais ce sont des données non mutables. On ne peut pas ajouter ou retirer des éléments, on ne peut pas les modifier : >>> L = (1,4,5) >>> L[1] 4 >>> L[1] = 7 Traceback (most recent call last): File "<string>", line 301, in runcode File "<interactive input>", line 1, in <module> TypeError: 'tuple' object does not support item assignment Les tuples de longueur 2 sont appelés simplement couples et ceux de longueur 3 sont appelés triplets. Comme pour les listes, on peut récupérer rapidement les valeurs dans des variables distinctes : A = (1, 2, 3) x, y, z = A Enfin on peut passer d’un type à l’autre à l’aide des fonctions list et tuple 82 M INOS list Tuple (1,2,3) Liste [1,2,3] tuple 2 - Dessiner un labyrinthe Dans cette partie nous allons chercher à dessiner un labyrinthe. Dans notre jeu, il sera de dimension 20 × 20 cases. Chaque case est entourée de 0 à 4 murs notés N, S, E, O. À chaque direction on associe un entier, comme l’explique le schéma ci-contre. Pour déterminer le code d’une case, on ajoutera les valeurs des murs présents. Par exemple, la case sera codée par le nombre 1 + 8 = 9. N∶1 E∶2 O∶8 S∶4 E X C4 Pour voir si vous avez compris : 1. Quel est le code représentant la case ? 2. Dessiner la case correspondant au nombre 7. Le labyrinthe peut alors être représenté par une matrice de nombres : image M ⎛13 5 1 7⎞ ⎜11 9 4 7⎟ ⎝12 4 5 7⎠ matrice = [[13, [11, [12, 5, 9, 4, 1, 4, 5, 7], 7], 7]] Python 83 C HAPITRE C E X C5 Pour le moment nous prendrons l’exemple de ce labyrinthe que vous pou- vez télécharger en tapant le code LABY : L = [[13, 5, 5, 5, 1, 1, 5, 7,11,11, 9, 7,11,11, 9, 1, 5, 1, 3,11], [ 9, 5, 5, 5, 6,10,11,11,10, 8, 6, 9, 4, 4, 2,14,11,14, 8, 6], [10, 9, 7,11, 9, 2, 8, 4, 4, 4, 3,10,11,13, 4, 5, 4, 7,10,11], [ 8, 2,13, 2,14,12, 0, 7,13, 5, 4, 4, 0, 7, 9, 7,13, 5, 6,10], [10,12, 1, 4, 7, 9, 6,13, 5, 1, 3,13, 0, 1, 6,13, 3,11,13, 2], [12, 3,12, 3,11,10,13, 5, 5, 6,10, 9, 6, 8, 5, 7,10,10,11,10], [ 9, 4, 3,14,10,12, 5, 5, 5, 7,12, 4, 7, 8, 5, 5, 2,10, 8, 2], [ 8, 7,14,13, 0, 5, 1, 5, 5, 7,13, 5, 3,10, 9, 1, 0, 4, 6,14], [12, 5, 5, 1, 4, 3,12, 1, 7, 9, 3,13, 0, 6,14,10,12, 5, 1, 7], [ 9, 7,11,12, 3,12, 3,14, 9, 2,14,13, 4, 7,13, 2,13, 3, 8, 7], [ 8, 5, 6, 9, 0, 7, 8, 7,10,14,13, 3,11,13, 5, 0, 3,12, 4, 3], [10,13, 3,14,14,11,12, 5, 2, 9, 5, 4, 0, 5, 7,10,14, 9, 3,14], [ 8, 7,10,13, 1, 2, 9, 5, 4, 4, 5, 7,10, 9, 3, 8, 7,10,12, 7], [ 8, 5, 0, 5, 2,14, 8, 5, 7,11,11,11,14,14,12, 4, 1, 4, 7,11], [10, 9, 4, 7,12, 5, 0, 7,11,10,10,12, 5, 3,13, 5, 2,11,11,10], [14,10,13, 3,11,13, 0, 7, 8, 4, 6, 9, 5, 2,11, 9, 4, 4, 0, 6], [ 9, 6, 9, 0, 6,11,12, 5, 2, 9, 1, 0, 7,12, 4, 4, 1, 7, 8, 7], [ 8, 1, 2, 8, 7,10,11,11,14,10,10,14,13, 1, 3,11,14,11,12, 3], [10,10,10,12, 1, 0, 6, 8, 7,10,14, 9, 1, 6,12, 2, 9, 4, 5, 2], [14,14,12, 7,14,12, 5, 4, 7,12, 7,14,12, 7,13, 4, 4, 7,13, 6]] 1. Représenter sur une feuille les premières cases du labyrinthe défini par L. 2. Quelle est la liste représentant la configuration ci-contre ? Si on regarde à présent le code d’une case en écriture binaire, on voit alors facilement où il y a des murs : case code décimal 1 + 2 + 8 = 11 code binaire 2 1011 en liste [1,1,0,1] Noter que dans l’écriture sous forme de liste, le nombre est écrit ≪ à l’envers ≫ pour simplifier les choses. En effet, si L est la liste représentant un nombre, on peut retrouver ce nombre en écriture décimale ainsi : 3 L[0]+2L[1]+4L[2]+8L[3] = L[0]×20 + L[1]×21 + L[2]×22 + L[3]×23 = ∑ L[i]×2i i=0 E X C6 Écrire une fonction mur2bin (on lit ”mur to bin”) qui reçoit un entier n et retourne une liste de 4 éléments représentant la décomposition de n sur 4 bits. Par exemple mur2bin(5) retourne [1,0,1,0]. 84 M INOS E X C7 Écrire une fonction mur qui possède deux paramètres c et m et qui indique par un booléen (True ou False) si le code c d’une case indique la présence d’un mur de type m où m ∈ {1,2,4,8}. Par exemple mur(9,1) retourne True alors que mur(9,2) retourne False. La présence de murs autour d’une case étant maintenant détectable, nous pouvons dessiner le labyrinthe à l’écran. E X C8 On dispose de 2 images de taille 644 × 644 fond.jpg murs.jpg Chaque case du labyrinthe est de taille 32 × 32 pixels. Nous allons dessiner des murs de 4 pixels de large que nous allons prendre dans l’image murs.jpg. À noter que nous ne dessinerons dans chaque case que les murs Nord et Ouest (lorsqu’ils sont présents), car les autres seront construits par les cases voisines. En effet, l’information concernant les murs est présente deux fois : sur le schéma suivant A B représentant un labyrinthe de 2 cases, la présence du mur entre A et B est indiquée dans la case A par le code 2 (Est) et dans la case B par le code 8 (Ouest). Inutile donc de le dessiner deux fois. C’est aussi pour cette raison que l’image de fond est un carré de 644 = 20 × 32 + 4 où les murs Est et Sud de toutes les cases de droite et du bas ont déjà été dessinés. À vous de jouer en écrivant une fonction dessine qui reçoit une liste représentant le labyrinthe et renvoie une surface contenant le fond du jeu. 85 C HAPITRE C 3 - Générer un labyrinthe Notre plateau de jeu est opérationnel, mais quel dommage de toujours devoir jouer sur le même niveau ! Nous allons découvrir comment générer nos propres labyrinthes. En réalité, nous allons nous contenter de générer des labyrinthes dits parfaits. Un labyrinthe parfait est un labyrinthe pour lequel, quelle que soit la case d’entrée et la case de sortie, il existe un et un unique chemin pour s’y rendre. On pourra toujours supprimer des murs existants si l’on souhaite multiplier les possibilités de chemins ensuite ; mais au moins nous serons sûrs qu’il en existe au moins un. Si cette partie ne vous intéresse pas, vous pouvez la passer et y revenir plus tard, quand vous serez fatigué de toujours jouer avec le même décor. Pour ceux qui ont décidé de rester, nous allons construire le labyrinthe via l’algorithme suivant : ● Au départ, toutes les cases contiennent 4 murs et on attribue à chaque case une ”couleur” unique. ● Tant qu’il y a au moins deux couleurs présentes dans le labyrinthe : ◇ on choisit 2 cases voisines possédant des couleurs différentes ; ◇ on casse le mur entre ces deux cases ; ◇ on fusionne les couleurs des deux cases. Ainsi pour deux cases d’une même couleur il existe toujours un chemin qui relie ces deux cases. 86 M INOS ● À la fin, toutes les cases sont de la même couleur, on peut donc relier n’importe quelle case du labyrinthe à n’importe quelle autre (en plus ce chemin est unique, mais c’est une autre histoire). Comme d’habitude, un schéma est souvent plus parlant. En partant de cette situation initiale de 3 × 3 cases : G H D E F A B C G H I G H I G H I G H I D E F D E F D E F D E F A B C A B C A B C A B C on fusionne D et E on fusionne E et B on fusionne F et I on fusionne G et H G H I G H I G H I G H I D E F D E F D E F D E F A B C A B C A B C A B C on fusionne E et F on fusionne C et F on fusionne A et B I on fusionne D et G En pratique, les couleurs importent peu puisqu’elles ne sont utilisées que dans l’algorithme de construction du labyrinthes et ne sont jamais représentées à l’écran. Nous allons utiliser des nombres pour les coder, ce qui sera plus simple et aussi efficace. E X C9 On propose la fonction suivante qui renvoie une liste2D de taille n×n remplie de 0. 1. Le programme fonctionne-t-il si on affiche A ? 2. Si on tape A[1][1] = 3 que se passe-t-il pour A ? 3. Expliquer et corriger ce problème. def init(n) : ligne = [0] * n res = [] for i in range(n) : res.append(ligne) return res A = init(5) E X C10 Écrire une fonction initCoul(n) qui retourne une liste de listes d’entiers deux à deux distincts de taille n × n. On peut casser un mur, s’il y en a un, et si les cases situées de part et d’autre de ce mur ont des couleurs distinctes. En réalité, on peut s’abstenir de tester la présence de murs et ne regarder que les couleurs. En effet, si deux cases voisines sont de couleurs différentes, il n’existe pas encore de chemin pour aller de l’une à l’autre, il y a donc forcément un mur. 87 C HAPITRE C E X C11 Écrire une fonction cassables telle que cassables(couleurs,l,c) retourne la liste des directions dans lesquelles on peut casser un mur appartenant à la case située à la ligne l et colonne c et où couleurs est une grille (de dimension quelconque) représentant les couleurs des cases. Sur l’exemple ci-contre, cassables(coul, 0, 1) renvoie [2,4,8] et cassables(coul, 2, 0) renvoie [1,2]. Maintenant que l’on sait quels murs sont cassables, il ne nous reste plus qu’à les casser ! E X C12 Écrire une fonction casse telle que casse(M,Coul,l,c,d) effec- tue la suppression du mur de la case (l,c) dans la direction d ∈ {1,2,4,8}. Cette fonction ne renvoie rien, mais les grilles M et Coul sont mises à jour. On suppose ici que cette casse est possible. Nous avons à présent toutes les fonctions nécessaires pour réaliser l’algorithme détaillé en début de partie. E X C13 Terminer le programme en créant une fonction laby(n) qui crée un labyrinthe parfait de taille n × n en retournant la liste2D générée. 4 - Les événements clavier, retour au jeu ! Faisons le point... nous savons construire et représenter le terrain de jeu. Il va nous falloir déplacer un personnage dans le labyrinthe. Dans le chapitre précédent, la gestion du clavier avait déjà été programmée pour alléger la difficulté du projet. Regardons de plus près comment cela fonctionne. À chaque fois qu’un événement se produit, celui-ci est mémorisé dans une file qui repose sur le même principe qu’une file d’attente dans la vie courante, c’est-à-dire que le premier événement qui y a été placé sera le premier à être traité. 88 M INOS Récupération par Pygame Arrivée des événements Appuie sur Relâche Fenêtre la touche A la touche CTRL redimensionnée QUEUE Un exemple de file d’événements. TETE Principe FIFO : First In, First Out : Premier arrivé, premier servi ! Le module event de Pygame permet de gérer cette file d’événements. On récupère alors chaque événement sous forme d’objets informatiques de type Event. Comme d’habitude, si vous avez importé Pygame de manière classique, il vous faudra faire précéder ces fonctions de pygame.event Fonction poll() Effet Renvoie (et supprime de la file) l’événement en tête de file. get() Retourne la liste de tous les événements présents dans la file et vide cette file. clear(t) Vide la file des événements de type t. Si t n’est pas précisé, la file est entièrement vidée. wait() Attend qu’un événement se produise puis le renvoie. Lorsqu’un événement est récupéré, on peut avoir accès à diverses informations supplémentaires en fonction du type de celui-ci. Si evt est un événement, on récupère son type avec evt.type. En voici quelques-uns, nous en verrons d’autres au fur et à mesure du livre : ● QUIT : cet événement se produit quand l’utilisateur tente de quitter le programme (clique sur la croix de fermeture de fenêtre). ● KEYUP : cet événement se produit lorsqu’une touche est relâchée. On peut alors récupérer d’autres informations : ◇ evt.key : donne le code de la touche enfoncée. ◇ evt.mod : est un entier indiquant si, en plus de la touche, une touche spéciale est enfoncée ( CTRL , ALT ...), ces valeurs sont 89 C HAPITRE C définies par les constantes Pygame importées lors du from pygame.locals import *. En voici quelques-unes dont les noms parlent d’eux-mêmes : KMOD LSHIFT, KMOD RSHIFT, KMOD LALT, KMOD NUM, KMOD CAPS... On peut aussi ajouter ces valeurs pour détecter des combinaisons de touches. ● KEYDOWN : cet événement se produit lorsqu’une touche est enfoncée. On peut alors récupérer les mêmes informations qu’avec KEYUP et en plus : ◇ evt.unicode : donne sous forme d’une chaı̂ne de caractères la lettre réellement tapée. Contrairement à key, cet attribut unicode tient compte des majuscules et minuscules. Pour les touches spéciales, unicode est la chaı̂ne vide. ◇ evt.scancode : fonctionne comme key, mais le code renvoyé est spécifique au système d’exploitation. Cela sert par exemple pour les touches spéciales (Windows, volume, ...). Après différents essais, voilà une technique que j’utilise souvent pour gérer les touches. C’est la même idée que celle utilisée et présentée dans le premier tome (chapitre M, page 169) avec Tkinter : ● On initialise une liste vide Touches. ● À chaque fois qu’une touche est enfoncée, on l’ajoute à la liste. ● À chaque fois que la touche est relâchée, on la retire de cette liste. Ainsi, en temps réel la liste Touches contient les touches pressées. Avant de la supprimer ou de l’ajouter, on vérifie sa présence dans la liste (imaginer si la touche est pressée ou relâchée alors que la fenêtre du jeu n’est pas active). Comme l’information unicode n’est pas présente au moment où l’on relâche la touche, on va plutôt stocker la valeur evt.key. Ce qui donne ce petit programme. Il existe une autre technique, que nous présenterons plus tard : ... Touches = [] ... while continuer: for event in pygame.event.get(): if event.type == QUIT : continuer = False elif event.type == KEYDOWN and event.key not in Touches : Touches.append(event.key) elif event.type == KEYUP and event.key in Touches : Touches.remove(event.key) ... 90 M INOS On peut alors savoir si une touche est enfoncée en testant si son code est présent dans la liste Touches. Pour ce faire, soit on connaı̂t les valeurs de ces touches, soit on utilise encore une fois les valeurs des constantes définies par Pygame. En voici quelques-unes : K K K K K K K K K K BACKSPACE RETURN UP DOWN RIGHT LEFT SPACE RSHIFT RCTRL RALT Retour arrière Return Flèche haut Flèche bas Flèche droite Flèche gauche Espace Shift droit Control droit ALT droit K K K K K K K K K K TAB PAUSE ESCAPE F1 à K F15 0 à K 9 KP0 à K KP9 a à K z LSHIFT LCTRL LALT Tabulation Pause Echap F1 à F15 0 à 9 0 à 9 clavier numérique A à Z Shift gauche Control gauche ALT gauche ✍ Tout cela semble parfait... un peu trop peut-être. Le module Pygame a été pensé et programmé sous Linux ; de ce fait certains problèmes apparaissent sous Windows, comme la détection du type de clavier... Si vous poussez un peu les essais, vous vous apercevrez que votre clavier est reconnu comme un clavier QWERTY, ainsi vous risquez d’avoir certaines surprises si vous appuyez sur A puisque c’est la touche Q qui est détectée. E X C14 Réaliser un programme qui pourrait servir de page de menu pour le projet. En fond sera placée l’image du jeu avec le personnage qui court de gauche à droite. On ajoute alors l’image ”appuyer sur espace pour commencer” qui clignote. Lorsque l’on appuie sur espace, le programme s’arrête. Comme d’habitude, on dispose des ressources nécessaires, 6 images de notre héros en pleine course : S0.png S1.png S2.png S3.png S4.png S5.png 91 C HAPITRE C Ainsi qu’une image espace.png et du fond fond.jpg E X C15 Pour finir le jeu, il nous faudra choisir une case de départ sous la forme (lD, cD) indiquant respectivement la ligne et la colonne de la case et une case d’arrivée sous le forme (lA, cA). Pour que le jeu ne soit pas trop facile, nous allons imposer que ces cases soient espacées de 10 cases à vol d’oiseau (sans tenir compte des murs). Écrire une fonction distance(l1,c1,l2,c2) qui retourne la distance entre deux cases (l1, c1) et (l2, c2). Attention, on ne peut se déplacer que horizontalement et verticalement, il ne s’agit donc pas de la distance euclidienne classique. À présent, nous pouvons planter le décor et y faire se déplacer le personnage. Nous allons enfin pouvoir jouer ! E X C16 Après avoir généré un labyrinthe parfait et l’avoir affiché, placer le per- sonnage et la case de sortie et terminer le projet pour qu’il soit jouable. Pour simplifier, le personnage ne sera pas animé mais simplement représenté par un pion se déplaçant de case en case. Il en sera de même pour la case d’arrivée : perso.png arrivee.png Lorsque le personnage atteint la case d’arrivée, on se contentera d’afficher ”GAGNE !” dans la console et de stopper le jeu. 5 - Pour aller plus loin : sortir Cette dernière partie peut être passée en première lecture si vous êtes impatient de découvrir le projet suivant. Elle a pour objectif de trouver un 92 M INOS chemin de sortie et de le dessiner pour l’indiquer au joueur. C’est l’occasion de travailler à nouveau sur la notion de file présentée précédemment pour la gestion du clavier. Il existe de nombreux algorithmes pour déterminer un chemin dans un labyrinthe ; en voici un assez simple à mettre en œuvre : un labyrinthe peut être vu comme un graphe où les cellules communicantes sont reliées par des arcs. Sur ce nouvel exemple : Départ Arrivée On peut alors effectuer un parcours en largeur de ce graphe : partant de la case de départ, on marque tous les sommets à une distance de 1, puis ceux à une distance de 2 qui n’ont pas encore été marqués, et ainsi de suite, jusqu’à atteindre la case d’arrivée : Départ 0 1 4 5 1 2 3 6 4 3 8 7 Arrivée 93 C HAPITRE C Il suffit alors de partir cette fois de l’arrivée, et d’aller de case en case dans le sens strictement décroissant pour obtenir le chemin. E X C17 Écrire une fonction dist1 qui reçoit comme paramètres M, l1 , c1 , l2 et c2 représentant respectivement les murs d’un labyrinthe, la case de départ (l1 ,c1 ) et celle d’arrivée (l2 ,c2 ) et retourne une grille avec les distances au départ comme cela vient d’être présenté. On propose de s’appuyer sur l’algorithme suivant : ● sol est initialisée comme une liste 2D de même taille que M remplie de -1 pour indiquer que pour le moment aucune case n’a été visitée ; ● on initialise une variable pas à 0 et on met 0 dans la case de départ (l1 ,c1 ) ; ● on parcourt toutes les cases de sol. À chaque fois que la valeur pas est trouvée, on met pas + 1 dans toutes les cases voisines qui sont accessibles (pas de mur) et qui contiennent la valeur -1 (c’est-à-dire qu’on ne connaı̂t pas encore la distance au départ) ; ● on incrémente pas de 1 et on recommence l’étape précédente. Évidemment, dès que la case de sortie est atteinte, on arrête et retourne la liste2D sol. Si vous souhaitez tester cette fonction avec l’exemple du livre, tapez le numéro de l’exercice pour télécharger le labyrinthe. Sur cet exemple, on trouve comme l’illustre la figure précédente : >>> dist1(L,0,0,2,2) [[0, 1, 4, 5], [1, 2, 3, 6], [4, 3, 8, 7]] E X C18 En utilisant l’exercice précédent, écrire une fonction solution qui reçoit comme paramètres M, l1 , c1 , l2 et c2 représentant respectivement les murs d’un labyrinthe, la case de départ (l1 ,c1 ) et celle d’arrivée (l2 ,c2 ) et retourne la liste des cases indiquant le chemin à prendre pour relier (l1 ,c1 ) à (l2 ,c2 ). Par exemple, sur le labyrinthe du livre : >>> solution(M,(0,0),(2,3)) [(0, 0), (1, 0), (1, 1), (1, 2), (0, 2), (0, 3), (1, 3), (2, 3), (2,2)] E X C19 Écrire enfin une fonction represente qui reçoit un labyrinthe M et une liste de cases donnant le chemin et qui dessine sur la fenêtre la solution à l’aide de petits cailloux de taille 8 × 8. L’image disponible sur le site provient de l’image du soleil que nous avons générée dans ce chapitre. Il ne vous reste plus qu’à incorporer ces fonctions dans votre projet. En appuyant sur la touche S , le chemin apparaı̂t. 94 M INOS Une dernière amélioration pour ce projet serait d’optimiser la fonction dist1 de l’exercice 17. Celle-ci est assez gourmande en temps machine, même si cela semble instantané pour un labyrinthe deMpetite INOS taille. En effet pour chaque valeur de pas, 20 × 20 = 400 tests sont réalisés, ce qui peut ralentir le jeu pour de gros labyrinthes. Au lieu de regarder les valeurs des Une dernière pour ce projet serait d’optimiser la fonction 400amélioration cases pour tester si on peut les affecter à pas+1, il suffit de regarder ist1 de l’exercice 17. Celle-ci est assez gourmande en temps machine, ”autour” des cases de valeurs pas dont nous connaissons la liste puisque ême si cela semble instantané pour un labyrinthe petite précédente. taille. En effet nous venons de les traiter lors de lade boucle Pour cela nous our chaque valeur de pas,une 20 file × 20dans = 400 tests sont ce qui peut allons utiliser laquelle nousréalisés, mémoriserons les cases étudiées : lentir le jeu pour de gros labyrinthes. Au lieu de regarder les valeurs des ● Ausidébut, la les file affecter est initialisée avecillasuffit case de de regarder départ. 00 cases pour tester on peut à pas+1, autour” des cases de valeurs pas dont nous connaissons la liste puisque ● Tant que la case en tête de file n’est pas la case d’arrivée : ous venons de les traiter lors de la boucle précédente. Pour cela nous lons utiliser une file dans laquelle mémoriserons : de la file), ◇ on prend nous la case en tête de fileles (etcases on laétudiées supprime ◇ pour toutes les cases accessibles et non visitées : ● Au début, la file est initialisée avec la casevoisines de départ. ● Tant que la case en tête deon filemet n’est la case : la pas valeur de lad’arrivée case à pas+1, on place cette nouvelle case dans file : ◇ on prend la case en tête de file (et on la supprime de lalafile), ◇ pour toutes◇lesoncases voisines accessibles et nouvelle non visitées recommence l’étude de la case: en tête de file. on met la valeur de la case à pas+1, E X C20 Programmer dans une fonction dist2 ce nouvel algorithme. on place cette nouvelle case dans la file : ◇ on recommence l’étude la nouvelle casede ences tête de file. Pour comparer lesde vitesses de calcul deux fonctions, on a appelé dist1 et dist2 pour se déplacer d’une case à une autre diamétralement opposée d’un labyrinthe de taille n × n généré aléatoirement en faisant vaX C20 Programmer dans une fonction dist2 ce nouvel algorithme. rier n. Voici les courbes des temps obtenues : Pour comparertemps les vitesses de calcul de ces deux fonctions, on a appelé ist1 et dist2 pour se déplacer d’une case à une autre diamétralement pposée d’un labyrinthe de taille n × n généré aléatoirement en faisant vadist1 er n. Voici les courbes des temps obtenues : temps dist2 dist1 valeurs de n 10 20 30 40 50 60 70 dist2 20 30 40 50 60 70 80 90 95 valeurs de n 10 80 90 C HAPITRE C Pour un labyrinthe de taille 20 × 20, la fonction dist2 est deux fois plus rapide, mais pour un de taille 70×70, elle l’est 4 fois plus. En fait si on trace le rapport des temps de l’appel à dist2 par rapport à dist1, on obtient une courbe qui se rapproche de 0. On dit que le temps de dist2 est négligeable par rapport à dist1. 1.0 0.8 0.6 0.4 0.2 0 0 10 20 30 40 50 60 70 80 90 6 - Aide pour les exercices Aide pour l’exercice C1 On rappelle que l’on peut calculer la distance entre deux points A(xA ; yA) et B(xB ; yB ) avec la formule AB = √ (xB − xA )2 + (yB − yA )2 . Il faudra donc sûrement importer la fonction sqrt du module math. Aide pour l’exercice C2 Un quadrillage peut aider (chaque case mesure 16 × 16 pixels) : Aide pour l’exercice C6 Il s’agit là d’un algorithme de décomposition en binaire d’un nombre : prenons un nombre n s’écrivant en liste [a,b,c,d] 2 soit dcba en binaire, alors n = a+2b+4c+8d = a+2(b+2c+4d). Ainsi comme a = 0 ou 1, a est le reste de la division de n par 2 et b + 2c + 4d le quotient. On peut alors recommencer pour trouver b : 96 M INOS n a + 2b + 4c + 8d b + 2c + 4d c + 2d d quotient b + 2c + 4d c + 2d d 0 reste a b c d Avec cet algorithme, on obtient les valeurs de a,b,c et d dans l’ordre inverse de l’écriture binaire classique, mais dans l’ordre souhaité pour notre liste. Aide pour l’exercice C7 N’hésitez pas à réutiliser la fonction précédente. Aide pour l’exercice C10 Inutile de chercher compliqué, il y a n2 cases dans une grille n × n, on peut donc les remplir avec les nombres de 1 à n2 . Aide pour l’exercice C12 Il y a 3 étapes à réaliser : ● Supprimer le mur d dans la case (l,c) ce qui est très facile puisqu’il suffit de retirer d à la valeur de la case. En effet : 1 + 2 + 4 − 2 = 1 + 4. ● Supprimer le mur correspondant dans la case voisine. Pour cela, on peut établir une table de correspondance : 1 ↔ 4 et 2 ↔ 8. ● Fusionner les couleurs des deux cases et de toutes les autres cases de la même couleur. Aide pour l’exercice C14 Pour gérer l’animation, on peut charger les six images une à une : perso0 perso1 perso2 perso3 perso4 perso5 = = = = = = pygame.image.load('S0.png').convert_alpha() pygame.image.load('S1.png').convert_alpha() pygame.image.load('S2.png').convert_alpha() pygame.image.load('S3.png').convert_alpha() pygame.image.load('S4.png').convert_alpha() pygame.image.load('S5.png').convert_alpha() Cependant, la gestion risque ensuite de ne pas être simple. On a tout à gagner à utiliser une liste d’images : perso= [] for i in range(6) : perso.append(pygame.image.load('S'+str(i)+'.png').convert_alpha()) 97 C HAPITRE C Aide pour l’exercice C15 Pour visualiser ces deux distances, c1 on a représenté en pointillés la distance usuelle dE appelée distance euclidienne et en trait plein la distance qui nous intéresse ici dM appelée distance de Manhattan , car elle indique la distance effectuée par une voiture se déplaçant dans une ville au réseau routier en forme de quadrillage. dE = √ (c2 − c1 )2 + (l2 − l1 )2 et c2 l2 l1 dM = ∣c2 − c1 ∣ + ∣l2 − l1 ∣ Aide pour l’exercice C16 Pensez à réutiliser les fonctions que vous avez déjà programmées dans le chapitre. Aide pour l’exercice C20 Nous devons nous-mêmes modéliser la file, le plus simple est d’utiliser une liste : ● File vide : f = [] ● Enfiler x : f.append(x) ● Défiler et stocker dans v : v = f.pop(0) Avec cette solution, la tête de file est représentée à gauche : >>> >>> >>> >>> >>> f = [] f.append(7) f.append(3) x = f.pop(0) f.append(4) # # # # # [] [7] [7,3] [3] [3,4] Initialisation Enfiler 7 Enfiler 3 Défiler (x vaut 7) Enfiler 4 7 - Solutions des exercices Retour sur l’exercice C1 Vues les dimensions de l’image souhaitée, le Soleil a un rayon de 64, on peut le multiplier par 4 pour obtenir un nombre jusque 256, d’où la proposition suivante (n’hésitez pas à faire une figure pour comprendre) : 98 M INOS import pygame from pygame.locals import * from math import sqrt img = pygame.Surface((128,128),SRCALPHA) img.fill((255,255,255,0)) for y in range(128) : for x in range(128) : r = int(sqrt((64-x)**2+(64-y)**2)) if r < 64 : img.set_at((x,y),(255,255,0,4*(63-r))) pygame.image.save(img,'soleil.png') import pygame img = pygame.Surface((128,64)) img.fill((254,245,216)) # Coté gauche for y in range(32) : for x in range(y//2,48+y//2) : img.set_at((x,y),(160,173,192)) img.set_at((x,63-y),(124,138,163)) # Symétrie centrale : for x in range(64) : for y in range(64) : img.set_at((127-x,63-y),img.get_at((x,y))) pygame.image.save(img,'motif.png') Retour sur l’exercice C3 On réalise le pavage à l’aide d’une boucle avec un pas de 128 en abscisse et 64 en ordonnée : import pygame from pygame.locals import * pygame.init() fenetre = pygame.display.set_mode((640,640)) motif = pygame.image.load('motif.png') pygame.display.set_caption('Pavage') pygame.display.set_icon(motif) for x in range(0, 640, 128) : for y in range(0, 640, 64) : fenetre.blit(motif,(x,y)) continuer = True while continuer: for event in pygame.event.get(): if event.type == QUIT : continuer = False pygame.display.flip() pygame.quit() 99 Solutions des exercices Retour sur l’exercice C2 Voici une solution (il y en a plein d’autres) : C HAPITRE C Retour sur l’exercice C4 La case de la première question correspond au code 2+4+8 = 14. Pour la seconde question, on peut décomposer le nombre 7 en 7 = 1 + 2 + 4, il s’agit donc de la case : Retour sur l’exercice C5 Pour la première question, voici l’intégralité du labyrinthe : Pour la seconde, la liste correspondant au dessin est : [[ 9, [10, [12, 7, 9, 3], 9, 2, 14], 6, 12, 7]] Retour sur l’exercice C6 En appliquant l’algorithme proposé dans l’aide, on peut faire ainsi : def mur2bin(n) : """ En entrée : un nombre entier n entre 0 et 15 En sortie : une liste de 4 bits représentant le nombre n en binaire """ resultat = [] for _ in range(4) : resultat.append(n%2) n = n // 2 return resultat 100 M INOS À noter le nom original de la variable dans la boucle, c’est une variable comme les autres, on l’utilise parfois pour signifier que l’on ne l’utilise pas à l’intérieur de la boucle. Vous pouvez mettre i ou j si cela vous perturbe, cela ne change absolument rien. Retour sur l’exercice C7 La première idée peut être de faire un raisonnement par disjonction de cas selon les valeurs de m : Solutions des exercices def mur(c,m) : """ En entrée : c est un entier entre 0 et 15 et m = 1, 2, 4 ou 8 En sortie : Renvoie un booléen indiquant la présence du mur m dans une case de code c """ b = mur2bin(c) if m == 1 : if b[0] == 1 : return True else : return False elif m == 2 : if b[1] == 1 : return True else : return False elif m == 4 : if b[2] == 1 : return True else : return False else : if b[3] == 1 : return True else : return False À noter qu’ici les elif sont inutiles puisque dès que l’on rencontre un return, on sort de la fonction. On peut d’ailleurs utiliser cette remarque pour raccourcir le code en traitant d’abord tous les cas VRAI : def mur(c,m) : """ En entrée : c est un entier entre 0 et 15 et m = 1, 2, 4 ou 8 En sortie : Renvoie un booléen indiquant la présence du mur m dans une case de code c """ b = mur2bin(c) if m == 1 and b[0] == 1 : return True if m == 2 and b[1] == 1 : return True if m == 4 and b[2] == 1 : return True if b[3] == 1 : return True return False 101 C HAPITRE C Une autre technique, un peu plus courte encore et surtout plus cohérente d’un point de vu informatique, peut être de renvoyer le résultat du test, en effet : if b[2] == 1 : return True else : return False peut être remplacé par : return b[2] == 1 Enfin plutôt que d’utiliser 4 tests pour les valeurs de m, on pourrait utiliser avantageusement un dictionnaire : def mur(c,m) : """ En entrée : c est un entier entre 0 et 15 et m = 1,2,4 ou 8 En sortie : Renvoie un booléen indiquant la présence du mur m dans une case de code c """ b = mur2bin(c) indice = {1:0, 2:1, 4:2, 8:3} return b[indice(m)] == 1 On est passé de 21 lignes au départ à 3 lignes, c’est mieux. On peut faire encore plus court et plus rapide en une ligne et même sans passer par la décomposition binaire en utilisant les masques binaires que nous n’aurons pas le temps d’aborder ici. ✍ Plutôt qu’un dictionnaire, les matheux auront remarqué qu’après importation du module math, on peut écrire b = mur2bin(c) indice = int(log2(m)) return b[indice(m)] == 1 ln 1 ln2 ln4 2 ln2 = 0, log2 (2) = = 1, log2 (4) = = =2 ln 2 ln2 ln2 ln2 ln8 ln(23 ) 3 ln2 et log2 (8) = = = = 3. ln2 ln2 ln2 En effet log2 (1) = 102 M INOS Retour sur l’exercice C8 Il suffit de regarder une à une les cases et de tester la présence d’un mur au Nord et à l’Ouest : def dessine(L) : fond = pygame.image.load('fond.jpg') murs = pygame.image.load('murs.jpg') for ligne in range(20) : for colonne in range(20) : if mur(L[ligne][colonne],1) : # Mur au Nord ? x, y = colonne * 32, ligne * 32 fond.blit(murs,(x,y),(x,y,36,4)) if mur(L[ligne][colonne],8) : # Mur à Ouest ? x, y = colonne * 32, ligne * 32 fond.blit(murs,(x,y),(x,y,4,36)) return fond Pour les murs Nord, on copie un rectangle de taille 36 × 4 ce qui peut paraı̂tre surprenant car cela dépasse la largeur d’une case mais vous verrez que les virages sont assez vilains si vous vous restreignez à 32 pixels. Le résultat en ”débordant” est plus joli. Retour sur l’exercice C9 Le programme semble fonctionner à première vue, mais en y regardant de plus près ... Dès que l’on change une cellule de A, toutes les >>> A[1][1] = 3 lignes sont modifiées. Après réflexion, on com>>> A [[0, 3, 0, 0, 0], prend que lorsque l’on ajoute ligne dans la fonc[0, 3, 0, 0, 0], tion, c’est l’alias (l’adresse) de la liste qui est copié [0, 3, 0, 0, 0], et non son contenu. Les 5 lignes de A pointent donc [0, 3, 0, 0, 0], [0, 3, 0, 0, 0]] sur le même objet et dès que l’on en modifie un, les autres se retrouvent aussi modifiés. Avec le code ligne = [0] * 5, cela ne def se produit pas, car lors d’additions, de multiplications ou d’extractions de listes, un nouvel objet est créé. On peut utiliser cette remarque pour corriger la fonction. init(n) : ligne = [0] * n res = [] for i in range(n) : res.append(ligne[:]) return res 103 Solutions des exercices C HAPITRE C Une autre solution élégante est d’utiliser les listes en compréhension : def init(n) : return [[0 for i in range(n)] for j in range(n)] Ou, un peu plus court : def init(n) : return [[0] * n for j in range(n)] Enfin, l’idée lumineuse suivante ne fonctionne pas non plus : def init(n) : return [[0] * n]*n Car l’objet [0]*5 est créé (une fois), puis recopié n fois. Retour sur l’exercice C10 Dans la solution proposée ci-dessous, la boucle sur les j permet de remplir une ligne. Une fois cette ligne complétée, on ajoute à la liste C et on vide la ligne pour commencer la suivante : def initCoul(n) : val = 1 C = [] for i in range(n) : ligne = [] for j in range(n) : ligne.append(val) val = val + 1 C.append(ligne) return C 104 M INOS On peut tester : >>> initCoul(3) [[1, 2, 3], [4, 5, 6], [7, 8, 9]] En remarquant qu’avec cette fonction, le nombre situé à la ligne i et la colonne j vaut 1 + j + i × n, on peut programmer la fonction précédente en une ligne en utilisant des déclarations de listes en compréhension comme cela a été présenté dans l’exercice précédent : def initCoul(n) : return [[1+j+i*n for j in range(n)] for i in range(n)] Retour sur l’exercice C11 Pour cette question, il n’y a pas vraiment d’astuce pour gagner du temps en factorisant le code. On compare les couleurs des cases voisines en prenant garde aux bords : def cassables(couleurs, l, c) : Lmax = len(couleurs)-1 Cmax = len(couleurs[0])-1 murs = [] if l > 0 and couleurs[l][c] != couleurs[l-1][c] : murs.append(1) # Mur au Nord ? if c < Cmax and couleurs[l][c] != couleurs[l][c+1] : murs.append(2) # Mur à l'Est ? if l < Lmax and couleurs[l][c] != couleurs[l+1][c] : murs.append(4) # Mur au Sud ? if c > 0 and couleurs[l][c] != couleurs[l][c-1] : murs.append(8) # Mur à l'Ouest ? return murs Retour sur l’exercice C12 Reprenons les 3 étapes détaillées dans l’aide : ● Pour supprimer le mur en question, rien de plus facile. M[l][c] = M[l][c] - d ● Pour supprimer le mur correspondant, on peut le faire par disjonction de cas : if d elif elif else == 1 : M[l-1][c] = M[l-1][c] - 4 d == 2 : M[l][c+1] = M[l][c+1] - 8 d == 4 : M[l+1][c] = M[l+1][c] - 1 : M[l][c-1] = M[l][c-1] - 2 105 Solutions des exercices C HAPITRE C Si on veut limiter les if, on peut utiliser un dictionnaire de correspondances : mur 1 2 4 8 mur correspondant 4 8 1 2 décalage ligne -1 0 1 0 décalage colonne 0 1 0 -1 corresp = {1 : (4,-1,0), 2 : (8,0,1), 4 : (1,1,0), 8 : (2,0,-1)} new_d, Dl, Dc = corresp[d] M[l+Dl][c+Dc] = M[l+Dl][c+Dc] - new_d ou pour ceux qui aiment les calculs, en utilisant les nombres complexes...(testez pour les 4 valeurs de d possibles, ça fonctionne) n = int(log2(d)) new_d = 2**((n+2) % 4) Dl, Dc = int(((1j)**(n+3)).imag), int(((1j)**(n)).imag) M[l+Dl][c+Dc] = M[l+Dl][c+Dc] - new_d ● Enfin, il faut fusionner les couleurs : ancienne_couleur = Coul[l][c] nouvelle_couleur = Coul[l+Dl][c+Dc] for i in range(len(M)) : for j in range(len(M[0])) : if Coul[i][j] == ancienne_couleur : Coul[i][j] = nouvelle_couleur Il existe des algorithmes bien plus efficaces pour fusionner les couleurs, mais ce n’est pas le but cherché ici. Retour sur l’exercice C13 En suivant l’algorithme donné, on initialise une liste2D de taille n × n ne contenant que la valeur 15 pour modéliser les murs, puis une seconde liste de couleurs. Tant qu’il reste au moins 2 couleurs dans le labyrinthe, on fusionne deux cases au hasard. def laby(n): M = [[15 for i in range(n)] for j in range(n)] C = initCoul(n) nb_coul = n**2 while nb_coul > 1 : l, c = randint(0,n-1), randint(0,n-1) cass = cassables(C,l,c) if len(cass) > 0 : d = choice(cass) casse(M, C, l, c, d) nb_coul = nb_coul - 1 return M 106 élément dans une liste. ✍ Dans le code précédent, on a utilisé la fonction choice du module Ici l’exécution de la fonction est assez rapide (il y a 400 cases à notre random.mais Celle-ci choisit aléatoirement et delamanière équiprobable un labyrinthe), selon la taille, il se peut que casse des derniers murs élément dans une liste. prenne un peu de temps (on peut tirer des cases longtemps sans trouver de murs à casser). On pourrait donc améliorer la fonction de cette manière : si laIci case (l,c) n’a pas defonction mur cassable, au lieu de tirer une case, l’exécution de la est assez rapide (il yàanouveau 400 cases à notre on se décale vers la droite, puis vers le bas si on arrive en bout de ligne, labyrinthe), mais selon la taille, il se peut que la casse des derniers murs jusqu’à un en peu trouver une qui : cases longtemps sans trouver de prenne de temps (onconvienne peut tirer des murs à casser). On pourrait donc améliorer la fonction de cette manière : l’exécution de la est assez rapide (il yàanouveau 400 cases à notre si laIci case (l,c) n’a pas defonction mur cassable, au lieu de tirer une case, def laby(n): mais selon la taille, il se peut que la casse des derniers murs labyrinthe), on se décale vers la droite, puis vers le bas si on arrive en bout de ligne, M = [[15 for i in range(n)] for j in range(n)] prenne un peu de temps (onconvienne peut tirer des jusqu’à trouver une qui : cases longtemps sans trouver de C = en initCoul(n) murs à casser). On nb_coul = n** 2 pourrait donc améliorer la fonction de cette manière : nb_coul > 1 de : mur cassable, au lieu de tirer à nouveau une case, si lawhile case (l,c) n’a pas l, c = randint(0,n-1), randint(0,n-1) on se décale vers la droite, puis vers le bas si on arrive en bout de ligne, def laby(n): cass = cassables(C,l,c) M = en [[15 for i une in range(n)] j in while len(cass) == 0convienne : for # pas de mur à casser ? jusqu’à trouver qui : range(n)] C = initCoul(n) c = c + 1 # On se décale vers la droite nb_coul = ifn** c 2 == n : # Si on arrive au bout de la ligne while nb_coul c > = 1 0 : # On se place au début l, c = randint(0,n-1), randint(0,n-1) l = l + 1 # de la ligne suivante def laby(n): cassif = cassables(C,l,c) l i ==in n range(n)] : # Sijon en bas de la grille M = [[15 for for inarrive range(n)] while len(cass) ==00 : # On pasrepart de murde à la casser ? l, c = 0, première case C = initCoul(n) c =** c2 1 # On se décale vers la droite cass =+cassables(C,l,c) nb_coul = n if c == n : : # Si on arrive au bout de la ligne d nb_coul = choice(cass) while > 1 cC, = l, 0 c, d) # On se place au début casse(M, l, c = randint(0,n-1), randint(0,n-1) l nb_coul = l + 1 - 1 # de la ligne suivante nb_coul = cass = cassables(C,l,c) l == n : == 0 : # bas de return Miflen(cass) while # Si pason dearrive mur à en casser ? la grille l,+c1= 0, 0 # On On se repart de la première case c = c # décale vers la droite cass if c = ==cassables(C,l,c) n : # Si on arrive au bout de la ligne d = choice(cass) c = 0 # On se place au début casse(M,lC, = l, l +c, 1 d) # de la ligne suivante nb_coul nb_coul - 1 if l=== n : # Si on arrive en bas de la grille return M l, c = 0, 0 # On repart de la première case cass = cassables(C,l,c) d = choice(cass) casse(M, C, l, c, d) nb_coul = nb_coul - 1 Retour sur Ml’exercice C14 Après quelques essais pour placer à bonne return hau teur les éléments, voici un programme au rendu sympathique (une version avec du son est disponible sur le site) : 107 Retour sur l’exercice C14 Après quelques essais pour placer à bonne hauteur les éléments, voici un programme au rendu sympathique (une version avec du son est disponible sur le site) : Retour sur l’exercice C14 Après quelques essais pour placer à bonne hau107 teur les éléments, voici un programme au rendu sympathique (une version avec du son est disponible sur le site) : 107 Solutions des exercices ✍ Dans le code précédent, on a utilisé la fonction choice du module M INOS random. Celle-ci choisit aléatoirement et de manière équiprobable un élément dans une liste. C HAPITRE C import pygame from pygame.locals import * pygame.init() pygame.display.set_caption('MINOS') fenetre = pygame.display.set_mode((1280,720)) fond = pygame.image.load('fond.jpg').convert() perso = [pygame.image.load('S'+str(i)+'.png').convert_alpha() for i in ⤦ � range(6)] espace = pygame.image.load('espace.png').convert_alpha() x = -250 espaceT =0 continuer = True clock = pygame.time.Clock() while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False if event.type == KEYDOWN and event.key == K_SPACE : continuer = False fenetre.blit(fond,(0,0)) num_perso = (x//25) % 6 fenetre.blit(perso[num_perso],(x,20)) if espaceT < 20 : fenetre.blit(espace,(440,300)) espaceT = (espaceT + 1) % 30 x = x + 5 if x > 2000 : x = -250 pygame.display.flip() pygame.quit() Quelques explications : on charge les 6 images de l’animation dans une liste perso comme cela a été suggéré dans l’aide. La variable x représente l’abscisse du personnage qui commence à −250 (hors de l’écran à gauche). Si on change l’image à chaque fois que le personnage avance, l’animation est trop rapide et non réaliste. Dans notre correction, j’ai choisi de changer d’image tous les 5 tours de boucle. On peut décider d’utiliser une variable qui sera incrémentée à chaque fois que x est un multiple de 25 (5 tours de 5 pixels). Une solution plus élégante peut être de déterminer le numéro de l’image par le calcul (x // 25) % 6, une explication illustrée sur les premières valeurs de x permet de mieux comprendre : x −250 −200 −150 −100 −50 0 x // 25 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 (x//25)%6 108 2 3 4 5 0 1 2 3 4 5 100 150 200 0 1 50 2 3 4 5 6 7 8 9 0 1 2 3 4 5 0 1 2 3 M INOS M INOS On procède de même avec la variable espaceT qui varie sur [0; 30[. M INOS Entre et 19, l’image est affichée. LorsqueespaceT cette variable arrive à 30, elle On0procède de même avec la variable qui varie sur [0; 30[. Entre 0 età 19, l’image est affichée. Lorsque cette variable arrive à 30, elle espaceT = (espaceT + 1) % 30 retourne 0 grâce au calcul : qui est une On procède de même avec la variable espaceT qui varie sur [0; 30[. espaceT = (espaceT + 1) % 30 retourne à 0 grâce aud’écrire calcul :: qui est une manière plus simple Entre 0 et 19, l’image est affichée. Lorsque cette variable arrive à 30, elle manière plus simple d’écrire : espaceT = (espaceT + 1) % 30 retourne à 0 grâce au calcul : qui est une espaceT = espaceT + 1 if espaceT == simple 30 : manière espaceT =plus espaceT + 1 d’écrire espaceT = 0 if espaceT == 30 : espaceT = 0 espaceT = espaceT + 1 if espaceT == 30 : espaceT = 0 : Retour sur l’exercice C15 Avec la formule présentée dans l’aide, on obtient le sur code suivant C15 : Retour l’exercice Avec la formule présentée dans l’aide, on obtient le code suivant : Retour sur l’exercice Avec def distance(l1, c1, C15 l2, c2) : la formule présentée dans l’aide, on obreturn abs(c2-c1)+abs(l2-l1) tient le code suivant def distance(l1, c1, :l2, c2) : return abs(c2-c1)+abs(l2-l1) def distance(l1, c1, l2, c2) : return À noter queabs(c2-c1)+abs(l2-l1) la fonction abs est présente dans Python sans avoir à importer le module À noter quemath. la fonction abs est présente dans Python sans avoir à importer le module math. À noter que la fonction abs est présente dans Python sans avoir à importer le module math. Retour sur l’exercice C16 On commence par importer les modules et les fonctions (les codes ne sont pas redonnés) Retour surnécessaires l’exercice C16 On commence par importer: les modules et les fonctions nécessaires (les codes ne sont pas redonnés) : Retour sur l’exercice C16 On commence par importer les modules et les import pygame from pygame.locals import * fonctions nécessaires (les codes ne sont pas redonnés) : import pygame from random import randint, choice from pygame.locals import * from random import randint, choice def distance(l1, c1, l2, c2) : ... import pygame def initCoul(n) : ... fromdistance(l1, pygame.locals import def c1, l2, c2) : ... def cassables(couleurs, l,*c) : ... from random import randint, choice def initCoul(n) : ... def casse(M, Coul, l, c, d) : ... def cassables(couleurs, l, c) : ... def laby(n): ... distance(l1, c1, c2)::... ... def casse(M, Coul, l,l2, c, d) def mur2bin(n) : ... laby(n): ...: ... def initCoul(n) def mur(c,m) : ... mur2bin(n) : ... def cassables(couleurs, l, c) : ... def dessine(L) : ... mur(c,m) Coul, : ... l, c, d) : ... def casse(M, def laby(n): ... dessine(L) : ... def mur2bin(n) : ... def mur(c,m) : ... programme principal ne devrait def Le dessine(L) : ... pas poser de problème : Le programme principal ne devrait pas poser de problème : Le programme principal ne devrait pas poser de problème : 109 109 109 Solutions des exercices C HAPITRE C pygame.init() pygame.display.set_caption('MINOS') fenetre = pygame.display.set_mode((644, 644)) perso = pygame.image.load('perso.png').convert_alpha() arrivee = pygame.image.load('arrivee.png').convert_alpha() pygame.display.set_icon(perso) continuer = True Touches = [] L = laby(20) ImgL = dessine(L) lD, cD, lA, cA = [randint(0,19) for _ in range(4)] while distance(lD, cD, lA, cA) < 10 : lD, cD, lA, cA = [randint(0,19) for _ in range(4)] clock = pygame.time.Clock() while continuer: clock.tick(10) for event in pygame.event.get(): if event.type == QUIT : continuer = False elif event.type == KEYDOWN and event.key not in Touches : Touches.append(event.key) elif event.type == KEYUP and event.key in Touches : Touches.remove(event.key) m = L[lD][cD] if K_UP in Touches and not mur(m,1) : lD = lD - 1 elif K_DOWN in Touches and not mur(m,4) : lD = lD + 1 elif K_LEFT in Touches and not mur(m,8): cD = cD - 1 elif K_RIGHT in Touches and not mur(m,2): cD = cD + 1 fenetre.blit(ImgL,(0,0)) fenetre.blit(arrivee,(cA*32+4,lA*32+4)) fenetre.blit(perso,(cD*32+4,lD*32+4)) if distance(lD, cD, lA, cA) == 0 : print('GAGNE !') continuer = False pygame.display.flip() pygame.quit() Quelques remarques : ● lD, cD, lA, cA = [randint(0,19) for _ in range(4)] Permet en une ligne de choisir 4 valeurs aléatoires en générant une liste de 4 nombres que l’on stocke directement dans ces 4 variables. ● On a baissé le nombre de FPS à 10 pour que le jeu ne soit pas trop rapide puisque l’on se promène de case en case. ● Dans les tests de touches, on utilise des elif au lieu de if pour empêcher le joueur d’aller en diagonale. 110 M INOS Retour sur l’exercice C17 Avec les fonctions précédemment définies, on applique l’algorithme proposé : j in range(nb_lignes)] and sol[li-1][co] == -1 : + 1 and sol[li][co+1] == -1 : + 1 and sol[li+1][co] == -1 : + 1 and sol[li][co-1] == -1 : + 1 Retour sur l’exercice C18 On implémente la fin de la technique proposée : dans le code qui suit, les variables l et c indiquent la case où l’on se trouve à l’instant présent, qui sont donc initialisées à la case d’arrivée. La variable pas représente la distance qu’il reste à parcourir pour rejoindre la case de départ. On parcourt alors le chemin à l’envers par valeurs strictement décroissantes de pas, on choisit une (seule) des 4 directions (d’où les elif). def solution(M, l1, c1, l2, c2) : sol = dist1(M, l1, c1, l2, c2) pas, l, c = sol[l2][c2], l2, c2 chemin = [(l,c)] while pas > 0 : if not mur(M[l][c],1) and sol[l-1][c] == pas - 1 : # au Nord ? l = l - 1 elif not mur(M[l][c],2) and sol[l][c+1] == pas - 1 : # à l'Est ? c = c + 1 elif not mur(M[l][c],4) and sol[l+1][c] == pas - 1 : # au Sud ? l = l + 1 else : # à l'Ouest ? c = c - 1 chemin.insert(0,(l,c)) pas = pas - 1 return chemin 111 Solutions des exercices def dist1(M, l1, c1, l2, c2) : nb_lignes, nb_cols = len(M), len(M[0]) sol = [[-1 for i in range(nb_cols)] for pas = 0 sol[l1][c1] = 0 while sol[l2][c2] == -1 : for li in range(nb_lignes) : for co in range(nb_cols) : if sol[li][co] == pas : # Au Nord ? if not mur(M[li][co],1) sol[li-1][co] = pas # A l'Est ? if not mur(M[li][co],2) sol[li][co+1] = pas # Au Sud ? if not mur(M[li][co],4) sol[li+1][co] = pas # A l'Ouest ? if not mur(M[li][co],8) sol[li][co-1] = pas pas = pas + 1 return sol C HAPITRE C À noter la présence du insert qui permet d’insérer la nouvelle case en tête de liste et non à la fin comme avec un append (sinon il vous faudra retourner le chemin généré avant de le renvoyer). Retour sur l’exercice C19 Le code de la fonction tient en 2 lignes, après avoir chargé l’image caillou.png : caillou = pygame.image.load('caillou.png').convert_alpha() ... def represente(L) : for l,c in L : fenetre.blit(caillou,(c*32+14,l*32+14)) Remarquez avec quelle simplicité on peut réaliser une boucle sur les couples (l,c) présents dans la liste L. Sur le site une version mettant en place cette fonction dans le projet final peut être téléchargée en tapant le numéro de cet exercice. Retour sur l’exercice C20 C’est le même principe que pour dist1, mais la double boucle for est remplacée par l’étude de la case (l, c) que l’on vient récupérer à la tête de la file. En temps normal, il faudrait s’assurer que la file n’est pas vide avant de se servir, mais comme ici on est en présence d’un labyrinthe parfait, on est certain de trouver un chemin : def dist2(M, l1, c1, l2, c2) : nb_lignes, nb_cols = len(M), len(M[0]) sol = [[-1 for i in range(nb_cols)] for j in sol[l1][c1] = 0 AVoir = [(l1,c1)] l, c = (l1,c1) while (l, c) != (l2, c2) : l, c = AVoir.pop(0) # Nouvelle case pas = sol[l][c] # En haut ? if not mur(M[l][c],1) and sol[l-1][c] == sol[l-1][c] = pas + 1 AVoir.append((l-1,c)) # Future # A droite ? if not mur(M[l][c],2) and sol[l][c+1] == sol[l][c+1] = pas + 1 AVoir.append((l,c+1)) # En bas ? if not mur(M[l][c],4) and sol[l+1][c] == sol[l+1][c] = pas + 1 AVoir.append((l+1,c)) # A gauche ? if not mur(M[l][c],8) and sol[l][c-1] == sol[l][c-1] = pas + 1 AVoir.append((l,c-1)) return sol 112 range(nb_lignes)] -1 : case à visiter -1 : -1 : -1 : Chapitre D Objectif Programmer un jeu de mots mêlés Programmation Chaı̂nes et encodage des caractères Les expressions régulières Algorithmique Générer une grille de jeu python Le package urllib Pygame Gestion des textes Gestion des événements souris Dessiner des surfaces SELEM-STOM Pour changer, nous allons réaliser un jeu un peu plus cérébral de motsmêlés. Pour ceux qui ne connaissent pas, il s’agit de retrouver des mots cachés dans une grille de lettres, les mots pouvant être écrits dans huit directions, comme l’illustre l’exemple ci-dessous représentant une partie en cours. MOMEL 113 C HAPITRE D 1 - Quelques fonctions pour commencer Dans ce projet, une grille de jeu sera modélisée par une liste2D de caractères. Lorsque l’on parlera d’une case de la grille cela signifiera un couple sous la forme (ligne,colonne). Dans la grille G1, la case (1,3) contient la lettre X. T S A M >>> G1=[['T','S','A','M'], ['Q','E','B','X'], Q E B X ['O','C','R','E'], ['C','O','U','P']] >>> G1[1][3] 'X' O C R E C O U P E X D1 Écrire une fonction affiche qui reçoit une grille, et affiche proprement la grille dans la console. grille G1 >>> affiche(G1) TSAM QEBX OCRE COUP On souhaite à présent lire un mot situé entre deux cases dans une grille. E X D2 Avant de lire le mot, il faut s’assurer qu’il existe un déplacement valide pour lire d’une case à l’autre. 1. Écrire une fonction direction(K1,K2) qui reçoit deux cases et retourne le couple indiquant le déplacement à suivre sous la forme d’un couple (Δligne ,Δcolonne ) pour se déplacer de la case K1 à la case K2 si cela est possible. La fonction renvoie False dans le cas contraire. Par exemple : (−1, −1) >>> direction((0,0),(2,1)) False >>> direction((0,0),(2,2)) (1, 1) >>> direction((0,3),(2,1)) (1, -1) (−1, 0) (−1, 1) (0, −1) (0, 1) (1, −1) (1, 0) (1, 1) 2. Écrire alors la fonction lis mot qui reçoit une grille et deux cases et retourne le mot formé en lisant de la première à la deuxième case lorsque cela est possible. Dans le cas contraire, le mot vide est renvoyé. >>> lis_mot(G1, (2,0), (2,3)) 'OCRE' >>> lis_mot(G1, (3,0), (1,0)) 'COQ' >>> lis_mot(G1, (3,3), (0,0)) 'PRET' 114 >>> lis_mot(G1, (0,1), (2,1)) 'SEC' >>> lis_mot(G1, (2,1), (0,1)) 'CES' >>> lis_mot(G1, (3,3), (0,2)) '' SELEM-STOM Pour ne pas nous lasser trop rapidement, nous allons avoir à générer une grille de jeu aléatoire. Pour remplir celle-ci, il faudra choisir des mots parmi une liste de possibilités. E X D3 Imaginer une fonction choisir, qui reçoit une liste de mots et en tire un au hasard de manière à ce que la probabilité de tirage d’un mot soit proportionnelle à sa longueur. Ainsi, un mot de 8 lettres aura deux fois plus de chance d’être choisi qu’un mot de 4, ce qui permettra d’éviter les trop petits mots dans la grille. Pour tester votre fonction, vous pouvez utiliser le code suivant qui utilise un dictionnaire vu au premier chapitre : le mot CHAT devrait apparaı̂tre environ deux fois moins que BALEINES. mots = ['CHAT', 'LAPIN', 'BALEINES'] nb = {'CHAT' : 0, 'LAPIN': 0 , 'BALEINES' : 0} for i in range(1000) : c = choisir(mots) nb[c] = nb[c] + 1 print(nb) Une fois qu’un mot m aura été choisi et placé dans la grille, il est préférable de le supprimer de la liste des mots possibles. Il faudra supprimer aussi les facteurs du mot m, c’est à dire les mots inclus dans m ainsi que les mots contenant m. Par exemple, si on a placé BOITE, il faut s’interdire les mots BOIT ou BOITER ou encore DEBOITER. E X D4 Écrire une fonction supprime telle que supprime(L,m) efface dans la liste L le mot m ainsi que ceux où l’on retrouve m. La fonction ne renvoie rien, mais la liste L est modifiée. En tapant le numéro de l’exercice qui suit, vous pourrez télécharger sur le site un fichier contenant plus de 3500 mots écrits en majuscules sans accent et d’au minimum 4 lettres. Nous allons charger ce fichier dans une liste de mots. E X D5 Écrire une fonction lis fichier qui reçoit une chaı̂ne de caractères indiquant un nom de fichier texte et retourne une liste où chaque élément est une ligne de ce fichier. Vérifier que Python vous indique bien par exemple que le premier mot a une longueur de 8 ! Se reporter au tome 1, chapitre E si vous voulez revoir comment lire un fichier. 115 C HAPITRE D E X D6 Imaginer une fonction init qui reçoit un entier n et une liste L et ren- voie un couple formé d’un mot choisi au hasard dans la liste L et d’une liste2D de taille n×n où ce mot a été placé quelque part verticalement. Les autres cases vides sont identifiées par le caractère (caractère underscore) . Avant de retourner la grille, on effacera de L les mots proches du mot choisi, comme on l’a fait précédemment. 2 - Remplissage et expressions régulières Nous voilà donc en présence d’une grille qui contient un mot et de nombreux trous qu’il va falloir remplir étape par étape. Imaginons que nous ayons déjà placé les mots SABLE, SOIE et FLUTE comme sur la grille G2 et que nous cherchions à placer un mot à la ligne d’indice 1 de gauche à droite. On peut y placer des mots : E I O T U S A B L E F grille G2 ● de 5 lettres : BOITE, FUITE, PUITS, SUITE, ... ● de 4 lettres en collant à gauche : FAIT, HUIT, LAIT, NUIT, TOIT, ... ● de 4 lettres en collant à droite : CITE, GITE, VITE, ... Il s’agit donc de déterminer tous les mots correspondant au motif IT où le caractère peut être une lettre ou vide... En réalité le problème n’est pas si simple car dans cette même grille à la ligne suivante, le motif est O U , pour autant le symbole du milieu ne peut être un caractère vide : il faut nécessairement une lettre entre le O et le U. Une fois n’est pas coutume, nous allons scinder notre problème en plusieurs fonctions. E X D7 Écrire une fonction debutOK(motif, mot) et qui retourne un booléen indiquant si le mot respecte ou non le motif donné où seuls les symboles de fin de motif peuvent être vides. Voici un certain nombre d’exemples que je vous invite à tester. 116 SELEM-STOM >>> debutOK('__IT_','BOITE') True >>> debutOK('__IT_','FAIT') True >>> debutOK('__IT_','CITE') False >>> debutOK('__IT_','FUITER') False >>> debutOK('__IT_','ICI') True >>> debutOK('__IT_','HA') True Nous pouvons alors terminer le travail demandé : E X D8 Programmer une fonction possible(motif, mot) qui indique par un nombre si le mot respecte ou non le motif donné. La fonction retourne -1 si le mot ne convient pas, sinon, elle retourne le nombre de cases à décaler pour placer le mot. Par exemple : >>> possible('__IT_','BOITE') 0 >>> possible('__IT_','FAIT') 0 >>> possible('__IT_','CITE') 1 >>> possible('__IT_','FUITER') -1 >>> possible('__IT_','ITT') 2 >>> possible('__IT_','Z') 0 Bien compliqué tout cela... mais nous allons pouvoir simplifier (un peu) les choses ! Dans les années 1960, Stephen Cole Kleene travaille sur les ensembles de mots et cherche une manière simple de décrire ces ensembles. Ainsi naissent les expressions régulières : des expressions sous forme de chaı̂nes de caractères, respectant une certaine syntaxe et représentant un ensemble de chaı̂nes de caractères possibles. De nos jours, tous les langages évolués permettent de travailler sur les expressions régulières, le langage Python ne fait pas exception avec le module re présent dans toutes les distributions. Étudions de plus près quelques règles et les caractères spéciaux permettant de générer ces expressions régulières (on utilise parfois le terme regex(regular expression) en abrégé). Comme d’habitude, il ne s’agit que d’un premier tour d’horizon, les expressions régulières pouvant à elles seules constituer un livre. Dans les exemples qui suivent, une expression régulière est donnée (▸), suivie d’exemples de mots reconnus (✓) par cette expression ou non (✘). 117 C HAPITRE D Indiquer les caractères autorisés : on peut ● donner simplement les caractères ▸ PYTHON ✓ PYTHON ✘ Python ● utiliser le point, qui signifie ”n’importe quel caractère” ▸ .AGE ✓ GAGE, MAGE, RAGE ✘ AGE, NAGER, ETAGE ● utiliser des crochets pour donner un ensemble de possibilités : ici un mot commençant par une lettre de B à F et se terminant par un T ou un L. ▸ [B-F].UI[TL] ✓ BRUIT, DEUIL, FRUIT ✘ PLUIE, SEUIL ● utiliser des parenthèses pour faire des groupes ● utiliser le symbole ∣ pour indiquer un choix (ou exclusif) ▸ B[AEIOU](CH|LL|T)E ✓BULLE, BETE, BUCHE ✘BOTTE, BOUCHE ✍ Lorsque l’on met des crochets, on peut utiliser le symbole - pour signifier jusqu’à. Par exemple [A-Za-z] signifie ”une lettre minuscule ou majuscule” alors que [0-9] signifie ”un chiffre”. Voici un petit exercice pour voir si les explications ont été claires ! E X D9 Testez le motif [CHO][AEIOU].., le motif (CHO)[AEIOU].., le motif (CH|O)[AEIOU].. et enfin [C-O][AEIOU].. sur les mots suivants : CAFE, CHOEUR, CHAIR, OEUF, CHENE et NOIX. 118 SELEM-STOM SELEM-STOM Avec le module re, on peut tester des des expressions expressions régulières régulières sur sur des des chaı̂nes de caractères : ● fullmatch(m,ch) : teste si la chaı̂ne chaı̂ne ch ch est est un un représentant représentantdu dumomotif m. ● match(m,ch) : même effet que la fonction fonction précédente, précédente,mais mais teste testesisi la chaı̂ne ch commence par un représentant représentant du du motif motif m. m. ● search(m,ch) : même effet que la fonction fonction précédente, précédente, mais mais teste teste si la chaı̂ne ch contient un représentant représentant du du motif motif m. m. Ces trois fonctions renvoient None (soit (soit "Rien") "Rien") si si le le test test n’est n’est pas pas concluant, sinon elles renvoient un objet objet de de type type sre.SRE sre.SRE Match Match sur lequel nous reviendrons un peu plus plus loin. loin. À À savoir savoir que que le le None None se comporte comme un False lors d’un d’un test. test. Par Par exemple, exemple, on on peut peut tester les réponses de l’exercice précédent précédent ainsi ainsi (code (code MATCH MATCH pour pour télécharger l’exemple) : import re motifs = ['[CHO][AEIOU]..', '(CHO)[AEIOU]..', '(CHO)[AEIOU]..', '(CH|O)[AEIOU]..', '[C-O][AEIOU]..'] '[C-O][AEIOU]..'] chaı̂nes = ['CAFE', 'CHOEUR', 'CHAIR', 'CHAIR', 'OEUF', 'OEUF', 'CHENE', 'CHENE', 'NOIX'] 'NOIX'] for m in motifs : print("-----> Motif",m) for ch in chaı̂nes : if re.fullmatch(m, ch) :: print('OUI :', ch) else : print('NON :', ch) D’autres informations de recherche peuvent peuvent être être présentes présentesdans dansles lesexexpressions régulières comme des contraintes contraintes de de répétitions répétitions :: Indiquer des répétitions : ● ? : signifie une fois ou aucune fois ● + : signifie au moins une fois ● * : signifie aucune, une ou plusieurs fois fois ● {n} : exactement n fois ● {n1 , n2 } : entre n1 et n2 fois (inclus) ● {, n2 } : entre 0 et n2 fois ● {n1 , } : au moins n1 fois ● Si on veut répéter des groupes de symboles, symboles, on on utilise utilisedes desparenthèses. parenthèses. 119 119 C HAPITRE D E X D10 Écrire une expression régulière permettant de reconnaı̂tre si une chaı̂ne de caractères représente un nombre (entier ou décimal). E X D11 Source Wikipédia : Une adresse IP est un numéro d’identification qui est attribué de façon permanente ou provisoire à chaque appareil connecté à un réseau informatique utilisant l’Internet Protocol. Il existe des adresses IP de version 4 (sur 32 bits, soit 4 octets) et de version 6 (sur 128 bits, soit 16 octets). La version 4 est actuellement la plus utilisée. Elle est généralement représentée en notation décimale avec quatre nombres compris entre 0 et 255, séparés par des points, ce qui donne par exemple : 212.85.150.134. Les plages d’adresses IPv4 étant proches de la saturation, les opérateurs incitent à la transition d’IPv4 vers IPv6. Écrire une fonction qui reçoit une chaı̂ne de caractères et, à l’aide d’expressions régulières, renvoie un booléen indiquant si cette chaı̂ne représente une adresse IPv4 valide ou non. ✍ Si on veut détecter le caractère + ou ? ou un autre caractère utilisé dans la syntaxe des regex, on utilise le / comme dans l’exercice qui suit. E X D12 Trouver une expression régulière permettant de vérifier si une chaı̂ne de caractères peut représenter un numéro de téléphone portable. Par exemple le numéro 07 65 43 21 00 peut être saisi de nombreuses manières : "0765432100", "07 65 43 21 00", "07.65.43.21.00", "+33 7 65 43 21 00"... Revenons à notre problème sur un exemple : imaginons que nous souhaitions placer un mot dans cet espace de 6 cases : A E . ● Une première idée est de chercher les mots représentés par l’expression régulière "..A.E." comme le mot PLAIES. ● Mais des mots plus courts peuvent aussi convenir, on peut alors penser à rendre optionnel les places de début et de fin ".{,2}A.E.?". Cette fois, on a en plus les mots PLAIE, RARE... mais il nous manque encore le mot RAT, si on ne prend pas le E... ● Ajoutons alors que le mot peut se terminer par A, A , A E ou A E avec le motif : ".{,2}(A|A.|A.E|A.E.)" ou plus court avec le motif ".{,2}(A|A.|A.E|A.E.?)". Le mot RAT est alors reconnu ! 120 SELEM-STOM Malheureusement cette dernière solution n’est toujours pas satisfaisante. Le mot CES utilisant le E n’est toujours pas représenté dans cette expression... Pas simple. Je vous propose alors l’algorithme suivant illustré avec l’espace de A E 6 cases . Soit n le nombre de lettres du mot que l’on cherche à placer. Soit N le nombre de cases disponibles. SI n > N ALORS : le mot ne convient pas SINON : on fait ≪ glisser ≫ une fenêtre de n cases sur l’espace et l’on compare avec le mot. Par exemple si le mot est composé de 4 lettres, on testera s’il est de la forme A ou A E ou A E E X D13 Réécrire la fonction possible de l’exercice 8 en utilisant l’algorithme précédent et les expressions régulières. On refusera un mot de la forme qui n’aurait alors aucune lettre commune avec le reste de la grille. Pour remplir la grille, nous choisirons aléatoirement une case vide de la grille, puis une direction. E X D14 Écrire une fonction emplacement qui reçoit une grille et qui : ● choisit une case vide, ici ☀(2,3) ; ● une direction, ici (-1,1) ; ● ”recule” jusqu’à un bord de la grille ; ● renvoie les coordonnées de la case en bordure ○, la direction et le motif à chercher. S’il n’y a plus de case vide, la fonction renvoie -1. E I O R ☀ S A B L E A grille G3 Dans notre exemple : >>> emplacement(G3) [(4, 1), (-1, 1), '_B__'] En utilisant les fonctions précédemment programmées, nous pouvons enfin réaliser la fonction qui génère une grille de jeu. E X D15 Écrire une fonction genereGrille qui reçoit un entier n et retourne un couple (G,L) où G est une grille de jeu de taille n × n et L la liste des mots à trouver. 121 C HAPITRE D 3 - Dessiner la grille de jeu Dans cette partie, nous allons nous intéresser à la représentation de la grille, il va donc falloir apprendre à écrire du texte pour générer une image. Dans le projet PONG, la partie qui affichait le score avait déjà été programmée, regardons à présent le module font de plus près. Pour écrire un texte, il faut créer un objet de type Font (Font signifie police de caractères en français). ● Font(f ich, s) renvoie un objet représentant la police du fichier f ich et de taille s. On peut ajouter bold = True ou italic = True si l’on souhaite. Si f ich vaut None la police par défaut du système est chargée. On peut donc fournir le fichier contenant la police désirée en même temps que le projet, ou chercher dans les polices déjà présentes dans le système d’exploitation : ● get fonts() retourne la liste des polices installées dans le système d’exploitation. ● match font(txt) retourne, sous forme de chaı̂ne, le chemin de la police contenant le mot txt. On peut donner plusieurs noms en les séparant par des virgules. Si aucune police n’est trouvée, None est retourné (la police par défaut sera alors chargée). >>> pygame.font.match_font('arial, courier') 'C:\\WINDOWS\\Fonts\\ARIALN.TTF' Une fois l’objet Font créé, il n’est plus possible de modifier sa taille. On peut alors créer une surface en générant un texte : ● f nt.render(txt,l,c) retourne une surface contenant le texte txt écrit avec la police f nt et avec la couleur c donnée sous forme d’un triplet (R,V,B). Le paramètre l (appelé antialias) est un booléen indiquant si le texte doit être lissé. On peut aussi préciser un fond en ajoutant background=.... S’il n’est pas précisé, le fond est transparent. Un exemple simple : 122 SELEM-STOM import pygame pygame.init() fichier = pygame.font.match_font('arial, courier') fnt = pygame.font.Font(fichier, 40) s = fnt.render("COUCOU", True, (255,0,0) ) pygame.image.save(s, 'demo.png') pygame.quit() ● f nt.size(txt) retourne un couple indiquant la largeur et la hauteur en pixels du texte txt écrit avec la police f nt. E X D16 Écrire un programme où le texte ”WINNERS DON’T USE DRUGS” rebondit à l’écran. À chaque rebond, on incrémente de 1 la taille du texte. Ce slogan était présent sur les bornes d’arcades américaines entre les années 1989 et 2000. À partir d’une grille donnée sous forme de liste2D, nous allons devoir générer une image la représentant. La taille des cases va dépendre de la place dont on dispose à l’écran et du nombre de cases de la grille à représenter de manière à maximiser l’espace disponible. E X D17 Écrire une fonction calcule(G, l, h) qui retourne un triplet (c,Δx ,Δy ) indiquant la taille intérieure c (pour côté) maximale d’une case, ainsi que le décalage à effectuer à gauche (Δx ) et en haut (Δy ) pour placer la première case comme l’illustre le schéma ci-dessous pour une grille de 3 lignes et 4 colonnes. l Δy h c Δx 123 C HAPITRE D E X D18 Imaginer une fonction taillePolice(G,c) qui détermine la plus grande taille de police possible pour que les lettres de la grille G ne dépassent pas c pixels. Il n’y a plus qu’à dessiner cette fameuse grille. E X D19 Écrire une fonction dessine(G, l, h) qui retourne une surface représentant la grille G sur une image de taille l×h. La taille de la grille sera calculée afin de maximiser l’espace. Il faut encore afficher sur le côté de la grille les mots à trouver. E X D20 Programmer une fonction dessine mots(M1, M2) qui reçoit deux listes de mots, la première contient les mots à trouver et la seconde les mots déjà trouvés et qui renvoie une surface de taille 300 × 700 où les mots à trouver sont écrits d’une couleur, suivis des mots déjà trouvés d’une autre. On pourra mettre plusieurs mots par ligne en prenant garde de ne pas dépasser l’espace alloué. E X D21 Enfin vous pouvez assembler toutes ces fonctions et avoir un début de jeu. Par exemple, la fenêtre mesurera 1000 × 700 pixels : une grille de 700 × 700 et la liste sur la droite. 124 SELEM-STOM 4 - Gestion de la souris Comme pour le clavier, on peut surveiller grâce à Pygame les actions de la souris. 3 types d’événements sont reconnus : ● MOUSEBUTTONDOWN lorsqu’un bouton de la souris est enfoncé. ● MOUSEBUTTONUP lorsqu’un bouton est relâché. ● MOUSEMOTION lorsque la souris est déplacée. À noter que l’utilisation de la roulette de la souris est assimilée à l’utilisation d’un bouton (bouton n°4 lorsque l’on roule vers le haut et n°5 vers le bas). Lorsqu’un événement souris evt est capturé, deux attributs permettent d’en savoir plus : ● evt.pos : indique sous la forme d’un couple (x,y) la position du pointeur de la souris au moment de l’événement. ● evt.button : Indique le numéro du bouton en action (sur les événements de type MOUSEBUTTONDOWN et MOUSEBUTTONUP). E X D22 Nous allons créer une pastille transparente qui va suivre la souris sur la grille. 1. Écrire une fonction pastille(r, coul) qui renvoie une surface représentant un disque de rayon r et de couleur coul où coul est un quadruplet indiquant une couleur RVGA. 2. Compléter votre programme pour que cette pastille (de diamètre la moitié de la taille d’une case) suive votre souris (centre sur le pointeur de la souris). À présent nous allons modéliser le surlignage d’un mot par un stylo : lorsque l’on déplace la souris avec le bouton enfoncé, la pastille laisse derrière elle une traı̂née... E X D23 Programmer cet effet de manière à ce que le marquage disparaisse dès que le bouton est relâché. 125 C HAPITRE D E X D24 Pour la suite, nous allons devoir convertir les coordonnées cartésiennes de la souris (x,y) en numéros de ligne et de colonne (l,c) correspondants. 1. Écrire une fonction XY2Case(XY, l, Dx, Dy, N) qui reçoit un couple XY de coordonnées cartésiennes, la largeur intérieure l d’une case, le décalage (Dx, Dy) du coin supérieur gauche de la grille de taille N × N par rapport à la fenêtre. Cette fonction renvoie un couple (l,c) indiquant la case présente sous le point de coordonnées XY. 2. Écrire une fonction Case2XY(C, larg, Dx, Dy) ayant l’effet inverse : connaissant la ligne et la colonne d’une case, elle renvoie les coordonnées du point situé en haut à gauche de cette case. Lorsque l’on relâche le bouton, il nous faut étudier le mot entre les 2 cases sélectionnées. E X D25 Programmer l’amélioration suivante : si c’est bien un mot à trouver, ● on colorie de façon nette ce mot, ● on le déplace dans la liste des mots trouvés. Si vous êtes arrivé au bout de l’exercice, vous aurez rapidement remarqué que le fait d’utiliser une pastille, même transparente, pour dessiner un trait de stylo, efface les lettres d’origine. On aimerait dessiner les lettres par-dessus ces marques. Or la fonction dessine représente cette grille sur un fond noir. Une idée pourrait consister à modifier la fonction pour qu’elle dessine sur un fond transparent. Nous allons plutôt nous orienter vers une autre technique qui utilise des paramètres optionnels pour notre fonction. Actuellement la fonction dessine commence ainsi (la mienne en tout cas, mais je pense que la vôtre doit être assez semblable) : def dessine(G, l, h) : c, Dx, Dy = calcule(G, l, h) surf = pygame.Surface((l,h)) surf.fill((0,0,0)) lG, cG = len(G), len(G[0]) ... 126 SELEM-STOM Les différentes lettres sont ensuite placées sur la surface surf. Nous allons modifier la fonction ainsi : def dessine(G, l, h, surf=None) : c, Dx, Dy = calcule(G, l, h) # dessine les lignes if surf == None : surf = pygame.Surface((l,h)) surf.fill((0,0,0)) lG, cG = len(G), len(G[0]) ... Ainsi, si le quatrième paramètre n’est pas précisé, il vaut None, et la variable surf est initialisée comme une surface vide. Dans le cas contraire, on dessinera les lettres au-dessus de la surface surf donnée en argument. Il suffit alors d’ajouter : imgG = dessine(G,700,700,imgG) dans notre code pour redessiner les lettres par-dessus notre marque de stylo. def f onc(a=val) : permet de donner une valeur par défaut au paramètre a de la fonction f onc si celui-ci n’est pas précisé. Ainsi, le projet du jeu de mots mêlés se termine, comme les autres fois, vous pouvez télécharger la version avec le code MOMEL . E X D26 On peut encore réaliser quelques améliorations : 1. Changer la couleur de la pastille à chaque mot trouvé. 2. Écrire ≪ GAGNE ! ≫ à la fin du jeu. 3. Mettre un nombre aléatoire de cases. 5 - Une grille à thème ! Dans cette dernière partie, je vous propose d’étudier la possibilité de réaliser des grilles à thèmes. Pour cela, une petite visite sur le site https://www.rimessolides.com/ va orienter vos recherches... Cet excellent site réalisé par Éric Desrosiers permet, entre autres, d’afficher un ensemble de mots du même champ lexical qu’un mot donné. 127 C HAPITRE C HAPITRE C HAPITRE C HAPITRE D D D D Mot du même champ lexical que PROGRAMMATION Mot du même champ lexical que PROGRAMMATION Mot du même champ lexical que PROGRAMMATION Mot du même champ lexical que PROGRAMMATION En analysant l’adresse de la page web appelée pour afficher le résultat de notre requête ,l’adresse on lit l’adresse : En analysant de la page web appelée pour afficher le résultat En analysant l’adresse de la page web appelée pour afficher le résultat de notre requête , on lit l’adresse : de notre requête ,l’adresse on lit l’adresse : En analysant de la page web appelée pour afficher le résultat de notre requête , on lit l’adresse : https://www.rimessolides.com/motscles.aspx?m=programmation https://www.rimessolides.com/motscles.aspx?m=programmation https://www.rimessolides.com/motscles.aspx?m=programmation https://www.rimessolides.com/motscles.aspx?m=programmation ce qui laisse penser qu’en téléchargeant cette page, on peut récupérer les informations souhaitées avec l’aval de l’auteur que jeon remercie ici pour son ce qui laisse penser qu’en téléchargeant cette page, peut récupérer les ce qui laisse penser qu’en téléchargeant cette page, on peut récupérer les autorisation. informations souhaitées avec l’aval de l’auteur que je remercie ici pour son informations souhaitées avec l’aval de l’auteur que jeon remercie ici pour son ce qui laisse penser qu’en téléchargeant cette page, peut récupérer les autorisation. autorisation. informations souhaitées avec l’aval de l’auteur que je remercie ici pour son autorisation. Le module request du package urllib permet de request téléchargerduune page urllib web ou Le module package Le module request du package urllib toute autre disponible surweb le web permet de ressource télécharger une page ou permet de request téléchargerduune page urllib web ou Le module package dont connaı̂t l’adresse. L’exemple suitoute on autre ressource disponible sur le web toute autre disponible surweb le web permet de ressource télécharger une page ou vant comment télécharger l’image dont illustre on connaı̂t l’adresse. L’exemple suidont on connaı̂t l’adresse. L’exemple suitoute autre ressource disponible sur le web david.jpg surl’image le site vant illustre ci-contre comment hébergée télécharger vant illustre comment télécharger l’image dont on connaı̂t l’adresse. L’exemple suidu livre : david.jpg ci-contre hébergée sur le site david.jpg surl’image le site vant illustre ci-contre comment hébergée télécharger du livre : du livre : david.jpg ci-contre hébergée sur le site 128 du livre : 128 128 128 SELEM-STOM SELEM-STOM SELEM-STOM SELEM-STOM 1 12 12 31 23 42 34 53 45 64 56 576 7 76 7 from urllib.request import urlopen from urllib.request import urlopen from urllib.request import urlopen requete = urlopen('http://creations-numeriques.fr/images/david.jpg') from urllib.request import urlopen requete contenu = = urlopen('http://creations-numeriques.fr/images/david.jpg') requete.read() requete = urlopen('http://creations-numeriques.fr/images/david.jpg') contenu = requete.read() f = open('david.jpg','wb') requete = requete.read() urlopen('http://creations-numeriques.fr/images/david.jpg') contenu = f = open('david.jpg','wb') f.write(contenu) contenu = requete.read() f = open('david.jpg','wb') f.write(contenu) f.close() f = open('david.jpg','wb') f.write(contenu) f.close() f.write(contenu) f.close() f.close() Quelques explications : Quelques explications : Quelques explications : Quelques explications : ● ● ● ● ● ● ● ● ● ● ● ● à la ligne 3, on crée un objet de type http.client.HTTPResponse ; à la ligne 3, on crée un objet de type http.client.HTTPResponse ; à la ligne 3, on crée un objet de type http.client.HTTPResponse ; à la ligne 3, on crée un objet de type http.client.HTTPResponse ; à la ligne 4, on charge la ressource demandée et on stocke le contenu à la ligne 4, on charge la ressource demandée et on stocke le contenu une 4,variable contenu qui est de type Bytes, c’est-à-dire un àdans la ligne on charge la ressource demandée et on stocke le contenu dans une 4,variable contenu qui est de type Bytes, c’est-à-dire un à la ligne on charge la ressource demandée et on stocke le contenu tableau d’octets : les informations lues octet parc’est-à-dire octet (ou par dans une variable contenu qui estsont de type Bytes, un tableau d’octets : les informations lues octet parc’est-à-dire octet (ou par dans une variable contenu qui estsont de type Bytes, un paquet d’octets) et recopiées telles quelles ; tableau d’octets : les informations sont lues octet par octet (ou par paquet d’octets) recopiées telles quelles ; octet par octet (ou par tableau d’octets et : les informations sont lues paquet d’octets) et recopiées telles quelles ; paquet d’octets) et recopiées telles quelles ; sur les 3 dernières lignes, on enregistre le contenu qui a été lu. Le sur les 3 dernières lignes, on enregistre le contenu qui a été lu. Le mode du fichier enregistré est important, il qui indique le sur les’wb’ 3 dernières lignes, on enregistre le contenu a été que lu. Le mode du fichier enregistré est important, il qui indique le sur les’wb’ 3 dernières lignes, on enregistre le contenu a été que lu. Le fichier est ouvert en écriture (w) mais surtout en mode binaire mode ’wb’ du fichier enregistré est important, il indique que(b), le fichier est ouvert en écriture (w) mais surtout en mode binaire mode ’wb’ du fichier enregistré est important, il indique que(b), le c’est-à-dire que chaque octet lu enregistré (sans converfichier est ouvert en écriture (w)est mais surtouttel enquel mode binaire (b), c’est-à-dire que chaque octet lu enregistré (sans converfichier est ouvert en écriture (w)est mais surtouttel enquel mode binaire (b), sion). Ainsique unechaque copie conforme l’image est enregistrée sur le c’est-à-dire octet lu estde enregistré tel quel (sans conversion). Ainsique unechaque copie conforme l’image est enregistrée sur le c’est-à-dire octet lu estde enregistré tel quel (sans converdisqueAinsi dur. une copie conforme de l’image est enregistrée sur le sion). disque dur. une copie conforme de l’image est enregistrée sur le sion). Ainsi disque dur. disque dur. On peut ainsi télécharger n’importe quel fichier disponible sur InterOn peut ainsi télécharger n’importe quel fichier disponible sur Internet,On ce peut une image, une musique quel ou encore undisponible fichier texte. peutêtre ainsi télécharger n’importe fichier surParfois Internet,On ce peut une image, une musique quel ou encore undisponible fichier texte. peutêtre ainsi télécharger n’importe fichier surParfois Intermême le fichier texteimage, peut être dynamique. On parle net, ce peut être une uneretourné musiquede oumanière encore un fichier texte. Parfois même le fichier texteimage, peut être dynamique. On parle net, ce peut être une uneretourné musiquede oumanière encore un fichier texte. Parfois alors d’API. même le fichier texte peut être retourné de manière dynamique. On parle alors mêmed’API. le fichier texte peut être retourné de manière dynamique. On parle alors d’API. alors d’API. Une API (Application Programming Interface) est donc une interUne API (Application Programming Interface) est donc une interface, c’est (Application à dire une manière d’interagir avec un est site donc web (contenant Une API Programming Interface) une interface, c’est (Application à dire une manière d’interagir avec un est site donc web (contenant Une API Programming Interface) une interici une base de données) à l’aide de requêtes. face, c’est à dire une manière d’interagir avec un site web (contenant ici une base de données) à l’aide de requêtes. face, c’est à dire une manière d’interagir avec un site web (contenant ici une base de données) à l’aide de requêtes. ici une base de données) à l’aide de requêtes. 129 129 129 129 C HAPITRE D La plupart des API retournent des informations au format JSON (JavaScript Object Notation ou Notation Objet issue de JavaScript). Il s’agit d’un format d’échange de données. Les navigateurs actuels savent lire ce format et l’afficher de manière lisible. Par exemple, le site http://api.zippopotam.us offre une API gratuite et sans compte à créer (ce qui est assez rare) permettant de retrouver des informations sur les villes d’un code postal donné. En tapant par exemple l’adresse http://api.zippopotam.us/fr/80170 dans votre navigateur, on visualise le résultat ci-contre. Si on souhaite charger et afficher le résultat de la page en Python, il faut commencer par lire le contenu à l’aide de la fonction read sous forme ”brute” (comme dans l’exemple précédent pour le chargement d’une image), puis le convertir en chaı̂ne de caractères. On précise alors l’encodage de la page (le plus souvent UTF-8 de nos jours) pour que Python applique les bonnes règles de conversion : from urllib.request import urlopen requete = urlopen('http://api.zippopotam.us/fr/80170') contenu = requete.read().decode('utf-8') print(contenu) {"post code": "80170", "country": "France", "country abbreviation": "FR", "places": [{"place name": "Guillaucourt", "longitude": "2.6317", "state": "Picardie", "state abbreviation": "B6", "latitude": "49.8425"}, {"place name": "Bayonvillers", "longitude": "2.6287", "state": "Picardie", "state abbreviation": "B6", "latitude": "49.8614"}, {"place name": "Wiencourtl’\u00c9quip\u00e9e", "longitude": "2.6117", "state": "Picardie", "state abbreviation": "B6", "latitude": "49.8462"}, .... {"place name": "Rosi\u00e8res-en-Santerre", "longitude": "2.7009", "state": "Picardie", "state abbreviation": "B6", "latitude": "49.8143"}]} 130 SELEM-STOM On a donc une grande chaı̂ne contenant tous les résultats. Certains caractères spéciaux (les accents par exemple) sont encodés de manière peu lisible d’ailleurs. Il ne nous reste plus qu’à extraire les résultats qui nous intéressent. Cherchons donc toutes les villes correspondant au code postal 80170... Comme le format JSON est très répandu, vous aurez deviné qu’un module permet de gérer les données JSON. Il s’agit du module json : import json from urllib.request import urlopen requete = urlopen('http://api.zippopotam.us/fr/80170') contenu = requete.read().decode('utf-8') contenu = json.loads(contenu) Dans la console : >>> contenu.keys() dict_keys(['country', 'country abbreviation', 'post code', 'places']) >>> contenu['places'] [{'latitude': '49.8425', 'longitude': '2.6317', 'place name': 'Guillaucourt', 'state': 'Picardie', 'state abbreviation': 'B6'}, ..... {'latitude': '49.7977', 'longitude': '2.732', 'place name': 'Méharicourt', 'state': 'Picardie', 'state abbreviation': 'B6'}, {'latitude': '49.7936', 'longitude': '2.7541', 'place name': 'Maucourt', 'state': 'Picardie', 'state abbreviation': 'B6'}] La fonction loads permet donc de convertir le contenu donné sous forme de chaı̂ne de caractères en un dictionnaire. Petite cerise sur le gâteau, les caractères spéciaux sont convertis pour être lus par un humain. E X D27 En analysant la structure du dictionnaire, modifier le programme pour réaliser une fonction qui reçoit en paramètre un code postal et renvoie la liste des villes correspondant à ce code postal. La plupart des sites proposant des API demandent au minimum une inscription. On obtient alors une clé d’identification qui permet de faire des requêtes que l’on place ensuite dans l’adresse pour s’identifier. Ceci évite par exemple de faire un programme qui aspirerait totalement la base de données du site. 131 C HAPITRE D Revenons à notre problématique, c’est-à-dire récupérer à partir du site www.rimessolides.com, les mots du même champ lexical que ≪ programmation ≫. Le site ne prévoit pas de sortie JSON des résultats, nous allons donc faire sans en utilisant la page HTML directement, ce qui n’en sera que plus intéressant. Analysons le code source de la page, vous pouvez obtenir ce contenu avec votre navigateur en effectuant un clic-droit sur la page puis ”Afficher la source”. Ci-dessous, on a volontairement ignoré les lignes peu utiles de la page pour nos besoins : <!DOCTYPE html> <html lang="fr" xml:lang="fr" xmlns="http://www.w3.org/... <head> <meta http-equiv="content-type" content="text/html; charset=... <title>Champ lexical avec programmation</title> <meta name="keywords" content="champs lexicaux, dictionnaire... <meta name="description" content="Champ lexical avec ... <style type="text/css"> ... </style> </head> <body> <div id="fb-root"></div> ... </div> <div class="header"> ... </div> <div class="container"> <div class="container2"> <div class="container-left"> <div class="contenu" style="padding-bottom:30px;"> <div id="divResultat"> <div id="divMotcle"> <h1><span class="first-char">C</span>hamp lexical... </br> <table class="tb-motcle"><tr> <td ><a class="l-black" href="motscles.aspx?m=informatique"> informatique</a> </td> <td ><a class="l-black" href="motscles.aspx?m=langage"> langage</a> </td> ... <td ><a class="l-black" href="motscles.aspx?m=r%c3%a9cursion"> récursion</a> </td> </tr></table> </div> <div id="divMotcleMobile"> .... </html> 132 SELEM-STOM L’objectif n’est pas ici de faire une analyse détaillée du code HTML de la page (n’hésitez pas à consulter l’ouvrage de la même collection sur le thème si cela vous intéresse). On remarque néanmoins que : ● les mots apparaissent après <div id="divMotcle"> ; ● on retrouve ensuite pour chaque mot le motif suivant : <a class="l-black" href="motscles.aspx?m=☀">⧫</a> où ⧫ est le mot à afficher et ☀ le même mot, mais où les caractères spéciaux ont été remplacés par des codes HTML ; ● les mêmes mots sont répétés une seconde fois avec le même format après <div id="divMotcleMobile"> pour un affichage adapté sur téléphone portable. E X D28 En s’appuyant sur les considérations précédentes, écrire une fonction qui reçoit un mot et retourne une liste de mots du même champ lexical. On pourra utiliser la méthode find décrite ci-dessous. txt.find(ch,deb) : renvoie la position de la première occurrence de la chaı̂ne ch dans la chaı̂ne txt à partir de la position deb si elle est précisée (depuis le début sinon). Si la chaı̂ne ch n’est pas présente dans txt, la fonction renvoie -1. Essayons de faire plus simple à l’aide des expressions régulières. Pour cela revenons à l’objet généré lorsque l’on effectue une recherche : L’objet renvoyé par search, match ou fullmatch est soit False lorsque la recherche a été infructueuse, soit de type SRE Match sur lequel on peut appliquer différentes méthodes ou lire certains attributs : ● sre.start() : renvoie la position de départ de la chaı̂ne trouvée. ● sre.end() : renvoie la position de la fin de la chaı̂ne trouvée. ● sre.string : renvoie la chaı̂ne dans laquelle s’est effectuée la recherche. On peut alors récupérer la chaı̂ne ainsi : sre.string[sre.start():sre.end()] ou beaucoup plus simplement : ● sre.group(0) : renvoie la chaı̂ne trouvée. 133 C HAPITRE D ● sre.group(i) : renvoie la partie de la chaı̂ne trouvée correspondant au i ème groupe (à la i ème paire de parenthèses). Prenons un exemple avec un extrait de ce superbe texte du slameur Govrache : ≪ le bout de la table ≫ txt="Je me souviens du bout de la table comme d'un fragment d'éternité. " txt+="C'est Noël. J'ai quatre ans. Je suis le plus jeune de la famille. " txt+="A l'autre bout en face de moi mon arrière-grand-mère est figée " txt+="Elle n'entend plus que des yeux, et semble sourire sans béquille." >>> resultat = re.search('la (chaise|table)',txt) >>> resultat.group(0) 'la table' >>> resultat.group(1) 'table' >>> resultat = re.search('la (chaise|table)(.*)(m{2})e',txt) >>> resultat.group(0) 'la table comme' >>> resultat.group(2) ' co' >>> resultat.group(3) 'mm' Jusque-là, tout va bien... mais imaginons à présent que nous voulions récupérer les noms communs qui se trouvent après le déterminant ”la”. Comme précédemment, on peut chercher ”la ” suivi de n’importe quoi (notre nom commun) suivi ensuite d’un espace : >>> resultat = re.search('la (.*) ',txt) >>> resultat.group(1) "table comme d'un fragment d'éternité.C'est Noël. J'ai [...]sourire sans" Ce qui n’est pas le résultat prévu. Ceci s’explique en allant lire la documentation officielle du package re : The ’*’, ’+’, and ’ ?’ qualifiers are all greedy ; they match as much text as possible. Sometimes this behaviour isn’t desired. Ce qui pourrait se traduire par : les opérateurs *, + et ? sont gourmands et vont tenter de trouver la plus grande chaı̂ne possible. On peut y remédier en ajoutant un second point d’interrogation : *?, +? ou ??. >>> resultat = re.search('la (.*?) ',txt) >>> resultat.group(1) 'table' 134 SELEM-STOM Finissons cet aparté sur les expressions régulières en présentant une fonction qui permet d’effectuer une recherche sur tous les motifs désirés : findall(m, ch) : renvoie sous forme d’une liste l’ensemble des chaı̂nes correspondant au motif m dans le texte ch. >>> re.findall('la (.*?) ',txt) ['table', 'famille.'] S’il y a plusieurs groupes, on a une liste de tuples : >>> re.findall('(la|des) (.*?) ',txt) [('la', 'table'), ('la', 'famille.'), ('des', 'yeux,')] E X D29 Tout compris ? Testons vos connaissances avec un autre chanteur à texte : Carlos et son tirelipinpon ! import re txt = "Tirelipimpon sur le chihuahua" txt+="Tirelipimpon avec la tête avec les bras" txt+="Tirelipimpon un coup en l'air un coup en bas" txt+="Touche mes castagnettes moi je touche à tes ananas" a b c d = = = = re.findall('T.*? ', txt) re.search('.?[aeiuoy]{2,}',txt).group(0) re.search('.*(.?[aeiuoy]{2,})',txt).group(1) re.match('mes',txt) Que contiennent les variables a, b, c et d à la fin du programme ? Revenons à notre projet d’extraire tous les mots d’un même champ lexical qu’un mot donné à l’aide d’une expression régulière : E X D30 Écrire une nouvelle fonction qui reçoit un mot et retourne une liste de mots du même champ lexical en utilisant les expressions régulières. Nous voilà au bout de l’aventure pour ce chapitre, reste un dernier problème à régler : mettre en majuscule les mots récupérés. Le problème serait simple si nous étions anglais et que nous n’utilisions pas d’accents ! Une première solution peut être de lister une à une les correspondances : é → e, ç → c,... Bien heureusement, ce problème s’est posé à d’autres personnes avant nous et un système de normalisation a vu le jour : 135 C HAPITRE D C HAPITRE D C HAPITRE D La normalisation Unicode est une normalisation de texte qui transforme La normalisation Unicode est une normalisation de texte qui transforme C HAPITRE D des caractères ou séquences deest caractères équivalents de entexte représentations fonLa normalisation Unicode une normalisation qui transforme des caractères ou séquences de caractères équivalents en représentations fondamentales afin celles-cide puissent facilement de comparées des caractères ouque séquences caractères équivalents entexte représentations fonLa normalisation Unicode est une être normalisation qui(Wikipédia). transforme damentales afin que celles-ci puissent être facilement comparées (Wikipédia). Nous utiliserons lacelles-ci normalisation NFKD pour comparées Normalization Form damentales afin puissent être facilement (Wikipédia). des caractères ouque séquences de caractères équivalents en représentations fonNous utiliserons la normalisation NFKD pour de Normalization Form La normalisation Unicode est une normalisation texte qui transforme Compatibility Decomposition (ne cherchez pas le K !) que l’on peut Nous utiliserons la normalisation NFKD pour Normalization Form damentales afin que celles-ci puissent être facilement comparées (Wikipédia). Compatibility (ne cherchez pas en le représentations K !) que l’on peut des caractères ouDecomposition séquences de caractères équivalents fonobtenir facilement le module : K !) que l’onForm Compatibility Decomposition (neunicodedata cherchez pasNormalization le peut Nous utiliserons lavia normalisation NFKD pour obtenir facilement via le module unicodedata : damentales afin que celles-ci puissent être facilement comparées (Wikipédia). obtenir facilement via le module : K !) que l’on peut Compatibility Decomposition (neunicodedata cherchez pas le import unicodedata Nous utiliserons la normalisation NFKD pour Normalization Form import unicodedata >>> txt = "Je m'appelle Gaëtan s'écria le garçon, obtenirunicodedata facilement via le module unicodedata : ça c'est sûr" import >>> txt = "Je m'appelle Gaëtan s'écria le garçon, ça c'est sûr" Compatibility (ne cherchez pas le K !) que l’on peut # Etape 1 : On Decomposition normalise la chaı̂ne >>> txt = "Je s'écria le garçon, ça c'est sûr" # Etape 1 Onm'appelle normaliseGaëtan la chaı̂ne import >>> txtunicodedata = : unicodedata.normalize('NFKD', txt) obtenir facilement via le module unicodedata : # Etape 1 : On normalise la chaı̂ne >>> txt = unicodedata.normalize('NFKD', txt) # Etape 2 : Onm'appelle la transforme ens'écria un tableau d'octets >>> txt = "Je Gaëtan le garçon, ça(type c'estBytes sûr" >>> txt 2 = unicodedata.normalize('NFKD', txt) # la transforme enindiquant un tableau d'octets (type Bytes # Etape déjà rencontré) en que les caractères non Etapeunicodedata 1 : : On On normalise la chaı̂ne import # Etape 2 : On la transforme en un tableau d'octets (type Bytes # déjà rencontré) en supprimés indiquanttxt) que les caractères non # compatibles seront >>> txt txt = = unicodedata.normalize('NFKD', >>> "Je m'appelle Gaëtan s'écria leque garçon, ça c'est sûr" # déjà rencontré) en indiquant les caractères non # compatibles seront supprimés # Etape Etape 2 : : On On la transforme en un tableau d'octets (type Bytes >>> txt 1 = txt.encode('ascii', 'ignore') # normalise la chaı̂ne # compatibles seront supprimés >>> txt = txt.encode('ascii', 'ignore') déjà rencontré) en indiquant que les caractères non # Etape On réencode la chaı̂ne en UTF-8 (valeur par défaut) >>> txt 3 = : unicodedata.normalize('NFKD', txt) >>> txt = txt.encode('ascii', 'ignore') # Etape : On réencode la chaı̂ne en UTF-8 (valeur par défaut) # compatibles seront supprimés >>> txt 3 = txt.decode() # Etape 2 : On la transforme en un tableau d'octets (type Bytes # Etape 3 : On réencode la chaı̂ne en UTF-8 (valeur par défaut) >>> txt = txt.decode() txt.encode('ascii', >>> txt = # déjà rencontré) en'ignore') indiquant que les caractères non >>> txt = txt.decode() >>> txt 3 # Etape : On réencode la chaı̂ne en UTF-8 (valeur par défaut) "Je m'appelle Gaetan s'ecria lesupprimés garcon, ca c'est sur" # compatibles seront >>> txt "Je m'appelle Gaetan s'ecria le garcon, ca c'est sur" >>> txt = txt.decode() >>> txt = txt.encode('ascii', 'ignore') "Je m'appelle Gaetan s'ecria le garcon, ca c'est sur" >>> txt 3 : On réencode la chaı̂ne en UTF-8 (valeur par défaut) # Etape "Je Gaetan s'ecria le garcon, ca c'est sur" >>> m'appelle txt = txt.decode() >>> txt "Je m'appelle Gaetan s'ecria le garcon, ca c'est sur" E X D31 Améliorer une dernière fois la fonction de l’exercice précédent pour qu’ E X D31 Améliorer une dernière fois la fonction de l’exercice précédent pour qu’ elle retourne une liste mots en contenant d’autres caEX D 31 Améliorer unededernière foismajuscule. la fonctionLes de mots l’exercice précédent pour qu’ elle retourne une liste de mots en majuscule. Les mots contenant d’autres caractères que des lettres seront ignorés. On pourra utiliser la méthode upper sur elle retourne une liste mots en contenant d’autres caEX D 31 Améliorer unededernière foismajuscule. la fonctionLes de mots l’exercice précédent pour qu’ ractères que des lettres seront ignorés. On pourra utiliser la méthode upper sur les pour mettre automatiquement en majuscules : ractères queprésentées des seront ignorés. On pourra la méthode upper sur ellechaı̂nes retourne unelettres listeci-dessous de mots en majuscule. Lesutiliser mots contenant d’autres cales présentéesune ci-dessous mettre automatiquement en majuscules : E Xchaı̂nes D31 Améliorer dernièrepour fois la fonction de l’exercice précédent pour qu’ les chaı̂nes présentées ci-dessous pour mettre automatiquement en majuscules : ractères que des lettres seront ignorés. On pourra utiliser la méthode upper sur elle retourne une liste de mots en majuscule. Les mots contenant d’autres cales chaı̂nes présentées ci-dessous pour mettre automatiquement en majuscules : ractères que des lettres seront ignorés. On pourra utiliser la méthode upper sur les chaı̂nes présentées pour renvoient mettre automatiquement en majuscules : ch.upper() et ci-dessous ch.lower() respectivement la chaı̂ne ch ch.upper() et ch.lower() renvoient respectivement la chaı̂ne ch en majusculesetetch.lower() minuscules. renvoient respectivement la chaı̂ne ch ch.upper() en majuscules et minuscules. en majusculesetetch.lower() minuscules. renvoient respectivement la chaı̂ne ch ch.upper() en majuscules et minuscules. et plus ch.lower() renvoient respectivement la défaut chaı̂ne par ch Ilch.upper() ne vous reste qu’à remplacer la liste des mots par Ilennemajuscules vous resteetplus qu’à remplacer la liste des mots par défaut par minuscules. cette de mots thème. Il liste ne vous resteà plus qu’à remplacer la liste des mots par défaut par cette liste de mots à thème. cette de mots thème. Il liste ne vous resteà plus qu’à remplacer la liste des mots par défaut par cette liste de mots à thème. vous reste plus qu’à remplacer la liste des mots par défaut par ✍ IlSine vous l’analyse de pages web plus en détails, le ✍ cette liste de êtes motsintéressé à thème.par vous êtes intéressé par l’analyse de pages web plus en détails, le ✍ Si package Beautiful Souppar pourra vousde être utile,web il comporte de nomSi vous êtes intéressé l’analyse pages plus en détails, le package Beautiful Soup pourra vous être utile, il comporte de nom✍ Si breux outils d’analyse, plus simples à utiliser qu’une extraction ”arpackage Beautiful Soup pourra vous être utile, il comporte de nomvous êtes intéressé par l’analyse de pages web plus en détails, le breux outils d’analyse, plus simples à utiliser qu’une extraction ”artisanale” comme nous présentée. breux outils d’analyse, plus simples utiliser extraction ”arpackage Beautiful Soupl’avons pourra vous àêtre utile,qu’une il comporte de nom✍ tisanale” comme nouspar l’avons présentée. Si vous êtes intéressé l’analyse de pages web plus en détails, le tisanale” comme nous l’avons présentée. breux outils d’analyse, plus simples à utiliser qu’une extraction ”arpackage Beautiful Soup pourra vous être utile, il comporte de nomtisanale” comme nous l’avons présentée. breux outils d’analyse, plus simples à utiliser qu’une extraction ”ar136 tisanale” comme nous l’avons présentée. 136 136 136 136 SELEM-STOM 6 - Aide pour les exercices Aide pour l’exercice D3 Une première idée pourrait être de recopier autant de fois le mot que le nombre de lettres qu’il contient, puis de choisir un mot dans cette nouvelle liste. Cependant, d’un point de vue de la mémoire, ce n’est pas du tout optimal et on risque rapidement de dépasser la possibilité de stockage de la liste. Voici une autre solution bien moins gourmande : imaginons que tous les mots soient collés. ● On calcule la longueur totale LT des mots de la liste. ● On choisit une lettre, c’est-à-dire un nombre entre 0 et LT - 1. ● On crée un compteur indice lettre, initialisé à 0 et qui augmente de la taille du mot jusqu’à devenir supérieur ou égal à lettre. Parallèlement, on crée un autre compteur indice mot qui retient le nombre de mots passés. ● On retourne alors le mot correspondant à la lettre tirée. lettre C H A T L A P indice lettre indice mot=0 I N B A L E indice mot=1 I N E S indice mot=2 Aide pour l’exercice D7 Regarder plutôt les cas où le retour est ”FAUX”, dans les autres cas, la réponse sera ”VRAI”. Aide pour l’exercice D12 Le numéro doit : ● commencer par 0 ou +33, ● être suivi éventuellement d’un séparateur (espace, point), ● puis être suivi d’un 6 ou d’un 7, ● et enfin, être suivi de 4 blocs constitués : ◇ d’un éventuel séparateur, ◇ d’un nombre de 2 chiffres. Aide pour l’exercice D15 Vous avez toutes les fonctions nécessaires pour programmer cette ultime fonction. Pensez à mettre un test d’arrêt : si au bout de x essais il est impossible de placer un nouveau mot, on complète la grille par des lettres au hasard. 137 C HAPITRE D Aide pour l’exercice D19 On peut dessiner les traits de la grille pixel par pixel. Aide pour l’exercice D23 À l’aide d’un booléen indiquant si le bouton est enfoncé ou non, on répond facilement à la question. Il suffit de ne pas dessiner le fond pour que la trace reste visible. Aide pour l’exercice D24 Il faut faire attention à deux choses : ● la ligne est en lien avec l’ordonnée alors que la colonne est en lien avec l’abscisse ; ● la largeur de la case est sa largeur intérieure, il faut donc ajouter 1 pour connaı̂tre le nombre de pixels utiles. On a ainsi les relations : Δy c (x,y) l larg ⎧ x = c × (larg + 1) + Δx ⎪ ⎪ ⎪ ⎨ ⎪ ⎪ ⎪ ⎩ y = l × (larg + 1) + Δy Δx Rappelez-vous aussi que les numéros de ligne et de colonne ne doivent pas dépasser l’intervalle d’entiers [0, N[ au cas où le joueur clique hors de la grille ! Aide pour l’exercice D25 Pour tester si le mot surligné est valide, on peut utiliser la fonction lis mot déjà programmée. Pour le coloriage, on pourra simplement faire glisser la pastille sur la surface représentant la grille, de manière à ce que la marque reste visible. 138 SELEM-STOM Aide pour l’exercice D28 Voici un algorithme qui peut répondre à notre problème : Remarque : le curseur deb est initialisé à la position du mot "divMotcleMobile" plutôt que "divMotcle" pour éviter d’avoir les mots 2 fois. Aide pour l’exercice D29 Voici les réponses. Allez voir la correction si vous ne comprenez pas les raisons de ces valeurs : a : [’Tirelipimpon ’, ’Tirelipimpon ’, ’Tirelipimpon ’, ’Touche ’] b : ’hua’ c : ’ou’ d : None 7 - Solutions des exercices Retour sur l’exercice D1 Pas de difficulté pour se mettre en route dans ce nouveau projet : G1=[['T','S','A','M'], ['Q','E','B','X'], ['O','C','R','E'], ['C','O','U','P']] def affiche(G) : for ligne in G : for lettre in ligne : print(lettre, end="") print() Le label end="" permet d’indiquer de ne pas aller à la ligne après le print. On peut aussi utiliser la méthode join sur les chaı̂nes de caractères : 139 Solutions des exercices lire le fichier demandé et placer son contenu dans une chaı̂ne html deb ← place de "divMotcleMobile" deb ← place de "a class="l-black" href="motscles.aspx?m=" après deb deb ← place de ">" après deb TANT QUE deb > 0 : f in ← place de "<" après deb récupérer le mot entre deb et f in deb ← position suivante C HAPITRE D C HAPITRE D C HAPITRE D ch.join(L) : Renvoie une chaı̂ne de caractères où tous les éléments ch.join(L) : Renvoie une chaı̂ne de caractères où tous les éléments de la liste L ont été concaténés en utilisant la chaı̂ne ch comme joinde la liste L ont été concaténés en utilisant la chaı̂ne ch comme joinch.join(L) : Renvoie une chaı̂ne de caractères où tous les éléments ture. Attention, les éléments de la liste L doivent déjà être des chaı̂nes ture. Attention, les éléments de la liste L doivent déjà être des chaı̂nes de la liste L ont été concaténés en utilisant la chaı̂ne ch comme joinde caractères. Par exemple : de caractères. Par ture. Attention, lesexemple éléments: de la liste L doivent déjà être des chaı̂nes >>> caractères. " / ".join(['14','03', '1976']) de Par exemple : >>> " / ".join(['14','03', '1976']) '14 / / " / '14 >>> '14 03 / 1976' 03 / 1976' / ".join(['14','03', '1976']) 03 / 1976' On peut alors raccourcir le code : On peut alors raccourcir le code : On peut alors raccourcir le code : def affiche(G) : def affiche(G) : G : for ligne in for ligne in G : print("".join(ligne)) def affiche(G) : for ligne print("".join(ligne)) in G : print("".join(ligne)) Ou encore plus court : Ou encore plus court : Ou encore plus:court : def affiche(G) def affiche(G) : print("\n".join(["".join(ligne) for ligne in G])) print("\n".join(["".join(ligne) for ligne in G])) affiche(G) : print("\n".join(["".join(ligne) for ligne in G])) def Retour sur l’exercice D2 Voici une idée pour la première question : on calRetour sur l’exercice D2 Voici une idée pour la première question : on calcule l’écart sur les lignes et sur les colonnes. Le déplacement est possible cule l’écart sur les lignes et surune les idée colonnes. Lepremière déplacement est possible Retour sur l’exercice D2 Voici pour la question : on calsi l’un des deux écarts est nul (dans ce cas le produit est nul), ou s’ils sont si l’un des deux écarts estet nul (dans ce cas le produit est nul), ou sont cule l’écart sur les lignes sur les colonnes. Le déplacement est s’ils possible égaux en valeur absolue. Lorsque le déplacement est valide, on calcule égaux en valeur absolue. Lorsque calcule si l’un des deux écarts est nul (dansle cedéplacement cas le produitest estvalide, nul), ouons’ils sont dans une variable long le nombre de pas pour déterminer la direction à dans une long leLorsque nombre le de déplacement pas pour déterminer la direction égaux en variable valeur absolue. est valide, on calculeà prendre prendre dans une variable long le nombre de pas pour déterminer la direction à prendre def direction(K1, K2) : def if direction(K1, : False K1 == K2 : K2) return if K1 K2 : return False c1== = K1 def l1, direction(K1, K2) : l1, c2 c1 = = K2 K1 l2, if K1 == K2 : return False l2, Dc c2 = = l2 K2 Dl, l1, c1 = K1 - l1, c2 - c1 Dl, Dc = l2 c2== - c1 if == 0 l1, or Dc Dl or Dc == -Dl : *Dc l2,Dl c2 = K2 if Dl Dc == 0 or Dc == Dl or Dc == -Dl : * long = max(abs(Dl), abs(Dc)) Dl, Dc = l2 - l1, c2 - c1 long = max(abs(Dl), abs(Dc)) return Dl//long, Dc//long if Dl*Dc == 0 or Dc == Dl or Dc == -Dl : Dl//long, Dc//long elsereturn : long = max(abs(Dl), abs(Dc)) elsereturn : False Dl//long, Dc//long elsereturn return False : return False La formule du calcul du nombre de pas s’explique par le fait que soit l’un La formule du calcul du nombre de pas s’explique par le fait que soit l’un des écarts est nul (déplacement vertical ou horizontal) soit ces écarts sont desformule écarts est (déplacement horizontal) soit ces écarts La dunul calcul du nombrevertical de pas ou s’explique par le fait que soit sont l’un égaux en valeur absolue, dans ce cas, on choisit n’importe lequel des deux. égaux en valeur dans cevertical cas, on choisit n’importe des deux. des écarts est nulabsolue, (déplacement ou horizontal) soitlequel ces écarts sont Cette distance est appelée distance de Tchebychev en dimension 2, elle Cette appelée distance en dimension elle égauxdistance en valeurest absolue, dans ce cas, de on Tchebychev choisit n’importe lequel des2, deux. complète la gamme des distances déjà présentées au chapitre précédent. complète la gamme des distances déjà au chapitre précédent. Cette distance est appelée distance deprésentées Tchebychev en dimension 2, elle complète la gamme des distances déjà présentées au chapitre précédent. 140 140 140 SELEM-STOM def lis_mot(G, K1, K2) : """ G : grille ; K1, K2 cases = (l,c) Retourne le mot """ l1, c1 = K1 D = direction(K1, K2) if D == False : return "" Dl, Dc = D rep = G[l1][c1] while (l1, c1) != K2 : l1, c1 = l1 + Dl, c1 + Dc rep = rep + G[l1][c1] return rep Retour sur l’exercice D3 En appliquant la solution proposée en aide, on obtient le code suivant : def choisir(L) : LT = sum([len(mot) for mot in L]) lettre = randint(0, LT-1) indice_lettre, indice_mot = 0, 0 while indice_lettre < lettre : indice_lettre = indice_lettre + len(L[indice_mot]) indice_mot = indice_mot + 1 return L[indice_mot-1] Retour sur l’exercice D4 A priori, pas de difficulté pour cette fonction si l’on connaı̂t l’opérateur in ou la méthode find qui teste justement la présence d’une chaı̂ne dans une autre : ch1 in ch2 : donne un booléen indiquant si la chaı̂ne ch1 est incluse dans chaı̂ne ch2 Attention cependant si vous utilisez une boucle for : def supprime(L, m) : for l in L : if m in l or l in m : L.remove(l) M = ['BOITE', 'BOIT', 'CODE', 'BOTTE', 'DEBOITER', 'CARTE'] supprime(M, 'BOITE') print(M) 141 Solutions des exercices Pour la seconde question, pas de difficulté, il suffit de se déplacer dans la direction calculée et de reconstruire le mot lettre à lettre : HAPITRE D D C HAPITRE Retourne:: [’BOIT’, [’BOIT’, ’CODE’, ’CODE’, ’BOTTE’, ’BOTTE’, ’CARTE’]... ’CARTE’]...LeLemot motBOIT BOIT Retourne été supprimé supprimé!! Et Et oui, oui, lala fonction fonctionprécédente précédenteest estéquivalente équivalenteauau n’a pas été code : def supprime(L, supprime(L, m) m) :: i == 00 while while ii << len(L) len(L) :: ll == L[i] L[i] if if mm in in ll or or ll in in mm :: L.remove(l) L.remove(l) ii == ii ++ 11 Exécutons étape étape par parétape étapepour pourcomprendre comprendre: : Exécutons LL [’BOITE’, [’BOITE’, ’BOIT’, ’BOIT’, ’CODE’, ’CODE’, ’BOTTE’, ’BOTTE’, ’DEBOITER’, ’DEBOITER’,’CARTE’] ’CARTE’] [’BOIT’, [’BOIT’, ’CODE’, ’CODE’, ’BOTTE’, ’BOTTE’, ’DEBOITER’, ’DEBOITER’,’CARTE’] ’CARTE’] [’BOIT’, [’BOIT’, ’CODE’, ’CODE’, ’BOTTE’, ’BOTTE’, ’DEBOITER’, ’DEBOITER’,’CARTE’] ’CARTE’] [’BOIT’, [’BOIT’, ’CODE’, ’CODE’, ’BOTTE’, ’BOTTE’, ’DEBOITER’, ’DEBOITER’,’CARTE’] ’CARTE’] [’BOIT’, [’BOIT’, ’CODE’, ’CODE’, ’BOTTE’, ’BOTTE’, ’CARTE’] ’CARTE’] inclusion ii ll inclusion ?? 00 BOITE BOITE OUI OUI 11 CODE CODE NON NON 22 BOTTE BOTTE NON NON 33 DEBOITER DEBOITER OUI OUI 44 FIN FIN Lorsqu’un Lorsqu’un élément élémentest estsupprimé, supprimé,les lesindices indicesdes desmots motsqui quisuivent suiventsont sont décalés, mais mais comme comme dans dans lelemême mêmetemps tempson onincrémente incrémentelalavaleur valeurdedei,i, on passe passe certains certains mots. mots.IlIlne nefaut fautdonc doncincrémenter incrémenterl’indice l’indiceque quesisil’on l’onnene trouve pas pas de de mot mot :: def supprime(L, supprime(L, m) m) :: i == 00 while while ii << len(L) len(L) :: ll == L[i] L[i] if if mm in in ll or or ll in in mm :: L.remove(l) L.remove(l) else else :: ii == ii ++ 11 Retour sur sur l’exercice l’exercice D5 D5 Encore Encoreune unefois, fois,lalamission missionn’est n’estpas pastrès trèsdifficile, difficile, cependant cependant l’idée l’idée la la plus plussimple simple: : def lis_fichier(f) lis_fichier(f) :: fichier fichier == open(f,'r') open(f,'r') return return fichier.readlines() fichier.readlines() fichier.close() fichier.close() L = lis_fichier('mots.txt') lis_fichier('mots.txt') print(L[0], print(L[0], len(L[0])) len(L[0])) ne fonctionne fonctionne pas pas car car le le premier premiermot motABAISSER ABAISSERest estannoncé annoncécomme commeun un mot de 99 lettres... lettres... Cela Cela vient vientdu ducaractère caractèrepolluant polluant/n /nsous sousWindows Windowsdéjà déjà évoqué dans dans le le tome tome 1. 1. 142 SELEM-STOM SELEM-STOM Voici une nouvelle nouvelle fonction fonctionqui quirègle règleleleproblème problème: : def def lis_fichier(f) lis_fichier(f) :: fichier = open(f,'r') open(f,'r') return [m.replace('\n','') [m.replace('\n','') for for mm in in fichier.readlines()] fichier.readlines()] fichier.close() fichier.close() def def init(n, L) : G = [['_' for ii in in range(n)] range(n)] for for jj in in range(n)] range(n)] mot = choisir(L) choisir(L) while len(mot) len(mot) >> nn :: mot = choisir(L) choisir(L) ... On On tire au hasard un un mot mot jusqu’à jusqu’àen entrouver trouverun unqui quirentre rentredans danslalagrille... grille... Évidemment Évidemment si vous vous demandez demandezune unegrille grille33××3,3,cela celarisque risquede deposer poserquelquelques ques soucis vu qu’il qu’il n’existe n’existe pas pas de demots motsde demoins moinsde de44lettres. lettres.On Onpourpourrait rait : ● Ajouter une clause clause avec avec un unassert assertjuste justeen endessous dessousdu dudef def: : def init(n, L) L) :: assert(n>3) assert(n>3) Si cette condition condition n’est n’estpas pasrespectée, respectée,leleprogramme programmeplante planteetets’arrête... s’arrête... Finalement ce ce n’est n’est pas pas beaucoup beaucoupmieux mieuxque quede detourner tournerà àl’infini... l’infini... ● Commencer par par supprimer supprimerde delalaliste listeLLtous tousles lesmots motstrop tropgrands grandsetet si cette liste n’est n’est pas pas vide, vide,tirer tirerun unmot. mot.Cette Cettesolution solutionest estd’ailleurs d’ailleurs bien meilleure, meilleure, car car les les mots mots seront serontsupprimés supprimésde deLLune unefois foispour pour toutes. Attention, la solution solution suivante suivantede degauche gauchene nefonctionne fonctionnequ’à qu’àmoitié moitié: : les mots sont bien bien retirés retirés de dela laliste listeavant avantleletirage, tirage,mais maislalaliste listen’est n’est pas modifiée dans dans le le reste restedu duprogramme. programme.L’éditeur L’éditeurPyScripter PyScripterd’Edud’EduPython ne s’y trompe trompe pas pas d’ailleurs d’ailleurs: : 143 143 Solutions des exercices Retour Retour sur l’exercice l’exercice D6 D6 Pas Pas de de difficulté difficulté spécifique spécifiquepour pourcette cettefonction fonction puisque puisque nous avons avons déjà déjà programmé programmépas pasmal malde dechoses... choses...Comme Commed’had’habitude bitude avec les listes listes cela cela vaut vaut la la peine peinede dese seposer posernéanmoins néanmoinsquelques quelques instants.... instants.... C HAPITRE D Dans la fonction de gauche, une nouvelle liste est créée et stockée dans L. Comme le paramètre d’une fonction ne peut être modifié (au sens de prendre une nouvelle adresse), l’objet L devient une variable locale, tout se passe comme prévu à l’intérieur de la fonction, mais lorsque l’on sort de celle-ci, la liste donnée en argument n’a subi aucun changement. Avec la solution de droite, la méthode remove agit directement sur le paramètre L (l’adresse mémoire). Ainsi c’est l’objet passé en argument qui est modifié. Une fois cette subtilité comprise, le reste n’est pas trop compliqué. On place le mot à un emplacement aléatoire sans sortir de la grille. Si aucun mot n’est trouvé, on renvoie le couple (False,False) pour signaler le problème : def init(n, L) : G = [['_' for i in range(n)] for j in range(n)] i = 0 while i < len(L) : m = L[i] if len(m) > n : L.remove(m) else : i = i + 1 if L == [] : return False, False mot = choisir(L) supprime(L, mot) l = randint(0,n-1) c = randint(0,n-len(mot)) for i in range(len(mot)) : G[l][c+i] = mot[i] return G, m Retour sur l’exercice D7 Le mot ne convient pas si : ● il est plus long que le motif, ● une de ses lettres n’est pas la même que la lettre du motif de même rang (si celle-ci n’est pas le symbole ). Cela donne le code suivant : def debutOK(motif, mot) : if len(mot) > len(motif) : return False for i in range(len(mot)) : if motif[i] != "_" and motif[i] != mot[i] : return False return True 144 SELEM-STOM Retour sur l’exercice D8 Il suffit de retirer un à un les premiers caractères du motif pour voir si celui-ci convient : def possible(motif, mot) : dec = 0 while motif != "" : if debutOK(motif, mot) : return dec motif = motif[1:] dec = dec + 1 return -1 Retour sur l’exercice D9 Tous les mots doivent se terminer par une voyelle 1 : (exceptée Y) suivie de 2 lettres. Seul le début peut différer. Le motif [CHO][AEIOU].. signifie que le mot commence par un C, un H ou un 2 : (CHO)[AEIOU].. signifie le mot commence par CHO. O. Le motif 3 : (CH|O)[AEIOU].. signifie que le mot commence par CH Le motif 4 : [C-O][AEIOU].. signifie que le mot comou par O et enfin le motif mence par une lettre entre C et O (C, D, E, ..., N, O). On trouve ainsi les réponses suivantes : Motif 1 2 3 4 CAFE ✓ ✘ ✘ ✓ CHOEUR ✘ ✓ ✘ ✘ CHAIR ✘ ✘ ✓ ✘ OEUF ✓ ✘ ✓ ✓ CHENE ✘ ✘ ✓ ✘ NOIX ✘ ✘ ✘ ✓ Retour sur l’exercice D10 Un nombre (entier ou décimal) en écriture française est composé : ● d’au moins un chiffre (ou plus) : [0-9]+ ● suivi soit de rien (d’où le ? en fin de chaı̂ne), soit d’une virgule et d’au moins un chiffre ensuite : (,[0-9]+)? On obtient ainsi l’expression : [0-9]+(,[0-9]+)? Retour sur l’exercice D11 Une première idée simple serait de faire ainsi : import re def IP(ch) : expr = '[0?9]{1,3}.){3}.([0?9]{1,3}' return re.fullmatch(expr, ch) Mais cela n’est pas tout à fait correct, car l’adresse 999.999.999.999 serait considérée comme valide. Comment alors indiquer un nombre entre 0 et 255 ? 145 Solutions des exercices C HAPITRE D Voici une solution : (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) import re def IP(ch) : octet = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' expr = (octet+'.')*3+octet return re.fullmatch(expr, ch) Retour sur l’exercice D12 Il ne reste plus qu’à traduire les possibilités présentées dans l’énoncé : (0|\+33)[ \.]?[67]([ \.]?[0-9]{2}){4} Retour sur l’exercice D13 En appliquant l’algorithme, on obtient la fonction suivante : import re def possible(motif, mot) : motif = motif.replace('_','.') l = len(mot) for i in range(len(motif)-l+1) : # indice du début de la fenêtre expr = motif[i:i+l] # glissante if expr != '.'*l and re.fullmatch(motif[i:i+l], mot) : return i return -1 # Aucun motif ne correspond Remarquons que si le motif est plus court que le mot, aucune erreur ne se produit puisque l’on n’entre pas dans la boucle, et -1 est bien renvoyé. Retour sur l’exercice D14 La fonction comporte les 4 parties décrites dans l’énoncé : le première partie consiste à trouver une case vide. Cet algorithme a déjà été présenté au cours du projet MINOS. On choisit une case au hasard : si elle n’est pas vide, on regarde à la colonne suivante, puis la ligne suivante... Si on a échoué nbl× nbc fois, c’est qu’il n’y a plus de case libre : def emplacement(G) : # Recherche d'une case vide nbl, nbc, nbe = len(G), len(G[0]), 0 l, c = randint(0,nbl-1), randint(0, nbc-1) while G[l][c] != '_' : c = c + 1 nbe = nbe + 1 if nbe == nbl * nbc :# Plus de cases vides return -1 if c == nbc : c, l = 0, l+1 .../... 146 if l == nbl : l = 0 # Recherche d'une direction Dl, Dc = randint(-1,1), randint(-1,1) while (Dl, Dc) == (0,0) : Dl, Dc = randint(-1,1), randint(-1,1) # On recule jusqu'à un bord while l != 0 and c !=0 and l != nbl-1 and c != nbc-1 : l, c = l-Dl, c-Dc case = l,c # Lit le motif, cette fois-ci, on avance motif = G[l][c] l, c = l+Dl, c+Dc while l >= 0 and c >= 0 and l < nbl and c < nbc : motif = motif + G[l][c] l, c = l+Dl, c+Dc return [case, (Dl,Dc), motif] Retour sur l’exercice D15 Voici une proposition de solution : def genereGrille(n) : L = lis_fichier('mots.txt') G, mot = init(n,L) M = [mot] nbe = 0 # Nombre d'essais infructueux while nbe < 100 : retour = emplacement(G) if retour != -1 : case, direction , motif = retour possibles, positions = [], [] for m in L : p = possible(motif, m) if p != -1 : possibles.append(m) positions.append(p) if len(possibles) == 0 : nbe = nbe + 1 else : nbe = 0 mot = choisir(possibles) supprime(L, mot) pos = positions[possibles.index(mot)] M.append(mot) l , c = case Dl, Dc = direction for lettre in mot : G[l][c] = lettre l, c = l + Dl, c+Dc else: nbe = nbe + 1 # On comble le reste par des lettres : for i in range(n) : for j in range(n) : if G[i][j] == '_' : G[i][j] = choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ') return (G, M) 147 Solutions des exercices SELEM-STOM C HAPITRE D À noter l’utilisation de la fonction choice du module random : choice(x) : renvoie aléatoirement un élément de la liste x ou une lettre de la chaı̂ne x avec équiprobabilité. Retour sur l’exercice D16 Après un jeu de PONG, cet exercice devrait être une formalité ! On définit un vecteur de déplacement (vx, vy) pour gérer les rebonds : import pygame from pygame.locals import * pygame.init() pygame.display.set_caption("WINNERS DON'T USE DRUGS") fenetre = pygame.display.set_mode((770,770)) logo = pygame.image.load('winners.png').convert() continuer = True clock = pygame.time.Clock() x, y = 0, 0 vx, vy = 2,2 taille = 10 while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False fenetre.fill((115,140,156)) # Couleur du fond de l'image fenetre.blit(logo,(300,300)) fnt = pygame.font.Font(None, taille) txt = fnt.render("WINNERS DON'T USE DRUGS", True, (255,255,0)) x, y = x + vx, y + vy l, h = fnt.size("WINNERS DON'T USE DRUGS") if x < 0 : vx = -vx taille = taille + 1 x = 0 if x+l > 770 : vx = -vx taille = taille + 1 x = 770-l if y < 0 : vy = -vy taille = taille + 1 y = 0 if y+h > 770 : vy = -vy taille = taille + 1 y = 770-h fenetre.blit(txt,(x,y)) pygame.display.flip() pygame.quit() 148 SELEM-STOM SELEM-STOM Retour sur l’exercice D17 Cet exercice est plus un exercice de mathématiques que d’algorithmique. Si exercice on note est lG et cG le et de Retour sur l’exercice D17 Cet plus unnombre exercicededelignes mathémacolonnes de la grille, on a à disposition l − (c + 1) pixels en largeur (il tiques que d’algorithmique. Si on note lG et cGGle nombre de lignes et de faut retirer les traits verticaux) et h − (l + 1) pixels en hauteur, qu’il faut G l − (c + 1) pixels en largeur (il colonnes de la grille, on a à disposition G diviser par le nombre de colonnes et de pouren connaı̂tre dimenfaut retirer les traits verticaux) et h − (lG lignes + 1) pixels hauteur,les qu’il faut sions maximales en pixels de chaque case. Comme nous voulons des cases diviser par le nombre de colonnes et de lignes pour connaı̂tre les dimencarrées, on prendra la plusde petite descase. dimensions : sions maximales en pixels chaque Commetrouvées nous voulons des cases des dimensions trouvées : Retour sur l’exercice D18 Après avoir listé les lettres à afficher, on réalise une boucle dans laquelle tailleavoir de lalisté police jusqu’àon ceréalise qu’au Retour sur l’exercice D18 la Après lesaugmente lettres à afficher, moins une des lettres ne rentre plus dans la case : une boucle dans laquelle la taille de la police augmente jusqu’à ce qu’au def taillePolice(G,c) : rentre plus dans la case : moins une des lettres ne L = [] # Commençons par lister toutes les lettres def for taillePolice(G,c) ligne in G : : L = [] Commençons par:lister toutes les lettres for # lettre in ligne for ligne G : not in L : ifin lettre for lettre in ligne : L.append(lettre) lettre in L difficile : taille =if 1 # Sinonnot ce sera de jouer ! L.append(lettre) maxi = 0 taillemaxi = 1 < # c Sinon ce sera difficile de jouer ! while : maxitaille = 0 = taille + 1 while maxi < c : fnt = pygame.font.Font(None, taille) taille = taille + 1 maxi = 0 fnt lettre = pygame.font.Font(None, taille) for in L : maxil, = h 0 = fnt.size(lettre) for if lettre in L : l > maxi l, hmaxi = fnt.size(lettre) = l l > maxi : if h l maxi = h if h > maxi : return taille-1 maxi = h return taille-1 Retour sur l’exercice D19 Cette fonction comporte plusieurs parties : ● on dessine les lignes horizontales verticalesplusieurs pixel parparties pixel (nous Retour sur l’exercice D19 Cette fonctionetcomporte : étudierons plus tard comment faire cela plus efficacement) ; ● on dessine les lignes horizontales et verticales pixel par pixel (nous ● on détermine une taille adéquate decela police. ne pas qu’elle soit étudierons plus tard comment faire plusPour efficacement) ; grosse, j’ai fait le choix ici de 80%ne (4/5) de la taille ● trop on détermine une taille adéquate delimiter police. àPour pas qu’elle soit d’une case ; trop grosse, j’ai fait le choix ici de limiter à 80% (4/5) de la taille ● on dessine d’une case ;chaque lettre en la centrant sur la case. ● on dessine chaque lettre en la centrant sur la case. 149 149 Solutions des exercices def calcule(G, l, h) la : plus petite carrées, on prendra lG, cG = len(G), len(G[0]) def cx calcule(G, l, // h) cG : = (l-cG-1) lG,=cG = len(G), len(G[0]) cy (h-lG-1) // lG cx==min(cx, (l-cG-1) // cG c cy) cy = (l-c (h-lG-1) // lG// 2 Dx *cG-cG-1) c ==min(cx, cy) Dy (h-c*lG-lG-1) // 2 Dx = (l-c // 2 return c,*cG-cG-1) Dx, Dy Dy = (h-c*lG-lG-1) // 2 return c, Dx, Dy C HAPITRE D def dessine(G, l, h) : c, Dx, Dy = calcule(G, l, h) # dessine les lignes surf = pygame.Surface((l,h)) surf.fill((0,0,0)) lG, cG = len(G), len(G[0]) for li in range(lG+1) : for x in range(Dx,Dx+cG*(c+1)+1) : surf.set_at((x,Dy+li*(c+1)),(255,255,255)) for co in range(cG+1) : for y in range(Dy,Dy+lG*(c+1)+1) : surf.set_at((Dx+co*(c+1),y),(255,255,255)) # Dessin des lettres t = taillePolice(G, c*4//5) # On ne garde que 80% de la case for li in range(lG) : for co in range(cG): fnt = pygame.font.Font(None, t) ltxt, htxt = fnt.size(G[li][co]) xtxt = Dx+co*(c+1)+(c-ltxt)//2 ytxt = Dy+li*(c+1)+(c-htxt)//2 texte = fnt.render(G[li][co], True, (255,255,255)) surf.blit(texte, (xtxt, ytxt)) return surf Retour sur l’exercice D20 Dans la proposition qui suit, (x,y) représente l’emplacement du mot à écrire. On décide de garder 25 pixels de bordure pour éviter que les mots ne soient trop collés aux bords. On vérifie pour chaque mot que l’abscisse finale ne dépasse pas 275, sinon on va à la ligne. Pour ne pas recommencer deux fois la même chose, on fusionne les deux listes. On réalise une boucle sur les indices plutôt que sur les mots de manière à surveiller le changement de couleur et à mettre un point plutôt qu’une virgule à la fin du dernier mot : def dessine_mots(M1, M2) : surf = pygame.Surface((300,700)) surf.fill((0,0,0)) fnt = pygame.font.Font(None, 34) x, y = 25, 25 l1, l2, L = len(M1), len(M2), M1+M2 for i in range(l1+l2) : m = L[i]+'.' if i == l1+l2-1 else L[i]+', ' if i < l1 : texte = fnt.render(m, True, (255,255,100)) else : texte = fnt.render(m, True, (125,125,125)) ltxt, htxt = fnt.size(m) if x + ltxt > 275 : x, y = 25, y + htxt surf.blit(texte, (x, y)) x = x + ltxt return surf 150 SELEM-STOM Retour sur l’exercice D21 Cet exercice est corrigé sur le site, mais ne vaut pas la peine d’être présenté ici, il s’agit simplement d’un assemblage de fonctions déjà créées. Retour sur l’exercice D22 def pastille(r, coul) : img = pygame.Surface((2*r,2*r),SRCALPHA) img.fill((255,255,255,0)) for y in range(2*r) : for x in range(2*r) : ray = (r-x)**2+(r-y)**2 if ray < r**2 : img.set_at((x,y),coul) return img 2. Par rapport à l’exercice précédent, on va modifier quelque peu la boucle principale pour surveiller la position de la souris (le code complet est disponible sur le site) : largeur , Dx, Dy = calcule(G, 700, 700) xp, yp, lp = 100,100,largeur*9//10 past = pastille(largeur//2,(255,100,100,200)).convert_alpha() while continuer : for event in pygame.event.get(): if event.type == QUIT : continuer = False if event.type == MOUSEMOTION : xp, yp = event.pos fenetre.blit(imgG,(0,0)) fenetre.blit(past, (xp-lp//2, yp-lp//2)) fenetre.blit(dessine_mots(M1,M2),(700,0)) pygame.display.flip() Les variables xp et yp représentent les coordonnées de la pastille et lp son diamètre. Ces variables sont utiles pour conserver en mémoire ces informations : si on se contente de tracer la pastille lorsque l’événement MOUSEMOTION est déclenché, la pastille disparaı̂t dès que la souris est immobile. 151 Solutions des exercices 1. On peut s’inspirer du premier exercice du chapitre MINOS : C HAPITRE D C HAPITRE D Retour sur l’exercice D23 En appliquant l’astuce donnée en aide : C HAPITRE D Retour sur l’exercice D23 En appliquant l’astuce donnée en aide : enfonce = False while continuer : Retour l’exercice D23 En appliquant l’astuce donnée en aide for sur event in pygame.event.get(): enfonce = False if event.type == QUIT : while continuer : continuer = False for = event in pygame.event.get(): enfonce False if == : if event.type event.type == MOUSEMOTION QUIT : while continuer : xp, yp = event.pos continuer = False for event in pygame.event.get(): if event.type == MOUSEBUTTONDOWN : if event.type event.type == == QUIT MOUSEMOTION : if : enfonce =event.pos True xp, yp = continuer = False if event.type event.type == == MOUSEBUTTONDOWN MOUSEBUTTONUP : : if if event.type == MOUSEMOTION : enfonce = = True False enfonce xp, yp = event.pos if not enfonce : fenetre.blit(imgG,(0,0)) if event.type event.type == MOUSEBUTTONDOWN MOUSEBUTTONUP : : if == fenetre.blit(past, (xp-lp//2, yp-lp//2)) enfonce = True False enfonce = if enfonce if not not enfonce : : fenetre.blit(dessine_mots(M1,M2),(700,0)) fenetre.blit(imgG,(0,0)) if event.type == MOUSEBUTTONUP : pygame.display.flip() fenetre.blit(past, (xp-lp//2, yp-lp//2)) enfonce = False if not enfonce : fenetre.blit(imgG,(0,0)) fenetre.blit(dessine_mots(M1,M2),(700,0)) if not enfonce : pygame.display.flip() (xp-lp//2, yp-lp//2)) fenetre.blit(past, if not enfonce : fenetre.blit(dessine_mots(M1,M2),(700,0)) pygame.display.flip() : Retour sur l’exercice D24 Avec les formules données dans l’aide, on obtient les deux fonctions suivantes : Retour sur l’exercice D24 Avec les formules données dans l’aide, on obtient les deux fonctions suivantes : def XY2Case(XY, l, Dx, Dy, Avec N) : les formules données dans l’aide, on obRetour sur l’exercice D24 x, y = XY tientl,lesc deux fonctions suivantes : = (y-Dy)//(l+1), (x-Dx)//(l+1) def XY2Case(XY, l, Dx, Dy, N) : if y l = < XY 0 : l = 0 x, if c l = >=(y-Dy)//(l+1), N : l = N-1 def l, XY2Case(XY, l, Dx, Dy,(x-Dx)//(l+1) N) : if l c < < 0 0 : : c = 0 if x, y = XY l = 0 if l c >= >= N N : : l c = = N-1 N-1 if l, c = (y-Dy)//(l+1), (x-Dx)//(l+1) return l, cc = 0 if if c l < < 0 0 : : l = 0 if if c l >= >= N N : : c l = = N-1 N-1 def return Case2XY(C, Dx, Dy) : l,:clarg, if c < 0 c = 0 l, c = C if c >= N : c = N-1 return c*(larg+1)+Dx, l*(larg+1)+Dy def Case2XY(C, : return l, clarg, Dx, Dy) l, c = C c*(larg+1)+Dx, l*(larg+1)+Dy def return Case2XY(C, larg, Dx, Dy) : l, c = C return c*(larg+1)+Dx, l*(larg+1)+Dy Retour sur l’exercice D25 Les changements à réaliser sont : ● d’une part lorsque le bouton de la souris est enfoncé, on va garder en Retour sur l’exercice D25 Les changements à réaliser sont : mémoire la case choisie : ● d’une part lorsque le bouton de la souris est enfoncé, on va garder en Retour sur l’exercice D25 Les changements à réaliser sont : mémoire la case choisie : event.type == MOUSEBUTTONDOWN : la souris est enfoncé, on va garder en ●ifd’une part lorsque le bouton de enfonce = True mémoire choisie : posDebla=case XY2Case(event.pos, largeur, Dx, Dy, 7) event.type == MOUSEBUTTONDOWN : enfonce = True posDeb = XY2Case(event.pos, Dx, Dy, 7) if event.type MOUSEBUTTONDOWNlargeur, : enfonce = == True posDeb = XY2Case(event.pos, largeur, Dx, Dy, 7) if 152 152 152 SELEM-STOM if event.type == MOUSEBUTTONUP : enfonce = False posFin = XY2Case(event.pos, largeur, Dx, Dy, 7) # On lit le mot situé entre les cases de début et de fin m = lis_mot(G, posDeb, posFin) if m in M1 : dy, dx = direction(posDeb,posFin) # Coordonnées de la première pastille tracée x, y = Case2XY(posDeb, largeur, Dx, Dy) x, y = x + largeur//4, y + largeur // 4 # Coordonnées de la dernière pastille tracée xfin, yfin = Case2XY(posFin, largeur, Dx, Dy) xfin, yfin = xfin + largeur//4, yfin + largeur//4 while (x,y) != (xfin, yfin) : imgG.blit(past,(x,y)) x, y = x+dx, y + dy # On supprime m des mots à trouver. M1.remove(m) M2.append(m) La ligne x, y = x+largeur//4, y+largeur//4 se justifie par le fait que si l’on veut centrer l’image d’un disque de diamètre l/2 dans un carré de côté l, il faut la placer en la décalant de l/4. l 4 l 4 l 4 Retour sur l’exercice D26 Cet exercice ne présente pas d’intérêt spécial pour la suite, et n’est pas corrigé en détail. Vous pouvez cependant télécharger une solution sur le site. Retour sur l’exercice D27 Dans l’exemple précédent, contenu était un dictionnaire et contenu[’places’] une liste dont chaque élément est une ville représentée à son tour à l’aide d’un dictionnaire. Pour une ville donnée, la clé place name permet de retrouver le nom de celle-ci : import json from urllib.request import urlopen def villes(cp) : liste = [] requete = urlopen('http://api.zippopotam.us/fr/'+str(cp)) contenu = requete.read().decode('utf-8') contenu = json.loads(contenu) for v in contenu['places'] : liste.append(v['place name']) return liste 153 Solutions des exercices ● d’autre part quand le bouton est relâché (c’est là qu’il va y avoir le plus de travail) : C HAPITRE D Retour sur l’exercice D28 En appliquant l’algorithme proposé dans l’aide : import urllib.request as url def rimes1(mot) : reponse = [] page =url.urlopen('https://www.rimessolides.com/motscles.aspx?m='+mot) html = page.read().decode('utf-8') deb = html.find('divMotcleMobile') deb = html.find('<a class="l-black" href="motscles.aspx?m=', deb) deb = html.find('>', deb) while deb > 0 : fin = html.find('<', deb) reponse.append(html[deb+1:fin]) deb = html.find('<a class="l-black" href="motscles.aspx?m=', fin) deb = html.find('>', deb) return reponse Retour sur l’exercice D29 Les réponses ont été présentées dans l’aide, voici les justifications : a : On cherche tous les textes dont le motif est T suivi d’une série de caractères quelconques puis d’un espace. b : On cherche le premier texte contenant un caractère suivi d’au moins 2 voyelles. c : La recherche ressemble à la précédente mais on autorise ”n’importe quoi” avant (le plus long possible). d : On teste si la chaı̂ne commence par ”mes”, ce qui n’est pas le cas. Retour sur l’exercice D30 Vous avez toutes les connaissances nécessaires. Pensez à ne conserver qu’une partie de la page pour ne pas avoir de doublons : def rimes2(mot) : page = url.urlopen('https://www.rimessolides.com/motscles.aspx?m='+mot) html = page.read().decode('utf-8') html = html[:html.index('divMotcleMobile')] return re.findall('<a class="l-black" ⤦ � href="motscles.aspx\?m=.*?">(.*?)</a>',html) 154 SELEM-STOM def rimes3(mot) : liste = [] page = url.urlopen('https://www.rimessolides.com/motscles.aspx?m='+mot) html = page.read().decode('utf-8') html = html[:html.index('divMotcleMobile')] for l in re.findall('<a class="l-black" ⤦ � href="motscles.aspx\?m=.*?">(.*?)</a>',html) : l = unicodedata.normalize('NFKD', l).encode('ascii', ⤦ � 'ignore').decode().upper() if re.fullmatch('[A-Z]*',l) : liste.append(l) return liste Certains mots comme ”C++” ou ”Visual Basic” ont bien disparu de la liste. 155 Solutions des exercices Retour sur l’exercice D31 On récupère les mots, on retire les majuscules, on vérifie qu’ils ne contiennent que des lettres. On les ajoute alors dans la liste des possibilités : Objectif Programmer un jeu ... Programmation Utiliser l’objet Rect de Pygame Introduction à la programmation objet Mathématiques La courbe du chien Chapitre E Cupidon Jaloux de sa beauté, Jupiter jeta un sort à Cupidon le paralysant, l’empêchant ainsi de se retourner... À l’aide de votre arc et de vos flèches, déplacez-vous du mieux que vous pourrez et détruisez les horribles diables qui cherchent à vous dévorer ! Ce jeu m’a été inspiré par l’un de mes brillants étudiants Théo LEONARD en l’aidant à trouver un algorithme de déplacement pour un projet qu’il était en train de réaliser au club informatique du lycée. 157 C HAPITRE E 1 - L’object Rect de Pygame Dans le projet de PONG, les informations (position, dimensions) concernant la balle et les raquettes étaient consignées dans des dictionnaires. En réalité Pygame propose déjà un objet qui permet de gérer des zones rectangulaires très facilement : l’objet Rect. On peut définir une zone rectangulaire de différentes manières : Commande Rect(x,y,l,h) ou Rect((x,y),(l,h)) Effet Retourne un rectangle dont le coin supérieur gauche est situé en (x,y), de largeur l et de hauteur h. img.get rect() Retourne un rectangle dont le coin supérieur gauche se trouve en (0,0) et dont les dimensions sont celles de l’image img. On peut alors récupérer une multitude d’informations : ● des valeurs entières : left=x centerx top=y width=w centery bottom 158 height=h right C UPIDON ● des couples d’entiers donnés sous forme de tuples. topleft midtop topright midleft center bottomleft midbottom midright bottomright size est le couple (largeur, hauteur) ● Méthodes sur les objets de type Rect : Sur les objets Rect, on peut appliquer différentes méthodes (c’est-àdire des fonctions), en voici quelques-unes : Commande Effet R1.copy() Retourne une copie du rectangle R1. R1.move(x,y) Retourne un nouveau rectangle, obtenu comme image du rectangle R1 par ⃗ la translation de vecteur v(x,y). R1.collidepoint((x,y)) Renvoie un booléen indiquant si P(x,y) est à l’intérieur de R1. Renvoie un booléen indiquant s’il y a collision entre les rectangles R1 et R2. Retourne l’index de la première collision entre une liste R de Rects et R1. Si aucune collision n’est trouvée, un index de -1 est renvoyé. Retourne la liste de tous les indices des rectangles de R qui entrent en collision avec le rectangle R1. R1.colliderect(R2) R1.collidelist(R) R1.collidelistall(R) 159 C HAPITRE E ✍ Les bords ”Sud” et ”Est” ne sont pas considérés comme appartenant au rectangle. Plus rigoureusement, le rectangle Rect(a,b,l,h) est composé de l’ensemble des points de coordonnées (x,y) telles que : { a ⩽ x < a+l b ⩽ y < b+h On peut aussi utiliser des objets Rect en arguments avec la méthode blit : surf .blit(source, dest, area) copie sur la surface surf , la surface source dans le coin Nord-Ouest du rectangle dest (sa largeur et sa longueur ne sont pas prises en compte). Si le dernier argument area n’est pas précisé, l’intégralité de la surface est déposée, sinon, seule la partie délimitée par le rectangle area est recopiée. ● Un exemple pour résumer : On peut gérer la balle du pong qui rebondit sur les 4 coins d’une fenêtre très facilement : import pygame pygame.init() fenetre = pygame.display.set_mode((800,600)) continuer = True balle = pygame.image.load("balle.png").convert_alpha() v = (1,2) # Vitesse initiale balleR = balle.get_rect() clock = pygame.time.Clock() while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False fenetre.fill((0,0,0)) fenetre.blit(balle, balleR) if balleR.top < 0 or balleR.bottom > 600 : v = (v[0], -v[1]) if balleR.left < 0 or balleR.right > 800 : v = (-v[0], v[1]) balleR.move_ip(v) pygame.display.flip() pygame.quit() 160 C UPIDON 2 - Mise en place de Cupidon Pour ce jeu, on dispose d’un certain nombre d’images pour animer le personnage principal. A l’arrêt : perso 0.png perso 1.png perso 2.png perso 3.png Lorsque l’on décroche une flèche : perso 4.png perso 5.png perso 6.png perso 7.png Lorsque l’on est touché : perso 8.png perso 9.png perso 10.png perso 11.png perso 12.png Tous ces éléments se trouvent dans un fichier compressé téléchargeable sur le site en tapant CUPIDON . E X E1 Dans une fenêtre 1000 × 600, planter le décor : 1. Placer le fond ainsi que le personnage (on ne prendra que l’image perso 1 pour commencer par exemple). 2. Gérer les actions des flèches de directions pour déplacer le personnage. On prendra soin de définir un objet Rect qui ”suivra” le personnage pour gérer les dépassements de l’écran comme cela vient d’être présenté. Pour réaliser l’exercice précédent, vous vous serez sûrement inspiré du jeu MINOS déjà programmé en surveillant les événements KEYUP et KEYDOWN et en conservant en mémoire la liste des touches enfoncées. Pygame propose une fonction qui fait le travail à notre place : 161 C HAPITRE E get pressed() : cette fonction du module key de Pygame renvoie un tableau de booléens (plus exactement de 0 et de 1) indiquant une liste de l’état des touches enfoncées au moment de l’appel. Les indices correspondent aux codes des touches. Par exemple, si vous voulez savoir si la touche espace est enfoncée, il vous suffit de taper : touches = pygame.key.get_pressed() if touches[K_SPACE] : # Touche espace enfoncée ... Pour préciser : en Python les booléens sont identifiés avec les nombres 0 (Faux) et 1 (Vrai) : >>> True + True 2 E X E2 Utiliser cette nouvelle fonction pour alléger le code précédent. En fait les deux méthodes présentées pour surveiller le clavier sont intéressantes à connaı̂tre : si vous voulez demander à l’utilisateur d’entrer son nom par exemple, il est préférable d’utiliser les événements KEYUP et KEYDOWN car ils seront récupérés dans l’ordre dans lequel ils se sont déclenchés. Si c’est pour déplacer un personnage, la nouvelle fonction présentée sera très appréciable. Si vous avez réussi l’exercice, vous devriez avoir un écran semblable à celui ci-dessous. On s’aperçoit alors que le personnage ne peut pas aller tout contre les bords de l’écran. Cela est dû à l’image : le personnage n’est pas rectangulaire ! Il serait donc préférable de ne pas prendre pour rectangle la totalité de celle-ci mais seulement la zone du corps de Cupidon. Cela sera d’ailleurs d’autant mieux lorsque l’on testera les collisions avec les ennemis. 37 44 16 72 image 93 × 100 162 C UPIDON E X E3 Modifier le programme pour utiliser un rectangle autour du personnage de taille 44 × 72 comme indiqué sur le schéma précédent. E X E4 À présent, nous allons tenter d’animer notre personnage. 1. Charger toutes les images du personnage dans une liste. 2. Ajouter un compteur entier etat qui va indiquer l’état du personnage. À chaque tour de boucle, cet état sera augmenté de 1. Pour avoir le temps de voir les images défiler, on se propose de changer d’image à chaque fois que etat est un multiple de 10, comme l’illustre le schéma ci-dessous. 0 1 2 3 4 5 6 7 8 9 10 11 12 ... 18 19 20 21 22 ... 28 29 30 31 32 ... 38 39 ��� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � ��� � � � � � � � � � � � � � � � � � � � � � � � ��� � � � � � � � � � � � � � � � � � � � � � � � � ��� � � � � � � � � � � � � � � � � � � � � � � � �� � � � � � � � � � � � � � � � � � � � � � � � � ��� � � � � � � � � � � � � � � � � � � � � � � � ��� � � � � � � � � � � � � � � � � � � � � � � � � perso 0 perso 1 perso 2 perso 3 Ajouter ensuite l’animation à votre jeu. 3 - La flèche Il nous faut à présent gérer le décochement d’une flèche lors de l’appui sur la touche espace. Pour l’animation, nous utiliserons les images 4 et 5 du personnage (il a encore la flèche à la main) et les images 6 et 7, la flèche est lâchée, comme cela est présenté aux pages précédentes. E X E5 Dans un premier temps, modifier le code du programme pour qu’à chaque appui sur la touche espace, l’animation du personnage se déclenche (sans lâcher réellement une flèche pour le moment). E X E6 Au moment où Cupidon passe au personnage 6, il faut créer un nouvel élément : ”la flèche”. Comme il en faudra sûrement plusieurs, on se propose d’utiliser une liste de rectangles pour renseigner les positions de chacune d’entre elles. La figure ci-dessous indique comment placer l’image de la flèche (de taille 50 × 7) sur l’image de Cupidon au moment du lâcher. 1. Compléter le lâcher de flèches à l’appui sur la touche espace. 2. Pour que le jeu soit plus réaliste, limiter à 5 le nombre de flèches présentes à l’écran. 163 C HAPITRE E 22 37 + 44 16 36 = 72 4 - Les ennemis Contrairement à Cupidon, les ennemis peuvent se retourner pour venir le piquer. Nous disposons donc d’images : ● mechant 0.png à mechant 3.png pour un méchant en action se déplaçant vers la droite. ● mechant 4.png à mechant 9.png lorsqu’il est en train de mourir (en regardant vers la droite). ● mechant 10.png à mechant 19.png qui sont similaires mais où le monstre regarde vers la gauche. Le premier travail consiste à programmer le déplacement du méchant en direction de Cupidon à vitesse constante. Si on note C le centre de Cupidon et M celui du méchant, ce dernier doit se déplacer ��→ dans le sens MC et avec une vitesse constante v. Le v ��→ vecteur de translation v⃗ = ��→ MC convient. ∥ MC ∥ M C �→ Si Cupidon se déplace en ligne droite comme cela est illustré ci-dessus, on visualise le mouvement du monstre qui s’adapte en temps réel. Ce type de courbe est appelé ≪ courbe du chien ≫ ou parfois courbe de poursuite. 164 C UPIDON C UPIDON E X E7 Écrire une fonction vitesse qui reçoit en paramètre deux rectangles R1 etE XR2 retourne formevitesse d’un couple, les coordonnées du deux vecteur de translaE7etÉcrire unesous fonction qui reçoit en paramètre rectangles R1 tion pour que R1 aille en direction de R2 à une vitesse de 2 pixels. et R2 et retourne sous forme d’un couple, les coordonnées du vecteur de translation pour que R1 aille en direction de R2 à une vitesse de 2 pixels. E X E8 En utilisant la fonction précédemment programmée, placer un premier méchant (assez éloigné Cupidon) et qui se déplace vers sa cible. Pour E X E8 En utilisant la de fonction précédemment programmée, placer un l’animapremier tion du méchant, on utilisera comme pour Cupidon, deux variables : RMechant méchant (assez éloigné de Cupidon) et qui se déplace vers sa cible. Pour l’animapour le rectangle l’étatpour de l’animation. tion du méchant, et onetatM utiliserapour comme Cupidon, deux variables : RMechant pour le rectangle et etatM pour l’état de l’animation. E X E9 Avec un seul méchant, le jeu est bien trop facile, nous allons en mettre plus comme E X Een 9 utilisant Avec un des seullistes méchant, le d’habitude. jeu est bien trop facile, nous allons en mettre plus1.enModifier utilisantledes listes comme programme pourd’habitude. que tous les 100 tours de boucle, un nouveau méchant apparaisse à un endroit sur l’écran. 1. Modifier le programme pour que aléatoire tous les 100 tours de boucle, un nouveau à un endroit aléatoire sur l’écran. 2. méchant Améliorerapparaisse le code pour que : 2. Améliorer le code pour que : ● les méchants n’apparaissent pas n’importe où, mais plutôt dans le dos de Cupidon et ”assez loin” de luin’importe pour que oleù,jeu reste intéressant ● les méchants n’apparaissent pas mais plutôt dans le ;dos Cupidon et ”assez loin”d’un de lui pour que le jeu reste intéressant ; ● de la fréquence d’apparition nouveau méchant dépende du nombre de fréquence méchants d’apparition déjà présentsd’un à l’écran. Parméchant exemple,dépende à chaque un ● la nouveau dutour, nombre 1 de méchants déjà présents l’écran. Par exemple, nouveau monstre apparaı̂t àavec une probabilité de à chaque tour, où un N 100N1 + 10 est le nombre de méchants où N nouveau monstre apparaı̂t présents. avec une probabilité de 100N + 10 le nombre de méchants présents. 3. Enfin,estgérer les images des monstres, afin qu’ils regardent vers Cupidon lorsque ce dernier se déplace. 3. Enfin, gérer les images des monstres, afin qu’ils regardent vers Cupidon lorsque ce dernier se déplace. Il reste encore à gérer les collisions des différents éléments du jeu, c’està-dire les interactions méchant ↔ flèche méchant éléments ↔ Cupidon. Il reste encore à gérer les collisions desetdifférents du jeu, c’est- à-dire les interactions méchant ↔ flèche et méchant ↔ Cupidon. E X E10 Commencer par le premier type de collisions ce qui permettra d’effectuer les risquer de E Xtests E10 sans Commencer parmourir. le premier type de collisions ce qui permettra d’effectuer les tests sans risquer de mourir. E X E11 Sur le même principe, gérer les collisions entre Cupidon et les monstres. E X E11 Sur le même principe, gérer les collisions entre Cupidon et les monstres. E X E12 À présent que l’on sait dessiner un texte à l’écran, gérer les scores se- lon la12règle suivanteque : pour une dessiner flèche donnée, le premier EX E À présent l’on sait un texte à l’écran,monstre gérer lestouché scoresrapseporte 1 point, puis 2 pour le suivant, 4 .... et ainsi de suite avec une progression lon la règle suivante : pour une flèche donnée, le premier monstre touché rapgéométrique raison 2. le suivant, 4 .... et ainsi de suite avec une progression porte 1 point,depuis 2 pour géométrique de raison 2. Voici l’intégralité du code du jeu proposé duquel nous allons repartir pour la suite du chapitre : Voici l’intégralité du code du jeu proposé duquel nous allons repartir 165 pour la suite du chapitre : 165 C HAPITRE E C HAPITRE E from pygame.locals import * from math import sqrt from pygame.locals import * time import sleep from math import sqrt random import random, randint from time import sleep from random import random, randint def afficheScore(s): font=pygame.font.Font(pygame.font.match_font('arial'), 40, bold=True) def afficheScore(s): text = font.render(str(s),True,(0,0,0)) font=pygame.font.Font(pygame.font.match_font('arial'), 40, bold=True) fenetre.blit(text, (10,550)) text = font.render(str(s),True,(0,0,0)) (10,550)) def fenetre.blit(text, vitesse(R1,R2) : x1,y1 = R1.center def vitesse(R1,R2) : x2,y2 = R2.center x1,y1 dx, dy==R1.center x2-x1, y2-y1 x2,y2 = R2.center l = sqrt(dx **2+dy**2) dx, y2-y1 if ldy > = 0 x2-x1, : l = sqrt(dx return ** 2*2+dy dx/l, 2*dy/l **2) if l > else : 0 : return 2 0,0 *dx/l, 2*dy/l else : return 0,0 pygame.init() fenetre = pygame.display.set_mode((1000,600)) pygame.init() etat = 0 fenetre score = = 0 pygame.display.set_mode((1000,600)) etat perso==0[pygame.image.load('perso_'+str(i)+'.png').convert_alpha() for ⤦ score � =i 0in range(0,13)] perso for ⤦ Rperso==[pygame.image.load('perso_'+str(i)+'.png').convert_alpha() Rect(37,16,44,72) in range(0,13)] fond �= ipygame.image.load('fond.jpg').convert_alpha() Rperso fleche = Rect(37,16,44,72) pygame.image.load('fleche.png').convert_alpha() fond = pygame.image.load('fond.jpg').convert_alpha() Rfleches = [] fleche mechant==pygame.image.load('fleche.png').convert_alpha() [pygame.image.load('mechant_'+str(i)+'.png').convert_alpha() ⤦ Rfleches � for= i[]in range(0,20)] mechant ⤦ Rmechant==[pygame.image.load('mechant_'+str(i)+'.png').convert_alpha() [] � =for etatM [] i in range(0,20)] Rmechant continuer==[] True etatM clock = [] pygame.time.Clock() continuer = True while continuer: clock = pygame.time.Clock() clock.tick(100) while continuer: for event in pygame.event.get(): clock.tick(100) if event.type == QUIT : for event in pygame.event.get(): continuer = False if event.type == QUIT : touches = pygame.key.get_pressed() continuerand = False if touches[K_UP] Rperso.top > 0 : touches = pygame.key.get_pressed() Rperso.move_ip((0,-2)) if touches[K_UP] and Rperso.top > 0 :< 600 : touches[K_DOWN] and Rperso.bottom Rperso.move_ip((0,-2)) Rperso.move_ip((0,2)) if touches[K_DOWN] : touches[K_LEFT] and Rperso.bottom Rperso.left > < 0 600 : Rperso.move_ip((0,2)) Rperso.move_ip((-2,0)) if touches[K_LEFT] > 0 touches[K_RIGHT]and andRperso.left Rperso.right < : 1000: Rperso.move_ip((-2,0)) Rperso.move_ip((2,0)) if touches[K_RIGHT] touches[K_ESCAPE]and : Rperso.right < 1000: Rperso.move_ip((2,0)) continuer = False if touches[K_ESCAPE] : etat = etat + 1 continuer if etat == 40 = orFalse etat == 80 : etatetat = etat = 0+ 1 if etat == 40 == 80 : flèche 60 or : #etat Lâcher d'une etat = 0 166 if etat == 60 : # Lâcher d'une flèche 166 C UPIDON Rfleches.append((Rect(Rperso.x-25,Rperso.y+20,50,7),1)) if touches[K_SPACE] and etat < 40 and len(Rfleches) < 5 : etat = 40 fenetre.blit(fond,(0,0)) afficheScore(score) fenetre.blit(perso[etat//10],Rperso.move(-37,-16)) # Ajout d'un méchant ? if random() < 1/(100*len(Rmechant)+10) : x, y = Rperso.center # On est sûr que cette position ne ⤦ � convient pas. while (x-Rperso.x)**2+(y-Rperso.y)**2 < 100**2 : x, y = randint(Rperso.x,1000), randint(0,600) Rmechant.append(Rect(x,y,64,64)) etatM.append(0) # Gestion des méchants i = 0 while i < len(Rmechant) : v = vitesse(Rmechant[i],Rperso) Rmechant[i].move_ip(v) directionM = 10 if v[0] < 0 else 0 fenetre.blit(mechant[etatM[i]//10+directionM], Rmechant[i]) etatM[i] = etatM[i] + 1 if etatM[i] == 40 : etatM[i] = 0 if etatM[i] == 100 : etatM.pop(i) Rmechant.pop(i) else : i = i + 1 # Gestion des flèches for r,p in Rfleches : fenetre.blit(fleche,r) r.move_ip(-3,0) if r.right < 0 : Rfleches.remove((r,p)) # Collision entre une flèche et un méchant for j in range(len(Rfleches)) : r,p = Rfleches[j] L = r.collidelistall(Rmechant) for i in L : if etatM[i] < 40 : etatM[i] = 40 score = score + p p = p*2 Rfleches[j] = (r,p) # Collision entre un méchant et Cupidon if etat < 80 : for i in Rperso.collidelistall(Rmechant) : if etatM[i] < 40 : etat = 80 if etat == 129 : continuer = False pygame.display.flip() if etat == 129 : sleep(4) pygame.quit() 167 C HAPITRE E 5 - Introduction à la programmation objet Dès le premier chapitre, nous avons vu comment regrouper plusieurs informations dans un dictionnaire de manière à n’utiliser qu’une variable par entité (balle, raquette, ...). Nous allons faire mieux en créant des objets dans lesquels nous allons regrouper un certain nombre de variables (on parlera d’attributs) et un certain nombre de fonctions (on parlera de méthodes). La programmation objet ou POO (Programmation Orientée Objet) est un type de programmation qui permet de créer des entités indépendantes les unes des autres avec leurs propres informations et leurs propres actions. Ainsi dans une équipe de développeurs, chacun peut créer ses propres objets sans risquer d’entrer en conflit avec les autres.... Heureusement, nous verrons que les objets peuvent aussi interagir entre eux. Pour vous rassurer, sachez que dès votre premier programme en Python vous avez manipulé des objets sans même le savoir, car en Python tout est objet : un nombre, une liste... et même un module... Dans tous les cas, tout ce qui peut être réalisé en programmation objet peut aussi l’être de manière classique, mais vous verrez qu’à l’usage le code produit est souvent bien plus clair en POO. Pour utiliser des objets, nous allons définir un modèle que l’on appelle classe qui explicitera les attributs et les méthodes. Commençons par décrire la classe Mechant. On pourrait commencer ainsi en partant d’une nouvelle page vide : 1 2 class Mechant : """ Classe d'un méchant """ 3 4 5 6 7 168 def __init__(self) : self.x = 100 self.y = 200 self.etat = 0 C UPIDON Dans l’exemple précédent : 1. On a créé une nouvelle classe du nom de Mechant. La déclaration d’une nouvelle classe commence par le mot clé class suivi de son nom. À noter qu’il est d’usage que les noms des classes commencent par une majuscule. 2. Juste sous la déclaration de la classe, on place une chaı̂ne de caractères donnant une description de la classe en question, comme pour les fonctions (que l’on appelle docstring). 4. On a ensuite utilisé une fonction au nom particulier de init . Cette fonction, appelée constructeur en informatique, est invoquée à chaque fois qu’un objet de la classe est créé. Elle permet, entre autres, d’initialiser tous les attributs de ce nouvel objet. Cette méthode possède un paramètre, usuellement noté self, qui représente l’objet en question lors de l’appel. Toutes les méthodes que nous créerons comporteront ce paramètre self. Si vous voulez mettre un autre nom que self, vous le pouvez mais sachez que 99,99999999% des programmeurs utilisent celui-ci. 5. On déclare 3 attributs de ce nouvel objet : x et y qui seront les coordonnées du coin Nord-Ouest de la position initiale du joueur et etat, un entier pour connaı̂tre le numéro de l’animation à afficher. À présent que nous avons déclaré la classe Mechant, nous pouvons créer une instance de cette classe, c’est-à-dire un objet de type Mechant : jeanMarie = Mechant() On peut alors accéder aux informations de cette manière : >>> type(jeanMarie) <class '__main__.Mechant'> >>> jeanMarie.x 100 On accède donc à l’attribut a d’un objet obj en tapant obj.a (le point peut être vu comme le ≪ ’s ≫ de possession en anglais). 169 C HAPITRE E C HAPITRE E Ces variables sont accessibles en lecture / écriture (nous y reviendrons plus loin pour C HAPITRE E préciser certaines choses), mais deux objets créés dans cette Ces variables sont accessibles en lecture / écriture (nous y reviendrons classe sont indépendants. L’illustration qui suit y montre que x Ces variables sont accessibles en lecture / écriture (nous reviendrons C HAPITRE Etotalement plus loin pour préciser certaines choses), mais deux objets créés dans cette est clairement un attribut d’instance (chaque instance possède ses propres plus loin pour préciser certaines choses), mais deux objets créés dans cette classe totalement indépendants. L’illustration qui suit y montre que x Cessont variables sont accessibles en lecture / écriture (nous reviendrons attributs) classe sont: totalement indépendants. L’illustration qui suit montre que x est clairement attribut d’instance (chaque possède ses propres plus loinvariables pour un préciser certaines choses), maisinstance deux objets créés dans cette Ces accessibles en lecture écriture (nous y reviendrons est clairement un sont attribut d’instance (chaque/ instance possède ses propres attributs) : classe sont totalement indépendants. L’illustration qui suit montre que x plus loin pour préciser certaines choses), mais deux objets créés dans cette attributs) : est clairement un attribut d’instance (chaque instance ses propres classe sont totalement indépendants. L’illustration quipossède suit montre que x >>> jeanMarie =un Mechant() attributs) : est clairement attribut d’instance (chaque instance possède ses propres >>> donald = Mechant() attributs) : >>> jeanMarie.x + 10 >>> jeanMarie.x jeanMarie = = Mechant() >>> >>> >>> 110 >>> >>> >>> 100 >>> >>> 110 >>> 110 >>> >>> >>> 100 >>> >>> 100 110 >>> >>> 110 100 >>> 100 jeanMarie = Mechant() jeanMarie.x donald = Mechant() donald = Mechant() jeanMarie.x = jeanMarie.x donald.x jeanMarie.x jeanMarie.x jeanMarie = = Mechant() jeanMarie.x jeanMarie.x donald = Mechant() jeanMarie = Mechant() donald.x jeanMarie.x = jeanMarie.x donald = Mechant() donald.x jeanMarie.x jeanMarie.x = jeanMarie.x jeanMarie.x donald.x + 10 + 10 + 10 + 10 donald.x Il est possible de donner des valeurs à certains attributs au moment de la création de l’objet et même des valeurs par défaut : Il est possible de donner des valeurs à certains attributs au moment de Il est possible de donner des valeurs à certains attributs au moment de la création de l’objet et même des valeurs par défaut : la création de :l’objet et même des valeurs par défaut : class Mechant Il est possible de donner des valeurs à certains attributs au moment de """ Classe d'un méchant """ >>> vladimir = Mechant(300) la création de :l’objet et mêmedes desvaleurs valeursà par défaut : Il est possible de donner certains attributs au moment de >>> vladimir.x class Mechant class Mechant : 300 def __init__(self, xdebut, : """ Classe d'un méchant """ydebut=200) >>> vladimir = Mechant(300) la création de l’objet et même des valeurs par défaut : """ Classe méchant """ vladimir = Mechant(300) >>> vladimir.y self.x = d'un xdebut >>> vladimir.x 200 self.y = ydebut >>> class Mechant : 300 vladimir.x def __init__(self, xdebut, ydebut=200) : self.etat = 0 méchant def __init__(self, xdebut, 300 vladimir.y """ Classe """ydebut=200) : vladimir = Mechant(300) self.x = d'un xdebut class Mechantxdebut : >>> self.x >>> 200 self.y = = ydebut >>> vladimir.y vladimir.x """ Classe méchant """ >>> vladimir = Mechant(300) self.y = d'un ydebut 200 self.etat = 0 300 def __init__(self, xdebut, ydebut=200) : >>> vladimir.x self.etat = 0 >>> vladimir.y self.x = xdebut def __init__(self, : 300 Les attributs ne sexdebut, limitentydebut=200) pas à des entiers bien évidemment. On peut 200 self.y = ydebut >>> vladimir.y self.x = xdebut = 0 par self.etat exemple pré-charger toutes les images du méchant dans le construc- 200 self.y = ydebut Les attributs ne se limitent pas à des entiers bien évidemment. On peut self.etat = 0ne se limitent pas à des entiers bien évidemment. On peut teurLes ainsi : attributs par exemple pré-charger toutes les imagesdu méchant dans le construc- par exemple pré-charger toutes les images du méchant dans le constructeurLes ainsi : attributs ne se limitent pas à des entiers bien évidemment. On peut teur ainsi : par Les exemple pré-charger toutes lesà des images du bien méchant dans le construcattributs ne se limitent pas entiers évidemment. On peut def __init__(self, xdebut, ydebut) : teur ainsi : pré-charger toutes les images du méchant dans le construcpar exemple ... teur ainsi : self.images=[pygame.image.load('mechant_'+str(i)+'.png').convert_alpha() def __init__(self, xdebut, ydebut) : def __init__(self, xdebut, ydebut) : for i in range(0,20)] ... ... self.images=[pygame.image.load('mechant_'+str(i)+'.png').convert_alpha() self.images=[pygame.image.load('mechant_'+str(i)+'.png').convert_alpha() def __init__(self, ydebut) : for xdebut, i in range(0,20)] ... for i in range(0,20)] def __init__(self, xdebut, ydebut) : self.images=[pygame.image.load('mechant_'+str(i)+'.png').convert_alpha() ... for i in range(0,20)] self.images=[pygame.image.load('mechant_'+str(i)+'.png').convert_alpha() for i in range(0,20)] Laissons de côté la classe Mechant pour le moment. Laissons de côté la classe Mechant pour le moment. 170 Laissons de côté la classe Mechant pour le moment. 170 Laissons de côté la classe Mechant pour le moment. 170 Laissons de côté la classe Mechant pour le moment. 170 170 C UPIDON E X E13 On s’intéresse à la déclaration du héros Cupidon. 1. Écrire une classe Cupidon pour laquelle les objets possèderont les attributs : ● sprites : une liste contenant toutes les images du personnage Cupidon. ● rectangle : un objet de type Rect représentant la position de Cupidon. ● un entier etat : pour indiquer l’image en cours pour l’animation du héros. 2. Créer une instance cupi de cette classe et réaliser les modifications nécessaires au programme pour qu’il fonctionne en retirant les variables perso, Rperso et etat de la version d’origine. Même si le programme de l’exercice précédent fonctionne correctement avec les modifications apportées, tout n’est pas satisfaisant pour autant : en effet, on accède et modifie les attributs de l’objet cupi depuis le programme principal, ce qui est contraire au principe d’encapsulation. Lorsque l’on modifie des attributs d’un objet, il se peut que cela ait des conséquences. Par exemple qu’il faille effectuer des vérifications au préalable ou que cela entraı̂ne des modifications d’autres attributs. C’est pour cette raison qu’en programmation objet, on interdit, en principe, l’accès en lecture/écriture à ces informations depuis l’extérieur. Dans certains langages comme le Java ou le C++ cela se traduit par des attributs publics ou privés. Python, conforme à ses principes de grande souplesse d’utilisation, permet par défaut ces accès. Pour le moment, nous allons donc continuer à profiter de cette possibilité d’accéder aux attributs depuis l’extérieur de la classe, tout en gardant à l’esprit qu’il est préférable de s’en passer. Voici par exemple comment on peut déclarer une fonction qui agit sur les attributs à l’intérieur même d’une classe (on parle alors de méthode sur l’objet). L’exemple qui suit illustre comment modifier l’ordonnée d’un objet de type Mechant. class Mechant : ... def up(self) : self.y = self.y - 1 >>> >>> 20 >>> >>> 19 vladimir = Mechant(10,20) vladimir.y vladimir.up() vladimir.y 171 C HAPITRE E L’appel à la méthode vous est déjà familier, par exemple lors d’un l.sort() pour trier une liste l. Comme pour la fonction init . On peut bien entendu préciser un certain nombre de paramètres, mais le premier (self) est nécessaire pour définir une méthode : ... def deplace(self, vx, vy) : """ Effectue une translation de vecteur (vx, vy)""" self.x = self.x + vx self.y = self.y + vy >>> vladimir = Mechant(10,20) >>> vladimir.x, vladimir.y (10, 20) >>> vladimir.deplace(5,-3) >>> vladimir.x, vladimir.y (15, 17) Noter que lorsqu’une méthode agit sur les attributs d’un objet, il n’est pas nécessaire d’utiliser l’instruction return, puisqu’il n’y a rien à renvoyer. C’est ainsi que vous procédez déjà pour les listes avec la méthode append, d’ailleurs souvent source d’erreur chez les débutants qui tapent l = l.append(x) au lieu de l.append(x) détruisant ainsi la liste l. Avant de repartir sur quelques exercices, présentons une méthode au nom réservé str . Cette méthode est appelée lorsque Python tente de transformer un objet en chaı̂ne de caractères via l’instruction str ou encore lors d’un print (dans ce cas Python transforme en chaı̂ne sans que vous ne le sachiez avant d’afficher le résultat). Cette méthode spéciale permet donc d’afficher facilement les informations d’un objet : def __str__(self) infos = infos = infos + infos = infos + infos = infos + return infos : "Méchant : \n" "------------------------\n" "Le méchant est en ("+str(self.x)+","+str(self.y)+")\n" "dans l'état "+str(self.etat) Pour afficher ces informations, il faut mettre le print dans la console, sinon, c’est une autre méthode qui est appelée ( repr ). >>> vladimir = Mechant(10,20) >>> print(vladimir) Méchant : -----------------------Le méchant est en (10,20) dans l′ état 0 E X E14 Écrire pour la classe Cupidon une fonction update qui ne reçoit pas de paramètre (excepté pour préciser l’objet sur lequel elle agit). Dans cette fonction, on essaiera de placer un maximum d’actions de la boucle principale du jeu, sans s’occuper pour le moment des lancers de flèches, ni des interactions avec les ennemis. 172 C UPIDON Continuons la présentation de la programmation objet autour de notre exemple de la classe Mechant. Sa déclaration devrait ressembler à celle de Cupidon : un rectangle, un état et une liste d’images. A y réfléchir, c’est peut-être dommage de placer dans chaque instance de la classe la liste des images, puisqu’elles sont communes à tout objet de la classe. C’est là qu’interviennent les attributs de classe. L’exemple suivant permet de mieux comprendre la portée des variables : class Mechant : """ Classe d'un méchant """ etat = 10 def __init__(self, xdebut, ydebut= 200) : self.x = xdebut self.y = ydebut self.etat = 0 def attributs(self) : print(self.etat) print(Mechant.etat) print(etat) etat = 5 self.etat est un attribut d’instance : il >>> est propre à chaque objet. Mechant.etat >>> est un attribut de classe : il est commun à 0 10 la classe alors que le dernier etat est une 5 variable globale du programme. drDenfer = Mechant(5,5) drDenfer.attributs() Avant d’aller plus loin avec la classe Mechant, créons la classe Fleche qui est plus simple mais sur la même idée : elle comporte un attribut de classe sprite (l’image d’une flèche) ainsi que deux attributs d’instance rectangle et points, dont les noms explicites devraient suffire à comprendre leur utilité. Si on déclare ainsi la classe : class Fleche : sprite = pygame.image.load('fleche.png').convert_alpha() def __init__.... On obtient alors le message d’erreur suivant dès l’exécution du code : pygame.error: cannot convert without pygame.display initialized Ce qui s’explique par le fait que c’est dès le début du programme que la classe est lue et donc que la variable sprite est mise en mémoire (contrairement aux cas précédents où la lecture des images se faisait au moment de l’instanciation). Or la méthode convert alpha permet de convertir une 173 C HAPITRE E image dans le même format vidéo que la fenêtre d’affiche qui, ici, n’est pas encore créée. On peut néanmoins contourner le problème en chargeant les images dès le début, mais en ne les convertissant qu’ensuite, une fois que la fenêtre d’affichage a été déclarée. Pour cela, on aura recours à une fonction d’initialisation, par exemple : class Fleche : sprite = pygame.image.load('fleche.png').convert_alpha() def initialise() : Fleche.sprite = Fleche.sprite.convert_alpha() ...puis, dans le programme principal... fenetre = pygame.display.set_mode((1000,600)) Fleche.initialise() E X E15 Compléter : 1. la déclaration de la classe Fleche par : ● une méthode constructeur qui recevra en paramètre un objet de la classe Cupidon (pour savoir d’où part la flèche en question) ; ● une méthode update qui déplace un objet Fleche vers la gauche. Cette méthode renvoie True si la flèche sort de l’écran et False sinon. 2. le reste du programme pour gérer : ● les créations et suppressions de flèches, ● leurs déplacements, ● les collisions avec les méchants. Pour finaliser le projet sous sa forme POO, il ne reste plus qu’à s’occuper enfin de la classe Mechant, mais vous devez savoir faire cela à présent. E X E16 On peut évidemment s’inspirer de la déclaration de la classe précédente. Notre nouvelle classe possèdera : ● un attribut de classe (sprites) qui contiendra une liste d’images et qui sera converti une fois la fenêtre de jeu déclarée. ● un constructeur qui recevra le héros comme paramètre de manière à ce qu’un méchant n’apparaisse pas trop près de lui. ● une méthode update qui fera évoluer les monstres vers Cupidon (qu’il faudra donc penser à placer en paramètre), changer leurs états et qui renverra True s’il faut supprimer ce méchant. 174 C UPIDON C UPIDON C UPIDON Adapter le reste du programme pour que le jeu continue de fonctionner. Adapter le reste du programme pour que le jeu continue de fonctionner. Adapter le reste du programme pour que le jeu continue de fonctionner. Voici un projet qui touche à sa fin. Bien entendu certaines parties du Voici un projet qui touche à sa fin. Bien entendu certaines parties du code resteraient à améliorer le Bien fait de modifier des attributs Voici un projet qui touchecomme à sa fin. entendu certaines parties dedu code resteraient à améliorer comme le fait de modifier des attributs depuis l’extérieur la classe. comme On aurait aussi inclure des le score dansdela code resteraient de à améliorer le fait depu modifier attributs puis l’extérieur de la classe. On aurait aussi pu inclure le score dans la classel’extérieur Cupidon et toutes classes dans module pour puis deplacer la classe. Onces aurait aussi puuninclure leextérieur score dans la classe Cupidon et placer toutes ces classes dans un module extérieur pour réduireCupidon le code principal... Tant de reste à découvrir dans classe et placer toutes ceschoses classesqu’il dansnous un module extérieur pour réduire le code principal... Tant de choses qu’il nous reste à découvrir dans les prochains ! Tant de choses qu’il nous reste à découvrir dans réduire le codechapitres principal... les prochains chapitres ! les prochains chapitres ! 6 - Aide pour les exercices 6 6 -- Aide Aide pour pour les les exercices exercices Aide pour l’exercice E5 Inutile de créer de nouvelles variables ici, utilisez Aide pour l’exercice E5 Inutile de créer de nouvelles variables ici, utilisez à bonpour escient la variable etat etde pensez qu’il n’est physiquement posAide l’exercice E5 Inutile créer de nouvelles variables ici,pas utilisez à bon escient la variable etat et pensez qu’il n’est physiquement pas possible de lancer 2 flèches en même temps ! à bon escient la variable etat et pensez qu’il n’est physiquement pas possible de lancer 2 flèches en même temps ! sible de lancer 2 flèches en même temps ! Aide pour l’exercice E7 En considérant les centres des rectangles du Aide pour l’exercice E7 En considérant les centres des rectangles du méchant et de Cupidon on peut déterminer formules donnant Aide pour(M) l’exercice E7 En (C), considérant les centreslesdes rectangles du méchant (M) et de Cupidon (C), on peut déterminer les formules donnant le vecteur(M) vitesse de norme méchant et de(ici Cupidon (C),2). on peut déterminer les formules donnant le vecteur vitesse (ici de norme 2). le vecteur vitesse (ici de norme 2). � → → C → ● Si MC = 0, � v→ = � 0 � C → ● Si MC = 0, � v =� 0 C ● Si MC = 0, → v = 0 � � → 2 → �→, avec : ● Sinon � v→ = 2 � MC �→, avec : 2 � ● Sinon � v = MC MC � → ● Sinon MC , avec : √ v = MC 2 2 MC = √ (x − xMC M )2 + (yC − yM )2 MC = √(xC C − xM )2 + (yC − yM )2 MC = (xC − xM ) + (yC − yM ) ��→ xC − xM M �→ (xC − xM ) et � MC M � �→ (xy − yxM ) et MC M − yM ) et MC (yC yC C − yM Aide pour l’exercice E9 Aide pour l’exercice E9 Aide pour l’exercice E9 1. Pour déterminer une position à l’écran, on peut utiliser la fonction 1. Pour déterminer une position à l’écran, on peut utiliser la fonction du module de 1. randint Pour déterminer une random position :àrandint(a,b) l’écran, on peutrenvoie utiliserun la entier fonction randint du module random : randint(a,b) renvoie un entier de l’intervalledu [a,b]. randint module random : randint(a,b) renvoie un entier de l’intervalle [a,b]. l’intervalle [a,b]. 175 175 175 C HAPITRE E 2. Une technique pour faire une action avec une probabilité de p est de tirer aléatoirement un nombre dans l’intervalle [0,1[ selon la loi uniforme (situation d’équiprobabilité) à l’aide de l’instruction random() du module random. Si ce nombre est inférieur à p, on réalise l’action. 0 x = random() p 1 3. Pensez que la première composante du vecteur vitesse vous donne la direction où regarder. Aide pour l’exercice E10 Pensez à la méthode collidelistall qui vous renverra la liste des indices des méchants entrant en collision avec la flèche par exemple. Aide pour l’exercice E14 On peut inclure dans la méthode : ● la prise en compte des touches, ● l’évolution de l’attribut etat, ● l’affichage du personnage. 7 - Solutions des exercices Retour sur l’exercice E1 1. Voici une proposition pour la première question qui ne devrait pas nécessiter d’explication. On a ajouté une pause avant de quitter Pygame pour laisser le temps d’admirer le résultat : import pygame from time import sleep pygame.init() fenetre = pygame.display.set_mode((1000,600)) perso = pygame.image.load('perso_1.png').convert_alpha() Rperso = perso.get_rect() fond = pygame.image.load('fond.jpg').convert_alpha() fenetre.blit(fond,(0,0)) fenetre.blit(perso,Rperso) pygame.display.flip() sleep(5) pygame.quit() 176 C UPIDON 2. Pour les déplacements, on va s’inspirer de l’exemple de la balle du jeu de PONG donné précédemment. On ajoute aussi l’événement quitter pour terminer proprement le jeu. Après avoir importé les variables de Pygame from pygame.locals import * fenetre.blit(fond,(0,0)) fenetre.blit(perso,Rperso) pygame.display.flip() sleep(5) par cette boucle : Touches = [] continuer = True clock = pygame.time.Clock() while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False elif event.type == KEYDOWN and event.key not in Touches : Touches.append(event.key) elif event.type == KEYUP and event.key in Touches : Touches.remove(event.key) if K_UP in Touches and Rperso.top > 0 : Rperso.move_ip((0,-2)) if K_DOWN in Touches and Rperso.bottom < 600 : Rperso.move_ip((0,2)) if K_LEFT in Touches and Rperso.left > 0 : Rperso.move_ip((-2,0)) if K_RIGHT in Touches and Rperso.right < 1000: Rperso.move_ip((2,0)) fenetre.blit(fond,(0,0)) fenetre.blit(perso,Rperso) pygame.display.flip() 177 Solutions des exercices nous allons remplacer les 4 lignes C HAPITRE E Retour sur l’exercice E2 Il n’y a pas grand-chose à modifier : import pygame from pygame.locals import * from time import sleep pygame.init() fenetre = pygame.display.set_mode((1000,600)) perso = pygame.image.load('perso_1.png').convert_alpha() Rperso = perso.get_rect() fond = pygame.image.load('fond.jpg').convert_alpha() continuer = True clock = pygame.time.Clock() while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False touches = pygame.key.get_pressed() if touches[K_UP] and Rperso.top > 0 : Rperso.move_ip((0,-2)) if touches[K_DOWN] and Rperso.bottom < 600 : Rperso.move_ip((0,2)) if touches[K_LEFT] and Rperso.left > 0 : Rperso.move_ip((-2,0)) if touches[K_RIGHT] and Rperso.right < 1000: Rperso.move_ip((2,0)) if touches[K_ESCAPE] : continuer = False fenetre.blit(fond,(0,0)) fenetre.blit(perso,Rperso) pygame.display.flip() print('fin') pygame.quit() Retour sur l’exercice E3 Il n’y a que deux lignes à changer : ● L’initialisation : Rperso = perso.get_rect() Rperso = Rect(37,16,44,72) �⇒ ● et l’affichage : fenetre.blit(perso,Rperso) fenetre.blit(perso,Rperso.move(-37,-16)) �⇒ À noter que dans cette dernière ligne, on utilise un move au lieu d’un moveip car on ne souhaite pas modifier le rectangle, mais juste le décaler temporairement pour l’affichage. 178 C UPIDON Retour sur l’exercice E4 1. Une première méthode consiste à saisir la liste manuellement : perso = [pygame.image.load('perso_0.png').convert_alpha(), pygame.image.load('perso_1.png').convert_alpha(), pygame.image.load('perso_2.png').convert_alpha(), ... pygame.image.load('perso_13.png').convert_alpha()] perso = [] for i in range(0,13) : perso.append(pygame.image.load('perso_'+str(i)+'.png') ⤦ � .convert_alpha()) ou même en une ligne, à l’aide d’une liste en compréhension : perso = [pygame.image.load('perso_'+str(i)+'.png') ⤦ � .convert_alpha() for i in range(0,13)] 2. Pour l’animation, il nous faut : ● initialiser la valeur etat à 0 avant la boucle principale du jeu : etat = 0 ● incrémenter la valeur etat dans la boucle et la remettre à 0 si elle atteint 40 : etat = etat + 1 if etat == 40 : etat = 0 On pourrait optimiser ainsi : etat = (etat + 1) % 40 puisque dans ce cas, si etat+1 est strictement inférieur à 40 le reste de la division ne le modifiera pas, et lorsqu’il passera à 40, le reste vaudra alors 0. ● Enfin, on modifie légèrement la ligne d’affichage : fenetre.blit(perso[etat//10], Rperso.move(-37, -16)) Comme d’habitude, l’intégralité du code est disponible sur le site du livre. 179 Solutions des exercices Mais si on est peu courageux, comme tout bon programmeur, une boucle sera la bienvenue : C HAPITRE E Retour sur l’exercice E5 Pour lancer l’animation, il faut faire franchir à etat le seuil de 40. Une fois arrivé à 80, on revient alors à 0. Voici la partie incrémentation de etat modifiée : etat = etat + 1 if etat == 40 or etat == 80 : etat = 0 if touches[K_SPACE] and etat < 40 : etat = 40 Retour sur l’exercice E6 1. Dans la partie initialisation, on charge l’image et on initialise la liste des rectangles qui vont représenter les flèches : fleche = pygame.image.load('fleche.png').convert_alpha() Rfleches = [] Dans la boucle principale, lorsque le personnage passe à l’image 6 (c’est-à-dire etat = 60), on crée et ajoute un rectangle à la bonne place : if etat == 60 : # Lâcher d'une flèche Rfleches.append(Rect(Rperso.x-25,Rperso.y+20,50,7)) Reste alors à animer chaque flèche à l’aide d’une boucle for et à la supprimer lorsqu’elle est totalement sortie de l’écran : for r in Rfleches : fenetre.blit(fleche,r) r.move_ip(-3,0) if r.right < 0 : Rfleches.remove(r) 2. Pour ne pas utiliser plus de 5 flèches, il faut remplacer la ligne : if touches[K_SPACE] and etat < 40 : etat = 40 par : if touches[K_SPACE] and etat < 40 and len(Rfleches) < 5 : etat = 40 180 C UPIDON Retour sur l’exercice E7 En s’inspirant de la figure donnée dans l’aide, on peut calculer la distance puis appliquer la formule (pensez à importer la bibliothèque math) : from math import sqrt Retour sur l’exercice E8 La programmation de cet exercice ressemble beaucoup à ce qui a été fait pour le personnage : ● initialisation : mechant = [pygame.image.load('mechant_'+str(i)+'.png'). ⤦ � convert_alpha() for i in range(0,20)] Rmechant = mechant[0].get_rect() etatM = 0 ● et dans la boucle principale : Rmechant.move_ip(vitesse(Rmechant,Rperso)) fenetre.blit(mechant[etatM//10], Rmechant) etatM = etatM + 1 if etatM == 40 : etatM = 0 Retour sur l’exercice E9 1. Les deux variables suivantes vont être remplacées par des listes vides et on ajoute une variable qui compte le nombre de tours de boucle : Rmechant = mechant[0].get_rect() etatM = 0 devient : Rmechant = [] etatM = [] nbtours = 0 181 Solutions des exercices def vitesse(R1,R2) : x1,y1 = R1.center x2,y2 = R2.center dx, dy = x2-x1, y2-y1 l = sqrt(dx**2+dy**2) if l > 0 : return 2*dx/l, 2*dy/l else : return 0,0 C HAPITRE E Dans un second temps, il faut programmer l’apparition d’un joueur dans la boucle principale : nbtours = nbtours + 1 if nbtours == 100 : nbtours = 0 x, y = randint(0,1000), randint(0,600) Rmechant.append(Rect(x,y,64,64)) etatM.append(0) Enfin, la partie animation doit à présent utiliser les listes Rmechant et etatM : for i in range(len(Rmechant)) : Rmechant[i].move_ip(vitesse(Rmechant[i],Rperso)) fenetre.blit(mechant[etatM[i]//10], Rmechant[i]) etatM[i] = etatM[i] + 1 if etatM[i] == 40 : etatM[i] = 0 2. Pour le tirage aléatoire, l’aide apporte déjà des idées qu’il suffit d’implémenter en remplacement de la condition nbtours == 100 (on peut d’ailleurs supprimer cette variable). Pour la position d’apparition, on peut choisir une position aléatoire jusqu’à ce que la distance avec le monstre soit supérieure à 100 pixels : if random() < 1/(100*len(Rmechant)+10) : x, y = Rperso.center # Cette position ne convient pas. while (x-Rperso.x)**2+(y-Rperso.y)**2 < 100**2 : x, y = randint(Rperso.x,1000), randint(0,600) Rmechant.append(Rect(x,y,64,64)) etatM.append(0) 3. Enfin, pour gérer la direction, nous allons regarder pour chaque monstre si l’abscisse du vecteur vitesse est positive ou négative, pour déterminer le sens dans lequel celui-ci doit regarder. On va donc remplacer les 2 lignes : Rmechant[i].move_ip(vitesse(Rmechant[i],Rperso)) fenetre.blit(mechant[etatM[i]//10], Rmechant[i]) par le calcul d’une valeur directionM qui indique le décalage (ou non) de 10 images de manière à obtenir celles tournées vers la droite (0 à 9) ou celles vers la gauche (10 à 19) : 182 C UPIDON v = vitesse(Rmechant[i],Rperso) Rmechant[i].move_ip(v) if v[0] < 0 : directionM = 10 else : directionM = 0 Rmechant[i].move_ip(v) fenetre.blit(mechant[etatM[i]//10+directionM], Rmechant[i]) ou même plus court et plus pythonesque : Retour sur l’exercice E10 On peut ajouter ces quelques lignes avant le pygame.display.flip() de fin de boucle, à l’aide de la fonction collidelistall. On va, pour chaque flèche, regarder l’ensemble des indices des monstres touchés : for r in Rfleches : L = r.collidelistall(Rmechant) for i in L : if etatM[i] < 40 : etatM[i] = 40 # # # # # Pour chaque flèche On regarde les monstres touchés Pour chaque monstre touché, s'il n'est pas déjà mourant on le fait mourir Il faut encore gérer la disparition des monstres une fois carbonisés. Si vous aviez opté pour une boucle for pour gérer les monstres, comme proposé dans la correction de l’exercice 7, vous allez avoir des soucis, car lors de la suppression d’un monstre, la taille de la liste va être modifiée et vos indices (calculés au moment de l’entrée dans la boucle) vont dépasser la taille de celle-ci. Il faut donc remplacer : # Gestion des méchants for i in range(len(Rmechant)) : ... par # Gestion des méchants i = 0 while i < len(Rmechant) : ... if etatM[i] == 100 : etatM.pop(i) Rmechant.pop(i) else : i = i + 1 # Si l'état du monstre arrive à 100 # On le supprime # Sinon on passe au monstre suivant. 183 Solutions des exercices v = vitesse(Rmechant[i],Rperso) Rmechant[i].move_ip(v) directionM = 10 if v[0] < 0 else 0 fenetre.blit(mechant[etatM[i]//10+directionM], Rmechant[i]) C HAPITRE E Vous aurez remarqué que lorsque l’on supprime un monstre, la valeur de i n’est pas augmentée puisque, le monstre ayant disparu, on passe au suivant sans changer d’indice. Retour sur l’exercice E11 Cette fois, on peut utiliser la méthode collidelist puisqu’il n’est pas nécessaire de récupérer toutes les collisions : dès qu’un monstre touche Cupidon, le jeu s’arrête. On ajoute donc ces quelques lignes avant le display.flip : # Collision entre un méchant et Cupidon if etat < 80 and Rperso.collidelist(Rmechant) > -1 : etat = 80 Il faut aussi penser à quitter le jeu lorsque la partie est terminée : if etat == 129 : # Fin du jeu continuer = False Vous pourrez constater en le testant que lorsqu’un monstre est mourant, il ne faut (toujours) pas le toucher. On peut corriger ce problème, en récupérant cette fois les indices de tous les monstres touchés et en ne tenant compte que de ceux qui sont en pleine forme : if etat < 80 : for i in Rperso.collidelistall(Rmechant) : if etatM[i] < 40 : etat = 80 Enfin, on peut aussi ajouter avant le pygame.quit() la ligne : if etat == 129 : sleep(4) afin de laisser un petit délai avant la fermeture de la fenêtre. Retour sur l’exercice E12 On peut déjà commencer par créer une fonction qui affichera (ici en bas à gauche) le score en s’inspirant de la fonction proposée dans le projet PONG : def afficheScore(s): font=pygame.font.Font(pygame.font.match_font('arial'), 40, bold=True) text = font.render(str(s),True,(0,0,0)) fenetre.blit(text, (10,550)) 184 C UPIDON Il faut aussi créer, avant la boucle principale, une variable score que l’on initialise à 0, puis afficher ce score dans la boucle juste après le fond par exemple (tout dépend si vous préférez voir le score par dessus ou par dessous les personnages) : fenetre.blit(fond,(0,0)) afficheScore(score) Dans un second temps, il faut réaliser le calcul de ce score en associant à chaque flèche le nombre de points qu’elle rapporte par monstre touché (1, puis 2, 4...). Plusieurs solutions s’offrent à nous : on peut par exemple créer une seconde liste SFleches qui sera maintenue à jour en même temps que RFleches ou, et c’est ce que nous allons faire ici, ne conserver qu’une liste RFleches dans laquelle on placera des couples : (Rect, points). Quelques lignes vont devoir évoluer : ● lorsqu’une nouvelle flèche est créée : if etat == 60 : # Lâcher d'une flèche Rfleches.append(Rect(Rperso.x-25,Rperso.y+20,50,7)) devient : if etat == 60 : # Lâcher d'une flèche Rfleches.append((Rect(Rperso.x-25,Rperso.y+20,50,7),1)) ● lors du déplacement des flèches : for r in Rfleches : fenetre.blit(fleche,r) r.move_ip(-3,0) if r.right < 0 : Rfleches.remove(r) devient : for r,p in Rfleches : fenetre.blit(fleche,r) r.move_ip(-3,0) if r.right < 0 : Rfleches.remove((r,p)) 185 Solutions des exercices C HAPITRE E ● enfin, pour gérer les points lorsqu’une flèche touche un monstre, on ne peut plus utiliser la boucle for r,p in Rfleches puisque nous allons avoir à modifier les éléments de la liste Rfleches ; il faut donc réaliser une boucle sur les indices et non les valeurs : for r in Rfleches : L = r.collidelistall(Rmechant) for i in L : if etatM[i] < 40 : etatM[i] = 40 devient : for j in range(len(Rfleches)) : r,p = Rfleches[j] L = r.collidelistall(Rmechant) for i in L : if etatM[i] < 40 : etatM[i] = 40 score = score + p p = p*2 Rfleches[j] = (r,p) Retour sur l’exercice E13 Pour la première question, on peut s’inspirer de la classe Mechant : class Cupidon : """ La classe du joueur """ def __init__(self) : self.sprites = [pygame.image.load('perso_'+str(i)+'.png'). ⤦ � convert_alpha() for i in range(0,13)] self.rectangle = Rect(37,16,44,72) self.etat = 0 Pour la seconde question, on peut initialiser l’objet cupi en remplaçant les 3 lignes : perso = [pygame.image.load('perso_'+str(i)+'.png').convert_alpha() for ⤦ � i in range(0,13)] Rperso = Rect(37,16,44,72) etat = 0 par : cupi = Cupidon() Puis, dans le corps du programme de remplacer tous les ● perso par cupi.sprites ● Rperso par cupi.rectangle ● etat par cupi.etat 186 C UPIDON Retour sur l’exercice E14 Dans la méthode update : ● on surveille les touches enfoncées ● on fait évoluer l’attribut etat def update(self) : touches = pygame.key.get_pressed() if touches[K_UP] and cupi.rectangle.top > 0 : cupi.rectangle.move_ip((0,-2)) if touches[K_DOWN] and cupi.rectangle.bottom < 600 : cupi.rectangle.move_ip((0,2)) if touches[K_LEFT] and cupi.rectangle.left > 0 : cupi.rectangle.move_ip((-2,0)) if touches[K_RIGHT] and cupi.rectangle.right < 1000: cupi.rectangle.move_ip((2,0)) cupi.etat = cupi.etat + 1 if cupi.etat == 40 or cupi.etat == 80 : cupi.etat = 0 fenetre.blit(cupi.sprites[cupi.etat//10],cupi.rectangle.move(-37,-16)) Il suffit alors dans la boucle principale de supprimer ces lignes et de les remplacer par : cupi.update() Retour sur l’exercice E15 Commençons par la création complète de la classe. Elle ne comporte pas de difficulté particulière : la fonction initialise ayant déjà été présentée, le constructeur récupère l’objet représentant le joueur pour déterminer les coordonnées de départ de la flèche comme cela a déjà été fait dans la version non-objet : class Fleche : sprite = pygame.image.load('fleche.png') def initialise() : Fleche.sprite = Fleche.sprite.convert_alpha() def __init__(self, cup) : self.rectangle = Rect(cup.rectangle.x-25,cup.rectangle.y+20,50,7) self.points = 1 def update(self) : fenetre.blit(Fleche.sprite, self.rectangle) self.rectangle.move_ip(-3,0) if self.rectangle.right < 0 : return True return False 187 Solutions des exercices ● on finit par afficher le personnage à sa place C HAPITRE E À noter que les trois dernières lignes de la méthode update peuvent être avantageusement remplacées par : return self.rectangle.right < 0 Pour le reste du programme, il faut aussi : ● avant la boucle principale, initialiser la variable fleches qui contiendra une liste de flèches ainsi que la classe Fleche pour convertir l’image : fleches = [] Fleche.initialise() ● dans la boucle, mettre en place le lâcher de flèches lorsque l’état du joueur est à 60 : if cupi.etat == 60 : # Lâcher d'une flèche fleches.append(Fleche(cupi)) ● gérer les déplacements et suppressions de flèches. On peut faire le choix de modifier la liste fleches dans la méthode update car Python le permet. Personnellement, je préfère éviter (lorsque cela est possible) de modifier une variable globale à l’intérieur d’une fonction, c’est pour cela que la méthode update renvoie True s’il faut supprimer l’objet. Pour les mêmes raisons que dans la version nonobjet, on utilise un TANT QUE plutôt qu’un POUR car la taille de la liste évolue au fur et à mesure des disparitions de flèches. La collision avec les méchants quant à elle fonctionne sur le même principe que précédemment, c’est un peu plus simple à gérer dans la mesure où le rectangle de la flèche et le nombre de points sont deux variables séparées avec cette version objet : j = 0 while j < len(fleches) : f = fleches[j] if f.update() : # La flèche disparait ? fleches.remove(f) else : # Sinon, on teste le contact avec un méchant L = f.rectangle.collidelistall(Rmechant) for i in L : if etatM[i] < 40 : etatM[i] = 40 score = score + f.points f.points = 2*f.points j = j + 1 ● il faut aussi penser à remplacer tous les len(Rfleches) qui peuvent traı̂ner dans le reste du code par len(fleches). La version complète du programme est proposée sur le site. 188 C UPIDON Retour sur l’exercice E16 En suivant le plan qui est proposé, on peut donc déclarer la classe et les fonctions demandées : class Mechant : sprites = [pygame.image.load('mechant_'+str(i)+'.png') for i in ⤦ � range(0,20)] Solutions des exercices def vitesse(R1,R2) : x1,y1 = R1.center x2,y2 = R2.center dx, dy = x2-x1, y2-y1 l = sqrt(dx**2+dy**2) if l > 0 : return 2*dx/l, 2*dy/l else : return 0,0 def initialise() : for i in range(len(Mechant.sprites)) : Mechant.sprites[i] = Mechant.sprites[i].convert_alpha() def __init__(self, cup) : x, y = cup.rectangle.center while (x-cup.rectangle.x)**2+(y-cup.rectangle.y)**2 < 100**2 : x, y = randint(cup.rectangle.x,1000), randint(0,600) self.rectangle = Rect(x,y,64,64) self.etat = 0 def update(self, cup) : v = Mechant.vitesse(self.rectangle,cup.rectangle) self.rectangle.move_ip(v) directionM = 10 if v[0] < 0 else 0 image = Mechant.sprites[self.etat//10+directionM] fenetre.blit(image, self.rectangle) self.etat = self.etat + 1 if self.etat == 40 : self.etat = 0 return self.etat == 100 Le contenu du code ne devrait pas poser de difficulté dans la mesure où il a déjà été étudié en début de chapitre. Noter que l’on a inclus la fonction vitesse dans la classe Mechant, on l’appelle donc en indiquant le nom de la classe comme pour un module. L’ajout d’un méchant est géré selon la même idée que l’ajout de flèches, on initialise le chargement des images en début de jeu ainsi qu’une liste vide destinée à contenir les méchants : mechants = [] Mechant.initialise() 189 HAPITRE E E C HAPITRE peut alors alors ajouter ajouterde denouveaux nouveauxméchants méchantsde detemps tempsenentemps temps On peut : : random() << 1/(100 1/(100**len(mechants)+10) len(mechants)+10):: if random() mechants.append(Mechant(cupi)) mechants.append(Mechant(cupi)) faire évoluer évolueret etdisparaı̂tre disparaı̂tre: : Et les faire i = 0 len(mechants) :: while ii << len(mechants) m == mechants[i] mechants[i] if if m.update(cupi) m.update(cupi) :: mechants.remove(m) mechants.remove(m) else else :: ii == ii ++ 11 La collision collision d’un d’un méchant méchantavec avecCupidon Cupidondoit doitaussi aussiêtre êtreadaptée adaptéepuisque puisque dans la version versionprécédente, précédente,on ondisposait disposaitde delalaliste listedes desrectangles rectangles Rmechant, Rmechant, il faut donc donc la la recréer recréer:: if cupi.etat cupi.etat << 80 80 :: for ii in in cupi.rectangle.collidelistall([m.rectangle cupi.rectangle.collidelistall([m.rectanglefor form mininmechants]): mechants]): if if mechants[i].etat mechants[i].etat << 40 40 :: cupi.etat cupi.etat == 80 80 Il reste àà faire faire de de même même pour pour lalacollision collisiondes desflèches flèchesetetdes desméchants. méchants. Comme Comme d’habitude, d’habitude,l’ensemble l’ensembledu ducode codeest estdisponible disponiblesur surlelesite. site. cam 190 Objectif Réaliser un lanceur de jeux Programmation Lecture/Écriture d’un fichier de configuration Utiliser le module transform de pygame Mathématiques Courbes paramétrées Enroulement d’une image sur un cylindre Chapitre F Front-end On souhaite réaliser un menu de style rétro ressemblant à la capture d’écran ci-dessous. Ce menu nous servira à lancer les jeux de notre création. Ce type d’interface porte aussi le nom de ≪ front-end ≫ c’est-à-dire, d’une manière plus générale, la partie visible d’une application communicante avec d’autres. MENU Nous allons, au travers de ce décor vintage, revisiter quelques animations classiques des démos des années 90... À l’époque j’étais en seconde et mon frère en terminale et nous avions mis sur pied un petit groupe de création de démos sur Atari : la team LEGEND . Nous passions pas mal de temps à l’époque à chercher des idées de démos et à les coder des 191 C HAPITRE F après-midi durant. Pour les nostalgiques, ou les curieux, un site regroupe un certain nombre de ces écrans recodés pour l’occasion en HTML5 : We Are Back : http://www.wab.com/ WAB 1 - Manipuler les fichiers de configuration Pour ne pas avoir à modifier le code du programme à chaque fois que l’on ajoute ou supprime un jeu, nous allons utiliser un fichier de configuration pour y placer la liste des jeux que l’on souhaite afficher. Les fichiers de configuration sont des fichiers permettant de regrouper de manière structurée des informations. Ici nous présenterons les fichiers INI qui ont été introduits par Windows dès ses premières versions et qui sont de nos jours toujours très répandus. Voici par exemple un extrait du fichier de configuration de PyScripter, l’éditeur utilisé dans EduPython : ... Item88=[Default @ TabSplitter] Item89=SplitterRestorePos=60 Item90= Count=91 [Other Settings] PyScripter Version=2.6.0.0 Language=fr File Explorer Filter=TRUE File Explorer Path= Status Bar=TRUE Theme Name=Dream Active Project= [IDE Options] ; Options interface TimeOut=0 UndoAfterSave=TRUE SaveFilesBeforeRun=TRUE ;Enregistrer avant exécution PythonFileFilter=Python Files (*.py;*.pyw)|*.py;*.pyw AutoCheckForUpdates=FALSE DaysBetweenChecks=7 DetectUTF8Encoding=TRUE CheckSyntaxLineLimit=1000 [IDE Options\AutoCompletionFont] Charset=DEFAULT_CHARSET Color=clWindowText Height=-13 ... 192 F RONT- END On peut y voir que les informations sont regroupées par sections délimitées par des crochets à l’intérieur desquels on retrouve différentes clés (ou paramètres) et leurs valeurs séparées par le symbole =. La structure est donc celle-ci : [SECTION A] clé1 = valeur1 clé2 = valeur2 clé3 = valeur3 clé4 = valeur4 [SECTION B] clé1 = valeur1 ... On pourrait donc manipuler ces fichiers comme de simples fichiers texte comme nous l’avons appris dans le tome 1 au chapitre E, mais le module configparser va nous simplifier la vie. Création et enregistrement On peut soit créer et éditer manuellement un fichier INI avec un simple éditeur de texte type bloc-notes, soit le générer en Python, une fois l’objet de type configparser créé, la manipulation ressemble à celle des dictionnaires vus au premier chapitre : import configparser config = configparser.ConfigParser() # On renseigne toute une section config['ECRAN'] = { 'Largeur' : 1600, 'Hauteur' : 900, '16/9' : 'YES'} # Ou on en crée une que l'on complète au fur et à mesure config['TOUCHES'] = {} config['TOUCHES']['VALIDER'] = 'SPACE' config['TOUCHES']['UP'] = '8' config['TOUCHES']['DOWN'] = '2' # Enregistrement du fichier fichier = open('infos.ini', 'w') config.write(fichier) fichier.close() 193 C HAPITRE F On obtient alors le fichier ci-contre. On peut cependant remarquer que : ✍ ● L’ordre des clés a été modifié, ce qui n’est pas étonnant puisque les informations sont stockées dans un dictionnaire. ● Toutes les clés sont écrites en minuscules. ● Il n’y a pas de différence entre le nombre 900 et la chaı̂ne 8. [ECRAN] 16/9 = YES hauteur = 900 largeur = 1600 [TOUCHES] valider = SPACE up = 8 down = 2 Pour finir, on peut remarquer que, dans le code précédent, la variable fichier n’est utilisée que pour écrire et que nous n’en avons plus besoin ensuite. On pourrait la détruire : # Enregistrement du fichier fichier = open('infos.ini', 'w') config.write(fichier) fichier.close() del(fichier) ou la déclarer comme variable locale à l’aide de l’instruction with : # Enregistrement du fichier with open('infos.ini', 'w') as fichier : config.write(fichier) fichier.close() Ainsi, lorsque l’on quitte l’indentation, la variable fichier n’existe plus, selon le même principe que lorsque l’on quitte une fonction. Lecture des informations Lorsque l’on dispose d’un fichier INI, on peut lire les informations : import configparser config = configparser.ConfigParser() config.read('infos.ini') On peut récupérer la liste des sections : >>> config.sections() ['ECRAN', 'TOUCHES'] 194 F RONT- END Pour une section donnée, on peut lister les clés et les valeurs comme pour un dictionnaire : # Liste des clés dans une section : S = config['ECRAN'] for cle in S : print(cle, '--->', S[cle]) 16/9 ---> YES largeur ---> 1600 hauteur ---> 900 On peut directement accéder à la valeur d’une clé si on connaı̂t la clé et la section de celle-ci. Les 3 lignes suivantes ont le même effet : config['ECRAN']['largeur'] S['largeur'] config.get('ECRAN', 'Largeur') À noter qu’avec la méthode get , on peut préciser une valeur de retour si la clé n’est pas trouvée : >>> config.get('ECRAN', 'diagonale', fallback="Calcule !") 'Calcule !' Enfin, pour pallier au problème de typage de données (par défaut toute valeur est une chaı̂ne de caractères), on peut forcer le type à la lecture : config.getint('ECRAN', 'hauteur', fallback = -1 ) config.getboolean('ECRAN', '16/9', fallback = True ) config.getfloat('ECRAN', 'largeur' ) 900 True 1600.0 Revenons à notre projet. Notre fichier INI sera structuré comme le montre la capture ci-contre : les noms de sections sont les noms des jeux. Pour chaque section, un certain nombre de clés en dessous donnent des informations sur celui-ci : l’auteur, la date ou le nom de l’image à afficher pour présenter le jeu... E X F1 Après avoir téléchargé sur le site le fichier liste.ini, 1. Écrire un programme qui affiche la liste des jeux. 2. Le compléter pour qu’il affiche la liste des images associées. 195 C HAPITRE F Vous aurez donc remarqué que certains jeux n’ont pas de clé image associée et que pour d’autres, l’image est renseignée mais n’est en réalité pas présente dans le dossier images, ce qui va poser des problèmes pour la suite. En Python, on peut tester la présence d’un fichier à l’aide de la fonction isfile du module path du package os : isfile(f ich) : renvoie un booléen indiquant si le fichier f ich est présent. La chaı̂ne de caractères f ich peut contenir : ● un chemin absolu, par exemple "C:/monimage.png" (noter qu’il est préférable, même sous Windows, de mettre des / dans les chemins (comme sur le web ou Linux) plutôt que des /), ● un chemin relatif, par exemple "image.png". Dans ce cas Python cherche dans le dossier courant, c’est-à-dire par défaut le dossier où votre programme est enregistré, ● un chemin relatif mais qui comporte des dossiers, par exemple "images/monimage.png", dans ce cas Python cherche dans le dossier courant, un dossier images et dans ce dossier le fichier monimage.png. On peut aussi remonter dans l’arborescence du disque à l’aide de la chaı̂ne "../". Par exemple "../monimage.png" recherche le fichier dans le dossier père du dossier courant. E X F2 Prendre en compte ces remarques pour que le programme précédent affiche le nom de l’image si elle est définie et bien présente. Dans le cas contraire, on remplacera l’image par l’image notfound.png : turtle.png 196 notfound.png F RONT- END 2 - La bande de film Intéressons-nous dans un premier temps au plus gros du travail, c’està-dire la création de la bande de film déroulante qui contiendra la liste des jeux. Pour cela, on dispose d’un certain nombre d’images de taille 300 × 225 dans un dossier images dont nous avons déjà parlé dans la partie précédente. On dispose aussi d’une image pellicule.png de taille 420 × 300 dont la partie centrale est transparente et que nous allons donc superposer à nos images pour obtenir un effet de bande de film : + = 60 E X F3 Écrire une fonction montage qui reçoit un nom de fichier et réalise le montage de l’image (si elle n’est pas présente, l’image de substitution précédente est utilisée). La fonction renvoie le résultat sous la forme d’une surface. 13 image.png pellicule.png Dans notre menu, la bande vidéo sera placée sur le côté droit de l’écran, comme l’illustre la figure ci-dessous. On dispose d’une image d’un fond vintage de taille 1180 × 900 qui sera, elle, placée à gauche de l’écran. 197 C HAPITRE F 1180 1600 BAND E VIDE O 0 E X F4 Pour commencer, on s’intéresse à l’initialisation de la classe en créant quelques attributs : ● config qui représentera le contenu du fichier list.ini afin d’obtenir les informations sur les jeux au moment voulu, ● bande : une longue surface de 420 pixels de large où les images des différents jeux seront placées les unes en dessous des autres et montées sur l’image pellicule, ● nbj : le nombre de jeux présents. En vous appuyant sur les précédents exercices (lecture du fichier ini, création de la pellicule...) réaliser le constructeur de cette classe nommée Jeux. À noter qu’avec Pygame, la longueur et la largeur d’une surface sont limitées à 65535 = 216 − 1 de manière à être codées sur 2 octets. Ce qui nous laisse un potentiel de plus de 200 jeux, cela devrait nous suffire dans ce livre ! bande : 198 F RONT- END E X F5 Il nous faut à présent afficher la bande dans le menu. Pour cela, on va ajouter un attribut decale initialisé à 0 et qui représentera le décalage de la bande comme l’illustre la figure suivante. Programmer une méthode dessine pour la classe Jeux qui, comme son nom l’indique, placera la bande à l’écran avec le décalage souhaité. Cerise sur le gâteau, on place par dessus la bande, l’image transparente ombre.png pour donner un effet de halo. decale=150 + ombre.png On souhaite à présent pouvoir faire glisser la bande à l’aide des touches de direction du clavier. E X F6 Réaliser une méthode update qui surveille le clavier et affiche la bande en conséquence. Cette méthode renvoie False si la touche échappe est enfoncée et True sinon. Comme pour les autres projets, l’appel à cette méthode sera placé dans une boucle infinie jusqu’à ce que le joueur quitte le menu. Les problèmes de dépassements en début et fin de bande seront gérés dans l’exercice suivant. 1 E X F7 Comme vous l’aurez remarqué, il y a un souci lorsque l’on 2 monte trop ou descend trop. Pour cela, nous allons utiliser une astuce en recopiant le contenu de la hauteur d’un écran (900 pixels soit 3 images de 300 pixels) en fin de bande. Ainsi, lorsque la valeur de decale vaut la valeur max, on la remet à 0. Inversement, lorsque decale passe en dessous de 0 on le remet à max, donnant l’illusion que la bande est cyclique. Modifier le code précédent pour mettre en place cette amélioration. 3 max 1 2 3 Et voilà ! Pas si compliqué, au final. 199 C HAPITRE F E X F8 Il nous reste à gérer une action lorsque l’on sélectionne un jeu. 1. Compléter votre programme pour qu’il affiche le nom du jeu dans la console lors de l’appui sur la touche espace. 2. Améliorer le code pour qu’au fur et à mesure du scrolling de la bande vidéo, le jeu en cours de sélection apparaisse dans le téléviseur du bas. Ce dernier exercice termine la partie principale de notre menu. Si on souhaite réaliser une action, il nous faut lancer une ligne de commande, par exemple python monjeu.py pour lancer un nouveau script Python ou mame tnzs.zip pour démarrer le jeu ”The Newzealand Story” sur l’émulateur MAME (à condition qu’ils soient présents sur votre machine bien entendu !). system(cmd) : exécute la ligne de commande cmd. Cette fonction se trouve dans le module os de Python. Il ne reste plus qu’à placer quelques éléments de décoration sur notre menu. La partie suivante va nous apprendre à réaliser un Unlimited Sprites, un grand classique des démos des années 90. 3 - Unlimited sprites À l’époque où notre menu nous propulse, réaliser le moindre déplacement d’un personnage demandait un temps machine non négligeable. Pourtant, certains petits malins arrivaient à animer une infinité d’objets à l’écran... De nos jours, l’affichage est beaucoup plus rapide... toujours est-il qu’il reste - et restera - impossible de déplacer une infinité de sprites sur un écran. Attention, les pages qui suivent révèlent un tour de magie démasqué par mon frère (AKA Rick from LEGEND) ! Commençons par déplacer un unique sprite sur la télévision A dont les dimensions ont été mesurées à l’aide du logiciel de retouche d’images GIMP. 200 F RONT- END 218 149 A sprite.png 20 × 20 687 286 Le cadre n’étant pas grand, on a choisi un sprite assez petit de taille 20 × 20 pixels. Le déplacement de sprites se fait traditionnellement le long d’une courbe paramétrée. Pour notre menu, nous allons nous inspirer des courbes de Lissajous, un physicien français du XIXe siècle. Avant de nous lancer dans la détermination de l’équation de la trajectoire, estimons la place dont nous disposons : L’écran de télévision mesure 286 × 218 pixels auxquels il faut retirer 2 × 10 pixels sur les bords, soit 266 × 198 pixels. Nous allons donc construire une courbe dont les points auront une abscisse comprise entre −133 et 133 et une ordonnée entre −99 et 99. Un peu de géométrie analytique nous sera utile : 1 { −1 1 −1 x(t) = sin(t) y(t) = cos(t) [Équation d’un cercle] à t = 0 : on part du point blanc, puis lorsque t augmente, on tourne dans le sens des aiguilles d’une montre. Nous allons ”dilater” les dimensions pour rentrer dans l’écran.... 201 C HAPITRE F { −100 100 [Une ellipse] Nous allons à présent augmenter la fréquence sur les abscisses. { −100 100 100 202 100 x(t) = 133 sin(8t) y(t) = 99 cos(t) Puis la même idée sur les ordonnées... { −100 x(t) = 133 sin(2t) y(t) = 99 cos(t) Encore un peu plus... { −100 x(t) = 133 sin(t) y(t) = 99 cos(t) x(t) = 133 sin(8t) y(t) = 99 cos(5t) Il n’y a plus qu’à effectuer une translation... F RONT- END { x(t) = 133 + 133 sin(8t) y(t) = 99 + 99 cos(5t) E X F9 Écrire une classe USprites qui possèdera les attributs sprite (l’image) et t (le temps), ainsi qu’une méthode update qui aura pour effet de déplacer le sprite à l’écran (on ne s’occupe pas pour le moment d’effacer les marques au fur et à mesure). Mais comment passer de cet unique sprite à une infinité ? Il n’est évidemment pas question de tous les redessiner un à un à chaque déplacement ! L’astuce consiste à utiliser une technique de ≪ multi buffering ≫, c’est-à-dire utiliser plusieurs écrans virtuels comme l’illustre la figure ci-dessous avec 3 écrans : Écran 1 Écran 2 Écran 3 À l’étape suivante, on recommence à dessiner sur le premier écran : Ainsi à chaque fois que l’on retrace un sprite sur l’écran 1, on donne l’impression qu’un nouveau sprite apparait alors qu’en fait il s’agit de la trace du sprite précédent ! E X F10 À vous de programmer cet effet magique en ajoutant les attributs nécessaires et en mettant à jour la fonction update. 203 C HAPITRE F 4- 3D Starfield Un autre effet à la mode à l’époque était la réalisation de champs d’étoiles : une collection de pixels illuminés et animés. Capture de la démo Dark-Trip Le menu de la borne a été imaginé pour le club info que j’anime au lycée Thuillier d’Amiens où j’enseigne. Nous utiliserons donc ce logo, mais vous pouvez bien sûr le changer ! Comme vous pouvez le voir sur la vidéo en ligne associée à ce chapitre, nous allons représenter ce logo à l’aide d’un champ d’étoiles plaquées sur B Les dimensions de un cylindre. Nous le dessinerons dans le télécran . celui-ci sont données sur l’image ci-après : 120 123 B 99 160 Notre premier travail consiste à ≪ enrouler ≫ le logo autour d’un cylindre. Pour cela l’ordonnée d’un pixel sera conservée comme la hauteur de l’étoile et l’abscisse sera transformée en un angle par une transformation affine telle que 0 ↦ π et 160 ↦ 0 comme l’illustre la figure suivante. 204 F RONT- END 0 160 π 0 × × E X F11 Décrire une classe Etoile telle que : 1. Les objets contiendront 3 attributs : un entier hauteur, un flottant angle et un triplet couleur indiquant la couleur du pixel au format (R,V,B). Le constructeur sera appelé avec 3 paramètres (x,y,c) représentant respectivement l’abscisse, l’ordonnée et la couleur de l’étoile. 2. Les objets possèderont une méthode dessine(s) qui ”plaquera” l’étoile sur la surface s donnée en paramètre (n’hésitez pas à aller voir l’aide pour trouver cette nouvelle formule). E X F12 Créer une nouvelle classe Champ qui contiendra dans ses attributs une liste d’étoiles. Pour gérer la transparence, on ne va considérer comme étoile que les pixels qui ne sont pas de la couleur du fond du télécran (176,161,128). Ajouter enfin une méthode update qui affiche le résultat à l’écran. Notre menu est à présent terminé, comme pour chaque projet, un certain nombre d’améliorations sont possibles : ● Lancer effectivement une commande en ligne (cela peut se faire à l’aide de la fonction system du module os de Python). ● Accélérer la vitesse de la bande au fur et à mesure que les touches de direction sont enfoncées. ● Écrire les informations du jeu dans le dernier écran de télévision non utilisé, il faudra alors tourner l’image pour un meilleur rendu, vous allez apprendre à faire cela dans le prochain chapitre... 205 C HAPITRE F 5 - Aide pour les exercices Aide pour l’exercice F8 Pour le premier problème, comme on peut s’y attendre, il existe un lien entre la valeur de decale et le numéro du jeu sélectionné. Au départ par exemple, lorsque decale vaut 0, c’est le jeu numéro 1 qui est au centre de l’écran, jusqu’à ce que la valeur de decale soit égale à 150. Vous pouvez trouver une formule liant ces deux quantités : decale : 0 150 450 n° jeu : 1 2 max-150 750 | 3 ... max | 1 Pour le second, on peut utiliser la capture tv.png fournie sur le site de même dimension que les images des jeux (420 × 300). La position de la capture est donnée ci-dessous. 606 546 206 300 420 490 300 C 225 503 RONT--END END FFRONT Aide pour l’exercice F11 Pour Pour la la seconde secondequestion, question,on onva vaprojeter projeterles lespoints points (du cylindre) sur l’écran (axe) (axe) :: 1 Pour Pour 00⩽⩽aa⩽⩽π, π, −1 −1⩽⩽cos cosaa⩽⩽1, 1, −80 −80⩽⩽80 80cosa cosa⩽⩽80, 80, 00⩽⩽80 80++80 80cosa cosa⩽⩽160. 160. a 0 cos cosaa 11 6 - Solutions des exercices exercices Retour sur l’exercice F1 Pour Pour afficher afficher les les jeux, jeux, ilil suffit suffit d’afficher d’afficher les les secsections du fichier : import configparser config = configparser.ConfigParser() configparser.ConfigParser() config.read('liste.ini') config.read('liste.ini') print(config.sections()) print(config.sections()) Pour afficher les noms des images, images, on on cherche cherche la la clé clé souhaitée. souhaitée.Attention Attention certains jeux n’ont pas d’image, d’image, ilil faut faut donc donc gérer gérer le le problème. problème. import configparser config = configparser.ConfigParser() configparser.ConfigParser() config.read('liste.ini') config.read('liste.ini') for jeu in config.sections() config.sections() :: print (config.get(jeu, (config.get(jeu, 'image', 'image', fallback fallback == 'impossible')) 'impossible')) Retour sur l’exercice F2 Sur Sur le le même même principe principe que que dans dans l’exercice l’exercice précédent, mais on ajoute quelques quelques lignes lignes pour pour tester tester la la présence présence du du fifichier avant de l’afficher : import configparser import os for jeu in config.sections() config.sections() :: fichier = config.get(jeu, config.get(jeu, 'image', 'image', fallback fallback == 'impossible') 'impossible') if not os.path.isfile('images/'+fichier) os.path.isfile('images/'+fichier) :: fichier = 'notfound.png' 'notfound.png' print(fichier) 207 207 Solutions des exercices −1 C HAPITRE F Retour sur l’exercice F3 Il suffit d’appliquer deux fois la méthode blit : def montage(origine) : image = pygame.image.load(origine) pellicule = pygame.image.load('pellicule.png') finale = pygame.Surface((420,300)) finale.blit(image,(60,13)) finale.blit(pellicule,(0,0)) return finale Retour sur l’exercice F4 On peut inclure la fonction montage dans la classe puisqu’elle ne sera utilisée que dans celle-ci. Pour le reste, pas de difficulté majeure : on charge le contenu du fichier INI, récupère le nom du jeu et place dans la bande l’image montée sur la pellicule : class Jeux : def montage(origine) : image = pygame.image.load(origine) pellicule = pygame.image.load('pellicule.png') finale = pygame.Surface((420,300)) finale.blit(image,(60,13)) finale.blit(pellicule,(0,0)) return finale def __init__(self) : self.config = configparser.ConfigParser() self.config.read('liste.ini') self.nbj = len(self.config.sections()) self.bande = pygame.Surface((420,300*(self.nbj))) y = 0 for jeu in self.config.sections() : fichier = self.config.get(jeu, 'image', fallback = 'impossible') if not os.path.isfile('images/'+fichier) : fichier = 'notfound.png' img = Jeux.montage('images/'+fichier) self.bande.blit(img,(0,y)) y = y + 300 Si vous voulez pouvoir vérifier le résultat, n’hésitez pas à ajouter pygame.image.save(self.bande,'bande.png') Retour sur l’exercice F5 Il faut en effet créer deux nouveaux attributs : un premier decale et un second ombre qui contiendra l’image. Ensuite deux simples blit suffisent pour afficher la portion de la bande en question et l’effet souhaité. Merci David ! ! ! 208 F RONT- END F RONT- END class Jeux : Retour sur l’exercice F6 On peut s’inspirer du code de Cupidon pour gérer les interactions du clavier : peut s’inspirer du code de Cupidon pour gérer Retour sur l’exercice F6 On class Jeux : les interactions du clavier : ... class Jeux : def update(self) : ... touches = pygame.key.get_pressed() def update(self) : if touches[K_UP] : touches = pygame.key.get_pressed() self.decale = self.decale + 1 if touches[K_UP] : : touches[K_DOWN] + 1 self.decale = self.decale if touches[K_ESCAPE] touches[K_DOWN] : : self.decale return False= self.decale - 1 if touches[K_ESCAPE] : self.dessine() return return TrueFalse self.dessine() return True Il faut ensuite créer - enfin - la boucle principale. Dans cette correction, on peut quitter le menu fermant la fenêtre ou en appuyant sur la touche Il faut ensuite créer -en enfin - la boucle principale. Dans cette correction, échappe. on peut quitter le menu en fermant la fenêtre ou en appuyant sur la touche pygame.init() échappe. pygame.display.set_caption("SELECTIONNER VOTRE JEU") pygame.init() fenetre = pygame.display.set_mode((1600,900)) pygame.display.set_caption("SELECTIONNER VOTRE JEU") image = pygame.image.load('fond.png') fenetre = pygame.display.set_mode((1600,900)) fenetre.blit(image,(0,0)) image pygame.image.load('fond.png') jeux ==Jeux() fenetre.blit(image,(0,0)) jeux = Jeux() continuer = True clock = pygame.time.Clock() continuer = True while continuer: clock = pygame.time.Clock() clock.tick(100) while continuer: for event in pygame.event.get(): clock.tick(100) if event.type == QUIT : for event in pygame.event.get(): continuer = False if jeux.update() event.type == : QUIT : if not continuer = False continuer = False if not jeux.update() : pygame.display.flip() continuer = False pygame.quit() pygame.display.flip() pygame.quit() Retour sur l’exercice F7 Dans un premier temps, il faut modifier quelque peu la déclaration deF7 l’attribut bande en agrandissant surface :quelque Retour sur l’exercice Dans un premier temps, il fautlamodifier 209 peu la déclaration de l’attribut bande en agrandissant la surface : 209 Solutions des exercices class Jeux : def __init__(self) : ... def __init__(self) self.decale = 0: ...= pygame.image.load('ombre.png').convert_alpha() self.ombre self.decale ... = 0 self.ombre = pygame.image.load('ombre.png').convert_alpha() ... def dessine(self) : fenetre.blit(self.bande,(1180,0),(0, self.decale,420,900)) def dessine(self) : fenetre.blit(self.ombre,(1180,0)) fenetre.blit(self.bande,(1180,0),(0, self.decale,420,900)) fenetre.blit(self.ombre,(1180,0)) C HAPITRE F self.bande = pygame.Surface((420,300*self.nbj)) devient : self.bande = pygame.Surface((420,300*(self.nbj+3))) et on recopie les 3 premières images en fin de bande : self.bande.blit(self.bande,(0,300*self.nbj),(0,0,420,900)) On peut alors modifier la méthode update en conséquence avec les remarques précédentes : def update(self) : touches = pygame.key.get_pressed() if touches[K_UP] : self.decale = self.decale + 1 if self.decale == self.nbj * 300 : self.decale = 0 if touches[K_DOWN] : self.decale = self.decale - 1 if self.decale == -1 : self.decale = self.nbj*300-1 if touches[K_ESCAPE] : return False self.dessine() return True Ce code fonctionne mais si vous possédez plus de 10 jeux, vous allez rapidement vous rendre compte que la vitesse de défilement n’est pas assez rapide. La solution suivante permet de régler une variable pas de défilement. Il faut alors modifier un peu les conditions et les affectations : s’il y a 10 jeux et que vous avancez de 7 en 7, peu de chance que decale soit égale à 3000 à un moment ou un autre. En effet, le premier multiple de 7 qui dépasse 3000 est 3003, dans ce cas, il faut remettre decale à 3 et non à 0 : def update(self) : pas = 2 touches = pygame.key.get_pressed() if touches[K_UP] : self.decale = self.decale + pas if self.decale >= self.nbj * 300 : self.decale = self.decale - self.nbj * 300 if touches[K_DOWN] : self.decale = self.decale - pas if self.decale < 0 : self.decale = self.decale + self.nbj*300 if touches[K_ESCAPE] : return False self.dessine() return True 210 Retour sur l’exercice F8 Comme le numéro du jeu nj change à chaque fois que decale augmente de 300, nous allons effectuer une division par 300. Par exemple nj vaut 2 lorsque 150 ⩽ decale < 450. Si on réalise la division par 300, on va obtenir 2 valeurs possibles (0 ou 1). Il faut donc augmenter decale de 150. Avec la formule nj = (decale + 150)//300, nj vaut 1 tout l’intervalle considéré. Il faut encore incrémenter la valeur de nj de 1 ou, ce qui revient au même, augmenter decale de 300 : nj = (decale + 450)//300. Enfin, il faut gérer les dépassements lorsque nj dépasse le nombre maximum de jeux. On peut faire cela avec un test ou en calculant un reste, comme cela a déjà été fait plusieurs fois dans le livre. On ajoute alors ces quatre lignes dans la méthode update de notre classe : if touches[K_SPACE] : nj = ((self.decale+450) // 300) % self.nbj print(self.config.sections()[nj]) return False Enfin, pour afficher le jeu sélectionné dans le téléviseur, on ajoute un attribut tv qui contiendra l’image du téléviseur et qui sera chargé lors de l’instanciation de l’objet, puis on met à jour la méthode update en affichant l’image puis la TV aux coordonnées indiquées dans l’aide. def update(self) : pas = 2 touches = pygame.key.get_pressed() if touches[K_UP] : self.decale = self.decale + pas if self.decale >= self.nbj * 300 : self.decale = self.decale - self.nbj * 300 if touches[K_DOWN] : self.decale = self.decale - pas if self.decale < 0 : self.decale = self.decale + self.nbj*300 if touches[K_ESCAPE] : return False nj = ((self.decale+450) // 300) % self.nbj fichier = self.config.get(self.config.sections()[nj], 'image', ⤦ � fallback = 'impossible') if not os.path.isfile('images/'+fichier) : fichier = 'notfound.png' img = pygame.image.load('images/'+fichier) fenetre.blit(img,(606,503)) fenetre.blit(self.tv,(546,490)) if touches[K_SPACE] : print (self.config.sections()[nj]) return False self.dessine() return True 211 Solutions des exercices F RONT- END C HAPITRE F Retour sur l’exercice F9 Il n’y a pas de difficulté particulière puisque l’équation de la courbe a été donnée : class USprites() : def __init__(self) : self.sprite = pygame.image.load('sprite.png').convert_alpha() self.t = 0 def update(self) : x = 687 + 133 + 133*sin(8 * self.t) y = 149 + 99 + 99*cos(5 * self.t) self.t = self.t + 0.01 fenetre.blit(self.sprite,(x,y)) Il faut néanmoins penser : ● à importer les fonctions sinus et cosinus : from math import cos, sin ● déclarer une instance de la classe avant la boucle principale : unlimited = USprites() ● appeler la mise à jour dans la boucle principale : unlimited.update() Retour sur l’exercice F10 Après quelques essais, je vous propose d’utiliser 5 écrans pour un meilleur rendu. Commençons par ajouter deux attributs : ● ecrans est une liste de 5 surfaces de taille 286 × 218. ● numero indique le numéro de l’écran sur lequel on est en train de dessiner. def __init__(self) : self.sprite = pygame.image.load('sprite.png').convert_alpha() self.t = 0 self.ecrans = [pygame.Surface((286,218)) for i in range(5)] self.numero = 0 Il faut ensuite adapter la fonction update, on conserve le même principe que dans l’exercice précédent, mais au lieu de dessiner directement sur la fenêtre, on dessine sur l’écran virtuel, puis on recopie l’intégralité de cet écran sur la fenêtre, 212 F RONT- END def update(self) : x = 133 + 133*sin(8 * self.t) y = 99 + 99*cos(5 * self.t) self.t = self.t + 0.002 self.ecrans[self.numero].blit(self.sprite,(x,y)) fenetre.blit(self.ecrans[self.numero],(687,149)) self.numero = (self.numero + 1)%5 Retour sur l’exercice F11 1. Déterminons une formule qui permet de passer de l’abscisse x à l’angle a. L’énoncé nous dit que la fonction est affine donc on a a(x) = mx + p. Comme a(0) = π, on trouve immédiatement que p = π. D’autre part a(160) = 160m + π = 0 donc π π (160 − x)π m=− . Finalement a(x) = − x+π = 160 160 160 class Etoile : def __init__(self, x, y, c) : self.hauteur = y self.angle = (160-x)*pi/160 self.couleur = c 2. A l’aide de la formule expliquée dans l’aide, on peut écrire la méthode dessine sans difficulté : def dessine(self, surf) : x = int(80 + 80*cos(self.angle)) surf.set_at((x, self.hauteur), self.couleur) Retour sur l’exercice F12 En suivant les instructions, on obtient le code : class Champ : def __init__(self) : image = pygame.image.load('thuillier.png') self.ciel = [] for x in range(160) : for y in range(120) : c = image.get_at((x,y)) if c != (176,161,128) : self.ciel.append(Etoile(x, y, c)) def update(self) : fond = pygame.Surface((160,120)) fond.fill((176,161,128)) for e in self.ciel : e.dessine(fond) e.angle = e.angle + 0.01 fenetre.blit(fond,(99,123)) 213 Solutions des exercices C HAPITRE F Si on veut éviter de modifier l’attribut angle d’une étoile depuis l’extérieur de la classe, comme cela a déjà été conseillé, on peut faire le choix de modifier à l’intérieur de la méthode dessine : class Etoile : def __init__(self, x, y, c) : self.hauteur = y self.angle = (160-x)*pi/160 self.couleur = c def dessine(self, surf) : x = int(80+80*cos(self.angle)) surf.set_at((x, self.hauteur), self.couleur) self.angle = self.angle + 0.01 class Champ : def __init__(self) : image = pygame.image.load('thuillier.png') self.ciel = [] for x in range(160) : for y in range(120) : c = image.get_at((x,y)) if c != (176,161,128) : self.ciel.append(Etoile(x, y, c)) def update(self) : fond = pygame.Surface((160,120)) fond.fill((176,161,128)) for e in self.ciel : e.dessine(fond) fenetre.blit(fond,(99,123)) 214 Objectif Programmer un PAC-MAN Programmation Lecture d’un fichier CSV Traitement d’image Héritage et surcharge en POO Informatique Table de hashage. Chapitre G EHPAD-RUN EHPAD-RUN est un jeu directement inspiré du très célèbre jeu vidéo PacMan créé par Toru Iwatani pour la société Namco. Sa sortie au Japon le 22 mai 1980 marque un point de repère dans l’histoire du jeu vidéo. Ce jeu a généré plus de 2,5 milliards de dollars au cours des années 90, une des meilleures recettes pour un jeu vidéo. Par Namco (vectorisation vectorlib.free.fr) - Arcade Vector graphics Le jeu d’origine consiste à déplacer Pac-Man à l’intérieur d’un labyrinthe, afin de lui faire manger toutes les pac-gommes qui s’y trouvent, en évitant d’être touché par des fantômes. Dans cette version revisitée, vous incarnez un dentier qui tente de ramasser le plus de colle dentaire tout en étant poursuivi par des grabataires aux canines défaillantes. 215 C HAPITRE G 1 - Manipulation d’images Jusque-là, nous avons manipulé des images fournies sur le site du livre et réalisées par le talentueux David BEAUGET. Nous allons apprendre à en créer par nous-mêmes. Transformations d’images Lorsque l’on dispose d’un objet de type Surface, nous avons déjà présenté comment récupérer ou modifier la couleur d’un pixel via les fonctions get at et set at. Voici quelques méthodes supplémentaires : Méthode surf .get size() Effet Retourne la dimension de l’image en pixels sous la forme d’un couple (largeur, hauteur). surf .get width() Retourne la largeur de l’image. surf .get height() Retourne la hauteur de l’image. surf .get bytesize() Renvoie le nombre d’octets utilisés pour coder un pixel de l’image. Cela permet donc d’avoir des informations sur l’image(*) . surf .subsurface(r) Retourne une copie de la surface surf délimitée par le rectangle r qui peut être donnée par un quadruplet (x,y,l,h). ✍ (*) : 1 octet : Mode 256 couleurs (par exemple c’est le cas des images GIF, qui utilisent une palette de 256 couleurs) - 3 octets : Mode RVB (16 millions de couleurs) et 4 : Mode RVBA (16 millions de couleurs avec transparence). Attention, l’appel à cette fonction doit se faire avant l’appel de la méthode convert qui convertit justement l’image au format de la fenêtre de jeu. 216 EHPAD-RUN E X G1 Écrire un programme qui charge une image et affiche différentes informa- tions : ● Son poids (en octets). ● Sa taille (en pixels). ● Le mode de couleurs (256 couleurs ou 16 millions). ● S’il y a des pixels transparents. ● S’il n’y a pas transparence, le nombre de couleurs différentes utilisées. En tapant le code de l’exercice sur le site, vous pourrez télécharger des fichiers de tests, voici ce que vous devriez trouver : barbe.png Taille : 144224 octets Résolution : 400 x 335 pixels 16 millions de couleurs Image avec transparence. danseurs.png Taille : 177519 octets Résolution : 640 x 354 pixels 256 couleurs Image sans transparence 254 couleurs utilisées. danseurs.jpg Taille : 81139 octets Résolution : 640 x 354 pixels 16 millions de couleurs Image sans transparence 3686 couleurs utilisées. oiseau.gif Taille : 14949 octets Résolution : 157 x 216 pixels 256 couleurs Image sans transparence 255 couleurs utilisées. Intéressons nous à présent à quelques transformations géométriques simples sur ces images : E X G2 Écrire des fonctions qui reçoivent une surface RVB en argument et qui la transforment et renvoient une nouvelle surface en réalisant... 1. ... une symétrie d’axe vertical 217 C HAPITRE G 2. ... une symétrie d’axe horizontal 3. ... une rotation de 90○ dans le sens horaire. Comme vous l’aurez peut-être pressenti, ces transformations de base ont déjà été implémentées dans Pygame. Ces fonctions ont été regroupées dans le module transform : ● pygame.transform.flip(s,v,h) : renvoie l’image de la surface s obtenue par symétrie d’axe vertical si v vaut True et horizontal si h vaut True. Si h et v valent toutes les deux True, les deux transformations sont appliquées créant ainsi une symétrie centrale par rapport au centre de l’image. ● pygame.transform.rotate(s,a) : renvoie une surface contenant l’image de la surface s après rotation d’angle a (en degrés) dans le sens trigonométrique (anti-horaire). Attention les dimensions de la nouvelle image après rotation seront modifiées, comme l’illustre la figure ci-dessous. Illustration par Dmitry Abramov - avec une rotation de 30° 218 EHPAD-RUN Ainsi, dans l’exercice précédent, on aurait obtenu le même résultat en saisissant : def vertical(img): return pygame.transform.flip(img, True, False) def horizontal(img): return pygame.transform.flip(img, False, True) def rotation(img) : return pygame.transform.rotate(img, -90) E X G3 On peut fabriquer le négatif d’une image en transformant chaque pixel en son complément au blanc. Par exemple un pixel orange dont le code couleur RVB est (229,123,28) sera transformé en bleu turquoise (26,132,227) obtenu par le calcul suivant : (255−229,255−123,255−28). Écrire un programme qui affiche le négatif d’une image en mode RVB. E X G4 On cherche à présent à redimensionner une image. L’algorithme le plus naı̈f consiste à regarder d’où provient chaque point de l’image finale. Un simple produit en croix permet de trouver les relations : sx × sy newX × newY Avant Après Si on note (x,y) les coordonnées d’un pixel sur l’image d’origine et (x′ ,y ′ ) les coordonnées du pixel correspondant sur l’image redimensionnée, il nous faut donc x et y en fonction de x′ et y ′ . Abscisse avant Abscisse après 0 0 sx -1 newX -1 x x′ Ordonnée avant Ordonnée après 0 0 sy -1 newY -1 y y′ y ′ × (sy − 1) x′ × (sx − 1) et y = , newX − 1 newY − 1 qu’il faudra arrondir à l’entier le plus proche. Ainsi, on obtient : x = Écrire une fonction zoom qui reçoit comme paramètres une surface et ses nouvelles dimensions et renvoie l’image redimensionnée. Sur un exemple avec le fichier sphere.png de dimensions 2000 × 2000, disponible sur le site, on obtient cette image en la redimensionnant en 100 × 100 (les images ne sont pas à l’échelle pour mieux voir et comprendre) : 219 C HAPITRE G C HAPITRE G La nouvelle image est crénelée, on La nouvelle image d’aliasing, est crénelée, on parle de problème ce qui parle de problème d’aliasing, ce qui peut s’expliquer par le schéma ci-contre. peut s’expliquer par le schéma ci-contre. Pour passer d’une image de 2000 pixels Pour passer d’une image de 2000 pixels de large à 100, on a découpé l’image de large à 100, on a découpé l’image de départ sur sa largeur en 100 carrés sur(ici sa largeur 100 carrés de départ 20 pixels 5 pour en l’explication). de 20 pixels (ici 5 pour l’explication). Au lieu de remplacer chaque carré par Au pixel lieu dederemplacer chaque carré par un la couleur du coin hautun pixel de la couleur du coin hautgauche de celui-ci, il serait préférable de gauche celui-ci, de il serait faire la de moyenne tous préférable les pixels de (y faire la moyenne de tous les si pixels (y compris pour la transparence elle est compris pour la transparence si elle est présente). présente). Ainsi, avec le premier algorithme, les 5 cases de la première ligne deAinsi, avec le premier les 5 cases de la première ligne deviendraient blanches, alorsalgorithme, qu’avec la nouvelle méthode on obtiendrait viendraient blanches, alors qu’avec la nouvelle méthode on obtiendrait E X G5 Écrire une nouvelle fonction zoom1 qui reçoit une image de taille 2000× E X G5 Écrire une fnouvelle fonction zoom1 qui reçoit une de taille 2000× 2000 et un facteur qui divise ses dimensions et renvoie uneimage surface représentant 2000 et un facteurdont f quiles divise ses dimensions unepar surface l’image d’origine dimensions auront etétérenvoie divisées f enreprésentant utilisant la l’image d’origine dont les dimensions auront été divisées par f en utilisant la méthode d’anti-aliasing présentée. méthode d’anti-aliasing présentée. 220 220 EHPAD-RUN EHPAD-RUN Il existe de de nombreux nombreux algoalgorithmes pour pour modifier modifier la la taille taille d’une image, image, comme comme l’illustre l’illustre lala liste des choix choix de de filtres filtres dans dans Gimp : Un certain certain nombre nombre de de filtres filtressont sontdéjà déjàimplémentés implémentésdans danslelemomodule PIL (Python (PythonImaging ImagingLibrary) Library)qui quiest estun unmodule modulespécialisé spécialisédans dans le traitement traitement d’images d’images et et dont dontvous vouspouvez pouvezdécouvrir découvrirl’étendue l’étenduedes des possibilités sur sur le le site sitehttps https://pillow.readthedocs.io/ ://pillow.readthedocs.io/sisilelesujet sujetvous vous intéresse. Les algorithmes algorithmes ayant ayant les lesmeilleurs meilleursrendus rendussont sontsouvent souventles lesplus plus gourmands gourmands en en temps temps de de calcul. calcul.Pour Pourcela celaililexiste existedeux deuxmanières manièresdede réaliser un zoom zoom avec avec Pygame Pygame : : ● pygame.transform.scale(s,(l,h)) pygame.transform.scale(s,(l,h)): :renvoie renvoieune unesurface surfacecorcorrespondant respondant àà l’image l’image de de lalasurface surfacesszoomée zoomée(ou (ourétrécie) rétrécie)aux aux dimensions dimensions ll ××h. h. ● pygame.transform.smoothscale(s,(l,h)) pygame.transform.smoothscale(s,(l,h)): :même mêmeeffet, effet, mais avec avec une une meilleure meilleurequalité qualitéde detransformation transformation(mais (maisaussi aussi plus lente). lente). Ainsi, au cours cours de de vos vos jeux, jeux,vous vousdevrez devrezsouvent souventfaire faireun unchoix choixentre entre la qualité de de transformation transformation ou oulalavitesse vitessede detransformation transformation: :sisiune une image peut être être pré-calculée pré-calculéeavant avantleledéroulement déroulementdu dujeu, jeu,autant autantmimiser sur la qualité. qualité. Si Si par par contre contre lalatransformation transformationaalieu lieuau aucours coursdu du jeu, il est préférable préférable de demiser misersur surlalavitesse vitessede detransformation. transformation.ÀÀnoter noter que contrairement contrairement au au module modulePIL, PIL,ililn’est n’estactuellement actuellementpas paspossible possible de préciser la la qualité qualité de de la latransformation transformationpour pourune unerotation. rotation. Finalement, Finalement, les les deux deux exercices exercicesprécédents précédentspeuvent peuventêtre êtrecodés codésdede manière bien bien plus plus simple simple par par:: def zoom(img, zoom(img, newX, newX, newY) newY) :: return return pygame.transform.scale(img, pygame.transform.scale(img, (newX, (newX, newY)) newY)) def zoom1(img, zoom1(img, f) f) :: sx, sy sy == img.get_size() img.get_size() return return pygame.transform.smoothscale(img, pygame.transform.smoothscale(img, (sx (sx// //f, f,sy sy////f)) f)) 221 221 C HAPITRE G Dessiner des images Nous avons déjà utilisé à de nombreuses reprises la fonction blit qui permet de réaliser des copier-coller d’images. E X G6 À partir de l’image ci-dessous de fleur de lys de dimensions 250 × 150, réaliser une image de taille 600 × 400 représentant le drapeau officiel du Québec, aussi appelé drapeau fleur de lys. �⇒ E X G7 Réaliser un programme qui charge une image, la découpe en 100 morceaux (10 × 10) et la mélange tel un puzzle (pour jouer au jeu du taquin par exemple). On pourra se limiter à des images dont les dimensions sont des multiples de 10 pour simplifier le problème. E X G8 Un artiste a réalisé sur feuille le croquis d’un tatouage parfaitement symétrique. Malheureusement, un accident de parcours a dégradé l’œuvre de deux taches. Saurez-vous reconstituer le dessin d’origine ? Plus efficacement que de dessiner point par point, on peut, avec le module draw de Pygame, dessiner des formes. Dans la suite des explications, les noms des fonctions doivent être précédés de pygame.draw. . 222 × ✍ ✍ ✍ ✍ Dessine un segment reliant le point deb = (x1 , y1 ) × de coordonnées deb au pointlede coDessine un segment reliant point Une ligne : line(s,c,deb,f in,e) : ordonnées f in avec c. On de coordonnées deb la aucouleur point de codeb = (x1 , y1 ) × EHPAD-RUN peut préciser l’épaisseur e en pixels (1 ordonnées f in avec la couleur c. On Dessine un segment reliant le point par défaut). peut préciser l’épaisseur en pixels (1 de coordonnées deb au epoint de co× f in = (x2 , y2 ) par défaut). ordonnées f in avec la couleur c. On × f in = (x2 , y2 ) Une ligne : line(s,c,deb,f in,e) : peut préciser l’épaisseur e en pixels (1 deb = (x1 , y1 ) × par défaut). Dessine un segment reliant le point × f in = (x2 , y2 ) de coordonnées deb au point de coordonnées f in avec la couleur c. On peut préciser l’épaisseur e en pixels (1 par défaut). × in = (x2à, y2bout ) Il est aussi possible de dessiner plusieurs lignes f bout sous forme de lignes brisées la fonction lines et bout même Il est aussi possible de avec dessiner plusieurs lignes à des boutlignes sous plus avec anti-aliasing nouslines l’avons évoqué pour formejolies de lignes brisées avec comme la fonction et déjà même des lignes le zoom. N’hésitez pas à aller vous documenter sur les fonctions plus jolies avec anti-aliasing comme nous l’avons évoqué pour Il est aussi possible de dessiner plusieurs lignes déjà bout à bout sous aaline aalines queavec détaillerons pasetici. le zoom. pas ànous aller vous documenter sur lesdes fonctions forme deetN’hésitez lignes brisées lane fonction lines même lignes aaline et avec aalines que nous ne détaillerons pas ici. plus jolies anti-aliasing comme nous l’avons déjà évoqué pour le zoom. N’hésitez pas à aller vous documenter sur les fonctions aaline et aalines quedessiner nous ne plusieurs détaillerons pas ici. Il est aussi possible de lignes bout à bout sous forme de lignes brisées avec la fonction lines et même des lignes plus jolies avec anti-aliasing comme nous l’avons déjà évoqué pour Un rectangle : rect(s,c,r,e) le zoom. N’hésitez pas à aller vous documenter sur les fonctions la surface s un rectangle délimité par le rectangle r Un Dessine rectanglesur : rect(s,c,r,e) aaline et aalines que nous ne détaillerons pas ici. avecDessine la couleur c. l’épaisseur est précisée et strictement positive,r sur laSisurface s un erectangle délimité par le rectangle on un: rectangle vide dont contoureta strictement une épaisseur de e. avec la couleur c. Si l’épaisseur e estleprécisée positive, Un obtient rectangle rect(s,c,r,e) Dans les autres cas, le rectangle est plein. Pensez que pour dessiner on obtient un rectangle vide dont le contour a une épaisseur de e.r Dessine sur la surface s un rectangle délimité par le rectangle un rectangle plein, sur une surface la taille souDans autres cas, leméthode rectanglefill Pensez quedepour dessiner avec lalescouleur c. Silal’épaisseur eest estplein. précisée et strictement positive, haitée est bien plus la efficace ! dont un rectangle méthode filllesur une surface la taille de souon obtient unplein, rectangle vide contour a une de épaisseur e. haitée est bien plus efficace ! Dans les autres cas, le rectangle est plein. Pensez que pour dessiner Un rectangle : rect(s,c,r,e) un rectangle plein, la méthode sur une surface taille sou-r Dessine sur la surface s unfill rectangle délimité pardelelarectangle haitéelaest bien plus ! avec couleur c. Siefficace l’épaisseur e est précisée et strictement positive, r on obtient un rectangle vide dont le contour a une épaisseur de e. r Dans autres cas, le rectangle est plein. Pensez que pour dessiner Une les ellipse : ellipse(s,c,r,e) un rectangle plein, la méthode fill surdesune surface de la taille souAvec les mêmes paramètres que rect, Une ellipse : ellipse(s,c,r,e) r haitée est bien plus efficace ! que sine une ellipse inscrite dans unrect, rectangle Avec les mêmes paramètres des(imaginaire) sine une ellipse inscrite dans un rectangle Une ellipse :r.ellipse(s,c,r,e) (imaginaire) r. paramètres que rect, desAvec les mêmes sine une ellipse inscrite dans un rectangle r (imaginaire) r. 223 Une ellipse : ellipse(s,c,r,e) 223 Avec les mêmes paramètres que rect, dessine une ellipse inscrite dans un rectangle 223 (imaginaire) r. 223 C HAPITRE G C HAPITRE Un arc G d’ellipse : arc(s,c,r,ad,af ,e) C HAPITRE G Avec les mêmes paramètres que ellipse, Un arc d’ellipse : arc(s,c,r,ad,af dessine un arc d’ellipse délimité ,e) par l’angle Un arc d’ellipse : arc(s,c,r,ad,af ,e) de départ ad et l’angle de fin af . ContraiAvec mêmes: arc(s,c,r,ad,af paramètres que,e) ellipse, Un arc les d’ellipse Avec les mêmes paramètres que ellipse, rement rectangles ellipses, par les arcs ne dessine aux un arc d’ellipseetdélimité l’angle dessine arc d’ellipse délimité l’angle Avec lesun mêmes paramètres que par ellipse, peuvent être de départ adpleins. et l’angle de fin af . Contraide départ l’angle délimité de fin afpar . Contraidessine un ad arcetd’ellipse l’angle rement auxetrectangles et ellipses, les arcs ne Ici ad = 10 af = 130 rement auxad rectangles ellipses, arcs ne de départ et l’angleet de fin af les . Contraipeuvent être pleins. peuventaux êtrerectangles pleins. et ellipses, les arcs ne rement Ici ad = 10 et af = 130 peuvent être pleins. Ici ad = 10 et af = 130 Ici ad = 10 et af = 130 On peut dessiner un polygone : r r r r On peut dessiner un polygone : On peut dessiner un polygone : Un polygone : polygon(s, c, pts, fill= 0) On peut dessiner un polygone : Dessine sur la surface s un polygone dont les sommets sont donnés par polygone la liste pts: polygon(s, = [(x1 ,y1 ),(x2,y Un c,2 ),...,(x pts, fill= 0) de la couleur c. Le n ,yn )] et Un polygone : polygon(s, c, pts, fill= 0) dernier paramètre fill svaut 0 par défaut, polyDessine sur la surface un polygone dont c’est-à-dire les sommetsque sontledonnés Dessine sur la surface s un polygone dont les sommets sont donnés Un polygone : polygon(s, c, pts, fill= 0) gonelasera Si c’est un entier strictement il représente par listerempli. pts = [(x ,y1 ),(x2,y de la couleur c. Le n ,yn )] et positif, 2 ),...,(x par la liste de ptslala =ligne [(x11 ,y ),(x2,y ),...,(x ,yvide. etsommets de la couleur c. Le Dessine sur surface s un polygone sont donnés ndont n )]les l’épaisseur et1le polygone sera dernier paramètre fill vaut 0 2par défaut, c’est-à-dire que le polydernier paramètre fill vaut 0 par défaut, c’est-à-dire que le poly,y ),(x2,y ),...,(x ,y )] et de la couleur c. Le par la liste pts = [(x n n 1 un entier 2 gone sera rempli. Si 1c’est strictement positif, il représente gone sera rempli. Si c’est un entier strictement positif, il représente dernier paramètre fill vaut 0 par défaut, c’est-à-dire que le polyl’épaisseur de la ligne et le polygone sera vide. l’épaisseur de la ligne et leun polygone sera vide. positif, il représente gone sera rempli. Si c’est entier strictement l’épaisseur de la ligne et le polygone sera vide. E X G9 Dessiner le drapeau de la Jamaı̈que, sachant qu’il est deux fois plus large que haut et que, comme sur le schéma, les points marqués d’une bille se trouvent àEune distance d’un dixième de la longueur segment à partir d’une X G9 Dessiner le drapeau Jamaı̈que,du sachant qu’il est deux foisextrémité. plus large E X G9 Dessiner le drapeau de la Jamaı̈que, sachant qu’il est deux fois plus large que haut et que, comme sur le schéma, les points marqués d’une bille se trouvent que haut et que, comme sur ledeschéma, les points marqués d’une bille trouvent EX G 9 Dessiner le drapeau la Jamaı̈que, sachant qu’il est deux foisseplus large à une distance d’un dixième de la longueur du segment à partir d’une extrémité. à une distance dixième deschéma, la longueur du segment à partir extrémité. que haut et que,d’un comme sur le les points marqués d’uned’une bille se trouvent à une distance d’un dixième de la longueur du segment à partir d’une extrémité. 224 224 224 224 EHPAD-RUN 2 - L’interface du jeu E X G10 On cherche à dessiner un tuyau, comme sur la capture ci-dessous. Pour cela, je vous suggère de vous inspirer de la méthode proposée par le logiciel GIMP : on se donne une liste de couleurs et les emplacements de celles-ci (triangles noirs). Entre deux triangles, les couleurs évoluent de manière linéaire (si on ne tient pas compte des triangles blancs) : 225 C HAPITRE G Dans notre cas, on souhaiterait obtenir cette image. 1280+noir = [0, 0, 0] 880+noir = [0, 0, 0] 760+blanc = [255, 255, 255] 740+gris = [190, 190, 190] 640+jaune = [255, 255, 100] 540+gris = [190, 190, 190] 520+blanc = [255, 255, 255] 400+noir = [0, 0, 0] image tuyau.png - 1280 × 1280 0+noir = [0, 0, 0] 1. Pour le moment, on ne gère qu’une couche (par exemple la bleue). Écrire une fonction affine(couleurs, hauteurs, h) qui reçoit une liste d’entiers représentant la liste des couleurs, une seconde représentant la liste des hauteurs et un entier indiquant la hauteur souhaitée et renvoie la couleur à afficher à cette hauteur. Pour tester, vous pouvez vous appuyer sur l’exemple : >>> >>> >>> 0 >>> 128 >>> 136 >>> 100 C = [0, 0, 255, 190, 100, 190, 255, 0, 0] H = [0, 400, 520, 540, 640, 740, 760, 880, 1280] affine(C, H, 200) affine(C, H, 460) affine(C, H, 600) affine(C, H, 640) 2. Modifier la fonction précédente pour obtenir une fonction identique mais dont le premier paramètre est une liste de couleurs (les éléments sont des triplets (R,V,B) ) et la sortie un triplet (R,V,B). 3. En utilisant la fonction précédente, écrire un programme qui génère l’image tuyau.png souhaitée. 226 EHPAD-RUN On cherche à partir de l’image tuyau.png de taille 1280×1280 pixels, à générer une série de 11 images de taille 32×32 pour créer le décor comme ci-dessous : horiz.png vert.png croix.png 1.png 5.png 2.png 6.png 3.png 7.png 4.png 8.png E X G11 À l’aide de ce que vous avez appris dans ce chapitre, créer les images horiz.png et vert.png de taille 32 × 32. E X G12 On cherche à fusionner les images vert.png et horiz.png pour obtenir l’image croix.png ci-contre. 1. Donner l’équation des droites (D1 ) et (D2 ). vert 2. En déduire les conditions nécessaires et suffisantes pour qu’un point de coordonnées (x,y) de l’image croix.jpg provienne de l’image horiz.jpg. 3. Justifier que la précédente condition peut s’écrire horiz horiz vert D2 D1 (x − y)(31 − x − y) < 0 4. Écrire un programme générant l’image croix.png. E X G13 S’inspirer du programme précédent pour générer l’image 5.png, puis les images 6.png, 7.png et 8.png. E X G14 Plus difficile, on souhaite à présent ”tordre” l’image tuyau.png pour obtenir le quart de cercle 1.png. On travaille cette fois-ci avec des images en résolution 1280 × 1280 afin de garantir une meilleure qualité. 1. En remarquant que le point A(x,y) de la figure 1.png est de la couleur des points de la ligne d = OA de la figure tuyau.png, imaginer un programme qui crée le fichier 1.png qu’il faudra ensuite redimensionner en image 32 × 32 : 227 C HAPITRE G C HAPITRE G O O d d d d A A tuyau.png tuyau.png 1.png 1.png 2. Créer enfin les images 2.png, 3.png, et 4.png. 2. Créer enfin les images 2.png, 3.png, et 4.png. À présent que nous disposons des images pour dessiner le décor du jeu,Ànous allons pouvoir le dessiner. est dedessiner générer leundécor décordu à présent que nous disposons desL’objectif images pour partir d’unallons fichierpouvoir CSV. le dessiner. L’objectif est de générer un décor à jeu, nous partir d’un fichier CSV. �⇒ �⇒ Capture d’écran réalisée lors d’un atelier aux journées nationales de Capture d’écran réalisée lors d’un l’APMEP en 2015 à Laon. de atelier aux journées nationales l’APMEP en 2015 à Laon. Pour un fichier d’import/export de feuilles de calculs d’un tableur, le format (Comma Separated Values : Valeurs Séparées partableur, des VirPourCSV un fichier d’import/export de feuilles de calculs d’un le gules) est le plus commun. C’est un simple fichier Séparées texte (la mise en page format CSV (Comma Separated Values : Valeurs par des Virn’est où les contenus colonnes sont séparés par en despage virgules)pas estprésente) le plus commun. C’est undes simple fichier texte (la mise gules. Onprésente) peut donc lirecontenus très facilement comme un séparés simple fichier n’est pas oùleles des colonnes sont par destexte viravec Cependant, tous logiciels ne fonctionnant pas fichier de la même gules.Python. On peut donc le lire trèslesfacilement comme un simple texte manière (choix du séparateur delogiciels colonne,nechoix du séparateur pour un avec Python. Cependant, tous les fonctionnant pas de la même nombre le module de csvcolonne, permet choix de simplifier les accès à ces manière décimal...) (choix du séparateur du séparateur pour un fichiers, à l’instar des fichiers INI présentés au chapitre précédent. nombre décimal...) le module csv permet de simplifier les accès à ces fichiers, à l’instar des fichiers INI présentés au chapitre précédent. 228 228 EHPAD-RUN csv.reader(f , options) : renvoie un objet reader dont le contenu est celui du fichier f. On peut alors itérer sur cet objet pour parcourir le fichier, chaque ligne étant alors une liste de chaı̂nes de caractères. En option, on peut préciser le délimiteur de colonnes, par exemple delimiter=’;’. Voici un exemple simple qui lit et affiche le contenu du fichier niveau1.csv : import csv with open('niveau1.csv') as f: contenu = csv.reader(f, delimiter = ';') for ligne in contenu: print(ligne) On obtient alors des lignes ressemblant à : ['x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', ... ['x', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ... ['x', '', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', '', 'x', 'x', ... ... ✍ Ici, nous n’utiliserons pas ces fonctions, mais sachez que : ● vous pouvez aussi enregistrer des informations au format CSV avec la fonction writer. ● vous pouvez aussi lire/enregistrer dans des dictionnaires directement avec DictReader et DictWriter. Si votre feuille tableur contient par exemple des titres à vos colonnes (NOM, PRENOM, ...) vous pourrez alors avoir directement accès aux informations par ces titres qui seront alors les clés de ce dictionnaire. https://docs.python.org/fr/3/library/csv.html Enfin, si vous retapez le code précédent, ne faites pas comme moi en enregistrant votre programme sous le nom csv.py. Il y aurait alors un conflit pour charger le module du nom csv ! E X G15 Ecrire une fonction charge qui reçoit un nom de fichier et renvoie une liste de listes (on parlera de matrice par la suite) représentant le décor avec la convention : 0 : Case vide 1 : Mur 2 : Gomme 229 C HAPITRE G E X G16 Écrire une fonction decor qui reçoit la ma- trice précédente et retourne une surface où le vide est représenté par un carré blanc, les murs par un carré noir et les gommes par un cercle jaune. On souhaite à présent faire un décor plus soigné en ajoutant des courbes, des croisements ainsi que les autres images générées en début de chapitre. Pour cela, on va regarder les 4 voisins (nord, sud, est, ouest) de la case que l’on veut représenter. Par exemple si on a le motif x x x , on dessinera l’image À terme, regarder à chaque fois les 4 voisins, en prenant garde aux problèmes de bords va rapidement être rébarbatif à mettre en œuvre pour lister les 11 cas qui peuvent se présenter. On se propose alors de coder la configuration des voisins avec un nombre de 4 bits en donnant à chacun des voisins un poids : 1 2 x 4 . Dans l’exemple précédent, le code du virage est 4 + 8 = 12. 8 E X G17 Écrire une fonction code(m, l, c) qui reçoit le numéro de la ligne et de la colonne de la case à dessiner et renvoie le code correspondant. E X G18 À l’aide d’une table de correspondance (aussi appelée table de hashage), adapter la fonction decor pour qu’elle dessine un beau décor. Pour dessiner les gommes, on pourra prendre l’image gomme.png présente sur le site du livre et représentant une goutte de colle à dentier. 3 - Les personnages Pac-Man : le dentier Pour gérer le déplacement de notre héros, on peut s’inspirer du personnage de Cupidon à quelques exceptions près : ● il faut tenir compte des éléments de décor pour autoriser ou non le déplacement ; 230 EHPAD-RUN ● une fois un déplacement dans une direction demandée, le personnage se déplace jusqu’à toucher un mur ou jusqu’à ce qu’une autre touche de direction possible soit enfoncée : c’est la tradition du jeu. On dispose des images 32 × 32 pour réaliser l’animation : pac1.png pac2.png pac3.png pac4.png E X G19 Écrire la classe Pacman qui possèdera pour attributs : ● x et y : les coordonnées du coin Nord-Ouest du personnage en pixels. ● direction : la direction vers laquelle se déplace Pac-Man. On gardera la même convention que pour la recherche de voisins : 1, 2, 4, 8. ● animation : le numéro de l’animation en cours pour animer le personnage. ● images : une liste d’images : attente haut droite bas gauche et comme méthode : ● update : une fonction qui analyse les touches enfoncées et déplace PacMan dans la direction voulue (on ne s’occupe pas du décor pour le moment). Il nous faut à présent tenir compte du décor afin de savoir si le déplacement est possible ou non. Pensez que le point (x,y) est le point × du schéma ci-contre. Ainsi si on veut aller à droite par exemple il faudra calculer les futures coordonnées, puis vérifier si les deux points ⧫ et ● ne sont pas sur un obstacle. En effet, si vous ne vérifiez qu’un des points, votre personnage risque de chevaucher les murs. × )) 231 C HAPITRE G E X G20 Ajouter une méthode dep possible(m, d) qui reçoit la matrice de décor et la direction souhaitée (d ∈ {1; 2; 4; 8}) et renvoie un booléen indiquant si le déplacement est possible. E X G21 Mettre à jour la méthode update pour que le personnage puisse se déplacer en respectant le décor du jeu. E X G22 Terminer le ramassage des gommes pour qu’à chaque gomme avalée, le score augmente de 10 points. Les fantômes : les pensionnaires de l’EHPAD Nous allons ajouter des fantômes. Pour cela, créons donc une nouvelle classe Fantome possédant comme attributs : ● x et y : ses coordonnées, ● animation : le numéro de l’animation et nb anim le nombre d’animations, car attention, tous ne possèdent pas autant d’images pour les animer, ● direction : la direction en cours (1, 2, 4 ou 8), ● pas : le nombre de pixels dont avance le fantôme par frame lorsque celui-ci se déplace (sa vitesse en quelque sorte), ● images : une liste d’images, et comme méthodes : ● dessine : une méthode qui dessine le fantôme, ● dep possible : comme pour Pac-Man, ● ... E X G23 Créer le constructeur, qui sera appelé ainsi : Fantome(img, xd, yd) où (xd, yd) sont les coordonnées de départ et img une chaı̂ne indiquant le préfixe de l’image. Par exemple les images du premier fantôme se nomment F1-1.png et F1-2.png et il se trouve en (320,448) au début du jeu. L’instanciation se fera donc avec l’appel : Fantome(’F1’,320,448). E X G24 Ajouter la méthode update qui effectue le déplacement indiqué par l’attribut direction et anime le fantôme à l’écran. Attention, un fantôme ne mange pas de gomme ! 232 EHPAD-RUN Comme vous l’aurez remarqué, si on dissocie la partie affichage de la partie déplacement du Pac-Man, les fonctions d’affichage pour un fantôme EHPAD-RUN Comme vous l’aurez remarqué, si on dissocie la partie affichage de la ou notre héros sont identiques. C’est d’ailleurs pareil pour la méthode partie déplacement du Pac-Man, les fonctions d’affichage pour un fantôme dep possible qui serait un simple copier-coller... Comme vous vous en ou notre héros sont identiques. C’est d’ailleurs pareil pour la méthode Comme vous remarqué, si on dissocie la partie affichage de la doutez, inutile de l’aurez recommencer plusieurs fois la même chose. Découvrons dep possible qui serait un simple copier-coller... Comme vous vous en partie déplacement du Pac-Man, les fonctions d’affichage pour un fantôme ensemble la notion d’héritage. doutez, recommencer fois la même ou notreinutile hérosde sont identiques.plusieurs C’est d’ailleurs pareilchose. pour Découvrons la méthode Dans la nous allons ajouter différents fant ômes (Snoop, Reniensemble la suite, notion d’héritage. dep possible qui serait un simple copier-coller... Comme vous vous en fleur, Tom-Tom) plus ou moins intelligents. Un certain nombre de doutez, de recommencer plusieurs fois la même chose. Découvrons Dansinutile la suite, nous allons ajouter différents fantômes (Snoop, Reniméthodes la etnotion d’attributs seront communs à tous les fantômes, d’autres seensemble d’héritage. fleur, Tom-Tom) plus ou moins intelligents. Un certain nombre de ront spécifiques à tel ou tel fantôme comme le choix de la direction à méthodes d’attributs à tous les fant ômes, d’autres seDans laetsuite, nous seront allons communs ajouter différents fant ômes (Snoop, Reniprendre. ront spécifiques tel ou fantôme comme leUn choix de la nombre directionde à fleur, Tom-Tom) àplus outelmoins intelligents. certain prendre. et d’attributs seront communs à tous les fantômes, d’autres seméthodes ront spécifiques à tel ou tel fantôme comme le choix de la direction à prendre. Personnage attributs : x, y, animation, direction,.. Personnage méthodes : update, dep possible,... attributs : x, y, animation, direction,.. Personnage méthodes : update, dep possible,... Pacman Fantome attributs : x, y, animation, direction,.. Pacman Fantome méthodes : update, dep possible,... score,... nb anim,... mange,... collision,... Pacman Fantome score,... nb anim,... mange,... collision,... Snoop Renifleur score,... nb anim,... choix choix Snoop Renifleur mange,... collision,... choix choix Snoop Renifleur GPS Glouton choix choix choix choix GPS Glouton choix Glouton choix GPS choix choix Comme le personnage de Pac-Man est déjà opérationnel, nous n’allons pas dans ce chapitre créer la classe Personnage, mais cela pourra Comme le personnage de Pac-Man est déjà opérationnel, nous être réalisé pour d’autres projets. Par contre nous allons créer les 3 classes n’allons pas dans ce chapitre créer la classe Personnage, mais cela pourra Snoop, Renifleur et Tomtom à partir de la classe Fantome. êtreComme réalisé pour d’autres projets. Par contre est nousdéjà allons créer les 3 classes le personnage de Pac-Man opérationnel, nous Snoop, Renifleur et Tomtom à partir de la classe Fantome. n’allons pas dans ce chapitre créer la classe Personnage, mais cela pourra 233 être réalisé pour d’autres projets. Par contre nous allons créer les 3 classes 233 Snoop, Renifleur et Tomtom à partir de la classe Fantome. 233 C HAPITRE G Le fantôme Snoop : comprendre l’héritage et la surcharge Programmons ensemble le cas du fantôme Snoop afin de comprendre la notion d’héritage. Pour indiquer à Python que la classe Snoop est héritée de la classe Fantome, on déclare ainsi : class Fantome() : def __init__(self, self.x, self.y self.nb_anim = self.direction img, xdeb, ydeb) : = xdeb, ydeb 0 = 1 class Snoop(Fantome) : pass Et voilà ! Le simple fait de spécifier la classe mère au moment de la déclaration d’une classe fille suffit à indiquer l’héritage. La dernière ligne pass signifie simplement ≪ ne rien faire ≫ et évite à Python de planter, car il attend quelque chose après la déclaration de la classe. Vérifions le résultat obtenu : >>> s = Snoop('F1',32,320) >>> s <__main__.Snoop object at 0x0352F890> >>> s.x 32 >>> doe = Fantome('F1',32,320) >>> doe <__main__.Fantome object at 0x0352FFD0> Ainsi l’objet s est de type Snoop et doe de type Fantome, mais leurs comportements sont identiques... Pas trop utile de ce fait pour le moment... Ajoutons un constructeur à la classe Snoop et une méthode info : class Snoop(Fantome) : def __init__(self, img, xdeb, ydeb) : self.nom = 'Snoop' def info(self) : print('Mon nom est', self.nom) Les objets de la classe Snoop possèdent un nouvel attribut nom : >>> s = Snoop('F1',32,32) >>> s.info() Mon nom est Snoop 234 EHPAD-RUN Par contre, les autres attributs ont été perdus ! >>> s.x Traceback (most recent call last): File "<string>", line 301, in runcode File "<interactive input>", line 1, in <module> AttributeError: 'Snoop' object has no attribute 'x' Voici l’explication : lorsque l’on a défini la méthode init de la nouvelle classe, on a écrasé celle de la classe mère. On dit dans ce cas que l’on a surchargé la méthode. Pour Python, la portée des méthodes ou des attributs suit toujours la même logique : le moteur commence par chercher au plus près puis remonte dans la hiérarchie des classes jusqu’à la trouver et déclenche une erreur sinon. Dans ce dernier exemple, comme le constructeur est défini dans la classe Snoop, Python choisit cette déclaration et n’exécute donc pas la méthode init de la classe Fantome, contrairement à l’exemple précédent. Heureusement, si l’on souhaite instancier l’objet comme un objet de la classe Fantome, puis ajouter d’autres choses, on peut procéder comme suit, en précisant le nom de la classe de la méthode que l’on souhaite exécuter : class Snoop(Fantome) : def __init__(self, xdeb, ydeb) : Fantome.__init__(self, 'F1', xdeb, ydeb) self.nom = 'Snoop' >>> >>> Mon >>> 32 s = Snoop(32,320) s.info() nom est Snoop s.x E X G25 Écrire la description de la classe Snoop qui possède comme méthodes : ● un constructeur qui prend 2 paramètres xdeb et ydeb indiquant la position de départ et appellera à son tour le constructeur de la classe Fantome ; ● une fonction choix qui reçoit en paramètre une matrice de décor et modifie l’attribut direction du fantôme selon une règle très simple : si on peut aller vers le haut, on y va, sinon, on s’arrête... Cette fonction fera appel à la méthode dep possible de la classe Fantome que vous aurez ajoutée ; ● une fonction update qui sera une surcharge de la méthode update de la classe Fantome. Cette nouvelle méthode nécessite un paramètre m en entrée pour indiquer le décor et se chargera de faire l’appel à choix, puis d’afficher le fantôme. 235 C HAPITRE G E X G26 Améliorer la méthode choix précédente pour que le fantôme Snoop ait son comportement final : il avance jusqu’à toucher un mur et choisit alors une nouvelle direction aléatoire parmi les directions possibles. L’ennemi Renifleur Le fantôme de la classe ≪ Renifleur ≫ se dirige systématiquement vers une case qui minimise la distance de Manhattan jusqu’à Pac-Man (Voir chapitre B). E X G27 Créer une classe Renifleur ressemblant à la classe Snoop, mais dont la méthode choix correspond au comportement souhaité. Si vous avez réussi le précédent exercice, vous aurez remarqué que l’appel à update pour les 2 classes diffère un peu au niveau des paramètres, votre programme ressemble sûrement à : f1 = Snoop(320,448) f2 = Renifleur(320,448) ... while continuer : ... f1.update(matrice) f2.update(matrice, pm) ... On préfèrerait une solution plus harmonisée, ce qui permettrait d’automatiser les appels. Par exemple : ennemis = [Snoop(320,448), Renifleur(320,448)] ... while continuer : ... for f in ennemis : f.update(matrice, pm) ... E X G28 Effectuer les changements nécessaires pour uniformiser les appels. 236 EHPAD-RUN Le fantôme Tom-Tom Ce dernier fantôme a un sixième sens lui permettant de réaliser des prouesses tel un GPS. Pour trouver son chemin, nous allons utiliser à nouveau un parcours en largeur du décor tel une tâche d’huile qui se répand dans le labyrinthe permettant de connaı̂tre les distances de Pac-Man à chaque case, comme dans le chapitre B. Avant de nous lancer dans le code en lui-même, nous allons préparer la matrice de décor pour pouvoir faire les calculs. La convention retenue sera de mettre des -1 où il y a des murs et des -2 où il y a autre chose (gommes, cases vides....). E X G29 Écrire une fonction copie qui reçoit une matrice de décor et retourne une nouvelle matrice correspondant à la représentation exposée. E X G30 Programmer une fonction tache qui reçoit une matrice ne contenant que des -1 (il y a un mur) et des -2 (la case est accessible) ainsi que l et c indiquant la ligne et la colonne où se trouve Pac-Man. Cette fonction renvoie une matrice où, sur toute case accessible, se trouve le nombre de pas la séparant de Pac-Man. >>> matrice = [ [-1, -1, -1, [-1, -2, -2, [-1, -2, -1, [-1, -2, -2, [-1, -2, -1, [-1, -1, -1, -1, -2, -1, -2, -2, -1, -1, -2, -1, -1, -2, -1, -1, -1, -2, -2, -2, -1, −1 −1 −1 −1 −1 −1 −1 -1], -1], -1], -1], -1], -1]] 1 −1 2 3 2 3 −1 −1 −1 1 −1 −1 −1 9 −1 >>> tache(matrice,1,1) −1 0 donne : 4 −1 8 −1 −1 3 −1 5 6 7 −1 −1 −1 −1 −1 −1 −1 −1 L’intérêt d’avoir obtenu cette matrice est que, quel que soit l’endroit où se trouve le fantôme, on connaı̂t le chemin à prendre (il suffit de prendre la direction d’une case avec une valeur plus petite). E X G31 Écrire une fonction vers qui reçoit cette nouvelle matrice et deux entiers indiquant la ligne et la colonne du fantôme puis retourne un entier de l’ensemble {1; 2; 4; 8} donnant la direction à prendre pour rejoindre Pac-Man. >>> chemins = tache(matrice,1,1) >>> vers(chemins, 3, 2) 8 237 C HAPITRE G Un problème néanmoins subsiste : dès que Pac-Man se déplace, il faut recalculer toutes les distances avec la fonction tache, ce qui risque à terme, de ralentir le jeu. On se propose d’avoir recours à une table de hashage, comme cela a été présenté dans l’exercice 18. Cette table sera un dictionnaire dont les clés seront des quadruplets de la forme (lp,cp,lf ,cf ) indiquant les positions de Pac-Man et du fantôme et les valeurs de la direction à prendre. Dans notre exemple, table[(1,1,3,2)] vaut 8. E X G32 Écrire un programme qui renseigne cette table. E X G33 Écrire une classe Tomtom permettant de créer ce type de fantôme et inclure celle-ci dans la partie pour terminer le projet. Voilà, l’aventure de Pac-Man touche à sa fin, mais d’autres vont suivre. Bien entendu, on peut améliorer le projet : ● limiter la vitesse du fantôme Tom-Tom qui est un peu trop fort pour nous permettre de gagner. On peut par exemple diviser sa vitesse par 2, ou encore retirer des clés de table ; ● détecter la fin de partie quand on a gagné ou quand on a perdu ; ● placer les fantômes et Pac-Man en fonction des informations dans le fichier csv ; ● alléger la table en ne conservant en mémoire que les cases où le fantôme a des choix de directions (sinon, on continue dans la direction en cours) ; ● .... Ce projet pourrait être plus abouti en utilisant les connaissances développées dans les chapitres précédents, ce n’est pas notre objectif ici. Néanmoins une version plus complète mais non commentée est présente sur le site avec le code PACMAN . 4 - Aide pour les exercices Aide pour l’exercice G1 La première question n’a pas grand rapport avec les images, mais c’est l’occasion de découvrir la fonction getsize du module path disponible dans le package os qui renvoie la taille d’un fichier. 238 EHPAD-RUN Aide pour l’exercice G7 Une technique relativement simple pour mélanger deux blocs peut être : ● de choisir 2 blocs au hasard, ● de les échanger, ● de recommencer ces opérations un certain nombre de fois. Pour mémoire, l’appel randint(a,b) du module random renvoie un entier de l’intervalle [a,b]. Aide pour l’exercice G9 Si vous avez des difficultés à calculer les coordonnées, voici les coordonnées des différents points qui pourraient vous intéresser si le drapeau mesure 600 × 300 pixels. Pour les triangles verts, inutile de perdre du temps à calculer les coordonnées du sommet restant, on peut se contenter de dessiner un triangle un peu plus grand en allant jusqu’au milieu du drapeau. 0 30 0 60 540 600 270 300 Aide pour l’exercice G10 Si on représente par des points les couleurs et les hauteurs connues, on peut représenter les situations ainsi (les données sont fictives) : couleurs c1 A �→ h2 − h1 ��→ h − h1 AB ( ) et AM ( ) c2 − c1 c − c1 M c =? c2 B hauteurs h1 h h2 239 C HAPITRE G Pour répondre à notre problème, il faut donc commencer par trouver les points A et B dont les abscisses encadrent h, puis déterminer c. On �→ ��→ pourra remarquer que les vecteurs AB et AM sont colinéaires et donc que leurs coordonnées sont proportionnelles, on en déduit : (h2 − h1 ) × (c − c1 ) = (c2 − c1 ) × (h − h1 ) Aide pour l’exercice G18 Voici la table de correspondance : 0 1 2 3 4 5 6 1 8 1 2 8 1 2 8 1 2 8 1 2 8 1 2 8 7 1 2 8 1 2 8 2 4 4 4 4 4 4 4 4 horiz vert horiz 4 vert vert 3 7 8 9 10 11 12 13 14 15 1 1 1 1 1 1 1 8 2 8 2 8 2 8 2 8 2 8 2 8 1 2 8 2 4 4 4 4 4 4 4 4 horiz 1 horiz 8 2 5 6 croix Aide pour l’exercice G22 On pourra ajouter une méthode mange et un attribut score à la classe Pacman. Aide pour l’exercice G23 Pour charger les images, comme on ne connaı̂t pas à l’avance le nombre de celles-ci, vous pouvez utiliser la fonction isfile déjà présentée dans le livre. Aide pour l’exercice G27 On peut commencer par écrire une fonction manhattan(x1,y1,x2,y2) qui renvoie la distance de Manhattan entre 2 cases de coordonnées (x1,y1) et (x2,y2). Aide pour l’exercice G30 N’hésitez pas consulter l’exercice B20 si vous êtes bloqué. 5 - Solutions des exercices Retour sur l’exercice G1 Une fois la fonction getsize connue, il n’y a pas de difficulté pour le reste. Pour compter le nombre de couleurs, on peut créer une liste et y placer chaque couleur trouvée si elle n’y est pas encore présente : 240 EHPAD-RUN import pygame from os.path import getsize fichier = "barbe.png" print('Taille :',getsize(fichier), 'octets') m = img.get_bytesize() if m == 1 : print('256 couleurs') elif m >= 3 : print('16 millions couleurs') else : print('Autre mode') if m == 4 : print('Image avec transparence.') else : print('Image sans transparence') L = [] for x in range(sx) : for y in range(sy) : coul = img.get_at((x,y)) if coul not in L : L.append(coul) print(len(L),'couleurs utilisées.') Retour sur l’exercice G2 1. Pour effectuer une inversion verticale, on va ”retourner” chaque ligne, c’est-à-dire que : si on note sx la largeur de l’image, pendant qu’on lit les pixels de x = 0 à x = sx − 1, on écrit sur l’image finale de x = sx − 1 à x = 0. Ainsi on obtient le code suivant : import pygame def vertical(img) : taille = img.get_size() nouvelle = pygame.Surface(taille) sx, sy = taille for x in range(sx) : for y in range(sy) : c = img.get_at((x,y)) nouvelle.set_at((sx-x-1,y),c) return nouvelle image = pygame.image.load('pacman.png') pygame.image.save(vertical(image),'pacmanV.png') 241 Solutions des exercices img = pygame.image.load(fichier) sx, sy = img.get_size() print('Résolution :',sx,'x',sy,'pixels') C HAPITRE G 2. Même principe que précédemment mais on retourne les colonnes (seule la seconde partie du programme est modifiée) : def horizontal(img) : taille = img.get_size() nouvelle = pygame.Surface(taille) sx, sy = taille # Miroir vertical : for x in range(sx) : for y in range(sy) : c = img.get_at((x,y)) nouvelle.set_at((x,sy-y-1),c) return nouvelle 3. Pour la rotation, regardez où arrivent les 4 points aux coins de l’image. Trouver la formule n’est pas très difficile. Attention de créer une image aux bonnes dimensions, si celle-ci n’est pas carrée : def rotation(img) : sx, sy = img.get_size() nouvelle = pygame.Surface((sy, sx)) for x in range(sx) : for y in range(sy) : c = img.get_at((x,y)) nouvelle.set_at((sy-y-1,x),c) return nouvelle Retour sur l’exercice G3 Il suffit d’appliquer l’algorithme indiqué : import pygame def negatif(img) : if img.get_bytesize() != 3 : raise Exception("Cette image n'est pas en mode RVB") taille = img.get_size() nouvelle = pygame.Surface(taille) sx, sy = taille for x in range(sx) : for y in range(sy) : r, v, b, _ = img.get_at((x,y)) nouvelle.set_at((x,y),(255-r, 255-v, 255-b)) return nouvelle image = pygame.image.load('pacman.png') pygame.image.save(negatif(image),'pacmanN.png') Pour récupérer les 3 composantes rouge, vert, bleu, on pouvait écrire : c = img.get_at((x,y)) r, v, b = c[0], c[1], c[2] mais l’affectation directe telle qu’elle est présentée est bien plus efficace (on a une quatrième quantité qui vaut 255 puisqu’il n’y a pas de transparence). 242 EHPAD-RUN Ici on a fait le choix dans la correction de lever une exception à l’aide de la commande raise lorsque le mode de l’image n’est pas RGB, ce qui a pour effet de stopper l’exécution du programme.... Retour sur l’exercice G4 En appliquant l’algorithme présenté : def zoom(img, newX, newY) : taille = img.get_size() sx, sy = taille nouvelle = pygame.Surface((newX, newY)) for x in range(newX) : for y in range(newY) : x_origine = round(x*(sx-1)/(newX-1)) y_origine = round(y*(sy-1)/(newY-1)) c = img.get_at((x_origine,y_origine)) nouvelle.set_at((x,y),c) return nouvelle image = pygame.image.load('sphere.png') pygame.image.save(zoom(image, 100, 100), 'sphereZ.png') Retour sur l’exercice G5 Pour appliquer la méthode proposée, on a appelé ici x et y les coordonnées du pixel dans l’image finale, il faut donc les multiplier par f pour avoir les coordonnées dans l’image de départ. Les variables xc et yc permettent, elles, de se déplacer dans une ”case” et ainsi d’additionner toutes les couleurs. Pour obtenir la moyenne, il faut enfin diviser par f 2 qui est le nombre de pixels par case. import pygame def zoom1(img, f) : taille = img.get_size() sx, sy = taille if sx % f != 0 or sy % f != 0 : raise Exception("Trop dur pour moi !") nouvelle = pygame.Surface((sx//f, sy//f)) for x in range(sx//f) : for y in range(sy//f) : newr, newv, newb = 0, 0, 0 for xc in range(f) : for yc in range(f) : r, v, b, _ = img.get_at((f*x+xc, 20*y+yc)) newr, newv, newb = newr + r, newv + v, newb + b newr, newv, newb = newr // f**2, newv // f**2, newb // f**2 nouvelle.set_at((x,y),(newr, newv, newb)) return nouvelle image = pygame.image.load('sphere.png') pygame.image.save(zoom1(image, 20), 'sphereZ1.png') 243 Solutions des exercices import pygame C HAPITRE G zoom zoom1 Retour sur l’exercice G6 Ici, seule l’instruction blit sera utilisée 4 fois : import pygame lys = pygame.image.load('lys.png') drapeau = pygame.Surface((600,400)) drapeau.fill((255,255,255)) drapeau.blit(lys,(0,0)) drapeau.blit(lys,(350,0)) drapeau.blit(lys,(350,250)) drapeau.blit(lys,(0,250)) pygame.image.save(drapeau,'quebec.png') Retour sur l’exercice G7 En s’appuyant sur l’aide proposée, on va répéter 100 fois l’échange de 2 blocs choisis aléatoirement. Ici on a besoin d’une variable temporaire pour stocker le bloc n°1, car lorsque l’on copie le bloc n°2 sur le bloc n°1 ce dernier est écrasé. import pygame from random import randint puzzle = pygame.image.load('danseurs.png') sx, sy = puzzle.get_size() larg, haut = sx // 10, sy // 10 temporaire = pygame.Surface((larg, haut)) for i in range(100) : x1, y1 = larg*randint(0, 9), haut*randint(0, 9) # On choisit un bloc x2, y2 = larg*randint(0, 9), haut*randint(0, 9) # Puis un second temporaire.blit(puzzle, (0,0), (x1, y1, larg, haut)) puzzle.blit(puzzle, (x1, y1), (x2, y2, larg, haut)) puzzle.blit(temporaire, (x2,y2)) pygame.image.save(puzzle,'puzzle.png') 244 EHPAD-RUN EHPAD-RUN Retour sur l’exercice G8 Une première solution en utilisant une surface pour faire les copier-coller : Retour sur l’exercice G8 Une première solution en utilisant une surface pour faire les copier-coller : import pygame False), (0,0)) False), (0,0)) False), (215, 230)) False), (215, 230)) Une seconde sans surface intermédiaire : Une seconde sans surface intermédiaire : import pygame from pygame.transform import flip import pygame t = pygame.image.load('masque.png') from pygame.transform import flip t.blit(flip(t.subsurface((215,0,215,230)), t.blit(flip(t.subsurface((0,230,215,230)), t = pygame.image.load('masque.png') pygame.image.save(t, 'propre.png') t.blit(flip(t.subsurface((215,0,215,230)), t.blit(flip(t.subsurface((0,230,215,230)), pygame.image.save(t, 'propre.png') True, False), (0,0)) True, False), (215, 230)) True, False), (0,0)) True, False), (215, 230)) Retour sur l’exercice G9 Une solution : Retour sur l’exercice G9 Une solution : import pygame jama = pygame.Surface((600,300)) import pygame vert = (0,156,55) pygame.draw.polygon(jama, vert, [(60,0),(300,150),(570,0)]) jama = pygame.Surface((600,300)) pygame.draw.polygon(jama, vert, [(60,300),(300,150),(570,300)]) vert = (0,156,55) jaune = (254,210,0) pygame.draw.polygon(jama, vert, [(60,0),(300,150),(570,0)]) pygame.draw.polygon(jama, jaune, [(0,0), (60,0), (600,270), (600,300), ⤦ pygame.draw.polygon(jama, vert, [(60,300),(300,150),(570,300)]) � (540,300), (0,30)]) jaune = (254,210,0) pygame.draw.polygon(jama, jaune, [(0,300), (60,300), (600,30), (600,0), ⤦ pygame.draw.polygon(jama, jaune, [(0,0), (60,0), (600,270), (600,300), ⤦ � (540,0), (0,270)]) � (540,300), (0,30)]) pygame.draw.polygon(jama, jaune, [(0,300), (60,300), (600,30), (600,0), ⤦ pygame.image.save(jama, 'jamaique.png') � (540,0), (0,270)]) pygame.image.save(jama, 'jamaique.png') 245 245 Solutions des exercices tortue = pygame.image.load('masque.png') import pygame # On copie le coin Nord-Est propre = tortue.subsurface((215,0,215,230)) tortue = pygame.image.load('masque.png') # On le colle à l'Ouest avec un miroir vertical # On copie le coin Nord-Est tortue.blit(pygame.transform.flip(propre, True, propre = tortue.subsurface((215,0,215,230)) # On copie le coin Sud-Ouest # On le colle à l'Ouest avec un miroir vertical propre = tortue.subsurface((0,230,215,230)) tortue.blit(pygame.transform.flip(propre, True, # On le colle à l'Est avec un miroir vertical # On copie le coin Sud-Ouest tortue.blit(pygame.transform.flip(propre, True, propre = tortue.subsurface((0,230,215,230)) pygame.image.save(tortue, 'propre.png') # On le colle à l'Est avec un miroir vertical tortue.blit(pygame.transform.flip(propre, True, pygame.image.save(tortue, 'propre.png') C HAPITRE G Retour sur l’exercice G10 1. Avec la formule démontrée dans l’aide, on peut coder la fonction demandée : def affine(couleurs, hauteurs, h) : # On cherche les 2 couleurs qui nous intéressent for i in range(len(couleurs)-1) : if hauteurs[i] <= h <= hauteurs[i+1] : h1, c1 = hauteurs[i], couleurs[i] h2, c2 = hauteurs[i+1], couleurs[i+1] # Formule proposée : return c1 + round((h-h1)*(c2-c1)/(h2-h1)) 2. La seconde question ne pose pas de problème, il suffit d’effectuer les mêmes opérations sur les trois couches : def affine2(couleurs, hauteurs, h) : for i in range(len(couleurs)-1) : if hauteurs[i] <= h <= hauteurs[i+1] : h1 = hauteurs[i] r1, v1, b1 = couleurs[i] h2 = hauteurs[i+1] r2, v2, b2 = couleurs[i+1] return r1 + round((h-h1)*(r2-r1)/(h2-h1)), v1 + ⤦ � round((h-h1)*(v2-v1)/(h2-h1)), b1 + ⤦ � round((h-h1)*(b2-b1)/(h2-h1)) 3. On code alors l’image avec les informations de l’exercice : import pygame tuyau = pygame.Surface((1280, 1280)) noir = (0, 0, 0) jaune = (255, 255, 100) gris = (190, 190, 190) blanc = (255, 255, 255) C = [noir, noir, blanc, gris, jaune, gris, blanc, noir, noir] H = [0, 500, 520, 540, 640, 740, 760, 780, 1280] for h in range(1280) : coul = affine2(C, H, h) pygame.draw.line(tuyau, coul, (0,h),(1280,h)) pygame.image.save(tuyau, 'tuyau.png') Retour sur l’exercice G11 Avec les fonctions smoothscale et rotate, on crée facilement les 2 images : import pygame horiz = pygame.image.load('tuyau.png') horiz = pygame.transform.smoothscale(horiz,(32,32)) vert = pygame.transform.rotate(horiz,90) pygame.image.save(horiz, 'horiz.png') pygame.image.save(vert, 'vert.png') 246 EHPAD-RUN Retour sur l’exercice G12 1. La droite (D1 ) passe par les points (0,0) et (31,31). Son équation est donc D1 ∶ y = x. De même (D2 ) passe par les points (0,31) et (31,0), son équation est donc D2 ∶ y = −x + 31. horiz vert horiz horiz horiz 2. vert vert D1 D2 y >x y > −x + 31 Ainsi, on doit copier l’image horiz.png si et seulement si : { x−y ⩾ 0 y >x y ⩽x x−y < 0 ⇐⇒ { ou { . ou { 31 − x − y < 0 y ⩽ 31 − x y > 31 − x 31 − x − y ⩾ 0 3. Si on ne tient pas compte des cas d’égalité, on doit copier l’image horiz.png si et seulement si (x − y)(31 − x − y) < 0. 4. On obtient donc le programme suivant : import pygame horiz = pygame.image.load('horiz.png') vert = pygame.transform.rotate(horiz,90) croix = pygame.Surface((32,32)) for x in range(32) : for y in range(32) : if (x-y)*(31-x-y) < 0 : coul = horiz.get_at((x,y)) else: coul = vert.get_at((x,y)) croix.set_at((x,y), coul) pygame.image.save(croix, 'croix.png') 247 Solutions des exercices vert C HAPITRE G Retour sur l’exercice G13 Avec les explications de l’exercice précédent, on s’aperçoit que pour l’image 5.png, cette fois, il ne faut copier les pixels x−y < 0 de l’image horiz.png que lorsque { . Les 3 autres images 31 − x − y ⩾ 0 s’obtiennent ensuite par simple rotation : import pygame horiz = pygame.image.load('horiz.png') vert = pygame.image.load('vert.png') img5 = pygame.Surface((32,32)) for x in range(32) : for y in range(32) : if x-y < 0 and 31-x-y >= 0 : coul = horiz.get_at((x,y)) else: coul = vert.get_at((x,y)) img5.set_at((x,y), coul) pygame.image.save(img5, '5.png') pygame.image.save(pygame.transform.rotate(img5,90), '6.png') pygame.image.save(pygame.transform.rotate(img5,180), '7.png') pygame.image.save(pygame.transform.rotate(img5,270), '8.png') Retour sur l’exercice G14 En suivant les instructions données en énoncé, on arrive à nos fins : import pygame from math import sqrt horiz = pygame.image.load('tuyau.png') arc = pygame.Surface((1280,1280)) for x in range(1280) : for y in range(1280) : d = round(sqrt(x**2+y**2)) if d > 1279 : d = 1279 coul = horiz.get_at((0,d)) arc.set_at((x,y), coul) arc = pygame.transform.smoothscale(arc,(32,32)) pygame.image.save(arc, '1.png') pygame.image.save(pygame.transform.rotate(arc,90), '2.png') pygame.image.save(pygame.transform.rotate(arc,180), '3.png') pygame.image.save(pygame.transform.rotate(arc,270), '4.png') 248 EHPAD-RUN Retour sur l’exercice G15 Une solution s’inspirant de l’exemple du cours : def charge(fichier) : matrice = [] with open(fichier) as f: contenu = csv.reader(f, delimiter = ';') for ligne in contenu : ligne_decor = [] for case in ligne : if case == 'x' : ligne_decor.append(1) elif case == '' : ligne_decor.append(2) else : ligne_decor.append(0) matrice.append(ligne_decor) return matrice Toutes les cases autres qu’un mur ou une gomme sont considérées vides. Retour sur l’exercice G16 En utilisant la fonction charge précédemment créée : def decor(m) : nbl, nbc = len(m), len(m[0]) surf = pygame.Surface((nbc*32, nbl*32)) surf.fill((255,255,255)) for l in range(nbl) : for c in range(nbc) : if m[l][c] == 1 : surf.fill((0,0,0),(c*32,l*32,32,32)) elif m[l][c] == 2 : pygame.draw.ellipse(surf,(128,128,0),(c*32+8,l*32+8,16,16)) return surf Retour sur l’exercice G17 Cet exercice ne devrait pas poser de difficulté. On regarde successivement, lorsqu’on le peut, au dessus, à droite, en dessous et à gauche : def code(m, l, c) : res = 0 if l > 0 and m[l-1][c] == 1 : # On regarde dessus si c'est possible res = res + 1 if c < len(m[0])-1 and m[l][c+1] == 1 : # On regarde à droite si ⤦ � c'est possible res = res + 2 if l < len(m)-1 and m[l+1][c] == 1 : # On regarde dessous si c'est ⤦ � possible res = res + 4 if c > 0 and m[l][c-1] == 1 : # On regarde à gauche si c'est possible res = res + 8 return res 249 Solutions des exercices import csv C HAPITRE G Retour sur l’exercice G18 Avec la table de hashage déterminée dans l’aide, on peut écrire le code suivant : def decor(m) : table = ['horiz', 'vert', 'horiz', '4', 'vert', 'vert', '3', '7', 'horiz', '1', 'horiz','8','2','5','6','croix'] nbl, nbc = len(m), len(m[0]) surf = pygame.Surface((nbc*32, nbl*32)) for l in range(nbl) : for c in range(nbc) : if m[l][c] == 1 : nom = table[code(m,l,c)]+".png" surf.blit(pygame.image.load(nom), (c*32, l*32)) elif m[l][c] == 2 : surf.blit(pygame.image.load('gomme.png'), (c*32, l*32)) return surf Retour sur l’exercice G19 Il y a un peu de travail pour cet exercice, mais pas de difficulté, car la situation est identique à celle de Cupidon. On réalise la boucle principale habituelle, dans laquelle on utilise les fonctions qui ont été programmées pour afficher le décor : pygame.init() pygame.display.set_caption('PAC-MAN') fenetre = pygame.display.set_mode((960,640)) matrice = charge('niveau1.csv') fond = decor(matrice).convert() fenetre.blit(fond, (0,0)) pm = Pacman() continuer = True clock = pygame.time.Clock() while continuer: clock.tick(100) for event in pygame.event.get(): if event.type == QUIT : continuer = False pm.update() pygame.display.flip() pygame.quit() Pour la partie constructeur, on ne dispose pas de toutes les images. Il faut donc les construire, par rotation et symétries. 250 EHPAD-RUN EHPAD-RUN À noter que la commande images[-1] permet d’accéder au dernier À noter que la commande images[-1] permet d’accéder au dernier élément, images[-2] à l’avant dernier... élément, images[-2] à l’avant dernier... Enfin, si vous pensiez, comme moi, gagner du temps en effectuant une Enfin, si vous pensiez, comme moi, gagner du temps en effectuant une boucle et de simples rotations, vous vous apercevrez qu’à la deuxième boucle et de simples rotations, vous vous apercevrez qu’à la deuxième rotation, Pac-Man se retrouve avec un œil à l’envers ! rotation, Pac-Man se retrouve avec un œil à l’envers ! Reste à programmer la méthode update : on efface par un carré noir, Reste à programmer la méthode update : on efface par un carré noir, l’ancien Pac-Man et on met à jour la direction souhaitée en fonction des l’ancien Pac-Man et on met à jour la direction souhaitée en fonction des touches enfoncées (pour le moment, on ne s’occupe pas du décor). Les touches enfoncées (pour le moment, on ne s’occupe pas du décor). Les deux varaiables pas et vitesse anim représentent respectivement le deux varaiables pas et vitesse anim représentent respectivement le nombre de pixels dont on avance à chaque étape et le nombre de frames nombre de pixels dont on avance à chaque étape et le nombre de frames que dure chaque animation. On peut ainsi modifier ces informations rapique dure chaque animation. On peut ainsi modifier ces informations rapidement si besoin. Enfin pour connaı̂tre le numéro de l’image à afficher, on dement si besoin. Enfin pour connaı̂tre le numéro de l’image à afficher, on utilise encore une table de correspondance pour simplifier le code : utilise encore une table de correspondance pour simplifier le code : def update(self) : def update(self) : fenetre.fill((0,0,0), (self.x, self.y,32,32)) # efface ancienne pos. fenetre.fill((0,0,0), self.y,32,32)) # efface ancienne pos. pas, vitesse_anim = 2,(self.x, 10 pas, vitesse_anim = 2, 10 touches = pygame.key.get_pressed() touches = pygame.key.get_pressed() if touches[K_UP] : self.direction = 1 if : self.direction = 1= 2 if touches[K_UP] touches[K_RIGHT] : self.direction if touches[K_RIGHT] self.direction==42 if touches[K_DOWN] ::self.direction if if touches[K_DOWN] touches[K_LEFT] : : self.direction self.direction = = 4 8 if touches[K_LEFT] : self.direction decalage = {0:0, 1:1, 2:2, 4:3, 8:4}= 8 decalage = {0:0, 2:2, 4:3, 8:4} self.animation = 1:1, (self.animation + 1 ) % (2*vitesse_anim) self.animation = (self.animation + 1 ) % (2*vitesse_anim) if self.direction == 1 : if self.direction ==-1pas : self.y = self.y self.y = self.y pas if self.direction == 2 : if self.direction ==+2pas : self.x = self.x self.x = self.x if self.direction ==+4pas : if self.direction ==+4pas : self.y = self.y self.y = self.y if self.direction ==+8pas : if self.direction ==-8pas : self.x = self.x self.x = self.x - pas fenetre.blit(self.images[self.animation // vitesse_anim ⤦ fenetre.blit(self.images[self.animation // vitesse_anim � +2*decalage[self.direction]], (self.x, self.y)) ⤦ � +2*decalage[self.direction]], (self.x, self.y)) 251 251 Solutions des exercices class Pacman() : class Pacman() : def __init__(self) : def __init__(self) : self.x, self.y = 32, 32 self.x, self.y =self.animation 32, 32 self.direction, = 0, 0 self.direction, self.animation = 0, 0 self.images = [ self.images = [ pygame.image.load('pac1.png').convert_alpha(), pygame.image.load('pac1.png').convert_alpha(), pygame.image.load('pac2.png').convert_alpha(), pygame.image.load('pac2.png').convert_alpha(), pygame.image.load('pac3.png').convert_alpha(), pygame.image.load('pac3.png').convert_alpha(), pygame.image.load('pac4.png').convert_alpha()] pygame.image.load('pac4.png').convert_alpha()] self.images.append(pygame.transform.rotate(self.images[-2],-90)) self.images.append(pygame.transform.rotate(self.images[-2],-90)) self.images.append(pygame.transform.rotate(self.images[-2],-90)) self.images.append(pygame.transform.rotate(self.images[-2],-90))True)) self.images.append(pygame.transform.flip(self.images[-4],False, self.images.append(pygame.transform.flip(self.images[-4],False, True)) True)) self.images.append(pygame.transform.flip(self.images[-4],False, self.images.append(pygame.transform.flip(self.images[-4],False, True)) self.images.append(pygame.transform.flip(self.images[-4],True, False)) self.images.append(pygame.transform.flip(self.images[-4],True, False)) False)) self.images.append(pygame.transform.flip(self.images[-4],True, self.images.append(pygame.transform.flip(self.images[-4],True, False)) C HAPITRE G Retour sur l’exercice G20 Il faut diviser par 32 l’abscisse d’un point pour obtenir le numéro de la colonne correspondante dans la matrice et faire de même sur l’ordonnée pour avoir le numéro de la ligne. À chaque fois, il y a deux points à étudier. On s’aperçoit que l’on a à nouveau besoin de connaı̂tre la valeur de pas si on veut calculer les futures coordonnées du personnage. On passe alors pas comme attribut de la classe plutôt que variable de la fonction update : def dep_possible(self, m, d) : if d == 1 : # Vers le haut l1, c1 = (self.y-self.pas) // 32, self.x // 32 # croix l2, c2 = (self.y-self.pas) // 32, (self.x+31) // 32 # losange elif d == 2 : # Vers la droite l1, c1 = (self.y) // 32, (self.x+31+self.pas) // 32 # losange l2, c2 = (self.y+31) // 32, (self.x+31+self.pas) // 32 # Disque elif d == 4 : # Vers le bas l1, c1 = (self.y+31+self.pas) // 32, self.x // 32 # triangle l2, c2 = (self.y+31+self.pas) // 32, (self.x+31) // 32 # disque else : # Vers la gauche l1, c1 = (self.y) // 32, (self.x-self.pas) // 32 # croix l2, c2 = (self.y+31) // 32, (self.x-self.pas) // 32 # triangle return m[l1][c1] != 1 and m[l2][c2] != 1 Retour sur l’exercice G21 On ajoute les tests nécessaires : si Pac-Man heurte un mur, il s’arrête, sinon il continue dans la même direction, à moins qu’une touche ait été pressée : def update(self) : vitesse_anim = 10 # On efface l'ancienne position fenetre.fill((0,0,0), (self.x, self.y,32,32)) # Si on se cogne, on s'arrête if not self.dep_possible(matrice, self.direction) : self.direction = 0 # Si on a appuyé sur une touche : touches = pygame.key.get_pressed() if touches[K_UP] and self.dep_possible(matrice, 1) : self.direction = 1 if touches[K_RIGHT] and self.dep_possible(matrice, 2) : self.direction = 2 if touches[K_DOWN] and self.dep_possible(matrice, 4) : self.direction = 4 if touches[K_LEFT] and self.dep_possible(matrice, 8) : self.direction = 8 decalage = {0:0, 1:1, 2:2, 4:3, 8:4} self.animation = (self.animation + 1 ) % (2*vitesse_anim) .../... 252 EHPAD-RUN if self.direction == 1 : self.y = self.y - self.pas if self.direction == 2 : self.x = self.x + self.pas if self.direction == 4 : self.y = self.y + self.pas if self.direction == 8 : self.x = self.x - self.pas num_img = self.animation//vitesse_anim+2*decalage[self.direction] fenetre.blit(self.images[num_img], (self.x, self.y)) Retour sur l’exercice G22 En jetant un œil si besoin au chapitre Pong pour afficher le score, on obtient le code suivant : def mange(self, m) : l, c = (self.y+16) // 32, (self.x+16) // 32 #Au centre de Pac-Man if m[l][c] == 2 : self.score = self.score + 10 m[l][c] = 0 fichier = pygame.font.match_font('arial') fnt = pygame.font.Font(fichier, 80) texte = fnt.render(str(self.score), True, (200,200,0)) ltxt, htxt = fnt.size(str(self.score)) xtxt = 32*23+(32*6 - ltxt)//2 ytxt = 32 + (32*3 - htxt)//2 fenetre.fill((0,0,0),(32*23,32,32*6,32*3)) fenetre.blit(texte, (xtxt, ytxt)) Bien évidemment on pourrait améliorer le programme pour que le score s’affiche au bon endroit en fonction du décor du niveau. Retour sur l’exercice G23 On cherche tous les fichiers images avec le préfixe demandé pour renseigner les attributs de l’objet : class Fantome() : def __init__(self, img, xdeb, ydeb) : self.x, self.y = xdeb, ydeb self.nb_anim = 0 self.images = [] while os.path.isfile(img+'-'+str(self.nb_anim+1)+'.png') : self.images.append(pygame.image.load(img+'-' + ⤦ � str(self.nb_anim+1)+'.png').convert_alpha()) self.nb_anim = self.nb_anim + 1 self.pas = 2 self.animation = 0 self.direction = 0 253 Solutions des exercices C HAPITRE G Retour sur l’exercice G24 La méthode update pour les fantômes ressemble fortement à celle de Pac-Man. Les différences résident d’une part dans le fait que les fantômes sont animés, mais ont toujours même allure quelle que soit la direction et d’autre part dans le fait qu’il ne faut pas effectuer un fill pour effacer le fantôme avant de l’afficher à nouveau, sinon ce dernier mangerait les gommes, il faut donc copier un morceau du fond au lieu de déposer un carré noir : def update(self) : vitesse_anim = 10 # On efface l'ancienne position fenetre.blit(fond, (self.x, self.y), (self.x, self.y,32,32)) self.animation = (self.animation + 1 ) % (self.nb_anim*vitesse_anim) if self.direction == 1 : self.y = self.y - self.pas if self.direction == 2 : self.x = self.x + self.pas if self.direction == 4 : self.y = self.y + self.pas if self.direction == 8 : self.x = self.x - self.pas num_img = self.animation//vitesse_anim fenetre.blit(self.images[num_img], (self.x, self.y)) Un autre problème se pose : lorsque les fantômes repassent sur un chemin où des gommes avaient été mangées, elles réapparaissent ! Il est donc préférable de recopier aussi le fond avant de dessiner Pac-Man et d’effacer les gommes lorsque celles-ci sont mangées. Dans la méthode update de la classe Pacman, on remplace : fenetre.fill((0,0,0), (self.x, self.y,32,32)) par : fenetre.blit(fond, (self.x, self.y), (self.x, self.y,32,32)) et dans la méthode mange, on ajoute dans le test une ligne : if m[l][c] == 2 : self.score = self.score + 10 m[l][c] = 0 fond.fill((0,0,0), (c*32, l*32,32,32)) 254 EHPAD-RUN Retour sur l’exercice G25 Voici une déclaration possible qui correspond à l’énoncé : class Snoop(Fantome) : def __init__(self, xdeb, ydeb) : Fantome.__init__(self, 'F1', xdeb, ydeb) def update(self, m ) : self.choix(m) Fantome.update(self) En réalité, la variable matrice de notre programme étant une variable globale, on pourrait se passer de la mettre en paramètre des fonctions choix et update. Retour sur l’exercice G26 Il suffit de mettre à jour la méthode : on place dans une liste l’ensemble des directions possibles, puis on en choisit une à l’aide de la fonction choice : def choix(self, m) : if not self.dep_possible(m, self.direction) : choix = [] for d in(1,2,4,8) : if self.dep_possible(m, d) : choix.append(d) self.direction = choice(choix) Retour sur l’exercice G27 Voici le code d’une solution que nous allons commenter ensuite : class Renifleur(Fantome) : def __init__(self, xdeb, ydeb) : Fantome.__init__(self, 'F2', xdeb, ydeb) def manhattan(x1, y1, x2, y2) : return abs(x1-x2)+abs(y1-y2) .../... 255 Solutions des exercices def choix(self, m) : if self.dep_possible(m, 1) : self.direction = 1 else : self.direction = 0 C HAPITRE G def choix(self, m, pac) : choix = [] if self.dep_possible(m, 1) : dist = Renifleur.manhattan(pac.x, choix.append((dist,1)) if self.dep_possible(m, 2) : dist = Renifleur.manhattan(pac.x, choix.append((dist,2)) if self.dep_possible(m, 4) : dist = Renifleur.manhattan(pac.x, choix.append((dist,4)) if self.dep_possible(m, 8) : dist = Renifleur.manhattan(pac.x, choix.append((dist,8)) choix.sort() while choix[0][0] < choix[-1][0] : choix.pop(-1) self.direction = choice(choix)[1] pac.y, self.x, self.y-self.pas) pac.y, self.x+self.pas, self.y) pac.y, self.x, self.y+self.pas) pac.y, self.x-self.pas, self.y) def update(self, m, pac) : self.choix(m, pac) Fantome.update(self) ● La partie constructeur ne vous surprendra pas. On se contente de modifier l’image du fantôme et on déclare ensuite la fonction permettant de calculer la distance de Manhattan entre deux points : d(A; B) = ∣xB − xA ∣ + ∣yB − yA ∣ ● Pour la méthode choix, on ne peut plus utiliser une boucle for. On va donc créer à la main une liste contenant non seulement les directions possibles, mais aussi la distance qui séparera Pac-Man de la future position du fantôme. On stocke ces informations sous la forme (distance,direction). Ensuite on demande à Python de trier la liste. Comme ici il s’agit de couples à trier, le tri va se faire par ordre lexicographique, c’est à dire que la première composante du couple va servir de critère d’ordonnancement, puis, à premières composantes identiques, on départagera les ex-æquo en fonction de la seconde. On se retrouve donc avec une liste triée selon les distances restant à parcourir dans l’ordre croissant. On pourrait donc choisir le premier élément de cette nouvelle liste. Ici on gardera tous les couples à distance égale pour laisser un peu de hasard. On supprime donc le dernier élément de la liste (d’indice -1) tant que la distance du dernier couple est strictement supérieure à la distance du premier. Reste alors à tirer au hasard un élément et mettre à jour la distance souhaitée. 256 EHPAD-RUN ● Pour la méthode update, il faut prendre en compte le fait que nous avons non seulement besoin de la matrice comme dans le cas précédent, mais aussi de la position de Pac-Man Retour sur l’exercice G29 Il est en effet vraiment préférable de faire une copie, sinon vous écraseriez les valeurs présentes dans le décor et ne pourriez pas recommencer l’opération avec une autre position de départ (les listes sont des alias). Pour ce faire on peut, soit créer la nouvelle matrice valeur par valeur : def copie(m) : nouv = [] for l in range(len(m)) : ligne = [] for c in range(len(m[0])) : if m[l][c] == 1 : # un mur ligne.append(-1) else : ligne.append(-2) nouv.append(ligne) return nouv soit aussi utiliser des listes en compréhension : def copie(m) : def f(x) : return -1 if x == 1 else -2 return [[f(x) for x in ligne] for ligne in m] ou encore en une ligne, si vous êtes joueur : def copie(m) : return [[(lambda x : -1 if x==1 else -2)(x) for x in li] for li in m] Attention : si vous décidez de faire une copie de la matrice pour ensuite corriger les valeurs, la commande nouvelle = ancienne[:] ne suffit pas, car chaque élément de ancienne est, à son tour, une liste. On peut par contre utiliser la fonction deepcopy du module copy qui permet comme son nom l’indique de faire une copie ≪ en profondeur ≫. Comme l’illustre le schéma suivant, seule B3 est une réelle copie de la liste : 257 Solutions des exercices Retour sur l’exercice G28 Il suffit d’ajouter un paramètre à la fonction update de la classe Snoop et d’effectuer les modifications proposées. Pour limiter la place sur le livre, le code est seulement disponible sur le site. C HAPITRE G C HAPITRE G from copy import deepcopy from copy import deepcopy [-1,6]] A = [[8,4], A ==[[8,4], [-1,6]] B1 A B1 = = A[:] A B2 B2 = = deepcopy(A) A[:] B3 B3 = deepcopy(A) Capture réalisée via le site pythontutor.com Capture réalisée via le site pythontutor.com Retour sur l’exercice G30 Une fois une réelle copie obtenue, on peut alors Retour sur l’exercice G30 Une fois une réelle copie obtenue, on peut alors coder l’algorithme de parcours, en regardant à chaque étape les 4 direccoder l’algorithme de parcours, en regardant à chaque étape les 4 directions possibles, si elles n’ont pas été visitées : tions possibles, si elles n’ont pas été visitées : def tache(m, ldeb, cdeb) : def nouvelle tache(m, = ldeb, cdeb) : copie(m) nouvelle = copie(m) = 0 nouvelle[ldeb][cdeb] nouvelle[ldeb][cdeb] = 0 Atraiter = [(ldeb, cdeb)] Atraiter = [(ldeb, cdeb)] while Atraiter != [] : while != [] : l,Atraiter c = Atraiter.pop(0) l, = Atraiter.pop(0) if c nouvelle[l-1][c] == -2 : if nouvelle[l-1][c] : nouvelle[l-1][c]== = -2 nouvelle[l][c] nouvelle[l-1][c] = nouvelle[l][c] Atraiter.append((l-1,c)) Atraiter.append((l-1,c)) if nouvelle[l+1][c] == -2 : if nouvelle[l+1][c] : nouvelle[l+1][c]== = -2 nouvelle[l][c] nouvelle[l+1][c] = nouvelle[l][c] Atraiter.append((l+1,c)) Atraiter.append((l+1,c)) if nouvelle[l][c-1] == -2 : if nouvelle[l][c-1] : nouvelle[l][c-1]== = -2 nouvelle[l][c] nouvelle[l][c-1] = nouvelle[l][c] Atraiter.append((l,c-1)) Atraiter.append((l,c-1)) if nouvelle[l][c+1] == -2 : if nouvelle[l][c+1] : nouvelle[l][c+1]== = -2 nouvelle[l][c] nouvelle[l][c+1] = nouvelle[l][c] Atraiter.append((l,c+1)) Atraiter.append((l,c+1)) return nouvelle return nouvelle 258 258 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 EHPAD-RUN On peut, comme d’habitude, factoriser le code en évitant les 4 blocs qui se ressemblent fortement : def tache(m, ldeb, cdeb) : nouvelle = copie(m) nouvelle[ldeb][cdeb] = 0 Atraiter = [(ldeb, cdeb)] while Atraiter != [] : l, c = Atraiter.pop(0) for Dl, Dc in (-1,0), (0,-1), (1,0), (0,1) : if nouvelle[l+Dl][c+Dc] == -2 : nouvelle[l+Dl][c+Dc] = nouvelle[l][c] + 1 Atraiter.append((l+Dl,c+Dc)) return nouvelle Retour sur l’exercice G31 Quatre if suffisent à réaliser cette fonction : def vers(chem, l, c) : if -1 < chem[l-1][c] if -1 < chem[l][c+1] if -1 < chem[l+1][c] if -1 < chem[l][c-1] < < < < chem[l][c] chem[l][c] chem[l][c] chem[l][c] : : : : return return return return 1 2 4 8 Retour sur l’exercice G32 On parcourt toutes les cases de la matrice qui sont libres (leurs valeurs est à -2) : table = {} # Un dictionnaire vide for lp in range(len(matrice)) : for cp in range(len(matrice[0])) : if matrice[lp][cp] == -2 : # Dans ce case, on peut mettre Pac-Man chemins = tache(matrice, lp, cp) for lf in range(len(matrice)) : for cf in range(len(matrice[0])) : if matrice[lf][cf] == -2 and (lp, cp) != (lf, cf) : table[(lp, cp, lf, cf)] = vers(chemins, lf, cf) 259 Solutions des exercices C HAPITRE G Retour sur l’exercice G33 Voici la déclaration de la classe. Au moment de l’instanciation, la table table est calculée une fois pour toute et renseignée. Il suffit alors pour se déplacer, d’aller dans la direction souhaitée si cela est possible : class Tomtom(Fantome) : def __init__(self, xdeb, ydeb) : Fantome.__init__(self, 'F3', xdeb, ydeb) self.pas = 1 nouvelle = Tomtom.copie(matrice) self.table = {} # Un dictionnaire vide for lp in range(len(nouvelle)) : for cp in range(len(nouvelle[0])) : if nouvelle[lp][cp] == -2 : chemins = Tomtom.tache(nouvelle, lp, cp) for lf in range(len(nouvelle)) : for cf in range(len(nouvelle[0])) : if nouvelle[lf][cf] == -2 and (lp, cp) != (lf, cf) : self.table[(lp,cp,lf,cf)] = Tomtom.vers(chemins, lf, cf) def copie(m) : ... def tache(m, ldeb, cdeb) : ... def vers(chem, l, c) : ... def choix(self, m, pac) : lp, cp = (pac.y+16) // 32, (pac.x+16) // 32 lf, cf = (self.y+16) // 32, (self.x+16) // 32 if (lp, cp, lf, cf) in self.table : d = self.table[(lp, cp, lf, cf)] if self.dep_possible(matrice, d) : self.direction = d def update(self, m, pac) : self.choix(m, pac) Fantome.update(self) Vous aurez peut-être à ajouter le nom de la classe par-ci par-là pour appeler les fonctions. L’intégralité du code se trouve sur le site du livre. 260 Chapitre H Les bases de données 1 - Présentation Dans les précédents chapitres, nous avons vu comment gérer différents type de fichiers. Cela permet de lire et enregistrer des informations sur la machine (ou un serveur à distance) comme les identifiants, les scores, le nombre de parties jouées, voire les identités bancaires du joueur si vous comptez vivre de vos créations ! Pour stocker et accéder à diverses informations, on peut aussi utiliser des dictionnaires comme cela a été présenté dans le premier chapitre, cependant cela oblige à avoir le dictionnaire complet en mémoire ce qui n’est souvent pas utile et gourmand en mémoire si la quantité d’informations est très importante. Utiliser alors des fichiers est une alternative, mais il va rapidement devenir compliqué d’aller lire juste telle ou telle information sans parcourir tout le fichier. Les bases de données sont faites pour ça, elles permettent de lire et d’écrire des informations très simplement et très rapidement, le moteur de base de données se chargeant pour nous de la manière dont celles-ci sont stockées. Le système de base de données existant par défaut sous Python est SQLite. Ce système est particulièrement adapté à un usage local, d’ailleurs de nombreuses applications comme Thunderbird, Firefox ou encore Skype l’utilisent. Pour une utilisation et un stockage en ligne sur la toile, il existe d’autres solutions, MySQL est sûrement la plus connue. . 261 C HAPITRE H Nous n’allons pas programmer de jeu dans ce chapitre, mais de manière à être le plus concret possible, les explications vont s’appuyer sur l’exemple d’un jeu de type RPG comme World of Warcraft. sources Wikipédia Pour ceux qui ne connaissent pas ce jeu, il s’agit d’un jeu vidéo très populaire de type MMORPG, c’est-à-dire un jeu de rôle en ligne massivement multijoueur. Il est développé par la société Blizzard Entertainment et détient le Guinness World Record pour la plus grande popularité d’un MMORPG : le 7 octobre 2010, Blizzard annonce que plus de 12 millions de joueurs ont un compte World of Warcraft actif. Dans la suite on s’intéresse à ce que pourrait être une représentation de la base de données du jeu sous forme extrêmement simplifiée. Cette base a été construite de façon aléatoire via http ://fr.fakenamegenerator.com/, un site de générateur d’identité. Inutile donc de tenter de contacter les personnes de cette base, tout est fictif ! 2 - Utilité des tables Une table peut être vue comme un tableau à double entrée. Les lignes sont appelées enregistrements et les titres des colonnes sont appelés champs. Chaque colonne ne contient qu’un type de données. Pour conserver les données des clients du jeu, nous allons utiliser une table que nous nommerons membres et dont voici un extrait : 262 LLES ES BASES BASES DE DE DONN DONNÉES ÉES ●● La La table table comporte comporte 13 13 champs champs et et autant autant d’enregistrements d’enregistrements que que de de comptes comptes clients clients (3000 (3000 dans dans notre notre exemple). exemple). ●● Les Les champs champs id, id, sexe sexe et et cp cp (code (code postal) postal) sont sont de de type type INT INT (entiers) (entiers) les les autres autres sont sont de de type type TEXT TEXT (chaı̂nes (chaı̂nes de de caractères). caractères). Il Il n’existe n’existe que que 33 autres autres types types de de données données REAL REAL (pour (pour les les nombres nombres flottants), flottants), NUMERIC NUMERIC (qui (qui peut peut regrouper regrouper les les deux deux types types de de nombres) nombres) et et BLOB, BLOB, anciennement anciennement appelé appelé NONE, NONE, qui qui stocke stocke les les données données brutes brutes sans sans aucune aucune conversion. conversion. ●● Le Le champ champ id id est est une une clé clé primaire, primaire, c’est-à-dire c’est-à-dire qu’elle qu’elle est est inindexée dexée (ce (ce qui qui permet permet une une recherche recherche plus plus rapide rapide par par le le moteur moteur de de base base de de données) données) et et unique unique (impossible (impossible d’avoir d’avoir deux deux enreenregistrements gistrements avec avec la la même même clé) clé) .. Ce Ce champ champ permet permet donc donc d’idend’identifier tifier rapidement rapidement et et de de manière manière unique unique les les enregistrements. enregistrements. Sous Sous SQLite, SQLite, ilil existe existe donc donc peu peu de de types types de de champs, champs, d’ailleurs d’ailleurs lors lors de de la la création création de de la la table, table, le le type type indiqué indiqué n’est n’est qu’une qu’une affinité, affinité, c’est-à-dire c’est-à-dire que que le le moteur moteur SQLite SQLite tentera tentera de de stocker stocker la la quantité quantité sous sous le le type type indiqué indiqué s’il s’il le le peut, peut, sinon sinon ilil le le sauvegardera sauvegardera sous sous un un autre autre type. type. On On parle parle alors alors de de typage typage dynamique dynamique àà l’inverse l’inverse du du moteur moteur MySQL MySQL ooùù le le typage typage est est statique. statique. Dans Dans tous tous les les cas, cas, c’est c’est une une bonne bonne habitude habitude d’utiliser d’utiliser un un type type fixé fixé pour pour chaque chaque champ champ et et de de s’y s’y tenir tenir afin afin d’éviter d’éviter toute toute surprise. surprise. Dans Dans ce ce chapitre, chapitre, nous nous allons allons nous nous contenter contenter de de manipuler manipuler les les bases bases de de données données sans sans interaction interaction avec avec un un programme programme Python. Python. Le Le logiciel logiciel utilisé utilisé pour pour administrer administrer les les bases bases en en question question sera sera DB DB Browser Browser for for SQLite, SQLite, ilil s’agit s’agit d’un d’un logiciel logiciel libre libre et et multiplateforme. multiplateforme. Si Si vous vous avez avez fait fait le le (bon) (bon) choix choix d’utiliser d’utiliser EduPython, EduPython, ce ce logiciel logiciel yy est est déjà déjà inclus inclus àà partir partir de de la la version version 1.4. .. Sinon, 1.4. Pour Pour l’ouvrir l’ouvrir ilil suffit suffit de de cliquer cliquer sur sur l’ic l’icône ône Sinon, vous vous pouvez pouvez télécharger télécharger le le logiciel logiciel depuis depuis le le site site officiel officiel http http ://sqlitebrowser.org ://sqlitebrowser.org ou ou en en tapant tapant le le code code DBB DBB sur sur le le site site du du livre livre et et ainsi ainsi l’utiliser l’utiliser de de manière manière indépendante. indépendante. Une Une base base de de données données SQLite SQLite se se présente présente sous sous la la forme forme d’un d’un seul seul fifichier chier avec avec des des extensions extensions diverses diverses (.db, (.db, .db3, .db3, .sqlite .sqlite ou ou encore encore .sqlite3). .sqlite3). La La base base de de données données qui qui contient contient notre notre exemple exemple est est un un fichier fichier wow.db3, wow.db3, que que vous vous pouvez pouvez télécharger télécharger en en tapant tapant WOW WOW pour pour l’ouvrir l’ouvrir dans dans DB DB BrowBrowser. ser. Vous Vous obtiendrez obtiendrez un un écran écran semblable semblable àà l’image l’image suivante suivante qui qui contient contient toute toute la la structure structure de de la la base base sur sur laquelle laquelle nous nous reviendrons reviendrons plus plus tard. tard. Pour Pour le le moment, moment, on on peut peut déjà déjà remarquer remarquer que que l’on l’on yy retrouve retrouve la la table table membres membres déjà déjà présentée. présentée. 263 263 C HAPITRE H Capture d’écran de la structure de la base de données utilisée Dans l’onglet ”parcourir les données”, on peut afficher le contenu des tables une à une, ajouter et supprimer un enregistrement. Pensez à sauvegarder votre base au fur et à mesure si vous voulez que les changements soient effectifs dans le fichier. On ne peut pas (à l’heure actuelle) effectuer de recherches à l’exception de quelques filtres de base. Ici on a filtré les membres qui habitent dans une ”rue” à PARIS 264 L ES BASES DE DONN ÉES 3 - Premières requêtes Pour extraire les données avec plus de finesse, nous allons écrire des requêtes, c’est-à-dire des instructions, dans un nouveau langage appelé langage SQL (Structured Query Language, en français langage de requête structurée). Comme pour Python, il existe différentes versions de ce langage, nous présenterons ici le SQL3. Les requêtes seront tapées dans le dernier onglet du logiciel. Exemple de requête renvoyant tous les membres dont le prénom est Vincent. La sélection On appelle sélection l’extraction de tous les champs des enregistrements d’une table respectant certains critères. SELECT * FROM table SELECT * FROM table WHERE conditions ✍ SELECT * FROM membres WHERE prenom = "Vincent" extrait de la table membres, tous les enregistrements dont le champ prenom est Vincent. Si aucune instruction WHERE n’est précisée, l’intégralité des enregistrements est retournée. 265 C HAPITRE H Pour les conditions, excepté le test d’égalité qui ne comporte qu’un symbole d’égalité (on peut néanmoins en mettre deux), on retrouve les mêmes syntaxes et opérateurs qu’en Python ( >=, ! =, %, AND, OR, NOT...). Attention cependant, la division / sur des entiers renvoie le quotient entier (// n’existe pas). E X H1 Donner (et tester) les requêtes permettant : 1. D’afficher toutes les femmes inscrites. 2. D’afficher toutes les personnes résidant à Nice. 3. D’afficher toutes les femmes résidant à Nice. 4. D’afficher les personnes vivant dans la Somme (département 80). 5. D’afficher les Bretons, la Bretagne étant composée des départements Côtesd’Armor(22), Finistère(29), Ille-et-Vilaine(35) et Morbihan(56). Les requêtes sur les chaı̂nes de caractères respectent la casse, c’est-àdire les différences majuscules/minuscules. Par exemple, la requête SELECT * FROM membres WHERE prenom = "vincent" ne renvoie aucun enregistrement à cause du v minuscule. Pour pallier cette contrainte, on utilise souvent l’opérateur LIKE à la place du symbole égal. Ce dernier compare les chaı̂nes sans tenir compte de la casse, par contre cela ne règle pas (contrairement en MySQL) le problème des accents et des caractères spéciaux : si vous cherchez "Clemence" ou "Clémence", vous n’obtiendrez pas la même chose. D’autre part, on dispose du symbole joker % qui peut remplacer n’importe quelle chaı̂ne de caractères (même vide). Ainsi par exemple, la requête SELECT * FROM membres WHERE nom LIKE "%Z%" revoie toutes les personnes dont le nom contient un Z. E X H2 Trouver les requêtes permettant d’afficher 1. Les enregistrements représentant des personnes dont le login commence par la lettre C. 2. Les personnes nées en 1976. 3. Les personnes qui se sont inscrites en mars (champ inscription). 4. Les personnes dont le mail est hébergé par superrito.com. 266 L ES BASES DE DONN ÉES La projection En pratique, on a rarement besoin d’extraire tout l’enregistrement, car seulement quelques champs nous intéressent. On appelle projection l’extraction de quelques champs d’une table. SELECT champ1,champ2,... FROM table On mixe bien souvent les projections et les sélections : SELECT champ1,champ2,... FROM table WHERE conditions ✍ SELECT prenom, nom FROM membres WHERE pass LIKE "%z%z%" Renvoie les noms et prénoms des 17 personnes dont le mot de passe comporte (au moins) deux fois la lettre z. E X H3 Écrire les requêtes permettant de renvoyer 1. le login et le mot de passe d’Armand LALONDE. 2. les numéros de téléphone des personnes inscrites en 2015. 3. l’adresse de monsieur Abril. 4 - Compléments sur les requêtes Pour découvrir de nouvelles possibilités de requêtes, nous allons compléter notre base de données en ajoutant deux nouvelles tables. Dans notre jeu, chaque membre peut créer jusqu’à 3 personnages. On pourrait alors imaginer d’ajouter un champ personnage à notre table déjà existante, mais il faudrait alors faire autant de copies d’enregistrements que de personnages, ce qui serait très gourmand en terme de place et complexe au niveau de la maintenance : imaginez qu’un membre change d’adresse, il faudrait alors répercuter ces changements sur tous les enregistrements. C’est tout l’intérêt d’une base de données : éviter les répétitions inutiles. 267 C HAPITRE H Nous allons donc créer une nouvelle table persos dont la structure est la suivante : Chaque personnage dispose d’un identifiant idPerso unique (c’est une clé primaire pour la table persos) et est rattaché au compte du membre propriétaire via le champ idMembre, il s’agit là d’une clé étrangère, car elle correspond nécessairement à une clé primaire d’une autre table. idRace race Le champ nom indique le nom du personnage (et non 1 Humain celui de son propriétaire), les deux autres champs 2 Elf nbConnect et score contiennent respectivement le 3 Nain nombre de parties et le meilleur score réalisé par le 4 Hobbit personnage. Enfin, le champ idRace indique à quelle 5 Barbare ”race” appartient le personnage parmi les choix ci6 Reptile contre (désolé pour ce terme assez vilain, ”famille” 7 Démon serait plus judicieux, mais c’est le vocabulaire utilisé 8 Magicien dans les jeux RPG). 9 Dragon champ idPerso idMembre nom nbConnect score idRace type ENTIER ENTIER TEXTE ENTIER ENTIER ENTIER Pour les même raisons de non répétition on est donc amené à stocker le numéro de la race plutôt que son nom. On a donc recours à une troisième table races donnée ci-dessus. Elle contient deux champs : idRace une clé primaire et race une chaı̂ne de caractères indiquant la race. Nous verrons comment ces différentes tables peuvent être consultées de manière simultanée un peu plus tard, mais intéressons-nous pour le moment à quelques requêtes réalisables sur celle-ci. Clauses Pour éviter les doublons, on peut remplacer la commande SELECT... par SELECT DINSTINCT... E X H4 Écrire une requête affichant les différentes villes des membres du jeu. 268 L ES BASES DE DONN ÉES On peut ajouter en fin de requête quelques instructions que l’on appelle clause pour limiter ou ordonner les retours : LIMIT nb : Limite le nombre d’enregistrements renvoyés aux nb premiers LIMIT p,nb : Passe p enregistrements, et renvoie les nb suivants ORDER BY champ ASC : Renvoie les enregistrements en les triant selon le champ champ par ordre croissant. ● On peut écrire DESC pour classer dans l’ordre décroissant. ● On peut classer selon plusieurs champs, il suffit alors de les séparer par une virgule. ✍ Par exemple, pour avoir le nom du personnage de race Dragon qui s’est le plus connecté (Kissle), on pourra taper : SELECT nom FROM persos WHERE idRace = 9 ORDER BY nbConnect DESC ⤦ � LIMIT 1 E X H5 Donner les requêtes permettant d’afficher : 1. la liste des races par ordre alphabétique, 2. les 5 meilleurs scores et les noms des personnages qui l’ont réalisé. Fonctions d’agrégation Les fonctions d’agrégation sont des fonctions qui permettent d’effectuer un calcul statistique sur le résultat d’une requête. Nous en utiliserons cinq : fonction effet COUNT Renvoie le nombre d’enregistrements résultant d’une requête. SUM AVG Renvoient respectivement la somme et la moyenne d’un champ sur un ensemble d’enregistrements MAX MIN Renvoient respectivement la valeur maximale et la valeur minimale d’un champ parmi les enregistrements résultant d’une requête. 269 C HAPITRE H ✍ L’utilisation classique est la suivante : SELECT fonction(champ) FROM table ... Par exemple pour savoir le nombre moyen de connexions chez les Elf (race 2) : SELECT AVG(nbConnect) FROM persos WHERE idRace = 2 E X H6 En utilisant des fonctions d’agrégation, écrire des requêtes permettant de renvoyer : 1. Le nombre de personnages au total. 2. Le score moyen des Magiciens. 3. Le nombre de personnages que possède le premier joueur inscrit sur le site ainsi que son meilleur score. 5 - Les jointures On peut schématiser la base de données à l’étape actuelle de la manière suivante. Par convention : ● Les champs soulignés indiquent les clés primaires. ● Les flèches indiquent les clés étrangères. membres id (ENT) nom (TXT) prenom (TXT) login (TXT) pass (TXT) inscription (TXT) sexe (ENT) adresse (TXT) ville (TXT) cp (ENT) mail (TXT) telephone (TXT) dateNaiss (TXT) 270 persos idPerso (ENT) idMembre (ENT18) nom (TXT) nbConnect (ENT) score (ENT) idRace (ENT) races idRace (ENT) race (TXT) L ES BASES DE DONN ÉES Nous allons présenter comment réaliser des requêtes sur plusieurs tables simultanément. Pour cela nous allons effectuer une jointure qui peut être vue mathématiquement comme un produit cartésien, c’est-à-dire que l’on va créer une table contenant toutes les juxtapositions possibles entre un enregistrement d’une table et un enregistrement d’une autre table. SELECT * FROM table1 JOIN table2 JOIN table3 ... renvoie une jointure des différentes tables notée informatiquement table1 table2 ... ✍ Prenons l’exemple (fichier JOIN ) où l’on dispose de deux tables contenant chacune un champ : table1 : table2 : lettre nombre A 1 B 2 C On obtient le résultat de droite avec la requête : SELECT * FROM table1 JOIN table2 table1table2 lettre nombre A 1 A 2 B 1 B 2 C 1 C 2 Prenons un second exemple (fichier JOIN2 ) en considérant les deux tables suivantes : id 1 2 3 produits désignation Savon Brosse à dents Peigne stock prix idp quantite 2,5 et 1 2 1,5 2 0 3 3 3 SELECT SUM(quantite * prix) FROM produits JOIN stock l’appel renvoie 35 et non 14 comme on pourrait peut-être s’y attendre... L’explication se trouve à la page suivante... 271 C HAPITRE H SELECT La requête * FROM produits JOIN stock donne : produits � stock id désignation prix idp quantite 1 savon 2.5 1 2 1 savon 2.5 2 0 1 savon 2.5 3 3 2 brosse à dents 1.5 1 2 2 brosse à dents 1.5 2 0 2 brosse à dents 1.5 3 3 3 peigne 3.0 1 2 3 peigne 3.0 2 0 3 peigne 3.0 3 3 ��� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �� ��� � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � table produits table stock La jointure obtenue, contient donc beaucoup trop d’enregistrements et le résultat 35 provient du calcul suivant : 2 × 2,5 + 0 × 2,5 + 3 × 2,5 + 2 × 1, 5 + 0 × 1,5 + 3 × 1,5 + 2 × 3, 0 + 0 × 3, 0 + 3 × 3,0 = 35 Il faut donc préciser au moment de la jointure la condition de jointure : le champ id de la table produits doit être le même que le champ idp de la table stock. Cette contrainte s’exprime à l’aide de l’instruction ON : SELECT * FROM table1 JOIN table2 ON conditions Cette fois-ci, l’appel suivant renvoie bien 14 : SELECT SUM(quantite * prix) FROM produits JOIN stock ON id = idp Enfin, comme vous l’aurez remarqué dans l’exemple précédent, on peut mixer la jointure avec une projection ou une sélection : SELECT champ1 ,champs2 FROM table1 JOIN table2 ON conditions WHERE conditions 272 L ES BASES DE DONN ÉES E X H7 Donner les requêtes permettant d’afficher : 1. Les numéros de téléphone des dix Amiénois (habitants de la ville d’Amiens) ayant obtenu le meilleur score. 2. Le nombre total de parties jouées par la gent féminine. 3. Le prénom et le nombre de personnages que possède le membre dont le login est ”Frolit” Si à la dernière question de l’exercice précédent, on tente d’afficher le nom de famille du joueur en question plutôt que son prénom, on obtient le message d’erreur suivant : "ambiguous column name : nom : ..." car les tables membres et persos contiennent chacune un champ nom. Cette limitation va facilement pouvoir être levée par le complément qui suit. ● Lorsque l’on retrouve le même nom de champ dans deux tables différentes, pour ne pas qu’il y ait ambiguı̈té lors des requêtes, on doit précéder le nom du champ de celui de la table en les séparant par un point tel le ’s d’appartenance en anglais. Par exemple membres.nom signifie le champ nom de la table membres. ● Les deux tables persos et races possèdent toutes deux un champ nommé idRace, celui de la table perso est une clé étrangère associée à la clé primaire de la table races. Lorsque l’on fait la jointure, on fait donc naturellement : SELECT ... FROM persos JOIN races ON persos.idRace = races.idRace on peut simplifier cette requête en utilisant NATURAL JOIN : SELECT ... FROM persos NATURAL JOIN races E X H8 Donner les requêtes permettant d’afficher : 1. Les noms et prénoms des membres possédant un personnage de type Hobbit (race 4). 2. La race du personnage qui s’est le plus connecté. 3. Le nom et la race des personnages que possède Juliette Thibodeau. 273 C HAPITRE H 6 - Ajouter, modifier, supprimer des enregistrements Jusque là, nous nous sommes contentés de lire dans notre base de données, voyons à présent comment la modifier. Petit conseil : pensez à sauvegarder régulièrement votre base. On effectue souvent en début d’apprentissage des modifications non voulues et parfois irréversibles ! Ajouter des enregistrements On ajoute un nouvel enregistrement à l’aide de la fonction INSERT avec la syntaxe suivante : INSERT INTO table(champ1 , champ2 , ....) VALUES (val1 , val2 ,...) Si l’on souhaite ne renseigner que quelques champs, on précisera la liste de ces champs entre parenthèses, dans le cas contraire cette information est facultative. Lors d’un enregistrement, les champs qui ne sont pas renseignés sont alors ● soit complétés par la valeur indiquée dans la colonne défaut lors de la création de la table (comme dans la capture ci-dessous). ● soit auto-incrémentés s’il s’agit d’une clé primaire numérique (le moteur SQL attribue alors la valeur maxi + 1 où maxi est la valeur maximale actuelle du champ) Par exemple : ● INSERT INTO races VALUES(20, ’Ours’) Ajoute un enregistrement contenant le champ idRace à 20 et race à ”Ours”. ● INSERT INTO races VALUES(5, ’Drow’) Retourne l’erreur UNIQUE constraint failed : races.idRace : INSERT INTO races VALUES(5, ’Drow’) car le champ idRace est une clé primaire, il ne peut donc pas avoir deux enregistrements dont l’idRace vaut 5. ● INSERT INTO races(race) VALUES(’Orque’) Ajoute un nouvel enregistrement dont le champ idRace vaut 21 et race vaut ”Orque”. 274 L ES ● BASES DE DONN ÉES INSERT INTO persos(idMembre, nom, idRace) VALUES(123, ’Bob’, 10) Renvoie l’erreur FOREIGN KEY constraint failed : INSERT INTO persos(idMembre, nom, idRace) VALUES(123, ’Bob’, 10) car idRace est une clé étrangère et la valeur 10 n’a pas de correspondance dans la table races Sur cette capture de la table persos, on peut voir les deux clés étrangères et les valeurs par défaut des champs nbConnect et score. 275 C HAPITRE H Supprimer un enregistrement La suppression d’un enregistrement se fait de la manière suivante : DELETE FROM table WHERE conditions Prenez garde de mettre à la suite du WHERE une condition identifiant de manière unique (souvent une clé primaire) le ou les enregistrement(s) à supprimer ! On peut aussi supprimer une table entière : DROP TABLE table E X H9 1. Réaliser les instructions pour supprimer les deux nouvelles races créées précédemment (Ours et Orque). 2. Que se passe-t-il si l’on tente de supprimer la race des magiciens : DELETE FROM races WHERE idRace = 8 Modifier un enregistrement Terminons cette partie en présentant la manière de modifier des enregistrements. La modification de valeurs dans une table se fait de la manière suivante : UPDATE table SET champ1 =..., champ2 = ... WHERE conditions L’exercice suivant va vous permettre de réaliser quelques modifications sur la base. De manière à conserver la base initiale, n’enregistrez pas ces changements une fois l’exercice terminé . E X H10 Ecrire les requêtes SQL permettant de répondre aux demandes suivantes : 1. Une association de consommateurs s’est plainte du terme ”Nain” comme nom de race. Écrire une requête pour changer ce nom en Gnome. 2. Madame Voisine s’est mariée, son nouveau nom est à présent Madame Riverain. Effectuer la modification. 3. Le personnage du nom de Zu (il n’y en a qu’un) vient de se connecter. Mettre à jour le champ nbConnect 276 L ES BASES DE DONN ÉES Sous SQLite, il est impossible de réaliser un UPDATE avec une jointure. Imaginons que le membre dont le login est Lawen2005 ait obtenu un score de 420 175 avec son personnage Frey (il y a plusieurs personnages portant ce nom) et que l’on souhaite mettre à jour ce nouveau score. Voici une solution : avec un SELECT, on récupère l’id du membre dont on connaı̂t le login. Cet id est réutilisé dans la condition du UPDATE. UPDATE persos SET score = 420175 WHERE nom = "Frey" AND idMembre = ⤦ � (SELECT id FROM membres WHERE login = "Lawen2005") Si le SELECT avait renvoyé plusieurs valeurs, on aurait alors pu utiliser l’opérateur IN plutôt que le symbole d’égalité pour tester l’appartenance à la table retournée. E X H11 Pour la journée de la femme, augmenter les scores des femmes de 10% ! 7 - Compléments sur les dates et chaı̂nes Dans nos bases de données, nous aurons régulièrement à manipuler des textes ainsi que des dates. Prenons le temps d’étudier plus en détails ces derniers. Les chaı̂nes de caractères Quelques fonctions permettent de travailler avec les chaı̂nes : fonction LENGTH(ch) effet Renvoie la longueur de la chaı̂ne ch REPLACE(c1,c2 ,c3 ) Renvoie la chaı̂ne c1 où toutes les occurrences c2 sont remplacées par la chaı̂ne c3 SUBSTR(ch,deb,lg) Retourne la chaı̂ne de caractères extraite de ch en partant de la position définie par deb et sur la longueur lg. Si ce dernier paramètre n’est pas précisé, la chaı̂ne est extraite jusqu’à la fin. Attention : la numérotation des caractères commence à 1 et non 0 comme en Python 277 C HAPITRE H C HAPITRE H fonction C HAPITRE H ,c ) TRIM(c fonction 1 2 LTRIM(c TRIM(c fonction1,c 1,c 2) 2) RTRIM(c ,c LTRIM(c TRIM(c1,c 1 2) 2) fonction RTRIM(c1,c2 ) LTRIM(c TRIM(c1,c2 ) RTRIM(c1,c2 ) LTRIM(c1,c2 ) cRTRIM(c || c 1,c2 ) effet Renvoie la chaı̂ne c1 où tous les caractères c2 effet situés respectivement aux deux extrémités, Renvoie la chaı̂ne c1 où tous les caractères c2 effet avant et après c ont été supprimés. Si c situés respectivement aux deux extrémités, Renvoie la chaı̂ne 1c1 où tous les caractères 2c2 effet n’est précisé les espaces qui avant et après c1 ce ontsont été supprimés. Si c2 situéspas respectivement aux deux extrémités, Renvoie la chaı̂ne c osont ù tous les caractères c sont n’est pas précisé espaces qui avantsupprimés. et après c1 1ce ont été les supprimés. Si c2 2 situés respectivement aux deux extrémités, sont n’estsupprimés. pas précisé ce sont les espaces qui avant et c1 ont été(juxtaposition) supprimés. Si des c2 Renvoie la après concaténation sont supprimés. 1 2 n’est pas précisé ce sont les espaces qui chaı̂neslacconcaténation de la barredes c1 || c2 Renvoie (juxtaposition) 1 et c2 . Le symbole sont supprimés. verticale s’obtient avec chaı̂neslacconcaténation et c2en . Leanglais) symbole de la barre c1 || c2 Renvoie (juxtaposition) des 1(pipe et 6 . la touchec1(pipe AltcGr verticale en anglais) s’obtient avec chaı̂nes et . Le symbole de la barre 2 c1 || c2 Renvoie la concaténation (juxtaposition) des 6 . s’obtient avec la touche Alt Gren et verticale (pipe anglais) À noter que ces fonctions utilisées dans la sélection chaı̂nes c1peuvent et c2 . Leêtre symbole de la barre et 6 . la touche Alt Gr ou dans la projection. verticale (pipe en anglais) s’obtient À noter que ces fonctions peuvent être utilisées dansavec la sélection 6 utilisées . la touche peuvent Alt Gr etêtre ou dans la projection. À noter que ces fonctions dans la sélection ou dans la projection. À noter que ces fonctions peuvent être utilisées dans la sélection ou dans la projection. ✍ ✍ ✍ Ainsi, Ainsi, DISTINCT nom FROM membres WHERE LENGTH(nom) = 12 SELECT Ainsi, SELECT DISTINCT nom FROM membres WHERE LENGTH(nom) = 12 renvoie les huit noms de familles composés de 12 lettres alors que DISTINCT nom FROM membres WHERE LENGTH(nom) = 12 ✍ SELECT Ainsi, renvoie les huit LENGTH(nom) noms de familles composés de BY 12 LENGTH(nom) lettres alors ASC que SELECT DISTINCT FROM membres ORDER SELECT DISTINCT FROM WHERE LENGTH(nom) = 12 alors que renvoie les huit nom noms de membres familles composés de BY 12 LENGTH(nom) lettres SELECT DISTINCT LENGTH(nom) FROM membres ORDER ASC nous informe que les noms des membres contiennent entre 3 et 14 SELECT DISTINCT LENGTH(nom) FROM membres ORDER BY LENGTH(nom) ASC renvoie les huitque noms familles composéscontiennent de 12 lettresentre alors 3que lettres. nous informe les de noms des membres et 14 SELECT DISTINCTque LENGTH(nom) ORDER BY LENGTH(nom) lettres. nous informe les nomsFROM des membres membres contiennent entre ASC 3 et 14 lettres. nous informe que les noms des membres contiennent entre 3 et 14 lettres. E X H12 Pour des raisons de sécurité, on souhaite que les utilisateurs aient un mot de12 passe de des plusraisons de 8 caractères. Pour automatiquement un SMS EX H Pour de sécurité, onenvoyer souhaite que les utilisateurs aientaux un utilisateurs dont le raisons mot passe est Pour troponcourt, onautomatiquement cherche extraireun lesSMS numéros mot de12 passe de des plus de 8de caractères. envoyer EX H Pour de sécurité, souhaite que lesàutilisateurs aientaux un de de plus ces personnes sous forme d’un nombre 10 chiffres (sans les utilisateurs le mot passe est la trop court, onautomatiquement cherche à extraire lesSMS numéros mottéléphone de passedont de de 8de caractères. Pour envoyer un aux E X H12Écrire Pour des raisons de sécurité, on souhaite que les utilisateurs aient un points). la requête correspondante. de téléphonedont de ces personnes sous forme d’un 10 chiffres les utilisateurs le mot de passe est la trop court, on nombre cherche à extraire les (sans numéros mot de passe delaplus de 8 caractères. Pour envoyer automatiquement un SMS aux points). Écrire requête correspondante. de téléphone de ces personnes sous la forme d’un nombre à 10 chiffres (sans les utilisateurs dont le mot de passe est trop court, on cherche à extraire les numéros points). Écrire la requête correspondante. de téléphone de ces personnes sous la forme d’un nombre à 10 chiffres (sans les points). Écrireen la Python, requête correspondante. Comme si dans un SUBSTR, le second paramètre est négatif, on parcourt la chaı̂ne depuis fin. Comme en Python, si dans unlaSUBSTR, le second paramètre est négatif, on parcourt la chaı̂ne depuis la fin. Comme en Python, si dans un SUBSTR, le second paramètre est négatif, on parcourt la chaı̂ne depuis la fin. Comme en Python, si dans un SUBSTR, le second paramètre est négatif, on parcourt la chaı̂ne depuis la fin. 278 278 278 278 L ES BASES DE DONN ÉES E X H13 En utilisant la remarque précédente, donner sur une première colonne en sortie, les prénoms et noms des personnes membres habitant Vincennes et sur une seconde colonne leur âge au 1er janvier 2018. Le prochain exercice est important pour la suite, car il va nous permettre de mettre le champ de la date d’inscription dans un format standard compris par SQLite. Enregistrez les modifications dans la base une fois les questions terminées. E X H14 Le champ de la table perso n’est pas correctement formaté...C’est volontaire, je vous l’admets ! Les dates sont données sous forme de texte avec le format j /m/a où j et m représentent les numéros du jour et du mois sur 1 ou 2 chiffres et a les deux derniers (ou le dernier) chiffre de l’année. Nous allons les mettre sous forme AAAA − MM − JJ en quelques étapes. 1. Ajouter un 0 devant le numéro du jour si celui-ci ne comporte qu’un chiffre. 2. Ajouter les 0 éventuels devant le numéro du mois puis celui des années. 3. Inverser les valeurs des années avec les jours. 4. Remplacer les / par des -. 5. Ajouter ”20” devant le numéro de l’année (la première année étant 2005). E X H15 Corriger de la même manière le champ dateNaiss où seuls le nombre de chiffres pour coder le numéro du jour, l’ordre des informations et le séparateur posent problème. Enregistrez à présent la base de données corrigée ou récupérez-la sur le site où l’exercice précédent est corrigé pour étudier la fin du chapitre. Les heures et les dates Comme vous l’aurez peut-être remarqué, il n’existe pas de type date ou heure, il nous faut donc les stocker sous forme de chaı̂nes de caractères. Heureusement il existe quelques fonctions pour travailler sur les dates relativement facilement. Nous en présentons 3 : 279 C HAPITRE H fonction DATE(ch,m1,m2 ,...) effet Renvoie une chaı̂ne de caractères, au format ’AAAA-MM-JJ’, représentant la date ch éventuellement rectifiée par les modificateurs m1 , m2 .... TIME(ch,m1,m2 ,...) Même effet que DATE, mais renvoie une chaı̂ne de caractères au format ’HH:MM:SS’ représentant l’heure DATETIME(ch,m1,m2 ,...) Même effet que DATE, mais renvoie une chaı̂ne de caractères au format ’AAAA-MM-JJ HH:MM:SS’ représentant la date et l’heure À noter que : ● on peut demander en sortie des formats plus exotiques (numéro du jour de l’année, jour de la semaine, numéro de semaine, ...) en utilisant alors la fonction STRFTIME, mais nous n’en parlerons pas ici. D’ailleurs les 3 formats décrits ci-dessus à l’anglo-saxonne ont le gros avantage de pouvoir être très facilement comparés : l’ordre sur les chaı̂nes étant alors l’ordre chronologique. ● La chaı̂ne de caractères indiquant la date ou l’heure donnée en paramètre peut être à l’un des 3 formats précédents. On peut mettre ou non les secondes et on utilise parfois le caractère T à la place de l’espace pour séparer la date de l’heure. ● on dispose de la chaı̂ne ’now’ qui correspond à la date et l’heure du système en UTC (Temps Universel Coordonné), c’est-à-dire pour simplifier sur le méridien de Greenwich, il peut donc y avoir un décalage d’une heure ou deux selon les saisons (nous y reviendrons juste après) Quelques exemples pour éclairer mes propos (au moment où j’écris nous sommes le 17 février 2017 et il est 15 heures 10) : SELECT TIME(’2017-01-01 17:32’) SELECT DATE(’2017-01-01 17:32’) SELECT DATETIME(’now’) 280 �→ 17:32:00 �→ 2017-01-01 �→ 2017-02-17 14:10:47 L ES BASES DE DONN ÉES Revenons à présent sur les modificateurs. Il s’agit de mots-clés donnés sous forme de textes qui vont avoir pour effet de modifier la date, en voici quelques uns : modificateur ’n days’ ’n hours’ ’n minutes’ ’n seconds’ ’n months’ ’n years’ effet Ajoute respectivement n jours, heures, minutes, secondes, mois, années à la date donnée, n étant un entier positif ou négatif (voire flottant pour le nombre de secondes). Le ’s’ à la fin du mot est facultatif si vous voulez être en accord avec l’orthographe ! ’start of month’ ’start of year’ ’start of day’ Modifie la date au premier jour à 00 :00 du mois, de l’année ou respectivement du jour indiqué par la date donnée. ’weekday n’ Avance la date au prochain nème jour de la semaine, où n est un entier avec comme convention que dimanche = 0, lundi = 1, ... L’heure est quant à elle inchangée. ’localtime’ Renvoie l’heure locale (corrigeant ainsi les problèmes de fuseaux horaires) Ces modificateurs peuvent s’enchaı̂ner, par exemple : ● Le deuxième dimanche du mois de février 2016 : SELECT DATE(’2016-02-01’,’weekday 0’,’+7 days’) ● Le lundi de la semaine en cours : SELECT DATE(’now’, ’+1 day’, ’weekday 1’, ’-7 day’) ● Le dernier jour du mois : SELECT DATE(’now’, ’start of month’,’+1 month’, ’-1 day’) E X H16 Le beaujolais nouveau se célèbre chaque année, le troisième jeudi de novembre. Écrire une requête qui donne la date de la sortie de ce vin d’exception pour l’année en cours. 281 C HAPITRE H E X H17 Avec la base de données contenant les dates correctement formatées, écrire les requêtes permettant d’afficher les noms et prénoms des membres 1. inscrits en 2010 après le 14 mars ; 2. dont l’anniversaire tombe cette semaine. 8 - Les groupements Terminons ce chapitre par un ultime complément sur les possibilités de groupements en SQL. L’instruction GROUP BY permet de regrouper les résultats en vue d’utiliser une fonction d’agrégation par exemple SELECT champ1 ,f (champ2 ),... FROM table GROUP BY champ1 On peut alors effectuer une restriction avec l’instruction HAVING : SELECT champ1 ,f (champ2 ),... FROM table GROUP BY champ1 HAVING condition La fonction HAVING est presque similaire à la fonction WHERE, mais on peut mettre des fonctions d’agrégations dans la condition. ✍ Pour avoir le nombre de membres par ville, on peut procéder ainsi : SELECT ville, COUNT(id) FROM membres GROUP BY ville Le score moyen et l’id des membres possédant deux personnages : SELECT idMembre, AVG(score) FROM persos � count(score) = 2 GROUP BY idMembre HAVING ⤦ et si on souhaite afficher les noms et prénoms, on complète avec une jointure : SELECT prenom, membres.nom, AVG(score) FROM persos JOIN membres ON ⤦ � persos.idMembre = membres.id GROUP BY idMembre HAVING ⤦ � count(score) = 2 282 L ES BASES DE DONN ÉES E X H18 À l’aide de groupements, trouver : 1. Le meilleur score de chaque race, ainsi que le nom de la race, le nom du personnage et celui de son propriétaire. 2. Le mois de l’année où il y a eu le plus d’inscriptions. 3. Pour chaque Angloy (habitant d’Anglet), le nombre moyen de connexions. 9 - Aides pour les exercices Aide pour l’exercice H2 Pour les personnes inscrites en mars, pensez que le mois est entouré du caractère /. Aide pour l’exercice H10 Pour modifier le nombre de connexions, pensez que, comme en Python, la commande x = x + 1 ajoute 1 au champ x. Aide pour l’exercice H14 Pour savoir si le numéro du jour comporte un ou deux chiffres, on pourra regarder le 2e caractère du champ. 10 - Solutions des exercices Retour sur l’exercice H1 Voici des requêtes répondant à la demande : 1. Vous aurez remarqué que, pour les femmes le champ sexe est à 0 et 1 pour les hommes. (Ni voyez aucun sexisme ici mais plutôt un moyen mnémotechnique !) Il y a 1504 réponses. SELECT * FROM membres WHERE sexe = 0 2. Pas de difficulté pour cette requête qui retourne 26 enregistrements. SELECT * FROM membres WHERE ville = "NICE" 3. Il suffit de combiner les deux conditions pour trouver les 9 personnes répondant aux critères : SELECT * FROM membres WHERE sexe = 0 AND ville = "NICE" 4. En s’appuyant sur l’astuce donnée dans l’aide, on trouve 23 personnes résidant dans la Somme. SELECT * FROM membres WHERE cp/1000 = 80 283 Solutions des exercices Aide pour l’exercice H1 On peut extraire les deux premiers chiffres du code postal en calculant le quotient de la division par 1000. C HAPITRE H 5. Pour extraire les 62 Bretons de la liste, on peut utiliser l’opérateur OR : SELECT * FROM membres WHERE cp/1000 = 22 OR cp/1000 = 29 OR ⤦ � cp/1000 = 35 OR cp/1000 = 56 On peut simplifier cette requête avec l’opérateur IN qui s’utilise comme en Python, la liste des valeurs étant placée entre parenthèses SELECT * FROM membres WHERE cp/1000 IN (22, 29, 35, 56) Retour sur l’exercice H2 1. Si on souhaite que le login commence par c, le motif de recherche est ”c%” (190 réponses) : SELECT * FROM membres WHERE login LIKE "c%" 2. De la même manière, la date de naissance doit terminer par 1976 (51 réponses) : SELECT * FROM membres WHERE dateNaiss LIKE "%1976" 3. Comme le mois de mars est le troisième mois, on cherche le motif ”%/3/%”. (247 réponses) SELECT * FROM membres WHERE inscription LIKE "%/3/%" Il faut donc bien faire attention au format de la date, ici le mois de mars est codé 3 et non 03, nous verrons un peu plus tard comment gérer les requêtes sur les dates. 4. Pas de difficulté pour cette dernière requête (291 réponses) SELECT * FROM membres WHERE mail LIKE "%@superrito.com" Retour sur l’exercice H3 1. Son login est Farmay et son mot de passe Shoozae3oo SELECT login, pass FROM membres WHERE prenom LIKE "Armand" AND ⤦ � nom LIKE "Lalonde" 284 L ES BASES DE DONN ÉES 2. Il y a 231 numéros à afficher : SELECT telephone FROM membres WHERE inscription LIKE "%/15" 3. Il y a 6 personnes dont le nom est Abril, dont un seul homme Léon Abril, au 51 rue de Geneve - 80000 AMIENS. SELECT prenom, nom, adresse, cp, ville FROM membres WHERE nom ⤦ � LIKE "Abril" AND sexe = 1 Retour sur l’exercice H4 Il y a 452 villes différentes SELECT DISTINCT ville FROM membres Retour sur l’exercice H5 1. SELECT race FROM races ORDER BY race ASC On obtient : Barbare, Dragon, Démon, Elf, Hobbit, ... 2. SELECT nom, score FROM persos ORDER BY score DESC LIMIT 5 On obtient : Gildagil Gamwic (357 913 pts), Leest (357 777 pts), Drelg (357 673 pts), Gnordwy (357 670 pts) et Sago (357 657 pts) Retour sur l’exercice H6 1. En utilisant la fonction COUNT : SELECT COUNT(*) FROM persos Remarque : on peut aussi ne sélectionner qu’un champ (n’importe lequel). SELECT COUNT(idPerso) FROM persos 2. Les magiciens sont de la race 8, donc SELECT AVG(score) FROM persos WHERE race = 8 On trouve un score moyen d’environ 180 409 3. Le premier inscrit sur le site a pour identifiant 1, on trouve qu’il détient 2 personnages et que son meilleur score est 299300 SELECT MAX(score), COUNT(score) FROM persos WHERE idMembre = 1 285 Solutions des exercices C HAPITRE H HAPITRE H Retour sur l’exercice l’exercice H7 H7 l’exercice H7 1. Avec la la requête requêtesuivante suivante::: requête suivante SELECT telephone telephone telephone FROM FROM FROM membres membres membresJOIN JOIN JOIN JOINpersos persos persos persosON ON ON ONid id id id====idMembre idMembre idMembre idMembre WHERE WHERE WHERE ⤦⤦ SELECT WHERE ⤦⤦ � � ville ville ville === "AMIENS" "AMIENS" "AMIENS" ORDER ORDER ORDERBY BY BYscore score score scoreDESC DESC DESC DESCLIMIT LIMIT LIMIT LIMIT10 10 10 10 on obtient obtient bien bien bien 10 10 10enregistrements, enregistrements, enregistrements,cependant cependant cependant cependantvotre votre votre votre esprit esprit esprit esprit vif vif vif vif aura aura aura aura remarqué remarqué que que quepar par parexemple exemple exemplele lelenuméro numéro numéro03.68.16.84.21 03.68.16.84.21 03.68.16.84.21 03.68.16.84.21 de de de de E.Déziel E.Déziel E.Déziel E.Déziel apapapapeeee parait parait en en 44eee place place place(avec (avec (avec340957 340957 340957points) points) points)et eteten en en en10 10 10 10 place place place place (avec (avec (avec (avec 266687 266687 266687 266687 points). points). De De même même mêmepour pour pourG.Jardine G.Jardine G.Jardinedont dont dontlelelenuméro numéro numéro numéro est est est est 03.79.32.18.18 03.79.32.18.18 03.79.32.18.18 03.79.32.18.18 etet etet eee eee qui arrive arrive en en 222 et et et777 position position positionpuisque puisque puisqueces ces cesdeux deux deux deuxjoueurs joueurs joueurs joueurs ont ont ont ont chacun chacun chacun chacun deux personnages personnages personnagesbien bien bienclassés. classés. classés.On On Onpeut peut peutdonc donc donc donctenter tenter tenter tenter de de de de supprimer supprimer supprimer supprimer les doublons doublons ainsi ainsi ainsi::: SELECT DISTINCT DISTINCT DISTINCT telephone telephone telephoneFROM FROM FROMmembres membres membres membresJOIN JOIN JOIN JOINpersos persos persos persos ON ON ON id id id =⤦ ⤦⤦ SELECT ON id ===⤦ � � idMembre idMembre idMembre WHERE WHERE WHERE ville ville ville==="AMIENS" "AMIENS" "AMIENS" "AMIENS"ORDER ORDER ORDER ORDERBY BY BY BYscore score score score DESC DESC DESC DESC LIMIT LIMIT LIMIT LIMIT 10 10 10 10 Cette Cette fois, fois, surprise surprise surprise!!!E.Déziel E.Déziel E.Dézieldont dont dontlelelenuméro numéro numéro numéroest est est est03.68.16.84.21 03.68.16.84.21 03.68.16.84.21 03.68.16.84.21 est est est est passé passé en en première première première position, position, position,ce ce cequi qui quiest est estimpossible, impossible, impossible, impossible,d’autant d’autant d’autant d’autant que que que que lele lele meilleur meilleur joueur joueur joueur (03.33.17.58.09) (03.33.17.58.09) (03.33.17.58.09)aaalui lui luitotalement totalement totalement totalementdisparu. disparu. disparu. disparu. En En En En fait, fait, fait, fait, ilililil e e ee e ne devrait devrait yy avoir avoir avoirque que quedeux deux deuxnouveaux nouveaux nouveauxclassés classés classés classésen en en en 999e9eeet et etet 10 10 10 10 place. place. place. place. La raison raison de de ce ce ce résultat résultat résultatsurprenant surprenant surprenantvient vient vientdu du du dufait fait fait faitque que que que lele lele DISTINCT DISTINCT DISTINCT DISTINCT s’exécute s’exécuteavant avant avantles les lesautres autres autresinstructions instructions instructions(ORDER (ORDER (ORDER (ORDER etet etet LIMIT), LIMIT), LIMIT), LIMIT), ainsi, ainsi, ainsi, ainsi, comme comme comme comme le premier premier score score scorede de deG.Jardine G.Jardine G.Jardineest est est317965, 317965, 317965,les les lesscores scores scores scores suivants suivants suivants suivants (comme (comme (comme (comme celui de de 351839) 351839) 351839)sont sont sontignorés ignorés ignoréscar car carce ce cenuméro numéro numéroaaaadéjà déjà déjà déjà été été été été rencontré. rencontré. rencontré. rencontré. On peut peut s’en s’en sortir sortir sortir en en eneffectuant effectuant effectuantune une unerequête requête requête requêtede de de derequête requête requête requête : ::on :on on on efefefeffectue fectue la la première première première sélection sélection sélectionen en enclassant classant classantpar par par parscores scores scores scoresles les les les numéros numéros numéros numéros de de de de téléphones téléphones mais mais mais sans sans sans limiter limiter limiteraux aux aux10 10 10premières premières premières premièresréponses. réponses. réponses. réponses. Dans Dans Dans Dans lala lala table obtenue, obtenue, obtenue,on on oneffectue effectue effectuealors alors alorsune une unesélection sélection sélection des des des des numéros numéros numéros numéros de de de de téléphone téléphone téléphone téléphone avec un un DISTINCT, DISTINCT, DISTINCT,mais mais maiscette cette cettefois-ci fois-ci fois-ciles les lespremiers premiers premiers premiers numéros numéros numéros numéros rencontrés rencontrés rencontrés rencontrés sont les les meilleurs meilleurs meilleursscores scores scoresdonc donc donccela cela celane ne nepose pose poseplus plus plus plusde de de de problème. problème. problème. problème. Reste Reste Reste Reste alors àà ordonner ordonner ordonnerpar par parscores scores scorespuis puis puisàààlimiter limiter limiteraux aux aux aux10 10 10 10premiers premiers premiers premiers enregisenregisenregisenregistrements trements :: SELECT SELECT DISTINCT DISTINCT DISTINCT telephone telephone telephoneFROM FROM FROM(SELECT (SELECT (SELECT (SELECTtelephone, telephone, telephone, telephone,score score score score FROM FROM FROM FROM ⤦⤦ ⤦⤦ � � membres membres membres JOIN JOIN JOIN persos persos persosON ON ONid id id====idMembre idMembre idMembre idMembreWHERE WHERE WHERE WHEREville ville ville ville ==="AMIENS" ="AMIENS" "AMIENS" "AMIENS" ⤦⤦ ⤦⤦ � � ORDER ORDER ORDER BY BY BY score score score DESC) DESC) DESC)ORDER ORDER ORDER ORDERBY BY BY BYscore score score scoreDESC DESC DESC DESCLIMIT LIMIT LIMIT LIMIT 10 10 10 10 On peut peut simplifier simplifier simplifierla la larequête requête requêteen en enutilisant utilisant utilisantun un un ungroupement. groupement. groupement. groupement. Cette Cette Cette Cette nonononotion est est présentée présentée présentéeplus plus plusloin loin loindans dans dansle lelechapitre. chapitre. chapitre. 2. Les femmes femmes ont ont ontfait fait fait444569 569 569937 937 937parties parties parties:: : SELECT SELECT SUM(NbConnect) SUM(NbConnect) SUM(NbConnect) FROM FROM FROMmembres membres membres membresJOIN JOIN JOIN JOINpersos persos persos persosON ON ON ONid id id id ==== idMembre idMembre idMembre idMembre ⤦⤦ ⤦⤦ � � WHERE WHERE WHERE sexe sexe sexe === 000 286 L ES BASES DE DONN ÉES L ES BASES DE DONN ÉES 3. Avec la même jointure que précédemment, on trouve que Martine 3 personnages 3. possède Avec la même jointure :que précédemment, on trouve que Martine possède 3 personnages : SELECT prenom, COUNT(idPerso) FROM membres JOIN persos ON id = ⤦ � idMembre WHERE login = ’Frolit’ prenom, COUNT(idPerso) FROM membres JOIN persos ON id = ⤦ � idMembre WHERE login = ’Frolit’ SELECT Retour sur l’exercice H8 l’identifiant de la race Hobbit (4)membres : SELECT membres.nom, prenom FROM JOIN persos ON id = ⤦ � idMembre WHERE idRace = 4 SELECT membres.nom, prenom FROM membres JOIN persos ON id = ⤦ � idMembre WHERE idRace = 4 Soit utiliser la dénomination de la race. Il faut alors faire une seconde jointure aveclala table races :de la race. Il faut alors faire une seconde Soit utiliser dénomination jointuremembres.nom, avec la tableprenom races : membres JOIN persos ON id = ⤦ SELECT FROM � idMembre NATURAL JOIN races WHERE race = ’Hobbit’ membres.nom, prenom FROM membres JOIN persos ON id = ⤦ � idMembre NATURAL JOIN races WHERE race = ’Hobbit’ SELECT 2. On peut écrire la requête : 2. On peut écrire la persos requêteJOIN : SELECT race FROM races ON persos.idRace = ⤦ � races.idRace ORDER BY nbConnect DESC LIMIT 1 SELECT race FROM persos JOIN races ON persos.idRace = ⤦ � races.idRace ORDER BY nbConnect DESC LIMIT 1 ou avec une jointure naturelle : ou avecrace uneFROM jointure naturelle SELECT persos NATURAL: JOIN races ORDER BY nbConnect ⤦ � DESC LIMIT 1 SELECT race FROM persos NATURAL JOIN races ORDER BY nbConnect ⤦ � DESC LIMIT 1 3. Prêt pour une triple jointure ? En effet, nous avons besoin de la table pourtriple chercher Juliette la table persos pour 3. membres Prêt pour une jointure ? EnThibodeau, effet, nousde avons besoin de la table trouver nomchercher du personnage et enfin de la table table persos races pour pour membreslepour Juliette Thibodeau, de la connaı̂tre l’intitulé de la race à laquelle il correspond : trouver le nom du personnage et enfin de la table races pour connaı̂tre l’intitulé race de laFROM race membres à laquelle il correspond SELECT persos.nom, JOIN persos ON id: � SELECT � � � = idMembre ⤦ NATURAL JOIN races WHERE membres.nom = ’Thibodeau’ AND ⤦ persos.nom, race FROM membres JOIN persos ON id = idMembre ⤦ prenom = ’Juliette’ NATURAL JOIN races WHERE membres.nom = ’Thibodeau’ AND ⤦ prenom = ’Juliette’ À noter qu’il est plus efficace (et plus clair) d’effectuer les ON au fur etqu’il à mesure desefficace JOIN. (et plus clair) d’effectuer les ON À noter est plus au fur et à mesure des JOIN. 287 287 Solutions des exercices Retour 1. Il sur fautl’exercice penser àH8 préciser de quelle table on veut extraire le champ afficher les 946de réponses, on peut directement 1. nom. Il fautPour penser à préciser quelle table on soit veututiliser extraire le champ l’identifiant de la race Hobbit (4) : nom. Pour afficher les 946 réponses, on peut soit utiliser directement C HAPITRE H Retour sur l’exercice H9 1. On peut supprimer les deux enregistrements en une fois en utilisant le champ idRace : DELETE FROM races WHERE idRace >= 20 Ou encore en listant les races à supprimer : DELETE FROM races WHERE race IN ("Ours", "Orque") 2. Si l’on tente de supprimer les magiciens, on obtient l’erreur : FOREIGN KEY constraint failed : DELETE FROM races WHERE idRace = 8 puisqu’il y a des personnages qui sont magiciens dans la table persos et que la champ persos.idRace a été déclaré en tant que clé étrangère. Retour sur l’exercice H10 1. Pas de difficulté pour cette question : UPDATE races SET race = "Gnome" WHERE race = "Nain" 2. Attention de bien préciser le sexe, sinon vous allez modifier 3 enregistrements, car il y a une Madame Voisine et 2 Monsieur Voisine ! UPDATE membres SET nom = "Riverain" WHERE nom = "Voisine" AND ⤦ � sexe = 0 3. Avec la remarque donnée dans l’aide : UPDATE persos SET nbConnect = nbConnect + 1 WHERE nom = "Zu" Retour sur l’exercice H11 On va dans un premier temps récupérer les id de toutes les femmes via un SELECT, puis mettre à jour les scores correspondants. Voici une première solution : UPDATE persos SET score = score * 1.1 WHERE � FROM membres WHERE sexe = 0) idMembre IN (SELECT id ⤦ Cependant, les scores censés être entiers, ne le sont plus forcément... La fonction ROUND, comme en Python, va régler le problème : UPDATE persos SET score = score * 1.1 WHERE � FROM membres WHERE sexe = 0) 288 idMembre = IN (SELECT id ⤦ L ES BASES DE DONN ÉES Retour sur l’exercice H12 Voici une solution affichant les numéros de téléphone des 390 personnes concernées : SELECT REPLACE(telephone,’.’,’’) FROM membres WHERE LENGTH(pass) <= 8 Retour sur l’exercice H13 Il nous faut extraire les 4 derniers caractères du champ dateNaiss, ainsi on obtient : SELECT prenom || ’ ’ || nom, 2018-SUBSTR(dateNaiss,-4) FROM membres ⤦ � WHERE ville LIKE ’vincennes’ Les 5 membres de Vincennes sont donc Linette Rodrigue (33 ans), André Martin (47 ans), Vachel Charest (45 ans), Ambra Lanteigne (46 ans) et Granville Bertrand (38 ans). Noter que dans cet exemple, la conversion de la chaı̂ne représentant l’année en nombre s’est faite de manière implicite. On peut forcer ce chargement si besoin avec la fonction CAST valeur AS type Retour sur l’exercice H14 1. Si le 2e caractère est un /, on ajoute le caractère ”0” (986 enregistrements doivent être modifiés) UPDATE membres SET inscription = "0" || inscription WHERE ⤦ � SUBSTR(inscription,2,1) = "/" 2. De la même manière, pour les mois (2219 modifications) : UPDATE membres SET inscription = SUBSTR(inscription,1,3) || "0" ⤦ � || SUBSTR(inscription,4) WHERE SUBSTR(inscription,5,1) = "/" Pour les années (1194 modifications) : UPDATE membres SET inscription = SUBSTR(inscription,1,6) || "0" ⤦ � || SUBSTR(inscription,7) WHERE LENGTH(inscription) = 7 3. Avec SUBSTR, l’inversion ne pose pas de difficulté : UPDATE membres SET inscription = SUBSTR(inscription,7,2) || ⤦ � SUBSTR(inscription,3,4) || SUBSTR(inscription,1,2) 4. Pas de problème non plus pour transformer les symboles / en - avec la fonction REPLACE : UPDATE membres SET inscription = REPLACE(inscription,’/’,’-’) 289 Solutions des exercices C HAPITRE H C HAPITRE H 5. Pour l’ajout des centaines d’années : 5. UPDATE Pour l’ajout desSET centaines d’années : membres inscription = "20" || inscription UPDATE membres SET inscription = "20" || inscription Retour sur l’exercice H15 En reprenant les idées de l’exercice précédent, on obtient requêtes : En reprenant les idées de l’exercice précédent, Retour sur ces l’exercice H15 on obtient ces requêtes : -- Ajout éventuel d’un zéro UPDATE membres SET dateNaiss = "0" || dateNaiss WHERE LENGTH(dateNaiss)=9; -- Ajout éventuel zéro de chaine Mise de l’annéed’un en début UPDATE membres SET dateNaiss = "0" || dateNaiss WHERE|| LENGTH(dateNaiss)=9; SUBSTR(dateNaiss,7,4) ⤦ -- Mise de l’année en début de � SUBSTR(dateNaiss,3,4) || chaine SUBSTR(dateNaiss,1,2); UPDATE membres SET dateNaiss = SUBSTR(dateNaiss,7,4) || ⤦ -- Remplacement du / par � SUBSTR(dateNaiss,3,4) SUBSTR(dateNaiss,1,2); UPDATE membres SET dateNaiss || = REPLACE(dateNaiss,"/","-") -- Remplacement du / par UPDATE membres SET dateNaiss = REPLACE(dateNaiss,"/","-") À noter que : ● on peut À noter que : enchaı̂ner les requêtes en les séparant par un pointvirgule ; enchaı̂ner les requêtes en les séparant par un point● on peut ●virgule on peut ; placer des commentaires en les précédant de deux tirets ou, entre un /* et en */.les Cette deuxième ● on peutcomme placer en desC,commentaires précédant de solution deux tipermet en particulier de mettre un commentaire sur plusieurs rets ou, comme en C, entre un /* et */. Cette deuxième solution lignes. permet en particulier de mettre un commentaire sur plusieurs lignes. Retour sur l’exercice H16 Voici une possibilité : Retour sur l’exercice H16 of Voici une possibilité SELECT DATE(’now’, ’start year’,’10 months’, : ’weekday 4’, ’+14 days’) SELECT DATE(’now’, ’start of year’,’10 months’, ’weekday 4’, ’+14 days’) Partant de la date actuelle, on cherche le premier janvier de l’année en cours, le date 1er novembre, puis le 1er le jeudi de novembre et l’année enfin le en 3e Partantpuis de la actuelle, on cherche premier janvier de jeudi novembre 14 jours plus tard. cours,de puis le 1er novembre, puis le 1er jeudi de novembre et enfin le 3e jeudi de novembre 14 jours plus tard. Retour sur l’exercice H17 Retour l’exercice H17recours aux fonctions présentées dans cette par1. Icisur inutile d’avoir Les dates étantrecours correctement formatées, l’ordre alphabétique est 1. tie. Ici inutile d’avoir aux fonctions présentées dans cette paraussi l’ordre chronologique. Ainsi pour obtenir la liste des 196 pertie. Les dates étant correctement formatées, l’ordre alphabétique est sonnes concernées (classées par ordre d’inscription) : des 196 peraussi l’ordre chronologique. Ainsi pour obtenir la liste sonnes concernées par FROM ordremembres d’inscription) : SELECT prenom, nom, (classées inscription WHERE inscription � SELECT � � � 290 290 > ⤦ ’2010-03-14’ AND inscription < ’2011-01-01’ ORDER BY ⤦ prenom, nom,ASC inscription FROM membres WHERE inscription > ⤦ inscription ’2010-03-14’ AND inscription < ’2011-01-01’ ORDER BY ⤦ inscription ASC L ES BASES DE DONN ÉES 2. Pour cette question, on peut s’inspirer de l’exemple sur les modificateurs donnant le lundi de la semaine en cours. Pour comparer les dates il ne faut pas tenir compte des années, d’où la présence du SUBSTR. Voici une solution : SELECT � � � prenom, nom, dateNaiss FROM membres WHERE ⤦ SUBSTR(dateNaiss,6) >= SUBSTR(DATE(’now’, ’+1 day’, ⤦ ’weekday 1’, ’-7 day’),6) AND SUBSTR(dateNaiss,6) < ⤦ SUBSTR(DATE(’now’, ’+1 day’, ’weekday 1’),6) 1. Avec la table persos, on peut effectuer un regroupement sur le champ idRace : SELECT MAX(score), idRace FROM persos GROUP BY idRace Si on souhaite en plus écrire le nom de la race, on réalise une jointure : SELECT MAX(score), race FROM persos NATURAL JOIN races GROUP BY ⤦ � idRace et avec le nom des propriétaires : SELECT MAX(score), persos.nom, race, prenom, membres.nom FROM ⤦ � persos NATURAL JOIN races JOIN membres ON id = idMembre ⤦ � GROUP BY idRace 2. On peut déjà afficher le nombre d’inscrits en fonction des mois de l’année : SELECT SUBSTR(inscription,6,2), count(id) FROM membres GROUP BY ⤦ � SUBSTR(inscription,6,2) En ordonnant, on trouve que c’est en juillet, qu’il y a eu le plus d’inscriptions (278). SELECT SUBSTR(inscription,6,2), count(id) FROM membres GROUP BY ⤦ � SUBSTR(inscription,6,2) ORDER BY count(id) DESC LIMIT 1 291 Solutions des exercices Retour sur l’exercice H18 C HAPITRE H 3. Les 4 habitants d’Anglet, C. Harcourt, D. Jeanne, B. Platt et M. Quennel, se sont connectés en moyenne 1293, 1289, 1202 et 1606,5 fois. On peut soit utiliser un WHERE : SELECT membres.nom, prenom, AVG(NbConnect) FROM membres JOIN ⤦ � persos ON membres.id = persos.idMembre WHERE ville LIKE ⤦ � "ANGLET" GROUP BY idMembre soit un HAVING : SELECT membres.nom, prenom, AVG(NbConnect) FROM membres JOIN ⤦ � persos ON membres.id = persos.idMembre GROUP BY idMembre ⤦ � HAVING ville LIKE "ANGLET" Remarquons quand même que la première solution est environ deux fois plus rapide que la seconde, car le filtre d’être Angloy est effectué avant le groupement. 292 Objectif Programmer The Hardest Game Programmation Python Lire dans une base de données avec Python Propriétés en programmation objet Gestion des masques binaires dans Pygame Algorithmique Intersection de disques et de carrés Chapitre I O-But Installons-nous tranquillement sur une place ombragée d’un village du sud de la France aux abords d’un terrain de pétanque au son des cigales... Notre prochain jeu est un remake de The World’s Hardest Game, un jeu qui se revendique tout simplement être le jeu le plus dur au monde ! L’objectif du jeu d’origine est de déplacer le carré pour le faire arriver dans une zone de sécurité représentée en vert sans se faire toucher par les boules. 293 C HAPITRE I Dans notre version, vous piloterez une araignée traversant un terrain de pétanque pour rejoindre sa toile. Trois types d’embûches pourront être présentes : ● les bouteilles de breuvage anisé : elles sont fixes et posées au sol ● les boules de pétanque : elles se déplacent par translation ● les cochonnets : ils se déplacent par rotation. OBUT 1 - Chargement des niveaux Comme cela est souvent d’usage en réalité, nous allons utiliser une base de données comme fiches pour stoker facilement les informations sur le niveau, l’accès aux données étant facilité par les requêtes SQL. On dispose de 4 tables, voici la première : niveaux id : integer plateau : text image : text La table niveaux recense les informations d’un niveau : ● id est une clé primaire indiquant le numéro du niveau. ● plateau est une chaı̂ne de caractères composée de ’0’ (case vide) et de ’1’ (case noire) permettant de représenter le niveau. Le terrain de jeu étant ainsi ”quadrillé” par des cases de dimensions 48 × 48 pixels. Le caractère | (qui se prononce "pipe" à l’anglaise, c’est à dire tuyau) permet d’indiquer la fin de ligne. Enfin le caractère ’2’ 294 O-B UT indique la position de l’araignée Gibsy et le ’3’ le but à atteindre. Par exemple, est représenté par "111111|121301|101101|100001|111111". ● image est une chaı̂ne de caractères qui indique le nom d’un fichier à charger pour personnaliser le niveau. Si elle n’est pas renseignée, le fond est beige et les cases pleines sont noires. Le module sqlite3 présent par défaut dans Python permet d’interagir avec une base de données au format sqlite. La structure d’un programme utilisant ce module est typiquement : 1 2 3 import sqlite3 conn = sqlite3.connect('base.db') c = conn.cursor() # Ouverture d'une connexion # Création d'un curseur 4 5 c.execute(votre requete) # Exécution d'une requête 6 7 8 conn.commit() conn.close() # Enregistrement des changements (si nécessaire) # Fermeture de la connexion L’interrogation d’une base de données se passe en plusieurs temps, analysons les lignes du code ci-dessus : 2. ouverture d’une connexion, 3. création d’un curseur que nous interrogerons lors des requêtes, 7. si besoin on enregistre les modifications dans la base pour informer d’autres utilisateurs éventuels, 8. en fin d’utilisation, on ferme la connexion. La requête de la ligne 5. est donnée sous forme d’une chaı̂ne de caractères dans le langage SQL qui a été présenté dans le chapitre précédent. 295 C HAPITRE I ✍ Si vous avez besoin de paramètres dépendants de variables Python, vous pouvez générer une chaı̂ne de caractères, par exemple : niv = 3 c.execute('SELECT * FROM niveaux WHERE id = '+str(niv)) Cependant, cette méthode est une fausse bonne idée, en particulier s’il y a des interactions possibles avec l’utilisateur. Imaginez par exemple que vous demandiez le nom du joueur pour le stocker dans la table joueurs et qu’un utilisateur saisisse comme nom celui de ≪ DROPTABLE joueurs ≫... Une méthode plus fiable est d’utiliser le caractère ” ?” et d’ajouter un tuple contenant les variables : niv = (3,) # Un tuple ici à un élément. c.execute('SELECT * FROM niveaux WHERE id = ?', niv) ou avec plusieurs variables : var = (3,100) c.execute('SELECT * FROM niveaux WHERE id = ? AND largeur > ?', var) Il existe différentes solutions pour récupérer le résultat de notre requête une fois celle-ci effectuée : On a exécuté la requête suivante : c.execute('SELECT * FROM niveaux') ● c.fetchone() : renvoie un tuple contenant les colonnes du premier enregistrement retourné par la requête. ● c.fetchall() : même effet mais renvoie une liste de tuples correspondant à tous les enregistrements renvoyés par la requête. ● On peut aussi simplement parcourir les enregistrements à l’aide d’une simple boucle for : for ligne in c.execute('SELECT * FROM niveaux') : print(ligne) Dans ce dernier cas, ligne prend tour à tour chaque valeur des enregistrements sous forme d’un tuple. 296 O-B UT E X I1 La classe Niveau : pour instancier un objet, on donnera le numéro du niveau (valide), par exemple niv = Niveau(1) 1. Réaliser un programme qui décrit la classe dont les attributs sont : ● nbl, nbc : le nombre de lignes et de colonnes du niveau ; ● plateau : la chaı̂ne de caractères présente dans la base de données correspondant au niveau ; ● fond : une surface correspondant à l’image si elle est renseignée ou à un fond généré avec des carrés blancs et noirs. 2. Ajouter une méthode mur(x,y) qui renvoie un booléen pour indiquer la présence d’un mur sur le plateau de jeu au point de coordonnées (x,y) donné en pixels. Pour la suite du jeu, nous aurons besoin d’une classe générique, nommée Obstacle, que nous dériverons (c’est-à-dire que nous allons créer des classes héritées de celle-ci) en différentes classes selon les types d’embûches. E X I2 Écrire une classe Obstacle 1. Elle possède comme attributs x et y, les coordonnées du centre de l’obstacle ainsi que image une image représentant l’obstacle. Ces 3 valeurs seront placées en paramètre dans le constructeur. 2. Elle possède la méthode dessine qui, comme son nom l’indique, dessine l’obstacle sur la fenêtre de jeu à l’endroit donné par les attributs x et y. Intéressons-nous à la première classe dérivée, la classe Bouteille. Les objets bouteilles sont fixes, ils sont instanciés ainsi Bouteille(x,y,a) où (x, y) sont les coordonnées du centre et a l’angle de rotation en degré. (0° : Bouteille debout). Pour une meilleure qualité d’image lors de la rotation, l’image bouteille.png est 10 fois plus grande que nécessaire. À vous de la redimensionner une fois la rotation effectuée. E X I3 Écrire la classe Bouteille correspondant à la description donnée cidessus et déclarant les attributs et méthodes. Sur le même principe, on définit la classe Boule pour représenter les boules de pétanque qui se déplacent par ⃗ translation de vecteur v(vx,vy). 297 C HAPITRE I E X I4 Écrire la classe Boule correspondant à la description donnée ci-dessus. L’instanciation se faisant ainsi : Boule(x, y, vx, vy). E X I5 Ajouter une méthode update qui met à jour les attributs x et y et appelle la méthode dessine de la classe Obstacle. 1. À la classe Boule (pour le moment, on ne gère pas les dépassements de terrain). Tester cette méthode en créant une boule de pétanque arbitraire sur le niveau 1. 2. À la classe Bouteille, qui se contentera d’afficher celle-ci, pour harmoniser les appels. Reste à s’occuper de la classe Cochonnet. Ces obstacles se déplacent par rotation autour du point Ω(x0,y0) avec une vitesse angulaire a (angle dont le cochonnet tourne à chaque étape de temps) et un angle initial a0. On note enfin r la distance ΩCt (Ct étant la position du cochonnet à l’instant t). Ct+1 x = r × cos(angle) + x0 a y = r × sin(angle) + y0 Ct r Ω angle a0 (x0,y0) E X I6 Créer une classe Cochonnet qui possède les attributs nécessaires aux calculs et la méthode update. Les objets de cette classe seront instanciés par l’appel Cochonnet(x0,y0,a,a0,r). E X I7 Terminer le code du projet, pour que sur le terrain, s’anime une liste d’obstacles de manière automatique. Pour le moment, on se contente de créer manuellement une liste d’obstacles pour tester. 298 O-B UT O-B UT Sur le même principe que pour l’ensemble des niveaux, chaque type d’embûche possède sa propre table. Ces tables sont présentées ici : Sur le même principe que pour l’ensemble des niveaux, chaque type d’embûche possède sa propre table. Ces tables sont présentées ici : cochonnets boules bouteilles id : integer id : integer id : integer niv : integer cochonnets niv : integer boules niv : integer x0 bouteilles id : integer x id: integer : integer x : integer y0 id : integer niv: integer : integer y : integer niv : integer y : integer a niv : integer x0: real : integer vx : integer x : integer angle : integer a0 x : integer y0 : real integer vy : integer y : integer rayon y : integer a : real : real vx : integer angle : integer a0 : real vy : integer rayon : real Pour chacune de ces tables, id est la clé primaire alors que niv est une clé étrangère indiquant le numéro du niveau où se trouve cette embûche. Pour chacune de ces tables, id est la clé primaire alors que niv est une clé étrangère indiquant le numéro du niveau où se trouve cette embûche. Il est possible dans un programme Python d’identifier le résultat d’une requête SQLite par le nom des champs plutôt que par leurs indices. Il suffitdans d’ajouter la ligne suivante au moment de la Il est possible un programme Python d’identifier le création résultat de la connexion : d’une requête SQLite par le nom des champs plutôt que par leurs indices. Il suffit d’ajouter la ligne suivante au moment de la création conn.row_factory = sqlite3.Row de la connexion : On peut alors accéder aux informations plus simplement par le nom conn.row_factory = sqlite3.Row du champ. Par exemple le script suivant affiche la liste des images de chaque : On peutniveau alors accéder aux informations plus simplement par le nom du champ. Par exemple le script suivant affiche la liste des images de conn = sqlite3.connect("base.db3") conn.row_factory = sqlite3.Row chaque niveau : c = conn.cursor() connligne = sqlite3.connect("base.db3") for in c.execute('SELECT * FROM niveaux') : conn.row_factory = sqlite3.Row print(ligne['image']) c = conn.cursor() conn.commit() for ligne in c.execute('SELECT * FROM niveaux') : conn.close() print(ligne['image']) conn.commit() conn.close() E X I8 Écrire une fonction qui reçoit le numéro d’un niveau et renvoie une liste d’objets Obstacles en allant chercher les informations dans la base de données. E X I8 Écrire une fonction qui reçoit le numéro d’un niveau et renvoie une liste d’objets Obstacles en allant chercher les informations dans la base de données. 299 299 C HAPITRE I 2 - Déplacement des boules - Propriétés Les bouteilles étant fixes et les cochonnets placés de manière à ne pas entrer en collision avec le décor, intéressons-nous au déplacement des boules de pétanque, qui, dans notre cas, ne se déplaceront que verticalement et horizontalement. Pour gérer les collisions avec le décor, nous allons avoir besoin d’accéder à l’attribut plateau du niveau en cours. Or comme cela a été évoqué dès la présentation de la programmation objet dans le projet Cupidon, cela est contraire au principe d’encapsulation. En effet, on comprend assez aisément que la modification directe de l’attribut d’un objet peut avoir des répercussions sur d’autres attributs ou même sur des attributs d’autres objets. Prenez l’exemple de Mario qui mange un superchampignon et grandit, il va alors falloir vérifier l’endroit où Mario se trouve pour éviter qu’il ne défonce tout le décor ! Pour la lecture, même si cela semble moins problématique, l’accès direct à un attribut peut ne pas être pertinent. Prenons un exemple : vous avez un objet Joueur2 qui contient un attribut pos indiquant la position de ce joueur sur le plateau d’un jeu en réseau. Lorsque vous voulez accéder à cette information, il faut prévoir d’effectuer une requête sur le réseau pour mettre à jour cette information avant de la renvoyer. Donc autant prévoir une fois pour toute que la requête s’effectuera à chaque fois que l’on demande la valeur de pos. Nous allons avoir recours à des getters (ou accesseurs en français) pour lire les informations et des setters (ou mutateurs) pour les modifier. Pour comprendre, restons sur class Boule() : l’exemple de notre boule de def __init__(self,x,y,vx,vy) pétanque dont on simplifie la self.vx, self.vy = vx, vy self.x, self.y = x, y déclaration. Créons alors l’instance B placée en (20,50) et qui avance B = Boule(20, 50, 2, 0) selon le vecteur vitesse (−2,0). class Boule() : ... def recup_abscisse(self) : return self.x 300 : À présent au lieu de lire l’attribut avec abscisse = B.x, nous allons créer une méthode et nous ferons l’appel abscisse = B.recup abscisse(). O-B UT Boule() : De même, on remplacera class ... l’appel B.x = 30, par l’appel def change_abscisse(self, self.x = x1 B.change abscisse(30). x1) : Nous venons de fabriquer un getter (recup abscisse) et un setter (change abscisse) pour l’attribut x... Bon, pas sûr que je vous ai totalement convaincu et je vous comprends : d’une part c’est bien plus lourd dans la déclaration de la classe, mais aussi dans l’usage dans la suite du code. Pour tenter de vous convaincre un peu plus, voici quelques arguments sur la nécessité de faire ainsi parfois. Pensez qu’un jour vous créerez peut-être des bibliothèques avec des objets que d’autres utiliseront, vous aurez parfois besoin de conserver non accessibles ou non modifiables certains attributs pour assurer le bon fonctionnement de votre classe. D’autre part, en passant par un setter, vous pourrez facilement débugger un code Python, en surveillant les modifications de tel ou tel attribut. En C++ ou en Java, c’est au moment de la création de la classe que l’on précise si les attributs (et méthodes) sont publics ou privés. En Python, la philosophie est différente : on s’autorise à lire et modifier les attributs comme on le faisait dans les chapitres précédents, tant que cela ”ne risque rien”. Python est basé sur la confiance des usagers ; ainsi la convention est d’ajouter un underscore (caractère ) avant le nom d’un attribut ou d’une méthode pour préciser à l’utilisateur que son usage direct n’est pas souhaité. Pour accéder aux attributs sensibles ou que l’on veut protéger, on aura recours à des propriétés. S’il nous prend l’envie de sécuriser l’attribut x, on va procéder ainsi : 1 Dans la console : class Boule() : 2 3 4 5 def __init__(self, x, y, vx, vy) : self.vx, self.vy = vx, vy self._x, self.y = x, y 8 9 def _get_x(self) : print('il est passé par ici') return self._x 12 13 def _set_x(self, x1) : print('il repassera par là') self._x = x1 5 6 8 9 10 11 14 15 4 7 10 11 2 3 6 7 1 x = property(_get_x, _set_x) 12 >>> B = Boule(20, 50, 2, 0) >>> B.x il est passé par ici 20 >>> B.x = 10 il repassera par là >>> B.x il est passé par ici 10 >>> B.x = B.x + 5 il est passé par ici il repassera par là 301 C HAPITRE I Quelques explications sur ce que l’on vient de réaliser : au lieu de créer un attribut x comme précédemment, on a créé l’attribut x pour indiquer que l’on ne souhaite pas y accéder depuis l’extérieur. Tout cela n’est encore une fois qu’une histoire de convention et d’habitude ; si vous voulez appeler cette variable lavariablexquejeprotege vous pouvez ! Ensuite on a aussi fait précéder les noms de nos getters et setters d’un underscore pour préciser qu’à priori, on ne fera pas d’appel depuis l’extérieur à ces fonctions. Ici on a ajouté des print pour que vous compreniez ce qui se passe au moment des appels. En fin de déclaration de la classe on crée une propriété que l’on nomme x et dont on précise le comportement des contrôles d’accès. x=property(f 1, f 2 , f 3 , txt) définit une propriété x telle que : ● on appelle la fonction f 1 lorsqu’on lit la valeur de x, ● on appelle la fonction f 2 lorsqu’on modifie la valeur de x, ● on appelle la fonction f 3 lorsqu’on détruit la valeur de x, ● on renvoie le docstring txt lorsqu’on invoque l’aide de x. Chacun de ces paramètres est optionnel. Si par exemple vous ne renseignez qu’un seul paramètre et que vous tentez de modifier x, vous déclencherez une erreur. Une fois cette propriété déclarée, l’usage est totalement transparent pour l’utilisateur de la classe, comme vous pouvez le constater dans la capture précédente de la console. Quand, à la ligne 2, on demande la valeur de B.x, l’accesseur get x est exécuté. Quand, à la ligne 5, on modifie la valeur de B.x, c’est le mutateur set x qui l’est cette fois-ci. Pour l’appel de la ligne 10, les deux fonctions sont appelées : une première fois on demande la valeur de x, puis on ajoute 5, et enfin on modif ie la valeur de x. Encore un exemple : si on souhaite que _set_x(self, x1) : lorsque l’abscisse de la boule dépasse 500 elle defself._x = x1 revienne à 0, on peut modifier le setter comme if self._x > 500 : ci-contre et ainsi ne plus jamais avoir à se sou- self._x = self._x-500 cier des sorties d’écran ! 302 O-B UT Voyez donc l’usage des propriétés comme une nouvelle opportunité qui vous est offerte et non comme une contrainte. Cette ultime remarque me permet de clore la parenthèse puisque vous savez à présent encapsuler de manière propre vos objets. Il existe enfin une manière plus concise de procéder en utilisant les décorateurs de Python. Nous n’aurons pas le temps de la présenter ici car nos boules de pétanque sont toujours immobiles pour l’heure ! E X I9 Modifier la méthode update de la classe Boule pour gérer les rebonds sur le décor. Il ne semble pas utile d’avoir recours aux propriétés à priori. 3 - Déplacement du personnage - La classe Mask À présent notre décor est planté et les embûches sont mobiles. Nous allons donc nous atteler au déplacement de Gibsy l’araignée. gibsy0.png gibsy1.png gibsy2.png gibsy3.png Dans l’exercice précédent, nous avons géré assez facilement la collision d’une forme ronde avec des lignes horizontales ou verticales. Pour d’autres formes... ● La collision entre deux cercles est facile aussi à tester. Avec les notations de la figure, il suffit de regarder si : AB < r1 + r2 B r2 A r1 ● S’il s’agit de deux formes rectangulaires, c’est un peu plus difficile, mais heureusement Pygame sait faire le travail pour nous à l’aide 303 C HAPITRE I des méthodes contains, collidepoint, colliderect ou encore collidelist... que nous avons déjà présentées dans le chapitre Cupidon. ● Si on souhaite tester la collision entre un carré et un cercle (c’est par exemple le cas de Gibsy et d’une boule de pétanque), ce n’est pas simple non plus et cette fois-ci, Pygame ne vient pas à notre secours. La fonction suivante répond à la question : def collision(x1, y1, r, x2, y2, c) : deltaX, deltaY = abs(x1-x2), abs(y1-y2) if deltaX > c//2+r : return False if deltaY > c//2+r : return False if deltaX <= c//2 : return True if deltaY <= c//2 : return True dx, dy = deltaX-c//2, deltaY-c//2 return dx**2 + dy**2 <= r**2 Avec pour paramètres : (x1, y1) les coordonnées du centre d’un cercle de rayon r et (x2, y2) les coordonnées du centre d’un carré de côté c. E X I10 Écrire un programme (ou le télécharger, là n’est pas le plus important) qui permet de piloter un carré et un disque de côté et de rayon donnés. Afficher s’il y a ou non collision, en précisant quel return a terminé la fonction afin de comprendre le fonctionnement de celle-ci. Nous avons presque tout ce qu’il nous faut pour jouer, à l’exception des collisions avec les bouteilles sur lesquelles nous reviendrons juste après. E X I11 Effectuer quelques modifications pour pouvoir jouer : 1. Ajouter à la classe Niveau deux propriétés depart et arrivee permettant de lire les coordonnées (centres) du joueur au départ et de celles du but à atteindre. 2. Créer une classe Gibsy qui possède comme attributs : ● x et y : la position du centre du joueur. ● dep : un couple indiquant les coordonnées de départ (pour repartir au départ quand on perd). ● but : un couple indiquant les coordonnées du centre du but à atteindre. ● image : une surface représentant le personnage (on ne s’occupe pas pour le moment de l’animation) et toile la surface image de l’objectif à atteindre. 304 O-B UT On créera une instance de cette classe via la ligne gib = Gibsy(niv) où niv est le niveau en cours. 3. Comme pour les autres éléments, ajouter la méthode update qui surveille les touches du clavier et met à jour les coordonnées de Gibsy en respectant les décors. On ne tient pas compte des autres obstacles pour le moment, tester avec le niveau 1 qui est vide si vous voulez. Voici encore une autre utilisation des propriétés : imaginez que vous n’avez pas anticipé le fait que l’attribut image pouvait changer (selon la direction, si on veut animer l’araignée...). On peut grâce aux propriétés, sans changer le reste du code, mettre à jour de manière dynamique l’attribut image. E X I12 Ajouter pour la classe Gibsy trois attributs privés : ● images : une liste de 16 images dans cet ordre : haut0, haut1, haut2, haut3, droite0, droite1, droite2, droite3, bas0, bas1, bas2, bas3, gauche0, gauche1, gauche2, gauche3. ● frame : le numéro de l’animation en cours (0, 1, 2 ou 3). ● direction : le numéro de la direction en cours (0 : Haut, 1 : Droite, 2 : Bas ou 3 : Gauche). Modifier alors la déclaration de la classe Gibsy pour que l’attribut image devienne une propriété et ainsi renvoie la bonne surface à afficher. Reste à gérer les collisions avec les obstacles. Comme le test va dépendre du type d’obstacle, il est préférable de placer celui-ci en tant que méthode des classes Boule, Cochonnet et Bouteille plutôt que sur la classe Gibsy. 305 C HAPITRE I E X I13 Les méthodes touche reçoivent un objet de la classe Gibsy et renvoient un booléen indiquant s’il y a eu collision ou non. 1. Créer la méthode touche pour la classe Boule sachant que les boules de pétanque sont de diamètre 32 (pour vos tests, le niveau 2 ne contient que des boules). 2. Créer la méthode touche pour la classe Cochonnet, sachant que les cochonnets sont de diamètre 16 (le niveau 3 ne contenant que des cochonnets). 3. Inclure alors ce test de collision pour que dès que Gibsy touche un obstacle, elle reparte à sa position initiale et dès qu’elle atteint son objectif, le jeu s’arrête. Pour la collision avec les bouteilles au sol, le travail s’annonce bien plus complexe, car celles-ci ont des formes bien moins simples. Heureusement nous pouvons compter sur la classe Mask de Pygame ! Un petit code vaut parfois mieux qu’un long discours : import pygame from pygame.locals import * image = pygame.image.load('gibsy.png') m = pygame.mask.from_surface(image) xmax, ymax = m.get_size() for y in range(ymax) : ligne = "" for x in range(xmax) : if m.get_at((x,y)) == 1 : ligne = ligne + 'X' else : ligne = ligne + " " print(ligne) XX XX XXXX XXXX XXXX XXXX XXXX XXXX XXXXX XXXX XXXXXX XXXXXX XXXXXXX XXXXXX XXXXXXX XXXXXXX XXXXXXX XXXXXXX XXXXXXXX XXX XXX XXXXXXX XXXXXXXX XXXXXXXXXX XXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXX X XXXXXXXXXXXXXXXXXXXXXXXX XXX XX XXXXXXXXXXXXXXXXXXXXXX XXXX XXXXX XXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXX XXXXXXXXXXXXXX XXXX XXXX XXXXXXXXXX XXXX XXX XXXXXXXXXX XXX XXX XXXXXXXXXX XXXX XXX XXXXXXXX XXX XXX XXXXXX XXXX XXXX XXXX XXX XXX XXX XXX XXX X X La classe Mask va nous permettre de créer des masques binaires de nos images. Un masque binaire peut être vu comme une image en noir et blanc : elle ne contient que des 0 et des 1, c’est à dire que l’information d’un pixel tient sur 1 bit, contrairement à une information d’un pixel en mode RGBA qui nécessite 4 × 8 = 32 bits. Ainsi les tests de collisions seront bien plus rapides que des tests manuels pixel par pixel. En effet si l’algorithme de détection de collision est programmé de manière optimisée, un ordinateur 64 bits peut traiter en une opération élémentaire, une série de 64 points. 306 O-B UT Décrivons quelques fonctions et méthodes sur cette classe : ● m = pygame.mask.from surface(surf ,s) : place dans m un objet de type Mask généré à partir de la surface surf . On peut préciser un seuil s (entre 0 et 255) pour les images possédant un canal alpha de transparence. ● m = pygame.mask.Mask(s) : place dans m un objet de type Mask vide (que des 0) de taille s = (x,y). ● m.get bounding rects() : renvoie un objet Rect indiquant la position ”utile” du masque, c’est à dire le plus petit rectangle contenant tous les 1 du masque m (si par exemple il y a des 0 sur la bordure de l’image). ● m.get at((x,y)) : renvoie 1 si le pixel de coordonnées (x,y) est un point (noir) du masque binaire m et 0 sinon. ● m.set at((x,y),v) : affecte au pixel (x,y) du masque m la valeur v ∈ {0; 1}. ● m1 .overlap area(m2, d) : renvoie le nombre de pixels en collision lorsque le masque m1 chevauche le masque m2 après avoir subi un décalage de d = (Δx,Δy), comme l’illustre la figure. Le décalage peut être négatif si besoin (attention à l’ordre des masques) : Δy Δx m1 m2 307 C HAPITRE I E X I14 Créer la méthode touche pour la classe Bouteille et admirer le résultat sur les niveaux 4 et 5. 4 - Ce n’est qu’un au revoir... Voilà pour ce dernier chapitre. Il reste tant de choses à dire sur Pygame et sur Python, j’espère avoir éveillé votre curiosité et vous avoir donné envie d’aller plus loin. Souvent, il aurait été possible de faire mieux, plus efficace, plus rapide, d’utiliser d’autres outils. Par exemple, et pour vous donner encore quelques pistes, le module Pygame propose une classe Sprite que l’on peut dériver pour définir les différents objets d’un jeu. En effet, vous aurez remarqué que l’on a souvent besoin des mêmes informations : ● un attribut image pour représenter le personnage, ● un attribut rect de type Rect indiquant la position et les dimensions de ce dernier, ● une méthode update pour mettre à jour les informations le concernant et le représenter à l’écran si nécessaire, ● ... Cette classe permet donc de regrouper ces informations et même de les générer à la volée si besoin. Il existe même des classes pour regrouper ces objets Sprite en collections pour leur faire effectuer des tâches communes. Bref, en Python il existe toujours une classe répondant à nos attentes, mais quel plaisir de créer de toutes pièces nos propres fonctionnalités pour répondre à nos besoins. Comme pour le premier livre, souvenez-vous que le but ici est d’apprendre de nouveaux concepts, de nouveaux algorithmes et non de devenir un expert en Pygame. J’espère que vous aurez pris du plaisir à lire ce livre, je vous donne rendez-vous sur le forum du site web d’Edupython pour échanger et peutêtre dans un prochain livre pour découvrir l’interface KIVY qui permet de programmer en Python des applications sur ordinateur et tablettes Android. Portez-vous bien ! Le 13 avril 2020, un soir de confinement 308 O-B UT 5 - Aide pour les exercices Aide pour l’exercice I9 Puisque les déplacements de la boule sont uniquement horizontaux et verticaux, on peut utiliser la méthode mur du niveau, en ne testant qu’un point selon la direction souhaitée : 32 (x; y − 16 + vy ) (x − 16 + vx ; y) (x,y) (x + 16 + vx ; y) 32 (x; y + 16 + vy ) Aide pour l’exercice I14 Pour le masque représentant Gibsy, plusieurs solutions s’offrent à nous : ● soit on le recalcule à chaque fois (c’est un peu long, mais comme l’image est petite, c’est jouable) ; ● soit on les calcule tous à l’avance, comme pour les images ; ● soit enfin, on peut créer un masque plein de 32 × 32 pixels, car le dessin de l’araignée est presque carré : m = pygame.mask.Mask((32,32)) for x in range(32) : for y in range(32) : m.set_at((x,y),1) ou avec une image et un seuil de -1 : image = pygame.Surface((32,32)) m = pygame.mask.from_surface(image, -1) 309 C HAPITRE I C HAPITRE I 6 - Solutions des exercices 6 - Solutions des exercices Retour sur l’exercice I1 Retour sur l’exercice I1 1. Il suffit d’effectuer la requête permettant de récupérer les deux informations qui nous intéressent. La longueur d’une ligne est l’indice 1. Il suffit d’effectuer la requête permettant de récupérer les deux indu premier ’|’ trouvé. En effet s’il est en position 8, cela signifie bien formations qui nous intéressent. La longueur d’une ligne est l’indice qu’il y a 8 caractères avant (0 à 7). Pour connaı̂tre la hauteur, on peut du premier ’|’ trouvé. En effet s’il est en position 8, cela signifie bien ajouter 1 au nombre de symboles ’|’ présents dans la chaı̂ne (celle-ci qu’il y a 8 caractères avant (0 à 7). Pour connaı̂tre la hauteur, on peut ne se termine pas par un |) ou encore calculer le quotient de la lonajouter 1 au nombre de symboles ’|’ présents dans la chaı̂ne (celle-ci gueur de la chaı̂ne augmentée de 1 (pour le dernier |) par le nombre ne se termine pas par un |) ou encore calculer le quotient de la londe caractères sur une ligne (y compris le |). Pour la génération du gueur de la chaı̂ne augmentée de 1 (pour le dernier |) par le nombre plateau au cas où il n’y ait pas d’image précisée, le code devrait sufde caractères sur une ligne (y compris le |). Pour la génération du fire à comprendre. plateau au cas où il n’y ait pas d’image précisée, le code devrait suffire à comprendre. class Niveau() : def __init__(self, n) : class Niveau() : conn = sqlite3.connect('obut.db3') = conn.cursor() def c __init__(self, n) : c.execute('SELECT plateau, image FROM niveaux WHERE id ⤦ conn = sqlite3.connect('obut.db3') � =?',(n,)) c = conn.cursor() resultat = c.fetchone() c.execute('SELECT plateau, image FROM niveaux WHERE id ⤦ # Remplissage de la matrice � =?',(n,)) self.plateau = resultat[0] resultat = c.fetchone() self.largeur self.plateau.index('|') # Remplissage=de la matrice self.hauteur // (self.largeur+1) self.plateau = = (1+len(self.plateau)) resultat[0] # Fond self.largeur = self.plateau.index('|') if resultat[1] self.hauteur = : (1+len(self.plateau)) // (self.largeur+1) self.fond = pygame.image.load('images/'+resultat[1]) # Fond else: if resultat[1] : self.fond ⤦ self.fond = = pygame.image.load('images/'+resultat[1]) � pygame.Surface((self.largeur*48,self.hauteur*48)) else: self.fond.fill((242, 208, 95)) self.fond = ⤦ x, y �= pygame.Surface((self.largeur 0, 0 *48,self.hauteur*48)) for car in self.plateau : 95)) self.fond.fill((242, 208, x, yif = car 0, 0== '1' : y, 48, 48)) for car self.fond.fill(0,(x, in self.plateau : x x +== 48'1' : if=car if car == '|' : # Retour y, à la ligne. self.fond.fill(0,(x, 48, 48)) = 0, y + 48 x = x, x +y48 conn.close() if car == '|' : # Retour à la ligne. x, y = 0, y + 48 conn.close() 310 310 En téléchargeant le code sur le site, vous aurez un programme complet permettant d’afficher le niveau 1 pour visualiser le résultat. En téléchargeant le code sur le site, vous aurez un programme complet permettant d’afficher le niveau 1 pour visualiser le résultat. O-B UT 2. Pour savoir s’il y a un mur, vous pouvez vous inspirer de l’exercice A4. Pour être plus pédagogue, je propose une solution en 3 lignes, mais on peut la faire en une. def mur(self, x, y) : l, c = y // 48, x // 48 # On cherche la ligne et la colonne pos = l*(self.largeur+1)+c # On calcule l'indice dans la chaı̂ne return self.plateau[pos] == '1' class Obstacle() : def __init__(self, x, y, img) : self.x = x self.y = y self.image = img def dessine(self) : l, h = self.image.get_size() fenetre.blit(self.image, (self.x-l//2, self.y-h//2)) Retour sur l’exercice I3 Voici un code possible pour la classe demandée : class Bouteille(Obstacle) : def __init__(self, x, y, a) : image = pygame.image.load('images/bouteille.png').convert_alpha() image = pygame.transform.rotate(image, a) l, h = image.get_size() image = pygame.transform.smoothscale(image,(l//10, h//10)) Obstacle.__init__(self, x, y, image) À noter que l’on a utilisé la méthode smoothscale pour redimensionner l’image puisque cet appel ne se produit qu’une fois lors de l’instanciation. On peut donc privilégier la qualité à la vitesse. Retour sur l’exercice I4 Même principe que pour l’exercice précédent : class Boule(Obstacle) : def __init__(self, x, y, vx, vy) : image = pygame.image.load('images/boule.png').convert_alpha() self.vx, self.vy = vx, vy Obstacle.__init__(self, x, y, image) 311 Solutions des exercices Retour sur l’exercice I2 Il faut faire attention à centrer l’image lorsqu’on la dessine puisque les attributs x et y représentent le centre de celle-ci : C HAPITRE I Retour sur l’exercice I5 Ajoutons ces méthodes sur ces classes : 1. Pour la première, il suffit d’effectuer une translation : class Boule(Obstacle) : ... def update(self) : self.x = self.x+self.vx self.y = self.y+self.vy self.dessine() 2. Pour la seconde, il y a juste à afficher l’image : class Bouteille(Obstacle) : ... def update(self) : self.dessine() Retour sur l’exercice I6 Les arguments x0, y0, a et r donnés lorsque l’on instancie un objet doivent être conservés en mémoire pour permettre de calculer les coordonnées du cochonnet à l’instant t. D’autre part, on ajoute un attribut angle initialisé avec la valeur de a0 et qui évoluera au fil du temps : class Cochonnet(Obstacle) : def __init__(self, x0, y0, a, a0, r) : image = pygame.image.load('images/cochonnet.png').convert_alpha() self.a, self.angle = a, a0 self.x0, self.y0 = x0, y0 self.r = r self.x = self.x0+self.r*cos(self.angle) self.y = self.y0+self.r*sin(self.angle) Obstacle.__init__(self, self.x, self.y, image) def update(self) : self.angle = self.angle + self.a self.x = self.x0+self.r*cos(self.angle) self.y = self.y0+self.r*sin(self.angle) self.dessine() Pensez à importer les fonctions cos et sin du module math pour pouvoir effectuer les calculs. Retour sur l’exercice I7 Rien de trop compliqué dans cet exercice, il suffit d’initialiser une liste d’objets avant la boucle principale du jeu, par exemple : 312 O-B UT embuches = [Cochonnet(150,100,0.05,40,30), Bouteille(300,200,-45), ⤦ � Boule(600,300,-1,0) ] et d’ajouter à l’intérieur de la boucle : while continuer: ... for e in embuches : e.update() ... Retour sur l’exercice I8 Nous allons mettre en œuvre la récupération des informations par nom de colonne, comme nous venons de le voir : def charge_embuches(n) : liste = [] conn = sqlite3.connect('obut.db3') conn.row_factory = sqlite3.Row c = conn.cursor() # Les bouteilles for ligne in c.execute('SELECT * FROM bouteilles WHERE niv =?',(n,)): liste.append(Bouteille(ligne['x'], ligne['y'], ligne['a'])) # Les boules for ligne in c.execute('SELECT * FROM boules WHERE niv =?',(n,)): liste.append(Boule(ligne['x'], ligne['y'], ligne['vx'], ligne['vy'])) # Les cochonnets for ligne in c.execute('SELECT * FROM cochonnets WHERE niv =?',(n,)): liste.append(Cochonnet(ligne['x0'],ligne['y0'], ligne['a'], ⤦ � ligne['a0'], ligne['r'])) conn.close() return liste Retour sur l’exercice I9 Comme proposé dans l’aide, on peut utiliser plusieurs fois la méthode mur pour les 4 collisions possibles. Si on touche un mur, on modifie le vecteur vitesse en conséquence avant de modifier les coordonnées de la boule : def update(self) : if self. vx < 0 and niv.mur(self.x+self.vx-16, self.y) : self.vx = -self.vx elif self. vx > 0 and niv.mur(self.x+self.vx+16, self.y) : self.vx = -self.vx elif self. vy < 0 and niv.mur(self.x, self.y+self.vy-16) : self.vy = -self.vy elif self. vy > 0 and niv.mur(self.x, self.y+self.vy+16) : self.vy = -self.vy self.x = self.x + self.vx self.y = self.y + self.vy self.dessine() 313 Solutions des exercices pour mettre à jour tous les obstacles. L’intégralité du code est disponible sur le site. C HAPITRE I Retour sur l’exercice I10 Si vous avez téléchargé la solution que je propose, on déplace le carré avec les flèches de direction et le disque avec les flèches en maintenant simultanément la touche CTRL enfoncée. ● 1er cas : l’écart entre les abscisses est trop grand (zone grise), on est sûr qu’il n’y a pas collision : Δx × ● 2e cas : l’écart entre les ordonnées est trop grand (zone grise), on est sûr qu’il n’y a pas collision : × Δy ● 3e cas : l’écart entre les abscisses est trop petit (zone grise) et on n’est pas dans les cas hachurés déjà traités, on est donc sûr qu’il y a collision : 314 O-B UT ● 4e cas : avec la même idée, si l’écart entre les ordonnées est trop petit (zone grise) et puisque l’on n’est pas dans les cas hachurés déjà traités, on est sûr qu’il y a collision : × ● Dernier cas : si on arrive à cette ligne de code, c’est que le centre du cercle est à l’intérieur d’un des carrés blancs. Par symétrie, et puisque l’on a calculé des écarts absolus, ces 4 situations sont identiques : il s’agit de déterminer si le coin du carré est à l’intérieur du cercle. En soustrayant la demi-longueur du côté, on se retrouve avec ces quantités (on a zoomé sur le carré Sud-Ouest) : 315 Solutions des exercices × C HAPITRE I × dy ? dx On vérifie alors si ? ⩽ r, c’est à dire si √ dx2 + dy 2 ⩽ r ou encore si dx2 + dy 2 ⩽ r 2 Retour sur l’exercice I11 1. Chercher la position de départ ou d’arrivée relève du même principe : il faut chercher la position du caractère ’2’ (ou ’3’) dans l’attribut plateau, puis transformer cette position en ligne et colonne et enfin repasser en coordonnées cartésiennes. On peut donc créer cette fonction cherche une fois pour toutes (vous aurez remarqué que l’on précède cette méthode d’un pour préciser qu’il s’agit d’une méthode privée). Reste ensuite à créer la propriété : class Niveau() : ... def _cherche(self, car) : pos = self.plateau.index(car) l, c = pos // (self.largeur+1), pos % (self.largeur+1) return c*48+24, l*48+24 def _depart(self) : return self._cherche('2') depart = property(_depart) Comme souvent, on peut gagner un peu de temps en utilisant des fonctions anonymes appelées en Python des fonctions lambda. 316 O-B UT O-B UT Comme en mathématiques lorsqu’on écrit f ∶ x �→ 2x + 1, on peut créer une ligne une fonction, def2x: +O-B UT Comme enen mathématiques lorsqu’onsans écritutiliser f ∶ x �→ 1, on peutf créer une def2x: + 1, on Comme enen mathématiques lorsqu’onsans écritutiliser f ∶ x �→ >>> = lambda x : ligne 2*x+1une fonction, >>> f(4) peut créer en une ligne une fonction, sans utiliser def : >>> f = lambda x : 2*x+1 9 Comme en mathématiques lorsqu’on écrit f ∶ x �→ 2x + 1, on >>> f(4) >>> = lambda x : ligne 2*x+1une fonction, sans utiliser def : peutf créer en une 9 >>> f(4) L’instruction lambda renvoie une fonction qu’il n’est même 9 >>> f = lambda x : 2*x+1 ≪ fonction ano- pas nécessairelambda de nommer (d’oune ù l’appellation L’instruction renvoie fonction qu’il n’est même >>> f(4) ≫ nyme ) : 9 ≪ fonction pas nécessairelambda de nommer (d’oune ù l’appellation anoL’instruction renvoie fonction qu’il n’est même propriétés : peut procéder depart = property(lambda obj ainsi : obj._cherche('2')) depart, on en déclarant directement les Dans l’exemple précédent, inutile en fait de créer la méthode arrivee = property(lambda obj : obj._cherche('3')) propriétés : depart = property(lambda obj : obj._cherche('2')) depart, on peut procéder ainsi en déclarant directement les arrivee = property(lambda obj : obj._cherche('3')) depart = property(lambda obj : obj._cherche('2')) propriétés : arrivee = property(lambda obj : obj._cherche('3')) depart = property(lambda obj : obj._cherche('2')) arrivee = property(lambda obj : obj._cherche('3')) 2. On peut se demander quel est l’intérêt de créer les attributs dep et pourseles objets dequel la classe Gibsyde alors que 2. but On peut demander est l’intérêt créer lesl’information attributs depest et déjà accessible avec la classe niveau. Gardez en mémoire la remarque pourseles objets dequel la classe Gibsyde alors que 2. but On peut demander est l’intérêt créer lesl’information attributs depest et suivante, déjà accessible avec lade classe niveau. Gardez en que mémoire la remarque but pour les objets la classe Gibsy alors l’information est 2. On peut se demander quel est l’intérêt de créer les attributs dep et suivante, déjà accessible avec la classe niveau. Gardez en mémoire la remarque but pour les objets de la classe Gibsy alors que l’information est suivante, déjà accessible avec la classe niveau. Gardez en mémoire la remarque Attention à l’usage des propriétés : lorsque vous faites appel à suivante, une propriété, par exemple niv.debut, pensez s’agità Attention à l’usage des propriétés : lorsque vousqu’il faitesne appel pas d’un simple une donnée (numérique dans notre une propriété, paraccès exemple niv.debut, pensez ne s’agit Attention à l’usage des àpropriétés : lorsque vousqu’il faites appel à cas), mais bien à l’exécution d’une fonction. pas simple à une donnée (numérique dans notre une d’un propriété, paraccès exemple niv.debut, pensez qu’il ne s’agit Attention à l’usage des propriétés : lorsque vous faites appel à cas), mais simple bien à l’exécution d’une fonction. pas d’un accès à une donnée (numérique dans notre une propriété, par exemple niv.debut, pensez qu’il ne s’agit cas), mais bien à l’exécution d’une fonction. pas d’un simple accès à une donnée (numérique dans notre cas), mais l’exécution d’une Dans notre cas,bien si ààchaque frame vousfonction. cherchez à savoir si on est arrivé au but, retrouver la position du caractère ’3’sidans la Dans notre cas,il sifaut à chaque frame vous cherchez à savoir on est chaı̂ne plateau... ce qui est à priori inutile et co ûteux en temps. arrivénotre au but, retrouver la position du caractère ’3’sidans la Dans cas,il sifaut à chaque frame vous cherchez à savoir on est Autant stocker une fois pour toutes le résultat dans une variable chaı̂ne plateau... ce qui est à priori inutile et co ûteux en temps. arrivé au but, il faut retrouver la position du caractère ’3’ dans laà Dans notre siun à fois chaque frame vous cherchez à tête, savoir si on est laquelle on cas, auraune rapide. Une fois ceci en code Autant stocker pour toutes le inutile résultat uneleen variable à chaı̂ne plateau... ceaccès qui est à priori etdans coûteux temps. arrivé auon but, il faut retrouver la Une position ’3’code dansest la très simple :aura laquelle un fois accès rapide. fois du cecicaractère en tête, Autant stocker une pour toutes le résultat dans unelevariable à chaı̂ne plateau... ce qui est à priori inutile et coûteux en temps. très simple laquelle on :aura un accès rapide. Une fois ceci en tête, le code est Autant stocker une fois pour toutes le résultat dans une variable 317à très simple : laquelle on aura un accès rapide. Une fois ceci en tête, le code est 317 très simple : 317 317 Solutions des exercices ≫) : nyme >>> (lambda a,b de : 2nommer pas nécessaire (d’où l’appellation ≪ fonction ano*a-3*b)(4,5) L’instruction lambda renvoie une fonction qu’il n’est même -7 ≫) : nyme >>> (lambda a,b : 2*a-3*b)(4,5) pas nécessaire de nommer (d’où l’appellation ≪ fonction ano- -7 >>> (lambda a,b :précédent, 2*a-3*b)(4,5) ≫) : Dans l’exemple inutile en fait de créer la méthode nyme -7 depart, ona,b peut procéderinutile ainsi en directement les Dans l’exemple endéclarant fait de créer la méthode >>> (lambda :précédent, 2*a-3*b)(4,5) propriétés : -7 depart, on peut procéderinutile ainsi en directement les Dans l’exemple précédent, endéclarant fait de créer la méthode C HAPITRE I C HAPITRE I class IGibsy() C HAPITRE : def __init__(self, n) : self.dep = n.depart class Gibsy() : = n.arrivee self.but class Gibsy() : self.x, self.y = self.dep def self.image __init__(self, = ⤦ n) : def __init__(self, n) : self.dep = n.depart � pygame.image.load('images/gibsy1.png').convert_alpha() self.dep = self.but = n.depart n.arrivee self.toile = ⤦ self.but = n.arrivee self.x, self.y = self.dep � pygame.image.load('images/toile.png').convert_alpha() self.x, self.y self.image = ⤦ = self.dep self.image = ⤦ � pygame.image.load('images/gibsy1.png').convert_alpha() � pygame.image.load('images/gibsy1.png').convert_alpha() self.toile = ⤦ self.toile = ⤦ � pygame.image.load('images/toile.png').convert_alpha() � pygame.image.load('images/toile.png').convert_alpha() 3. Comme dans le chapitre EHPAD, il faut penser à regarder deux pixels pour s’assurer que le déplacement dans la direction demandée est valide et qu’on n’est pas ”à cheval” sur un mur. L’image de Gibsy 3. étant Comme le chapitre faut penser regarder deuxdepixels dedans dimension 32 ×EHPAD, 32 et (x,il y) étant lesààcoordonnées son 3. Comme dans le chapitre EHPAD, il faut penser regarder deux pixels pour s’assurer que le déplacement dans la direction demandée est centre, on a : pour s’assurer que le déplacement dans la direction demandée est valide et qu’on n’est pas ”à cheval” sur un mur. L’image de Gibsy valide et qu’on n’est pas ”à cheval” sur un mur. L’image de Gibsy étant de dimension 32 × 32 et (x, y) étant les coordonnées de son étantdefdeupdate(self) dimension 32 : × 32 et (x, y) étant les coordonnées de son centre, on a : touches centre, on a : = pygame.key.get_pressed() if touches[K_UP] and not niv.mur(self.x-16, self.y-17) ⤦ � and not niv.mur(self.x+15, self.y-17) : def update(self) self.y = : self.y - 1 def touches update(self) : = pygame.key.get_pressed() if touches[K_DOWN] and not niv.mur(self.x-16, self.y+16) ⤦ touches = pygame.key.get_pressed() if touches[K_UP] and not niv.mur(self.x-16, ⤦ � and not niv.mur(self.x+15, self.y+16) self.y-17) : if touches[K_UP] and not niv.mur(self.x-16, ⤦ � and not niv.mur(self.x+15, self.y-17) self.y-17) : self.y = self.y + 1 � and not niv.mur(self.x+15, self.y-17) : self.y = self.y - 1not niv.mur(self.x-17, if touches[K_LEFT] and self.y-16) ⤦ self.y = self.yand - 1not niv.mur(self.x-16, self.y+16) ⤦ if touches[K_DOWN] � and not niv.mur(self.x-17, self.y+15) : if touches[K_DOWN] and not niv.mur(self.x-16, � and not niv.mur(self.x+15, self.y+16) : self.y+16) ⤦ self.x = self.x - 1 � and not niv.mur(self.x+15, self.y+16) : self.y = self.y + 1 not niv.mur(self.x+16, if touches[K_RIGHT] and self.y-16) ⤦ self.y = self.yand + 1not niv.mur(self.x-17, self.y-16) ⤦ if touches[K_LEFT] � and not niv.mur(self.x+16, self.y+15) : if touches[K_LEFT] and not niv.mur(self.x-17, self.y-16) ⤦ � and not niv.mur(self.x-17, self.y+15) : self.x = self.x + 1 � and not niv.mur(self.x-17, self.y+15) : self.x = self.x 1 fenetre.blit(self.image, (self.x-16, self.y-16)) self.x = self.x and - 1 not niv.mur(self.x+16, self.y-16) ⤦ if touches[K_RIGHT] fenetre.blit(self.toile, (self.but[0]-16, self.but[1]-16)) if touches[K_RIGHT] and not niv.mur(self.x+16, � and not niv.mur(self.x+16, self.y+15) : self.y-16) ⤦ � and not niv.mur(self.x+16, self.y+15) : self.x = self.x + 1 self.x = self.x + 1 (self.x-16, self.y-16)) fenetre.blit(self.image, fenetre.blit(self.image, (self.but[0]-16, (self.x-16, self.y-16)) fenetre.blit(self.toile, self.but[1]-16)) fenetre.blit(self.toile, (self.but[0]-16, self.but[1]-16)) Retour sur l’exercice I12 On commence par modifier le constructeur en conséquence (vous noterez la manière astucieuse de récupérer toutes les directions de Gibsy en effectuant des rotations des images précédentes) : Retour sur l’exercice I12 On commence par modifier le constructeur en Retour sur l’exercice I12 On commence par modifier le constructeur en conséquence (vous noterez la manière astucieuse de récupérer toutes les conséquence (vous noterez la manière astucieuse de récupérer toutes les directions de Gibsy en effectuant des rotations des images précédentes) : directions de Gibsy en effectuant des rotations des images précédentes) : 318 318 318 O-B UT def __init__(self, n) : self.dep = n.depart self.but = n.arrivee self.x, self.y = self.dep self.toile = pygame.image.load('images/toile.png').convert_alpha() self._images = [ pygame.image.load('images/gibsy0.png').convert_alpha(), pygame.image.load('images/gibsy1.png').convert_alpha(), pygame.image.load('images/gibsy2.png').convert_alpha(), pygame.image.load('images/gibsy3.png').convert_alpha()] self._frame, self._direction = 0, 0 # Rotation des images for i in range(12) : self._images.append(pygame.transform.rotate(self._images[-4], -90)) puis on déclare la propriété : image = property(lambda o : o._images[o._frame+o._direction*4]) Il faut alors changer la méthode update en conséquence pour modifier les attributs frame et direction : def update(self) : touches = pygame.key.get_pressed() if touches[K_UP] : self._frame, self._direction = (self._frame+1)%4, 0 if not niv.mur(self.x-16, self.y-17) and not ⤦ � niv.mur(self.x+15, self.y-17) : self.y = self.y - 1 elif touches[K_DOWN] : self._frame, self._direction = (self._frame+1)%4, 2 if not niv.mur(self.x-16, self.y+16) and not ⤦ � niv.mur(self.x+15, self.y+16) : self.y = self.y + 1 elif touches[K_LEFT] : self._frame, self._direction = (self._frame+1)%4, 3 if not niv.mur(self.x-17, self.y-16) and not ⤦ � niv.mur(self.x-17, self.y+15) : self.x = self.x - 1 elif touches[K_RIGHT] : self._frame, self._direction = (self._frame+1)%4, 1 if not niv.mur(self.x+16, self.y-16) and not ⤦ � niv.mur(self.x+16, self.y+15) : self.x = self.x + 1 fenetre.blit(self.image, (self.x-16, self.y-16)) fenetre.blit(self.toile, (self.but[0]-16, self.but[1]-16)) 319 Solutions des exercices class Gibsy() : C HAPITRE I Retour sur l’exercice I13 Les deux premières questions sont assez simples dans la mesure où la fonction de collision a déjà été étudiée. 1. Pour la classe Boule : class Boule(Obstacle) : ... def touche(self, gib) : return collision(self.x, self.y, 16, gib.x, gib.y, 32) 2. Pour la classe Cochonnet : class Cochonnet(Obstacle) : ... def touche(self, gib) : return collision(self.x, self.y, 8, gib.x, gib.y, 32) 3. Reste à inclure ces tests dans la boucle principale (attention, si vous testez avec un niveau comportant des bouteilles, vous déclencherez une erreur car la classe de cet objet ne possède pas encore de méthode touche). continuer = True clock = pygame.time.Clock() while continuer: clock.tick(100) fenetre.blit(niv.fond,(0,0)) g.update() perdu = False for e in embuches : e.update() perdu = perdu or e.touche(g) if perdu : g.x, g.y = g.dep if (g.x-g.but[0])**2 + (g.y-g.but[1])**2 < 400 : continuer = False print('GAGNE') for event in pygame.event.get(): if event.type == QUIT : continuer = False pygame.display.flip() 320 O-B UT Retour sur l’exercice I14 Dans la méthode constructeur, on ajoute le calcul du masque de la bouteille et celui de Gibsy (j’ai opté pour la troisième méthode proposée dans l’aide) : def __init__(self, x, y, a) : image = pygame.image.load('images/bouteille.png').convert_alpha() image = pygame.transform.rotate(image, a) l, h = image.get_size() image = pygame.transform.smoothscale(image,(l//10, h//10)) Obstacle.__init__(self, x, y, image) self.masque = pygame.mask.from_surface(image) im = pygame.Surface((32,32)) self.masqueG = pygame.mask.from_surface(im, -1) voir(self.masque) Reste à coder la méthode touche : def touche(self, gib) : larg, long = self.image.get_size() Dx = (gib.x-16) - (self.x-larg//2) Dy = (gib.y-16) - (self.y-long//2) return self.masque.overlap_area(self.masqueG, (Dx, Dy)) > 0 Quelques explications sur le calcul de Dx et Dy sont peut-être nécessaires : les coordonnées sont celles des centres, or on a besoin de connaı̂tre le décalage par rapport au coin Nord-Ouest comme d’habitude : 32 y) haut (gib.x,gib.y) 32 x, Δ (Δ (self.x,self.y) larg 321 Solutions des exercices class Bouteille(Obstacle) : Glossaire Glossaire Les listes en Python Les listes en Python 27 len len(L) : renvoie le nombre d’éléments de la liste L. 27 27 in len 27 27 for in 27 27 append for 27 27 insert append e in L : teste si l’élément e est dans la liste L. len(L) : renvoie le nombre d’éléments de la liste L. for e in L : réalise une boucle dans laquelle la vaà tour les valeurs des éléments de la eriable in eLprend : teste tour si l’élément e est dans la liste L. liste L. for e in L : réalise une boucle dans laquelle la vaL.append(e) : ajoute l’élément e à la fin de la liste L. riable e prend tour à tour les valeurs des éléments de la liste L. L.insert(j ,e) : insère l’élément e au rang j de la liste L. L.append(e) : ajoute l’élément e à la fin de la liste L. 27 27 remove insert 27 27 pop remove 27 27 min, pop max min(L), : renvoie le plus petit (led’indice plus grand) L.pop(i)max(L) : supprime et renvoie l’élément i de élément de la liste L. la liste L. 27 27 min, sorted max sorted(L) : renvoie une nouvelle liste(lecontenant les plus grand) min(L), max(L) : renvoie le plus petit élémentsde ordonnés élément la liste L.de la liste L. 27 sorted sorted(L) : renvoie une nouvelle liste contenant les 323 éléments ordonnés de la liste L. L.remove(e) : supprime la première occurrence de L.insert(j ,e) : insère l’élément e au rang j de la liste l’élément e dans la liste L. L. L.pop(i) : supprime et renvoie l’élément d’indice i de L.remove(e) : supprime la première occurrence de la liste L. l’élément e dans la liste L. 323 Les chaı̂nes de caractères en Python Les chaı̂nes de caractères en Python 24 len len(ch) : renvoie la longueur de la chaı̂ne ch. 24 24 [i] len ch[i] : caractère d’indice i (accessible seulement en lecture). : renvoie la longueur de la chaı̂ne ch. len(ch) 24 [i ∶ j ] [i] ch[i ∶ j:]caractère : envoie une copiei de la chaı̂neseulement ch composée ch[i] d’indice (accessible en des caractères d’indice k ∈ i; j . lecture). 24 24 str [i ∶ j] 24 24 int str str(n) unecopie chaı̂ne contenant ch[i ∶ j ] : renvoie envoie une dede la caractères chaı̂ne ch composée l’écriture décimale du nombre n. des caractères d’indice k ∈ i; j . 24 24 for int 24 for 64 index 64 index upper lower 136 136 139 upper join lower 139 141 join in 141 in 133 find 133 find 324 int(ch): :renvoie transforme chaı̂ne en entier.contenant str(n) une la chaı̂ne dech caractères l’écriture décimale du nombre n. for c in ch : réalise une boucle dans laquelle la variable c va: transforme valoir tour àlatour chaque de ch. int(ch) chaı̂ne ch enlettre entier. txt.index(ch,deb) renvoie position la for c in ch : réalise :une boucle la dans laquellede la vapremière occurrence de la chaı̂ne ch dans la chaı̂ne riable c va valoir tour à tour chaque lettre de ch. txt à partir de la position deb si elle est précisée (ou txt.index(ch,deb) : renvoie la pas position deune la du début sinon). Si la chaı̂ne ch n’est présente première de la chaı̂ne ch dans la chaı̂ne exception occurrence est déclenchée. txt à partir de la position deb si elle est précisée (ou ch.upper() et ch.lower() renvoient respectivedu début sinon). Si la chaı̂ne ch: n’est pas présente une ment la chaı̂ne ch en majuscule et minuscule. exception est déclenchée. ch.join(L) et : renvoie une chaı̂ne de caractères où ch.upper() ch.lower() : renvoient respectivetous les éléments de majuscule la liste L ont été concaténés en ment la chaı̂ne ch en et minuscule. utilisant la chaı̂ne ch comme jointure. ch.join(L) : renvoie une chaı̂ne de caractères où ch1 in ch2 : donne indiquant si la chaı̂ne tous les éléments deun la booléen liste L ont été concaténés en ch1 est incluse dans chaı̂nejointure. ch2 . utilisant la chaı̂ne chlacomme txt.find(ch,deb) : renvoie la position de la ch 1 in ch2 : donne un booléen indiquant si la chaı̂ne première occurrence la chaı̂ne ch1 est incluse dans lade chaı̂ne ch2 . ch dans la chaı̂ne txt à partir de la position deb si elle est précisée (detxt.find(ch,deb) : la renvoie la n’est position de la puis le début sinon). Si chaı̂ne ch pas présente première occurrence de la chaı̂ne ch dans la chaı̂ne dans txt, la fonction renvoie -1. txt à partir de la position deb si elle est précisée (depuis le début sinon). Si la chaı̂ne ch n’est pas présente dans txt, la fonction renvoie -1. Les dictionnaires en Python 49 {} dict={} : crée un dictionnaire vide et le nomme dict. 49 [c] dict[cle] : élément associé à la clé cle du dictionnaire dict. 49 pop v=d.pop(cle) : supprime la clé cle après avoir renvoyé la valeur de l’élément associé. 49 copy d2=d1.copy() : crée une copie (indépendante) du dictionnaire d1 et la stocke dans le dictionnaire d2. 49 len len(dict) : renvoie le nombre d’enregistrements dans le dictionnaire dict. 49 in cle in dict : renvoie un booléen indiquant si cle est une clé du dictionnaire dict. 51 keys d.keys() : renvoie la liste des clés du dictionnaire d. 51 values d.values() : renvoie la liste des valeurs du dictionnaire d. 51 items d.items() : renvoie la liste des couples (clés, valeurs) de d. Le module math de Python √ x. 57 sqrt sqrt(x) : renvoie 57 cos, sin tan cos(x), sin(x) et tan(x) : représentent les fonctions trigonométriques cosinus, sinus et tangente. 57 degrees radians degrees(x), radians(x) : permet de convertir en degrés un angle en radians et inversement. 57 asin asin(x) : renvoie la valeur de l’arcsinus de x 70 copysign copysign(A,x) : renvoie la valeur (absolue) de A précédée du signe de x. En particulier copysign(1,x) : renvoie 1 si x ⩾ 0 et −1 sinon. 325 Le module re de Python (expressions régulières) fullmatch fullmatch(m,ch) : teste si la chaı̂ne ch est un représentant du motif m. 118 match match(m,ch) : comme la fonction précédente, mais teste si la chaı̂ne ch commence par un représentant du motif m. 118 search search(m,ch) : comme la fonction fullmatch, mais teste si la chaı̂ne ch contient un représentant du motif m. 134 findall findall(m, ch) : renvoie sous forme d’une liste l’ensemble de chaı̂nes correspondant au motif m dans le texte ch. 133 start sre.start() : renvoie la position de départ de la chaı̂ne trouvée. 133 end sre.end() : renvoie la position de la fin de la chaı̂ne trouvée. 133 string sre.string : renvoie la chaı̂ne dans laquelle s’est effectuée la recherche. group sre.group(0) : renvoie la chaı̂ne trouvée. sre.group(i) : renvoie la partie de la chaı̂ne trouvée correspondant au i ème groupe (à la i ème paire de parenthèses). 118 133 Le module random de Python 148 choice choice(x) : renvoie aléatoirement un élément de la liste x ou une lettre de la chaı̂ne x avec équiprobabilité. 239 randint randint(a,b) : renvoie un entier de l’intervalle [a,b]. 326 Le module os de Python 196 isfile isfile(f ich) : renvoie un booléen indiquant si le fichier f ich est présent. 200 system system(cmd) : exécute la ligne de commande cmd. Cette fonction se trouve dans le module os de Python. 238 getsize path.getsize(nf ) : renvoie, en octets, la taille du fichier portant le nom nf . PYGAME Le module pygame.display (fenêtre) 75 set mode set mode(s,opt) : renvoie une surface qui représentera la fenêtre présente à l’écran. 75 set caption set caption(txt) : affiche le texte txt dans la barre du haut de la fenêtre. 75 flip flip() : recopie la surface virtuelle à l’écran. 75 set icon set icon(s) : associe à la fenêtre l’icône définie par la surface s. Le module pygame.image 76 load load(f ich) : renvoie une surface représentant l’image contenue dans le fichier f ich. 76 save save(surf ,f ich) : sauvegarde la surface surf sous forme d’un fichier image nommé f ich. 327 Les objets Surface de Pygame Surface Surface(s,opt) : crée et renvoie une surface de dimension s donnée sous forme de couple. En option opt, on peut préciser SRCALPHA si on veut une image au format RGBA. fill surf .fill(coul) : remplit la surface surf de la couleur coul. Cette dernière peut être donnée par un triplet ou un quadruplet selon le format de la surface. set at surf .set at(p,coul) : colorie le pixel de coordonnées p = (x,y) de la couleur coul. Comme d’habitude le point de coordonnées (0,0) est le coin en haut à gauche de l’image. get at surf .get at(p) : renvoie la couleur du pixel de coordonnées p sous la forme d’un quadruplet (R, G, B, A). Si la surface n’a pas d’information de transparence, le canal alpha est à 255. 216 get size surf .get size() : retourne la dimension de l’image en pixels sous la forme d’un couple (largeur, hauteur). 216 get width surf .get width() : retourne la largeur de l’image. 216 get height surf .get height() : retourne la hauteur de l’image. 216 get bytesize surf .get bytesize() : retourne le nombre d’octets utilisés pour coder un pixel de l’image. Cela permet donc d’avoir des informations sur l’image. 216 subsurface surf .subsurface(r) : renvoie une copie de la surface surf délimitée par le rectangle r qui peut être donné par un quadruplet (x,y,l,h). 78 78 78 78 328 79 79 copy copy 79 79 blit blit 80 80 convert convert 80 80 convert alpha convert alpha surf .copy() : renvoie une copie de la surface surf ..copy() : renvoie une copie de la surface surf surf . surf .blit(s1,p,r) : copie la surface s1 sur la surf .blit(s copie la surface s1 sur la surface surf à1,p,r) partir: du point de coordonnées surface à partirrdu point de coordonnées p. Si unsurf quadruplet = (x 0 ,y0 ,l,h) est précisé, p. Si un quadruplet r = (x ,y ,l,h) estde précisé, 0 0 la surface s1 est limitée aux pixels coorla surface s est limitée aux pixels de 1 vérifiant : x0 ⩽ x < x0 + l etcoordonnées (x,y) y0 ⩽ données vérifiant : x0 ⩽ x l’intégralité < x0 + l et y0de ⩽ y < y0 + h.(x,y) Si r n’est pas précisé, yla<surface y0 + h. Si r n’est pas précisé, l’intégralité de s1 est recopiée. la surface s1 est recopiée. surf .convert() : convertit la surface surf surfformat .convert() : convertit la surface surf au de la fenêtre d’affichage. au format de la fenêtre d’affichage. surf .convert alpha() : convertit la surface surf .convert : convertit la surface surf au format alpha() de la fenêtre d’affichage en surf au format de la fenêtre d’affichage en conservant la transparence. conservant la transparence. Le module pygame.font Le module pygame.font 122 122 Font Font 122 122 get fonts get fonts 122 122 match font match font 122 122 render render 122 122 size size Font(f ich, s) : renvoie un objet représentant la Font(f ich, s) :f renvoie objets. représentant la police du fichier ich et deun taille police du fichier f ich et de taille s. get fonts() : retourne la liste des polices insget fonts() : retourne la liste des polices installées dans le système d’exploitation. tallées dans le système d’exploitation. match font(txt) : retourne, sous forme de match retourne, sous forme de chaı̂ne, font(txt) le chemin de: la police contenant le mot chaı̂ne, le chemin de la police contenant le mot txt cherché. txt cherché. f nt.render(txt,l,c) : retourne une surface fcontenant nt.render(txt,l,c) : retourne surface le texte txt écrit avec laune police f nt contenant le texte txt écrit avec la police f nt avec la couleur c donnée sous forme d’un triplet avec la couleur c donnée sous forme d’un triplet (R,V,B). Le paramètre l (appelé antialias) est un (R,V,B). paramètre (appelé booléen Le indiquant si lel texte doitantialias) être lissé. est un booléen indiquant si le texte doit être lissé. f nt.size(txt) : retourne un couple indiquant la flargeur nt.size(txt) : retourne un couple indiquant la et la hauteur en pixels du texte txt écrit largeur et la hauteur en pixels du texte txt écrit avec la police f nt. avec la police f nt. 329 Les objets Rect de Pygame Rect Rect(x,y,l,h) : renvoie un rectangle dont le coin supérieur gauche est situé en (x,y), de largeur l et de hauteur h. get rect img.get rect() : renvoie un rectangle dont le coin supérieur gauche se trouve en (0,0) et dont les dimensions sont celles de l’image img. x, y left, x, top, y, right, bottom, centerx, centery, width, w, height, h : sont des attributs entiers d’un objet Rect renseignant sa position. 159 topleft... topleft, topright, bottomright, bottomleft, midtop, midleft, midright, midbottom, center, size : sont des attributs d’un objet Rect indiquant la position du rectangle sous forme de couple. 159 copy R1.copy() : renvoie une copie du rectangle R1. 159 move R1.move(x,y) : renvoie un nouveau rectangle, obtenu comme image du rectangle R1 par la translation de vecteur v⃗(x,y). 159 collidepoint R1.collidepoint((x,y)) : renvoie un booléen indiquant si P(x,y) est à l’intérieur de R1. 159 colliderect R1.colliderect(R2) : renvoie un booléen indiquant s’il y a collision entre les rectangles R1 et R2. 158 158 158 330 159 collidelist 159 collidelist collidelistall 159 159 collidelist collidelistall 159 collidelistall de la première collision entre une liste R de Rects et R1. R1.collidelistall(R) : retourne la R1.collidelist(R) : retourne l’index liste tous lescollision indices des rectangles R de lade première entre une liste de R de qui entrent en collision avec le rectangle R1. Rects et R1. R1.collidelist(R) : retourne l’index R1.collidelistall(R) retourne la de la première collision entre:une liste R de liste Rectsde et tous R1. les indices des rectangles de R qui entrent en collision avec le rectangle R1. R1.collidelistall(R) : retourne la liste de tous les indices des rectangles de R qui entrent en collision avec le rectangle R1. Les modules pygame.event et pygame.key Les modules pygame.event et pygame.key 89 Les modules pygame.event et pygame.key QUIT : quand l’utilisateur tente de quitter QUIT le programme. 89 KEYUP 89 89 QUIT KEYDOWN KEYUP : une touche est relâchée. QUIT : quand l’utilisateur tente de quitter le programme. KEYDOWN : une touche est enfoncée. 89 89 125 KEYUP QUIT MOUSEBUTTONDOWN QUIT : quand l’utilisateur tente de quitter KEYUP : une touche est relâchée. Lorsqu’un bouton de la souris est enfoncé. le programme. 89 125 89 KEYDOWN MOUSEBUTTONUP KEYUP KEYDOWN : bouton une touche est enfoncée. Lorsqu’un KEYUP : une toucheest estrelâché. relâchée. 125 125 89 MOUSEBUTTONDOWN MOUSEMOTION KEYDOWN Lorsqu’un de déplacée. la souris est enfoncé. Lorsque la: bouton souris est KEYDOWN une touche est enfoncée. 125 125 MOUSEBUTTONUP MOUSEBUTTONDOWN 125 125 161 125 MOUSEMOTION MOUSEBUTTONUP get pressed MOUSEMOTION 161 get pressed 161 get pressed Lorsqu’un bouton est relâché. Lorsqu’un bouton de la souris est enfoncé. get pressed() : cette fonction du moLorsque souris est déplacée. dule keylade Pygame renvoie un tableau Lorsqu’un bouton est relâché. de 0 et de 1 indiquant une liste de l’état des touches enfoncées au moment de l’apLorsque la souris est déplacée. get pressed() : cette fonction ducodes mopel. Les indices correspondent aux dule key de Pygame renvoie un tableau des touches. de 0 et de 1 indiquant une liste de l’état gettouches pressed() : cette modes enfoncées au fonction moment du de l’apduleLes keyindices de Pygame renvoie un pel. correspondent auxtableau codes de 0 et de 1 indiquant une liste de l’état des touches. des touches enfoncées au moment de l’appel. Les indices correspondent aux codes des touches. 331 Quelques noms de touches usuelles dans Pygame Quelques noms de touches usuelles dans Pygame Quelques noms de touches usuelles dans Pygame 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 89 K K K K K K K K K K K K K K K K K K K K K K K K K K K K K K K K K K Quelques noms de touches usuelles dans Pygame BACKSPACE Retour arrière TAB Tabulation RETURN BACKSPACE Return Retour arrière PAUSE Pause TAB BACKSPACE Tabulation Retour arrière UP Flèche haut RETURN TAB Return Tabulation RIGHT Flèche PAUSE RETURN Pause Returndroite BACKSPACE Retour arrière Flèche gauche LEFT UP PAUSE Flèche haut Pause TAB Tabulation DOWN Flèche RIGHT UP droite Flèche bas haut RETURN Return Echap ESCAPE Flèche gauche LEFT RIGHT PAUSE Pause droite Espace SPACE DOWN Flèche bas gauche LEFT UP Flèche haut F1 F1 Echap bas ESCAPE DOWN RIGHT Flèche droite 0 0Espace SPACE Echap gauche ESCAPE Flèche LEFT a A F1 Espacebas SPACE F1 DOWN Flèche RSHIFT Shift 0 F1 0F1 droit Echap ESCAPE Control gauche LCTRL a 0 A Espace 0 SPACE RALT ALT droit RSHIFT a Shift droit A F1 F1 Control gauche LCTRL RSHIFT Shift droit 0 0 RALT Control gauche LCTRL ALT droit a A RALT ALT droit RSHIFT Shift droit Control gauche LCTRL RALT ALT droit Les objets Event de Pygame Les objets Event de Pygame Les objets Event de Pygame 89 key 89 125 89 key pos key 125 89 125 125 pos key button pos 125 125 125 button pos button 125 button 332 Les objets Event de Pygame evt.key : donne le code de la touche enfoncée. evt.pos :: donne indique sous de la la forme d’un couple (x,y) evt.key le code touche enfoncée. la position du pointeur de la souris au moment de evt.key : donne le code de la touche enfoncée. l’événement. evt.pos : indique sous la forme d’un couple (x,y) la position du pointeur de latouche souris aucouple moment de evt.pos :: donne indique sous de la la forme d’un (x,y) evt.key le code enfoncée. evt.button : indique le numéro du bouton en action. l’événement. la position du pointeur de la souris au moment de l’événement. evt.pos : indique sous la forme d’un couple (x,y) evt.button : indique le numéro du bouton en action. la position du pointeur de la souris au moment de evt.button : indique le numéro du bouton en action. l’événement. evt.button : indique le numéro du bouton en action. Le module pygame.transform Le module pygame.transform Le module pygame.transform 218 218 218 218 218 218 218 221 218 221 221 221 221 221 221 221 Le module pygame.transform flip(s,v,h) : renvoie l’image de la surface s flip obtenue par symétrie d’axe vertical (v) et/ou flip(s,v,h) horizontal (h).: renvoie l’image de la surface s flip obtenue par symétrie vertical et/ous flip(s,v,h) : renvoied’axe l’image de la (v) surface rotate(s,a) : renvoied’axe une surface horizontal (h).symétrie obtenue par vertical contenant (v) et/ou flip rotate l’image de (h). la surface s après rotation d’angle a horizontal flip(s,v,h) : renvoie l’image de la surface s rotate(s,a) : renvoie une surface contenant autour de son centre. flip obtenue par symétrie d’axe vertical (v) et/ou rotate l’image de la surface s après rotationcontenant d’angle a rotate(s,a) : renvoie une surface horizontal (h). : renvoie une surface corresscale(s,(l,h)) rotate autour centre. s après rotation d’angle a l’imagede deson la surface scale pondant l’image de la surface s zoomée (ou autour deàson centre. rotate(s,a) : renvoie uneune surface contenant scale(s,(l,h)) : renvoie rétrécie) aux dimensions l × h. surface corresrotate l’image de surface après rotation d’angle scale pondant à la l’image des la surface s zoomée (oua scale(s,(l,h)) : renvoie une surface corresautour de son centre. smoothscale(s,(l,h)) même mais avec rétrécie) dimensions × h. effet scale pondant aux à l’image de la :lsurface s zoomée (ou smoothscale une meilleure qualité de transformation (plus rétrécie) aux dimensions h. surface corresscale(s,(l,h)) : renvoiel ×une smoothscale(s,(l,h)) : même effet mais avec lente). scale pondant à l’image de la surface s zoomée (ou smoothscale une meilleure qualité de: même transformation smoothscale(s,(l,h)) effet mais(plus avec rétrécie) aux dimensions l × h. smoothscale lente). une meilleure qualité de transformation (plus lente). smoothscale(s,(l,h)) : même effet mais avec smoothscale une meilleure qualité de transformation (plus lente). Le module pygame.draw Le module pygame.draw Le module pygame.draw 222 line 222 222 222 222 222 222 222 222 222 222 222 222 line rect line rect ellipse rect line ellipse arc ellipse rect arc polygon arc ellipse 222 222 222 polygon arc polygon 222 polygon Le module pygame.draw line(s,c,deb,f in,e) : dessine une ligne. line(s,c,deb,f : dessine une ligne. rect(s,c,r,e) : in,e) dessine un rectangle. line(s,c,deb,f in,e) : dessine une ligne. rect(s,c,r,e) : dessine un rectangle. ellipse(s,c,r,e) : dessine une ellipse. rect(s,c,r,e) : dessine un rectangle. line(s,c,deb,f in,e) : dessine une ligne. ellipse(s,c,r,e) ellipse. arc(s,c,r,ad,af ,e): :dessine dessineune un arc d’ellipse. ellipse(s,c,r,e) : dessine une ellipse. rect(s,c,r,e) : dessine un rectangle. polygon(s, c,,e) pts, fill= arc(s,c,r,ad,af : dessine un0) arc: dessine d’ellipse.un gone. arc(s,c,r,ad,af ,e): :dessine dessineune un arc d’ellipse. ellipse(s,c,r,e) ellipse. polygon(s, c, pts, fill= 0) : dessine un gone. polygon(s, c,,e) pts, fill= arc(s,c,r,ad,af : dessine un0) arc: dessine d’ellipse.un gone. polygon(s, c, pts, fill= 0) : dessine un gone. polypolypolypoly- 333 306 306 306 306 306 306 306 306 306 306 306 306 334 Le module pygame.mask Le module pygame.mask from surface(surf ,s) : renvoie un objet type Mask généré à partir un de from surface from de surface(surf ,s) : renvoie la surface surfMask . objet de type généré à partir de from surface la surface surf . Mask(s) : renvoie un objet de type Mask Mask vide: de taille s un = (x,y). Mask(s) renvoie objet de type Mask Mask vide de taille s = (x,y). m.get bounding rects() : renvoie objetbounding Rect indiquant la : position get bounding rects un m.get rects() renvoie duRect masque. un objet indiquant la position get bounding rects ”utile” ”utile” du masque. m.get at((x,y)) : renvoie 1 si le pixel de at((x,y)) coordonnées: (x,y) est un get at m.get renvoie 1 point si le (noir) du masque binaire m et 0 sinon. pixel de coordonnées (x,y) est un point get at (noir) du masque binaire m et 0 sinon. m.set at((x,y),v) : affecte au pixel set at (x,y) duat((x,y),v) masque m un:valeur ∈ {0; 1}. m.set affectevau pixel set at (x,y) du masque m un valeur v ∈ {0; 1}. m1 .overlap area(m2, d) : renvoie le 1nombre de pixels en collision lorsque m .overlap area(m 2 , d) : renvoie masque de m1pixels chevauche le masque m2 overlap area le nombre en collision lorsque après avoir subi un décalage de d le masque m1 chevauche le masque m=2 overlap area (Δx,Δy). après avoir subi un décalage de d = (Δx,Δy). CRÉATIONS NUMÉRIQUES Ce livre, basé sur une progression originale de démarche de projets, vous propose d’approfondir vos connaissances en Python au travers de la réalisation de plusieurs jeux. Vous découvrirez l’interface Pygame et la plupart des modules qui la composent. Les projets vous permettront d’aborder de nouvelles notions, comme : • le parcours d’un graphe • les expressions régulières • la lecture en Python de pages web, de fichiers CSV ou JSON • la manipulation de bases de données et l’interfaçage avec Python • le paradigme de la programmation objet où l’héritage et les subtilités du principe d’encapsulation seront expliqués au fur et à mesure des chapitres. Comme pour les autres ouvrages de la collection « créations numériques », tous les exercices sont corrigés en ligne et l’intégralité des ressources graphiques nécessaires est téléchargeable. Illustrations de couverture : © David Beauget Que vous soyez lycéen en NSI, étudiant en CPGE, enseignant ou simple curieux, ce livre devrait aiguiser votre curiosité pour le Grand Oral, vos projets ou vos TIPE. Dans la même collection : -:HSMDOA=UYWVUU: