IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre à programmer avec Python

Apprendre programmer avec Python


prcdentsommairesuivant

Chapitre 15 : Analyse de programmes concrets

Dans ce chapitre, nous allons nous efforcer d'illustrer la dmarche de conception d'un programme graphique, depuis ses premires bauches jusqu' un stade de dveloppement relativement avanc. Nous souhaitons montrer ainsi combien la programmation oriente objet peut faciliter et surtout scuriser la stratgie de dveloppement incrmental que nous prconisons58.

L'utilisation de classes s'impose, lorsque l'on constate qu'un projet en cours de ralisation se rvle nettement plus complexe que ce que l'on avait imagin au dpart. Vous vivrez certainement vous-mme des cheminements similaires celui que nous dcrivons ci-dessous.

15.1. Jeu des bombardes

Ce projet de jeu59 s'inspire d'un travail similaire ralis par des lves de terminale.

Il est vivement recommand de commencer l'bauche d'un tel projet par une srie de petits dessins et de schmas, dans lesquels seront dcrits les diffrents lments graphiques construire, ainsi qu'un maximum de cas d'utilisations. Si vous rechignez utiliser pour cela la bonne vieille technologie papier/crayon (laquelle a pourtant bien fait ses preuves), vous pouvez tirer profit d'un logiciel de dessin technique, tel l'utilitaire Draw de la suite bureautique OpenOffice.org60. C'est l'outil qui a t utilis pour raliser le schma ci-dessous :

Image non disponible

L'ide de dpart est simple : deux joueurs s'affrontent au canon. Chacun doit ajuster son angle de tir pour tcher d'atteindre son adversaire, les obus dcrivant une trajectoire balistique.

L'emplacement des canons est dfini au dbut du jeu de manire alatoire (tout au moins en hauteur). Aprs chaque tir, les canons se dplacent (afin d'accrotre l'intrt du jeu, l'ajustement des tirs tant ainsi rendu plus difficile). Les coups au but sont comptabiliss.

Le dessin prliminaire que nous avons reproduit la page prcdente est l'une des formes que peut prendre votre travail d'analyse. Avant de commencer le dveloppement d'un projet de programmation, il vous faut en effet toujours vous efforcer d'tablir un cahier des charges dtaill. Cette tude pralable est trs importante. La plupart des dbutants commencent bien trop vite crire de nombreuses lignes de code au dpart d'une vague ide, en ngligeant de rechercher la structure d'ensemble. Leur programmation risque alors de devenir chaotique, parce qu'ils devront de toute faon mettre en place cette structure tt ou tard. Il s'apercevront alors bien souvent qu'il leur faut supprimer et r-crire des pans entiers d'un projet qu'ils ont conu d'une manire trop monolithique et/ou mal paramtre.

  • Trop monolithique : cela signifie que l'on a nglig de dcomposer un problme complexe en plusieurs sous-problmes plus simples. Par exemple, on a imbriqu plusieurs niveaux successifs d'instructions composes, au lieu de faire appel des fonctions ou des classes.
  • Mal paramtre : cela signifie que l'on a trait seulement un cas particulier, au lieu d'envisager le cas gnral. Par exemple, on a donn un objet graphique des dimensions fixes, au lieu de prvoir des variables pour permettre son redimensionnement.

Vous devez donc toujours commencer le dveloppement d'un projet par une phase d'analyse aussi fouille que possible, et concrtiser le rsultat de cette analyse dans un ensemble de documents (schmas, plans, descriptions...) qui constitueront le cahier des charges. Pour les projets de grande envergure, il existe d'ailleurs des mthodes d'analyse trs labores (UML, Merise...) que nous ne pouvons nous permettre de dcrire ici car elles font l'objet de livres entiers.

Cela tant dit, il faut malheureusement admettre qu'il est trs difficile (et mme probablement impossible) de raliser ds le dpart l'analyse tout fait complte d'un projet de programmation. C'est seulement lorsqu'il commence fonctionner vritablement qu'un programme rvle ses faiblesses. On constate alors qu'il reste des cas d'utilisation ou des contraintes qui n'avaient pas t prvues au dpart. D'autre part, un projet logiciel est pratiquement toujours destin voluer : il vous arrivera frquemment de devoir modifier le cahier des charges au cours du dveloppement luimme, pas ncessairement parce que l'analyse initiale a t mal faite, mais tout simplement parce que l'on souhaite encore ajouter des fonctionnalits supplmentaires.

En conclusion, tchez de toujours aborder un nouveau projet de programmation en respectant les deux consignes suivantes :

  • Dcrivez votre projet en profondeur avant de commencer la rdaction des premires lignes de code, en vous efforant de mettre en vidence les composants principaux et les relations qui les lient (pensez notamment dcrire les diffrents cas d'utilisation de votre programme)
  • Lorsque vous commencerez sa ralisation effective, vitez de vous laisser entraner rdiger de trop grands blocs d'instructions. Veillez au contraire dcouper votre application en un certain nombre de composants paramtrables bien encapsuls, de telle manire que vous puissiez aisment modifier l'un ou l'autre d'entre eux sans compromettre le fonctionnement des autres, et peut-tre mme les rutiliser dans diffrents contextes si le besoin s'en fait sentir.

    C'est pour satisfaire cette exigence que la programmation oriente objets est a t invente.

    Considrons par exemple l'bauche dessine la page prcdente.

L'apprenti programmeur sera peut-tre tent de commencer la ralisation de ce jeu en n'utilisant que la programmation procdurale seule (c'est--dire en omettant de dfinir de nouvelles classes). C'est d'ailleurs ainsi que nous avons procd nous-mme lors de notre premire approche des interfaces graphiques, tout au long du chapitre 8. Cette faon de procder ne se justifie cependant que pour de tout petits programmes (des exercices ou des tests prliminaires). Lorsque l'on s'attaque un projet d'une certaine importance, la complexit des problmes qui se prsentent se rvle rapidement trop importante, et il devient alors indispensable de fragmenter et de compartimenter.

L'outil logiciel qui va permettre cette fragmentation est la classe.

Nous pouvons peut-tre mieux comprendre son utilit en nous aidant d'une analogie :

Tous les appareils lectroniques sont constitus d'un petit nombre de composants de base, savoir des transistors, des diodes, des rsistances, des condensateurs, etc. Les premiers ordinateurs ont t construits directement partir de ces composants. Ils taient volumineux, trs chers, et pourtant ils n'avaient que trs peu de fonctionnalits et tombaient frquemment en panne.

On a alors dvelopp diffrentes techniques pour encapsuler dans un mme botier un certain nombre de composants lectroniques de base. Pour utiliser ces nouveaux circuits intgrs, il n'tait plus ncessaire de connatre leur contenu exact : seule importait leur fonction globale. Les premires fonctions intgres taient encore relativement simples : c'taient par exemple des portes logiques, des bascules, etc. En combinant ces circuits entre eux, on obtenait des caractristiques plus labores, telles que des registres ou des dcodeurs, qui purent leur tour tre intgrs, et ainsi de suite, jusqu'aux microprocesseurs actuels. Ceux-ci contiennent dornavant plusieurs millions de composants, et pourtant leur fiabilit reste extrmement leve.

En consquence, pour l'lectronicien moderne qui veut construire par exemple un compteur binaire (circuit qui ncessite un certain nombre de bascules), il est videmment bien plus simple, plus rapide et plus sr de se servir de bascules intgres, plutt que de s'chiner combiner sans erreur plusieurs centaines de transistors et de rsistances.

D'une manire analogue, le programmeur moderne que vous tes peut bnficier du travail accumul par ses prdcesseurs en utilisant la fonctionnalit intgre dans les nombreuses bibliothques de classes dj disponibles pour Python. Mieux encore, il peut aisment crer luimme de nouvelles classes pour encapsuler les principaux composants de son application, particulirement ceux qui y apparaissent en plusieurs exemplaires. Procder ainsi est plus simple, plus rapide et plus sr que de multiplier les blocs d'instructions similaires dans un corps de programme monolithique, de plus en plus volumineux et de moins en moins comprhensible.

Examinons par exemple notre bauche dessine. Les composants les plus importants de ce jeu sont bien videmment les petits canons, qu'il faudra pouvoir dessiner diffrents emplacements et dans diffrentes orientations, et dont il nous faudra au moins deux exemplaires.

Plutt que de les dessiner morceau par morceau dans le canevas au fur et mesure du droulement du jeu, nous avons intrt les considrer comme des objets logiciels part entire, dots de plusieurs proprits ainsi que d'un certain comportement (ce que nous voulons exprimer par l est le fait qu'il devront tre dots de divers mcanismes, que nous pourrons activer par programme l'aide de mthodes particulires). Il est donc certainement judicieux de leur consacrer une classe spcifique

58 Voir page 16 : Recherche des erreurs et exprimentation, et aussi page 216 : Fentres avec menus

59 Nous n'hsitons pas discuter ici le dveloppement d'un logiciel de jeu, parce qu'il s'agit d'un domaine directement accessible tous, et dans lequel les objectifs concrets sont aisment identifiables. Il va de soi que les mmes techniques de dveloppement peuvent s'appliquer d'autres applications plus "srieuses".

60 Il s'agit d'une suite bureautique complte, libre et gratuite, largement compatible avec MS-Office, disponible pour Linux, Windows, MacOS, Solaris ... Le prsent manuel a t entirement rdig avec son traitement de textes. Vous pouvez vous la procurer par tlchargement depuis le site Web : http://www.openoffice.org

15.1.1. Prototypage d'une classe Canon

En dfinissant une telle classe, nous gagnons sur plusieurs tableaux. Non seulement nous rassemblons ainsi tout le code correspondant au dessin et au fonctionnement du canon dans une mme capsule , bien l'cart du reste du programme, mais de surcrot nous nous donnons la possibilit d'instancier aisment un nombre quelconque de ces canons dans le jeu, ce qui nous ouvre des perspectives de dveloppements ultrieurs.

Lorsqu'une premire implmentation de la classe Canon() aura t construite et teste, il sera galement possible de la perfectionner en la dotant de caractristiques supplmentaires, sans modifier (ou trs peu) son interface, c'est--dire en quelque sorte son mode d'emploi : savoir les instructions ncessaires pour l'instancier et l'utiliser dans des applications diverses.

Entrons prsent dans le vif du sujet.

Le dessin de notre canon peut tre simplifi l'extrme. Nous avons estim qu'il pouvait se rsumer un cercle combin avec un rectangle, celui-ci pouvant d'ailleurs tre lui-mme considr comme un simple segment de ligne droite particulirement pais.

Si l'ensemble est rempli d'une couleur uniforme (en noir, par exemple), nous obtiendrons ainsi une sorte de petite bombarde suffisamment crdible.

Dans la suite du raisonnement, nous admettrons que la position du canon est en fait la position du centre du cercle (coordonnes x et y dans le dessin ci-contre). Ce point cl indique galement l'axe de rotation de la buse du canon, ainsi que l'une des extrmits de la ligne paisse qui reprsentera cette buse.

Pour terminer notre dessin, il nous restera alors dterminer les coordonnes de l'autre extrmit de cette ligne. Ces coordonnes peuvent tre calcules sans grande difficult, la condition de nous remmorer deux concepts fondamentaux de la trigonomtrie (le sinus et le cosinus) que vous devez certainement bien connatre :
Image non disponible
Dans un triangle rectangle, le rapport entre le cot oppos un angle et l'hypotnuse du triangle est une proprit spcifique de cet angle qu'on appelle sinus de l'angle. Le cosinus du mme angle est le rapport entre le ct adjacent l'angle et l'hypotnuse.

Ainsi, dans le schma ci-contre :Image non disponible et Image non disponible
Image non disponible

Pour reprsenter la buse de notre canon, en supposant que nous connaissions sa longueur l et l'angle de tir \x{03b1} , il nous faut donc tracer un segment de ligne droite paisse, partir des coordonnes du centre du cercle (x et y), jusqu' un autre point situ plus droite et plus haut, l'cart horizontal \x{0394}x tant gal l.cos \x{03b1} , et l'cart vertical \x{0394}y tant gal l.sin \x{03b1} .

En rsumant tout ce qui prcde, dessiner un canon au point x, y consistera simplement :

  • tracer un cercle noir centr sur x, y
  • tracer une ligne noire paisse depuis le point x, y jusqu'au point x + l.cos \x{03b1}, y + l.sin \x{03b1}.

Nous pouvons prsent commencer envisager une bauche de programmation correspondant une classe Canon . Il n'est pas encore question ici de programmer le jeu proprement dit. Nous voulons seulement vrifier si l'analyse que nous avons faite jusqu' prsent tient la route , en ralisant un premier prototype fonctionnel.

Un prototype est un petit programme destin exprimenter une ide, que l'on se propose d'intgrer ensuite dans une application plus vaste. Du fait de sa simplicit et de sa concision, Python se prte fort bien l'laboration de prototypes, et de nombreux programmeurs l'utilisent pour mettre au point divers composants logiciels qu'ils reprogrammeront ventuellement ensuite dans d'autres langages plus lourds , tels que le C par exemple.

Dans notre premier prototype, la classe Canon() ne comporte que deux mthodes : un constructeur qui cre les lments de base du dessin, et une mthode permettant de modifier celui-ci volont pour ajuster l'angle de tir (l'inclinaison de la buse). Comme nous l'avons souvent fait dans d'autres exemples, nous inclurons quelques lignes de code la fin du script afin de pouvoir tester la classe tout de suite :

 
Sélectionnez

1. from Tkinter import *
2. from math import pi, sin, cos
3.
4. class Canon:
5.     """Petit canon graphique"""
6.     def __init__(self, boss, x, y):
7.         self.boss = boss            # rfrence du canevas
8.         self.x1, self.y1 = x, y     # axe de rotation du canon
9.         # dessiner la buse du canon,  l'horizontale pour commencer :
10.        self.lbu = 50               # longueur de la buse
11.        self.x2, self.y2 = x + self.lbu, y
12.        self.buse = boss.create_line(self.x1, self.y1, self.x2, self.y2,
13.                                     width =10)
14.        # dessiner ensuite le corps du canon par-dessus :
15.        r = 15                      # rayon du cercle 
16.        boss.create_oval(x-r, y-r, x+r, y+r, fill='blue', width =3)
17.    
18.    def orienter(self, angle):
19.        "choisir l'angle de tir du canon"
20.        # rem : le paramtre <angle> est reu en tant que chane de car.
21.        # il faut le traduire en nombre rel, puis convertir en radians :
22.        self.angle = float(angle)*2*pi/360      
23.        self.x2 = self.x1 + self.lbu*cos(self.angle)
24.        self.y2 = self.y1 - self.lbu*sin(self.angle)
25.        self.boss.coords(self.buse, self.x1, self.y1, self.x2, self.y2)
26.        
27.if __name__ == '__main__':
28.    # Code pour tester sommairement la classe Canon : 
29.    f = Tk()
30.    can = Canvas(f,width =250, height =250, bg ='ivory')
31.    can.pack(padx =10, pady =10)
32.    c1 = Canon(can, 50, 200)
33.
34.    s1 =Scale(f, label='hausse', from_=90, to=0, command=c1.orienter)
35.    s1.pack(side=LEFT, pady =5, padx =20)
36.    s1.set(25)                          # angle de tir initial
37.
38.    f.mainloop()
 

Commentaires :

  • Ligne 6 : Dans la liste des paramtres qui devront tre transmis au constructeur lors de l'instanciation, nous prvoyons les coordonnes x et y, qui indiqueront l'emplacement du canon dans le canevas, mais galement une rfrence au canevas lui-mme (la variable boss). Cette rfrence est indispensable : elle sera utilise pour invoquer les mthodes du canevas.
    Nous pourrions inclure aussi un paramtre pour choisir un angle de tir initial, mais puisque nous avons l'intention d'implmenter une mthode spcifique pour rgler cette orientation, il sera plus judicieux de faire appel celle-ci au moment voulu.
  • Lignes 7 et 8 : Ces rfrences seront utilises un peu partout dans les diffrentes mthodes que nous allons dvelopper dans la classe. Il faut donc en faire des attributs d'instance.
  • Lignes 9 16 : Nous dessinons la buse d'abord, et le corps du canon ensuite. Ainsi une partie de la buse reste cache. Cela nous permet de colorer ventuellement le corps du canon.
  • Lignes 18 25 : Cette mthode sera invoque avec un argument angle , lequel sera fourni en degrs (compts partir de l'horizontale). S'il est produit l'aide d'un widget tel que Entry ou Scale, il sera transmis sous la forme d'une chane de caractres, et nous devrons donc le convertir d'abord en nombre rel avant de l'utiliser dans nos calculs (ceux-ci ont t dcrits la page prcdente).
  • Lignes 27 38 : Pour tester notre nouvelle classe, nous ferons usage d'un widget Scale. Pour dfinir la position initiale de son curseur, et donc fixer l'angle de hausse initial du canon, nous devons faire appel sa mthode set() (ligne 36).

15.1.2. Ajout de mthodes au prototype

Notre prototype est fonctionnel, mais beaucoup trop rudimentaire. Nous devons prsent le perfectionner pour lui ajouter la capacit de tirer des obus.

Ceux-ci seront traits plutt comme des boulets : ce seront de simples petits cercles que nous ferons partir de la bouche du canon avec une vitesse initiale d'orientation identique celle de sa buse. Pour leur faire suivre une trajectoire raliste, nous devons prsent nous replonger dans notre cours de physique :

Comment un objet laiss lui-mme volue-t-il dans l'espace, si l'on nglige les phnomnes secondaires tels que la rsistance de l'air ?

Ce problme peut vous paratre complexe, mais en ralit sa rsolution est trs simple : il vous suffit d'admettre que le boulet se dplace la fois horizontalement et verticalement, et que ces deux mouvements simultans sont tout fait indpendants l'un de l'autre.

Vous allez donc tablir une boucle d'animation dans laquelle vous recalculez les nouvelles coordonnes x et y du boulet intervalles de temps rguliers, en sachant que :
Image non disponible
  • Le mouvement horizontal est uniforme. chaque itration, il vous suffit d'augmenter graduellement la coordonne x du boulet, en lui ajoutant toujours un mme dplacement \x{0394}x.
  • Le mouvement vertical est uniformment acclr. Cela signifie simplement qu' chaque itration, vous devez ajouter la coordonne y un dplacement Dy qui augmente lui-mme graduellement, toujours de la mme quantit.

Voyons cela dans le script :

A) Pour commencer, il faut ajouter les lignes suivantes la fin de la mthode constructeur. Elles vont servir crer l'objet obus , et prparer une variable d'instance qui servira d'interrupteur de l'animation. L'obus est cr au dpart avec des dimensions minimales (un cercle d'un seul pixel) afin de rester presqu'invisible :

 
Sélectionnez

# dessiner un obus (rduit  un simple point, avant animation) :
self.obus =boss.create_oval(x, y, x, y, fill='red')
self.anim =False               # interrupteur d'animation
# retrouver la largeur et la hauteur du canevas :
self.xMax =int(boss.cget('width'))
self.yMax =int(boss.cget('height'))

Les deux dernires lignes utilisent la mthode cget() du widget matre (le canevas, ici), afin de retrouver certaines de ses caractristiques. Nous voulons en effet que notre classe Canon soit gnraliste, c'est--dire rutilisable dans n'importe quel contexte, et nous ne pouvons donc pas tabler l'avance sur des dimensions particulires pour le canevas dans lequel ce canon sera utilis. Note : Tkinter renvoie ces valeurs sous la forme de chanes de caractres. Il faut donc les convertir dans un type numrique si nous voulons pouvoir les utiliser dans un calcul.

B) Ensuite, nous devons ajouter deux nouvelles mthodes : l'une pour dclencher le tir, et l'autre pour grer l'animation du boulet une fois que celui-ci aura t lanc :

 
Sélectionnez

1.     def feu(self):
2.         "dclencher le tir d'un obus"
3.         if not self.anim:
4.             self.anim =True
5.             # position de dpart de l'obus (c'est la bouche du canon) :
6.             self.boss.coords(self.obus, self.x2 -3, self.y2 -3,
7.                                         self.x2 +3, self.y2 +3)
8.             v =15              # vitesse initiale
9.             # composantes verticale et horizontale de cette vitesse :
10.            self.vy = -v *sin(self.angle)
11.            self.vx = v *cos(self.angle)
12.            self.animer_obus()
13.    
14.    def animer_obus(self):
15.        "animation de l'obus (trajectoire balistique)"
16.        if self.anim:
17.            self.boss.move(self.obus, int(self.vx), int(self.vy))
18.            c = self.boss.coords(self.obus)     # coord. rsultantes
19.            xo, yo = c[0] +3, c[1] +3   # coord. du centre de l'obus
20.            if yo > self.yMax or xo > self.xMax:
21.                self.anim =False        # arrter l'animation
22.            self.vy += .5
23.            self.boss.after(30, self.animer_obus)

Commentaires :

  • Lignes 1 4 : Cette mthode sera invoque par appui sur un bouton. Elle dclenche le mouvement de l'obus, et attribue une valeur vraie notre interrupteur d'animation (la variable self.anim : voir ci-aprs). Il faut cependant nous assurer que pendant toute la dure de cette animation, un nouvel appui sur le bouton ne puisse pas activer d'autres boucles d'animation parasites. C'est le rle du test effectu la ligne 3 : le bloc d'instruction qui suit ne peut s'excuter que si la variable self.anim possde la valeur faux , ce qui signifie que l'animation n'a pas encore commenc.
  • Lignes 5 7 : Le canevas Tkinter dispose de deux mthodes pour dplacer les objets graphiques : La mthode coords() effectue un positionnement absolu ; il faut cependant lui fournir toutes les coordonnes de l'objet (comme si on le redessinait). La mthode move() , utilise la ligne 17, provoque quant elle un dplacement relatif ; elle s'utilise avec deux arguments seulement : les composantes horizontale et verticale du dplacement souhait.
  • Lignes 8 12 : La vitesse initiale de l'obus est choisie la ligne 8. Comme nous l'avons expliqu la page prcdente, le mouvement du boulet est la rsultante d'un mouvement horizontal et d'un mouvement vertical. Nous connaissons la valeur de la vitesse initiale ainsi que son inclinaison (c'est--dire l'angle de tir). Pour dterminer les composantes horizontale et verticale de cette vitesse, il nous suffit d'utiliser des relations trigonomtriques tout fait similaires que celles que nous avons dj exploites pour dessiner la buse du canon. Le signe - utilis la ligne 9 provient du fait que les coordonnes verticales se comptent de haut en bas.
    La ligne 12 active l'animation proprement dite.
  • Lignes 14 23 : Cette procdure se r-appelle elle-mme toutes les 30 millisecondes par l'intermdiaire de la mthode after() invoque la ligne 23. Cela continue aussi longtemps que la variable self.anim (notre interrupteur d'animation ) reste vraie , condition qui changera lorsque les coordonnes de l'obus sortiront des limites imposes (test de la ligne 20).
  • Lignes 18, 19 : Pour retrouver ces coordonnes aprs chaque dplacement, on fait appel encore une fois la mthode coords() du canevas : utilise avec la rfrence d'un objet graphique comme unique argument, elle renvoie ses quatre coordonnes dans un tuple.
  • Lignes 17 et 22 : La coordonne horizontale de l'obus augmente toujours de la mme quantit (mouvement uniforme), tandis que la coordonne verticale augmente d'une quantit qui est ellemme augmente chaque fois la ligne 24 (mouvement uniformment acclr). Le rsultat est une trajectoire parabolique.
    Rappel : l'oprateur += permet d'incrmenter une variable : a += 3 quivaut a = a + 3 .

C) Il reste enfin ajouter un bouton dclencheur dans la fentre principale. Une ligne telle que la suivante ( insrer dans le code de test) fera parfaitement l'affaire :

 
Sélectionnez

Button(f, text='Feu !', command =c1.feu).pack(side=LEFT)

15.1.3. Dveloppement de l'application

Disposant dsormais d'une classe d'objets canon assez bien dgrossie, nous pouvons prsent envisager l'laboration de l'application proprement dite. Et puisque nous sommes dcids exploiter la mthodologie de la programmation oriente objet, nous devons concevoir cette application comme un ensemble d'objets qui interagissent par l'intermdiaire de leurs mthodes.

Image non disponible

Plusieurs de ces objets proviendront de classes prexistantes, bien entendu : ainsi le canevas, les boutons, etc. Mais nous avons vu dans les pages prcdentes que nous avons intrt regrouper des ensembles bien dlimits de ces objets basiques dans de nouvelles classes, chaque fois que nous pouvons identifier pour ces ensembles une fonctionnalit particulire. C'tait le cas par exemple pour cet ensemble de cercles et de lignes mobiles que nous avons dcid d'appeler canon .

Pouvons-nous encore distinguer dans notre projet initial d'autres composants qui mriteraient d'tre encapsuls dans des nouvelles classes ? Certainement. Il y a par exemple le pupitre de contrle que nous voulons associer chaque canon : nous pouvons y rassembler le dispositif de rglage de la hausse (l'angle de tir), le bouton de mise feu, le score ralis, et peut-tre d'autres indications encore, comme le nom du joueur, par exemple. Il est d'autant plus intressant de lui consacrer une classe particulire, que nous savons d'emble qu'il nous en faudra deux instances.

Il y a aussi l'application elle-mme, bien sr. En l'encapsulant dans une classe, nous en ferons notre objet principal, celui qui dirigera tous les autres.

Veuillez prsent analyser le script ci-dessous. Vous y retrouverez la classe Canon() encore davantage dveloppe : nous y avons ajout quelques attributs et trois mthodes supplmentaires, afin de pouvoir grer les dplacements du canon lui-mme, ainsi que les coups au but.


La classe Application() remplace dsormais le code de test des prototypes prcdents. Nous y instancions deux objets Canon(), et deux objets de la nouvelle classe Pupitre(), que nous plaons dans des dictionnaires en prvision de dveloppements ultrieurs (nous pouvons en effet imaginer d'augmenter le nombre de canons et donc de pupitres). Le jeu est prsent fonctionnel : les canons se dplacent aprs chaque tir, et les coups au but sont comptabiliss.

 
Sélectionnez

1.  from Tkinter import *
2.  from math import sin, cos, pi
3.  from random import randrange
4.
5.  class Canon:
6.      """Petit canon graphique"""
7.      def __init__(self, boss, id, x, y, sens, coul):
8.          self.boss = boss            # rf. du canevas
9.          self.appli = boss.master    # rf. de la fentre d'application 
10.         self.id = id                # identifiant du canon (chane)
11.         self.coul = coul            # couleur associe au canon
12.         self.x1, self.y1 = x, y     # axe de rotation du canon
13.         self.sens = sens            # sens de tir (-1:gauche, +1:droite)
14.         self.lbu = 30               # longueur de la buse
15.         self.angle = 0              # hausse par dfaut (angle de tir)
16.         # retrouver la largeur et la hauteur du canevas :
17.         self.xMax = int(boss.cget('width'))
18.         self.yMax = int(boss.cget('height'))
19.         # dessiner la buse du canon (horizontale) :
20.         self.x2, self.y2 = x + self.lbu * sens, y
21.         self.buse = boss.create_line(self.x1, self.y1,
22.                                      self.x2, self.y2, width =10)
23.         # dessiner le corps du canon (cercle de couleur) :
24.         self.rc = 15                # rayon du cercle 
25.         self.corps = boss.create_oval(x -self.rc, y -self.rc, x +self.rc,
26.                                       y +self.rc, fill =coul)
27.         # pr-dessiner un obus cach (point en dehors du canevas) :
28.         self.obus = boss.create_oval(-10, -10, -10, -10, fill='red')
29.         self.anim = False           # indicateurs d'animation 
30.         self.explo = False          #    et d'explosion
31.
32.     def orienter(self, angle):
33.         "rgler la hausse du canon"
34.         # rem: le paramtre <angle> est reu en tant que chane.
35.         # Il faut donc le traduire en rel, puis le convertir en radians :
36.         self.angle = float(angle)*pi/180
37.         # rem: utiliser la mthode coords de prfrence avec des entiers :       
38.         self.x2 = int(self.x1 + self.lbu * cos(self.angle) * self.sens)
39.         self.y2 = int(self.y1 - self.lbu * sin(self.angle))
40.         self.boss.coords(self.buse, self.x1, self.y1, self.x2, self.y2)
41. 
42.     def deplacer(self, x, y):
43.         "amener le canon dans une nouvelle position x, y"
44.         dx, dy = x -self.x1, y -self.y1     # valeur du dplacement
45.         self.boss.move(self.buse, dx, dy) 
46.         self.boss.move(self.corps, dx, dy)
47.         self.x1 += dx
48.         self.y1 += dy
49.         self.x2 += dx
50.         self.y2 += dy
51.
52.     def feu(self):
53.         "tir d'un obus - seulement si le prcdent a fini son vol"
54.         if not (self.anim or self.explo):
55.             self.anim =True
56.             # rcuprer la description de tous les canons prsents :
57.             self.guns = self.appli.dictionnaireCanons()
58.             # position de dpart de l'obus (c'est la bouche du canon) :
59.             self.boss.coords(self.obus, self.x2 -3, self.y2 -3,
60.                                         self.x2 +3, self.y2 +3)
61.             v = 17              # vitesse initiale
62.             # composantes verticale et horizontale de cette vitesse :
63.             self.vy = -v *sin(self.angle)
64.             self.vx = v *cos(self.angle) *self.sens
65.             self.animer_obus()
66.             return True     # => signaler que le coup est parti
67.         else:
68.             return False    # => le coup n'a pas pu tre tir
69.
70.     def animer_obus(self):
71.         "animer l'obus (trajectoire balistique)"
72.         if self.anim:
73.             self.boss.move(self.obus, int(self.vx), int(self.vy))
74.             c = self.boss.coords(self.obus)     # coord. rsultantes
75.             xo, yo = c[0] +3, c[1] +3      # coord. du centre de l'obus
76.             self.test_obstacle(xo, yo)     # a-t-on atteint un obstacle ?
77.             self.vy += .4                  # acclration verticale
78.             self.boss.after(20, self.animer_obus)
79.         else:
80.             # animation termine - cacher l'obus et dplacer les canons :
81.             self.fin_animation()
82.   
83.     def test_obstacle(self, xo, yo):
84.         "valuer si l'obus a atteint une cible ou les limites du jeu"
85.         if yo >self.yMax or xo <0 or xo >self.xMax:
86.             self.anim =False
87.             return
88.         # analyser le dictionnaire des canons pour voir si les coord.
89.         # de l'un d'entre eux sont proches de celles de l'obus :
90.         for id in self.guns:              # id = clef dans dictionn.
91.             gun = self.guns[id]           # valeur correspondante
92.             if xo < gun.x1 +self.rc and xo > gun.x1 -self.rc \
93.             and yo < gun.y1 +self.rc and yo > gun.y1 -self.rc :
94.                 self.anim =False
95.                 # dessiner l'explosion de l'obus (cercle jaune) :
96.                 self.explo = self.boss.create_oval(xo -12, yo -12,
97.                              xo +12, yo +12, fill ='yellow', width =0)
98.                 self.hit =id       # rfrence de la cible touche
99.                 self.boss.after(150, self.fin_explosion)
100.                break         
101.   
102.    def fin_explosion(self):
103.        "effacer l'explosion ; r-initaliser l'obus ; grer le score"
104.        self.boss.delete(self.explo)    # effacer l'explosion
105.        self.explo =False               # autoriser un nouveau tir
106.        # signaler le succs  la fentre matresse :
107.        self.appli.goal(self.id, self.hit)
108.        
109.    def fin_animation(self):
110.        "actions  accomplir lorsque l'obus a termin sa trajectoire"
111.        self.appli.disperser()          # dplacer les canons
112.        # cacher l'obus (en l'expdiant hors du canevas) :
113.        self.boss.coords(self.obus, -10, -10, -10, -10)
114.
115.
116.class Pupitre(Frame):
117.    """Pupitre de pointage associ  un canon""" 
118.    def __init__(self, boss, canon):
119.        Frame.__init__(self, bd =3, relief =GROOVE)
120.        self.score =0
121.        self.appli =boss                # rf. de l'application
122.        self.canon =canon               # rf. du canon associ
123.        # Systme de rglage de l'angle de tir :
124.        self.regl =Scale(self, from_ =75, to =-15, troughcolor=canon.coul,
125.                         command =self.orienter)
126.        self.regl.set(45)               # angle initial de tir
127.        self.regl.pack(side =LEFT)
128.        # tiquette d'identification du canon :
129.        Label(self, text =canon.id).pack(side =TOP, anchor =W, pady =5)
130.        # Bouton de tir :
131.        self.bTir =Button(self, text ='Feu !', command =self.tirer)
132.        self.bTir.pack(side =BOTTOM, padx =5, pady =5)
133.        Label(self, text ="points").pack()
134.        self.points =Label(self, text=' 0 ', bg ='white')
135.        self.points.pack()
136.        # positionner  gauche ou  droite suivant le sens du canon :
137.        if canon.sens == -1:
138.            self.pack(padx =5, pady =5, side =RIGHT)
139.        else:
140.            self.pack(padx =5, pady =5, side =LEFT)
141.
142.    def tirer(self):
143.        "dclencher le tir du canon associ"
144.        self.canon.feu()
145.        
146.    def orienter(self, angle):
147.        "ajuster la hausse du canon associ"
148.        self.canon.orienter(angle)
149.
150.    def attribuerPoint(self, p):
151.        "incrmenter ou dcrmenter le score, de <p> points"
152.        self.score += p
153.        self.points.config(text = ' %s ' % self.score)
154.
155.class Application(Frame):
156.    '''Fentre principale de l'application'''
157.    def __init__(self):
158.        Frame.__init__(self)
159.        self.master.title('>>>>> Boum ! Boum ! <<<<<')
160.        self.pack()
161.        self.jeu = Canvas(self, width =400, height =250, bg ='ivory',
162.                          bd =3, relief =SUNKEN)
163.        self.jeu.pack(padx =8, pady =8, side =TOP)
164.
165.        self.guns ={}           # dictionnaire des canons prsents
166.        self.pupi ={}           # dictionnaire des pupitres prsents
167.        # Instanciation de 2 'objets canons (+1, -1 = sens opposs) :
168.        self.guns["Billy"] = Canon(self.jeu, "Billy", 30, 200, 1, "red")
169.        self.guns["Linus"] = Canon(self.jeu, "Linus", 370,200,-1, "blue")
170.        # Instanciation de 2 pupitres de pointage associs  ces canons :
171.        self.pupi["Billy"] = Pupitre(self, self.guns["Billy"])
172.        self.pupi["Linus"] = Pupitre(self, self.guns["Linus"])
173.
174.    def disperser(self):
175.        "dplacer alatoirement les canons"
176.        for id in self.guns:
177.            gun =self.guns[id]
178.            # positionner  gauche ou  droite, suivant sens du canon :
179.            if gun.sens == -1 :
180.                x = randrange(320,380)
181.            else:
182.                x = randrange(20,80)
183.            # dplacement proprement dit :
184.            gun.deplacer(x, randrange(150,240))
185.  
186.    def goal(self, i, j):
187.        "le canon <i> signale qu'il a atteint l'adversaire <j>"
188.        if i != j:
189.            self.pupi[i].attribuerPoint(1)    
190.        else:
191.            self.pupi[i].attribuerPoint(-1)
192.            
193.    def dictionnaireCanons(self):
194.        "renvoyer le dictionnaire dcrivant les canons prsents" 
195.        return self.guns
196.
197.if __name__ =='__main__':
198.    Application().mainloop()

Commentaires :

  • Ligne 7 : Par rapport au prototype, trois paramtres ont t ajouts la mthode constructeur. Le paramtre id nous permet d'identifier chaque instance de la classe Canon() l'aide d'un nom quelconque. Le paramtre sens indique s'il s'agit d'un canon qui tire vers la droite (sens = 1) ou vers la gauche (sens = -1). Le paramtre coul spcifie la couleur associe au canon.
  • Ligne 9 : Il faut savoir que tous les widgets Tkinter possdent un attribut master qui contient la rfrence leur widget matre ventuel (leur contenant ). Cette rfrence est donc pour nous celle de l'application principale. (Nous avons implment nous-mmes une technique similaire pour rfrencer le canevas, l'aide de l'attribut boss).
  • Lignes 42 50 : Cette mthode permet d'amener le canon dans un nouvel emplacement. Elle servira repositionner les canons au hasard aprs chaque tir, ce qui augmente l'intrt du jeu.
    Note : l'oprateur += permet d'incrmenter une variable : a += 3 quivaut a = a + 3 .
  • Lignes 56, 57 : Nous essayons de construire notre classe canon de telle manire qu'elle puisse tre rutilise dans des projets plus vastes, impliquant un nombre quelconque d'objets canons qui pourront apparatre et disparatre au fil des combats. Dans cette perspective, il faut que nous puissions disposer d'une description de tous les canons prsents, avant chaque tir, de manire pouvoir dterminer si une cible a t touche ou non. Cette description est gre par l'application principale, dans un dictionnaire, dont on peut lui demander une copie par l'intermdiaire de sa mthode dictionnaireCanons().
  • Lignes 66 68 : Dans cette mme perspective gnraliste, il peut tre utile d'informer ventuellement le programme appelant que le coup a effectivement t tir ou non.
  • Ligne 76 : L'animation de l'obus est dsormais traite par deux mthodes complmentaires. Afin de clarifier le code, nous avons plac dans une mthode distincte les instructions servant dterminer si une cible a t atteinte (mthode test_obstacle()).
  • Lignes 79 81 : Nous avons vu prcdemment que l'on interrompt l'animation de l'obus en attribuant une valeur fausse la variable self.anim. La mthode animer_obus() cesse alors de boucler et excute le code de la ligne 81.
  • Lignes 83 100 : Cette mthode value si les coordonnes actuelles de l'obus sortent des limites de la fentre, ou encore si elles s'approchent de celles d'un autre canon. Dans les deux cas, l'interrupteur d'animation est actionn, mais dans le second, on dessine une explosion jaune, et la rfrence du canon touch est mmorise. La mthode annexe fin_explosion() est invoque aprs un court laps de temps pour terminer le travail, c'est--dire effacer le cercle d'explosion et envoyer un message signalant le coup au but la fentre matresse.
  • Lignes 115 153 : La classe Pupitre() dfinit un nouveau widget par drivation de la classe Frame(), selon une technique qui doit dsormais vous tre devenue familire. Ce nouveau widget regroupe les commandes de hausse et de tir, ainsi que l'afficheur de points associs un canon bien dtermin. La correspondance visuelle entre les deux est assure par l'adoption d'une couleur commune. Les mthodes tirer() et orienter() communiquent avec l'objet Canon() associ, par l'intermdiaire des mthodes de celui-ci.
  • Lignes 155 172 : La fentre d'application est elle aussi un widget driv de Frame(). Son constructeur instancie les deux canons et leurs pupitres de pointage, en plaant ces objets dans les deux dictionnaires self.guns et self.pupi. Cela permet d'effectuer ensuite divers traitements systmatiques sur chacun d'eux (comme par exemple la mthode suivante). En procdant ainsi, on se rserve en outre la possibilit d'augmenter sans effort le nombre de ces canons si ncessaire, dans les dveloppements ultrieurs du programme.
  • Lignes 174 184 : Cette mthode est invoque aprs chaque tir pour dplacer alatoirement les deux canons, ce qui augmente la difficult du jeu.

15.1.4. Dveloppements complmentaires

Tel qu'il vient d'tre dcrit, notre programme correspond dj plus ou moins au cahier des charges initial, mais il est vident que nous pouvons continuer le perfectionner.

A) Nous devrions par exemple mieux le paramtrer. Qu'est-ce dire ? Dans sa forme actuelle, notre jeu comporte un canevas de taille prdtermine (400 x 250 pixels, voir ligne 161). Si nous voulons modifier ces valeurs, nous devons veiller modifier aussi les autres lignes du script o ces dimensions interviennent (comme par exemple aux lignes 168-169, ou 179-184). De telles lignes interdpendantes risquent de devenir nombreuses si nous ajoutons encore d'autres fonctionnalits. Il serait donc plus judicieux de dimensionner le canevas l'aide de variables, dont la valeur serait dfinie en un seul endroit. Ces variables seraient ensuite exploites dans toutes les lignes d'instructions o les dimensions du canevas interviennent.

Nous avons dj effectu une partie de ce travail : dans la classe Canon(), en effet, les dimensions du canevas sont rcupres l'aide d'une mthode prdfinie (voir lignes 17-18), et places dans des attributs d'instance qui peuvent tre utiliss partout dans la classe. B) Aprs chaque tir, nous provoquons un dplacement alatoire des canons, en redfinissant leurs coordonnes au hasard. Il serait probablement plus raliste de provoquer de vritables dplacements relatifs, plutt que de redfinir au hasard des positions absolues. Pour ce faire, il suffit de retravailler la mthode deplacer() de la classe Canon(). En fait, il serait encore plus intressant de faire en sorte que cette mthode puisse produire volont, aussi bien un dplacement relatif qu'un positionnement absolu, en fonction d'une valeur transmise en argument.

C) Le systme de commande des tirs devrait tre amlior : puisque nous ne disposons que d'une seule souris, il faut demander aux joueurs de tirer tour de rle, et nous n'avons mis en place aucun mcanisme pour les forcer le faire. Une meilleure approche consisterait prvoir des commandes de hausse et de tir utilisant certaines touches du clavier, qui soient distinctes pour les deux joueurs.

Image non disponible

D) Mais le dveloppement le plus intressant pour notre programme serait certainement d'en faire une application rseau. Le jeu serait alors install sur plusieurs machines communicantes, chaque joueur ayant le contrle d'un seul canon. Il serait d'ailleurs encore plus attrayant de permettre la mise en oeuvre de plus de deux canons, de manire autoriser des combats impliquant davantage de joueurs.

Ce type de dveloppement suppose cependant que nous ayons appris matriser au pralable deux domaines de programmation qui dbordent un peu le cadre de ce cours :

  • la technique des sockets, qui permet d'tablir une communication entre deux ordinateurs ;
  • la technique des threads, qui permet un mme programme d'effectuer plusieurs tches simultanment (cela nous sera ncessaire, si nous voulons construire une application capable de communiquer en mme temps avec plusieurs partenaires).

Ces matires ne font pas strictement partie des objectifs que nous nous sommes fixs pour ce cours, et leur leur traitement ncessite lui seul un chapitre entier. Nous n'aborderons donc pas cette question ici. Que ceux que le sujet intresse se rassurent cependant : ce chapitre existe, mais sous la forme d'un complment la fin du livre (chapitre 18) : vous y trouverez la version rseau de notre jeu de bombardes.

En attendant, voyons tout de mme comment nous pouvons encore progresser, en apportant notre projet quelques amliorations qui en feront un jeu pour 4 joueurs. Nous nous efforcerons aussi de mettre en place une programmation bien compartimente, de manire ce que les mthodes de nos classes soient rutilisables dans une large mesure. Nous allons voir au passage comment cette volution peut se faire sans modifier le code existant, en utilisant l'hritage pour produire de nouvelles classes partir de celles qui sont dj crites.

Commenons par sauvegarder notre ouvrage prcdent dans un fichier, dont nous admettrons pour la suite de ce texte que le nom est : canon03.py.

Nous disposons ainsi d'un module Python, que nous pouvons importer dans un nouveau script l'aide d'une seule ligne d'instruction. En exploitant cette technique, nous continuons perfectionner notre application, en ne conservant sous les yeux que les nouveauts :

 
Sélectionnez

1.  from Tkinter import *
2.  from math import sin, cos, pi
3.  from random import randrange
4.  import canon03
5.
6.  class Canon(canon03.Canon):
7.      """Canon amlior"""
8.      def __init__(self, boss, id, x, y, sens, coul):
9.          canon03.Canon.__init__(self, boss, id, x, y, sens, coul)
10.  
11.     def deplacer(self, x, y, rel =False):
12.         "dplacement, relatif si <rel> est vrai, absolu si <rel> est faux"
13.         if rel:
14.             dx, dy = x, y
15.         else:
16.             dx, dy = x -self.x1, y -self.y1
17.         # limites horizontales :
18.         if self.sens ==1:
19.             xa, xb = 20, int(self.xMax *.33)
20.         else:
21.             xa, xb = int(self.xMax *.66), self.xMax -20
22.         # ne dplacer que dans ces limites :
23.         if self.x1 +dx < xa:
24.             dx = xa -self.x1
25.         elif self.x1 +dx > xb:
26.             dx = xb -self.x1
27.         # limites verticales :
28.         ya, yb = int(self.yMax *.4), self.yMax -20
29.         # ne dplacer que dans ces limites :
30.         if self.y1 +dy < ya:
31.             dy = ya -self.y1
32.         elif self.y1 +dy > yb:
33.             dy = yb -self.y1
34.         # dplacement de la buse et du corps du canon :     
35.         self.boss.move(self.buse, dx, dy) 
36.         self.boss.move(self.corps, dx, dy) 
37.         # renvoyer les nouvelles coord. au programme appelant :
38.         self.x1 += dx
39.         self.y1 += dy
40.         self.x2 += dx
41.         self.y2 += dy
42.         return self.x1, self.y1  
43.
44.     def fin_animation(self):
45.         "actions  accomplir lorsque l'obus a termin sa trajectoire"
46.         # dplacer le canon qui vient de tirer :
47.         self.appli.depl_aleat_canon(self.id)
48.         # cacher l'obus (en l'expdiant hors du canevas) :
49.         self.boss.coords(self.obus, -10, -10, -10, -10)
50.
51.     def effacer(self):
52.         "faire disparatre le canon du canevas"
53.         self.boss.delete(self.buse)
54.         self.boss.delete(self.corps)
55.         self.boss.delete(self.obus)        
56.
57. class AppBombardes(Frame):
58.     '''Fentre principale de l'application'''
59.     def __init__(self, larg_c, haut_c):
60.         Frame.__init__(self)
61.         self.pack()
62.         self.xm, self.ym = larg_c, haut_c
63.         self.jeu = Canvas(self, width =self.xm, height =self.ym,
64.                           bg ='ivory', bd =3, relief =SUNKEN)
65.         self.jeu.pack(padx =4, pady =4, side =TOP)
66.
67.         self.guns ={}           # dictionnaire des canons prsents
68.         self.pupi ={}           # dictionnaire des pupitres prsents
69.         self.specificites()     # objets diffrents dans classes drives
70. 
71.     def specificites(self):
72.         "instanciation des canons et des pupitres de pointage"
73.         self.master.title('<<< Jeu des bombardes >>>')
74.         id_list =[("Paul","red"),("Romo","cyan"),
75.                   ("Virginie","orange"),("Juliette","blue")]
76.         s = False
77.         for id, coul in id_list:
78.             if s:
79.                 sens =1
80.             else:
81.                 sens =-1
82.             x, y = self.coord_aleat(sens)
83.             self.guns[id] = Canon(self.jeu, id, x, y, sens, coul)
84.             self.pupi[id] = canon03.Pupitre(self, self.guns[id])
85.             s = not s           # changer de ct  chaque itration
86.
87.     def depl_aleat_canon(self, id):
88.         "dplacer alatoirement le canon <id>"
89.         gun =self.guns[id]
90.         dx, dy = randrange(-60, 61), randrange(-60, 61)
91.         # dplacement (avec rcupration des nouvelles coordonnes) :
92.         x, y = gun.deplacer(dx, dy, True)
93.         return x, y
94.
95.     def coord_aleat(self, s):
96.         "coordonnes alatoires,  gauche (s =1) ou  droite (s =-1)" 
97.         y =randrange(int(self.ym /2), self.ym -20)
98.         if s == -1:
99.             x =randrange(int(self.xm *.7), self.xm -20)
100.        else:
101.            x =randrange(20, int(self.xm *.3))
102.        return x, y
103.  
104.    def goal(self, i, j):
105.        "le canon ni signale qu'il a atteint l'adversaire nj"
106.        # de quel camp font-ils partie chacun ?
107.        ti, tj = self.guns[i].sens, self.guns[j].sens        
108.        if ti != tj :               # ils sont de sens opposs :
109.            p = 1                   # on gagne 1 point
110.        else:                       # ils sont dans le mme sens :
111.            p = -2                  # on a touch un alli !!
112.        self.pupi[i].attribuerPoint(p)
113.        # celui qui est touch perd de toute faon un point :
114.        self.pupi[j].attribuerPoint(-1)
115.
116.    def dictionnaireCanons(self):
117.        "renvoyer le dictionnaire dcrivant les canons prsents" 
118.        return self.guns
119.
120.if __name__ =='__main__':
121.    AppBombardes(650,300).mainloop()
 

Commentaires :

  • Ligne 6 : La forme d'importation utilise la ligne 4 nous permet de redfinir une nouvelle classe Canon() drive de la prcdente, tout en lui conservant le mme nom. De cette manire, les portions de code qui utilisent cette classe ne devront pas tre modifies (Cela n'aurait pas t possible si nous avions utilis par exemple : from canon03 import * ).
  • Lignes 11 16 : La mthode dfinie ici porte le mme nom qu'une mthode de la classe parente. Elle va donc remplacer celle-ci dans la nouvelle classe (On pourra dire galement que la mthode deplacer() a t surcharge). Lorsque l'on ralise ce genre de modification, on s'efforce en gnral de faire en sorte que la nouvelle mthode effectue le mme travail que l'ancienne quand elle est invoque de la mme faon que l'tait cette dernire. On s'assure ainsi que les applications qui utilisaient la classe parente pourront aussi utiliser la classe fille, sans devoir tre elles-mmes modifies.
    Nous obtenons ce rsultat en ajoutant un ou plusieurs paramtres, dont les valeurs par dfaut forceront l'ancien comportement. Ainsi, lorsque l'on ne fournit aucun argument pour le paramtre rel, les paramtres x et y sont utiliss comme des coordonnes absolues (ancien comportement de la mthode). Par contre, si l'on fournit pour rel un argument vrai , alors les paramtres x et y sont traits comme des dplacements relatifs (nouveau comportement).
  • Lignes 17 33 : Les dplacements demands seront produits alatoirement. Il nous faut donc prvoir un systme de barrires logicielles, afin d'viter que l'objet ainsi dplac ne sorte du canevas.
  • Ligne 42 : Nous renvoyons les coordonnes rsultantes au programme appelant. Il se peut en effet que celui-ci commande un dplacement du canon sans connatre sa position initiale.
  • Lignes 44 49 : Il s'agit encore une fois de surcharger une mthode qui existait dans la classe parente, de manire obtenir un comportement diffrent : aprs chaque tir, dsormais on ne disperse plus tous les canons prsents, mais seulement celui qui vient de tirer.
  • Lignes 51 55 : Mthode ajoute en prvision d'applications qui souhaiteraient installer ou retirer des canons au fil du droulement du jeu.
  • Lignes 57 et suivantes : Cette nouvelle classe est conue ds le dpart de manire telle qu'elle puisse aisment tre drive. C'est la raison pour laquelle nous avons fragment son constructeur en deux parties : La mthode __init__() contient le code commun tous les objets, aussi bien ceux qui seront instancis partir de cette classe que ceux qui seront instancis partir d'une classe drive ventuelle. La mthode specificites() contient des portions de code plus spcifiques : cette mthode est clairement destine tre surcharge dans les classes drives ventuelles.

15.2. Jeu de Ping

Dans les pages qui suivent, vous trouverez le script correspondant un petit programme complet. Ce programme vous est fourni titre d'exemple de ce que vous pouvez envisager de dvelopper vous-mme comme projet personnel de synthse. Il vous montre encore une fois comment vous pouvez utiliser plusieurs classes afin de construire un script bien structur.

15.2.1. Principe

Le jeu mis en oeuvre ici est plutt une sorte d'exercice mathmatique. Il se joue sur un panneau ou est reprsent un quadrillage de dimensions variables, dont toutes les cases sont occupes par des pions. Ces pions possdent chacun une face blanche et une face noire (comme les pions du jeu Othello/Reversi), et au dbut de l'exercice ils prsentent tous leur face blanche par-dessus.

Lorsque l'on clique sur un pion l'aide de la souris, les 8 pions adjacents se retournent.

Le jeu consiste alors essayer de retourner tous les pions, en cliquant sur certains d'entre eux.

L'exercice est trs facile avec une grille de 2 x 2 cases (il suffit de cliquer sur chacun des 4 pions). Il devient plus difficile avec des grilles plus grandes, et est mme tout fait impossible avec certaines d'entre elles. A vous de dterminer lesquelles !

(Ne ngligez pas d'tudier le cas des grilles 1 x n).

Image non disponible

Note : Vous trouverez la discussion complte du jeu de Ping, sa thorie et ses extensions, dans la revue Pour la science n 298 - Aot 2002, pages 98 102.

15.2.2. Programmation

Lorsque vous dveloppez un projet logiciel, veillez toujours faire l'effort de dcrire votre dmarche le plus clairement possible. Commencez par tablir un cahier des charges dtaill, et ne ngligez pas de commenter ensuite trs soigneusement votre code, au fur et mesure de son laboration (et non aprs coup !).

En procdant ainsi, vous vous forcez vous-mme exprimer ce que vous souhaitez que la machine fasse, ce qui vous aide analyser les problmes et structurer convenablement votre code.




Cahier des charges du logiciel dvelopper


  • L'application sera construite sur la base d'une fentre principale comportant le panneau de jeu et une barre de menus.

  • L'ensemble devra tre extensible volont par l'utilisateur, les cases du panneau devant cependant rester carres.

  • Les options du menu permettront de :

    • choisir les dimensions de la grille (en nombre de cases)

    • rinitialiser le jeu (c'est--dire disposer tous les pions avec leur face blanche au-dessus)

    • afficher le principe du jeu dans une fentre d'aide

    • terminer.(fermer l'application)

  • La programmation fera appel trois classes :

    • une classe principale

    • une classe pour la barre de menus

    • une classe pour le panneau de jeu

  • Le panneau de jeu sera dessin dans un canevas, lui-mme install dans un cadre (frame). En fonction des redimensionnements oprs par l'utilisateur, le cadre occupera chaque fois toute la place disponible : il se prsente donc au programmeur comme un rectangle quelconque, dont les dimensions doivent servir de base au calcul des dimensions de la grille dessiner.

  • Puisque les cases de cette grille doivent rester carres, il est facile de commencer par calculer leur taille maximale, puis d'tablir les dimensions du canevas en fonction de celle-ci.

  • Gestion du clic de souris : on liera au canevas une mthode-gestionnaire pour l'vnement <clic du bouton gauche>. Les coordonnes de l'vnement serviront dterminer dans quelle case de la grille (n de ligne et n de colonne) le clic a t effectu, quelles que soient les dimensions de cette grille. Dans les 8 cases adjacentes, les pions prsents seront alors retourns (change des couleurs noire et blanche).

 
Sélectionnez

###########################################
#  Jeu de ping                            #
#  Rfrences : Voir article de la revue  #
#  <Pour la science>, Aout 2002           #
#                                         #
# (C) Grard Swinnen (Verviers, Belgique) #
# http://www.ulg.ac.be/cifen/inforef/swi  #
#                                         #
#  Version du 29/09/2002 - Licence : GPL  #
###########################################
 
from Tkinter import *
 
class MenuBar(Frame):
    """Barre de menus droulants"""
    def __init__(self, boss =None):
        Frame.__init__(self, borderwidth =2, relief =GROOVE)
        ##### Menu <Fichier> #####
        fileMenu = Menubutton(self, text ='Fichier')
        fileMenu.pack(side =LEFT, padx =5)
        me1 = Menu(fileMenu)
        me1.add_command(label ='Options', underline =0,
                        command = boss.options)
        me1.add_command(label ='Restart', underline =0,
                        command = boss.reset)
        me1.add_command(label ='Terminer', underline =0,
                        command = boss.quit)
        fileMenu.configure(menu = me1)    
 
        ##### Menu <Aide> #####
        helpMenu = Menubutton(self, text ='Aide')
        helpMenu.pack(side =LEFT, padx =5)
        me1 = Menu(helpMenu)
        me1.add_command(label ='Principe du jeu', underline =0,
                        command = boss.principe)
        me1.add_command(label ='A propos ...', underline =0,
                        command = boss.aPropos)
        helpMenu.configure(menu = me1)        
 
class Panneau(Frame):
    """Panneau de jeu (grille de n x m cases)"""
    def __init__(self, boss =None):
        # Ce panneau de jeu est constitu d'un cadre redimensionnable
        # contenant lui-mme un canevas. A chaque redimensionnement du
        # cadre, on calcule la plus grande taille possible pour les
        # cases (carres) de la grille, et on adapte les dimensions du
        # canevas en consquence.
        Frame.__init__(self)
        self.nlig, self.ncol = 4, 4         # Grille initiale = 4 x 4
        # Liaison de l'vnement <resize>  un gestionnaire appropri :
        self.bind("<Configure>", self.redim)
        # Canevas : 
        self.can =Canvas(self, bg ="dark olive green", borderwidth =0,
                         highlightthickness =1, highlightbackground ="white")
        # Liaison de l'vnement <clic de souris>  son gestionnaire :
        self.can.bind("<Button-1>", self.clic)
        self.can.pack()
        self.initJeu()
    def initJeu(self):
        "Initialisation de la liste mmorisant l'tat du jeu"
        self.etat =[]               	# construction d'une liste de listes
        for i in range(12):           # (quivalente  un tableau 
            self.etat.append([0]*12)	#  de 12 lignes x 12 colonnes) 
 
    def redim(self, event):
        "Oprations effectues  chaque redimensionnement"
        # Les proprits associes  l'vnement de reconfiguration
        # contiennent les nouvelles dimensions du cadre : 
        self.width, self.height = event.width -4, event.height -4
        # La diffrence de 4 pixels sert  compenser l'paisseur
        # de la 'highlightbordure" entourant le canevas)
        self.traceGrille()
 
    def traceGrille(self):
        "Dessin de la grille, en fonction des options & dimensions"
        # largeur et hauteur maximales possibles pour les cases :
        lmax = self.width/self.ncol        
        hmax = self.height/self.nlig
        # Le cot d'une case sera gal  la plus petite de ces dimensions :
        self.cote = min(lmax, hmax)
        # -> tablissement de nouvelles dimensions pour le canevas :
        larg, haut = self.cote*self.ncol, self.cote*self.nlig
        self.can.configure(width =larg, height =haut)
        # Trac de la grille :
        self.can.delete(ALL)                # Effacement dessins antrieurs
        s =self.cote                       
        for l in range(self.nlig -1):       # lignes horizontales
            self.can.create_line(0, s, larg, s, fill="white")
            s +=self.cote
        s =self.cote
        for c in range(self.ncol -1):       # lignes verticales
            self.can.create_line(s, 0, s, haut, fill ="white")
            s +=self.cote
        # Trac de tous les pions, blancs ou noirs suivant l'tat du jeu :    
        for l in range(self.nlig):
            for c in range(self.ncol):
                x1 = c *self.cote +5            # taille des pions = 
                x2 = (c +1)*self.cote -5        # taille de la case -10
                y1 = l *self.cote +5            #
                y2 = (l +1)*self.cote -5
                coul =["white","black"][self.etat[l][c]]
                self.can.create_oval(x1, y1, x2, y2, outline ="grey",
                                     width =1, fill =coul)
 
 
    def clic(self, event):
        "Gestion du clic de souris : retournement des pions"
        # On commence par dterminer la ligne et la colonne :
        lig, col = event.y/self.cote, event.x/self.cote
        # On traite ensuite les 8 cases adjacentes :
        for l in range(lig -1, lig+2):
            if l <0 or l >= self.nlig:
                continue
            for c in range(col -1, col +2):
                if c <0 or c >= self.ncol:
                    continue
                if l ==lig and c ==col:
                    continue
                # Retournement du pion par inversion logique :
                self.etat[l][c] = not (self.etat[l][c])
        self.traceGrille() 
 
 
class Ping(Frame):
    """corps principal du programme"""    
    def __init__(self):
        Frame.__init__(self)
        self.master.geometry("400x300")
        self.master.title(" Jeu de Ping")
 
        self.mbar = MenuBar(self)
        self.mbar.pack(side =TOP, expand =NO, fill =X)
 
        self.jeu =Panneau(self)
        self.jeu.pack(expand =YES, fill=BOTH, padx =8, pady =8)
 
        self.pack()
 
    def options(self):
        "Choix du nombre de lignes et de colonnes pour la grille"
        opt =Toplevel(self)
        curL =Scale(opt, length =200, label ="Nombre de lignes :",
              orient =HORIZONTAL,
              from_ =1, to =12, command =self.majLignes)
        curL.set(self.jeu.nlig)     # position initiale du curseur 
        curL.pack()
        curH =Scale(opt, length =200, label ="Nombre de colonnes :",
              orient =HORIZONTAL,        
              from_ =1, to =12, command =self.majColonnes)
        curH.set(self.jeu.ncol)      
        curH.pack()
 
    def majColonnes(self, n):
        self.jeu.ncol = int(n)
        self.jeu.traceGrille()
 
    def majLignes(self, n):
        self.jeu.nlig = int(n)      
        self.jeu.traceGrille()
 
 
    def reset(self):
        self.jeu.initJeu()
        self.jeu.traceGrille()
 
    def principe(self):
        "Fentre-message contenant la description sommaire du principe du jeu" 
        msg =Toplevel(self)
        Message(msg, bg ="navy", fg ="ivory", width =400,
            font ="Helvetica 10 bold", 
            text ="Les pions de ce jeu possdent chacun une face blanche et "\
            "une face noire. Lorsque l'on clique sur un pion, les 8 "\
            "pions adjacents se retournent.\nLe jeu consiste a essayer "\
            "de les retouner tous.\n\nSi l'exercice se rvle trs facile "\
            "avec une grille de 2 x 2 cases. Il devient plus difficile avec "\
            "des grilles plus grandes. Il est mme tout  fait impossible "\
            "avec certaines grilles.\nA vous de dterminer lesquelles !\n\n"\
            "Rf : revue 'Pour la Science' - Aout 2002")\
            .pack(padx =10, pady =10)        
 
    def aPropos(self):
        "Fentre-message indiquant l'auteur et le type de licence" 
        msg =Toplevel(self)
        Message(msg, width =200, aspect =100, justify =CENTER,
            text ="Jeu de Ping\n\n(C) Grard Swinnen, Aout 2002.\n"\
            "Licence = GPL").pack(padx =10, pady =10)
 
if __name__ == '__main__':
    Ping().mainloop()
 

Rappel : Si vous souhaitez exprimenter ces programmes sans avoir les rcrire, vous pouvez trouver leur code source l'adresse : http://www.ulg.ac.be/cifen/inforef/swi/python.htm


prcdentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Gérard Swinnen et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.