Dans ce chapitre, nous allons nous efforcer d'illustrer la démarche de conception d'un programme graphique, depuis ses premières ébauches jusqu'à un stade de développement relativement avancé. Nous souhaitons montrer ainsi combien la programmation orientée objet peut faciliter et surtout sécuriser la stratégie de développement incrémental que nous préconisons.
L'utilisation de classes s'impose, lorsque l'on constate qu'un projet en cours de réalisation se révèle nettement plus complexe que ce que l'on avait imaginé au départ. Vous vivrez certainement vous-même des cheminements similaires à celui que nous décrivons ci-dessous. 
Jeu des bombardes.
Ce projet de jeu s'inspire d'un travail similaire réalisé par des élèves de terminale.
Il est vivement recommandé de commencer l'ébauche d'un tel projet par une série de petits dessins et de schémas, dans lesquels seront décrits les différents éléments 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.org". 
L'idée de départ est simple : deux joueurs s'affrontent au canon. Chacun doit ajuster son angle de tir pour tâcher d'atteindre son adversaire, les obus décrivant une trajectoire balistique.
L'emplacement des canons est défini au début du jeu de manière aléatoire (tout au moins en hauteur). Après chaque tir, les canons se déplacent (afin d'accroître l'intérêt du jeu, l'ajustement des tirs étant ainsi rendu plus difficile). Les coups au but sont comptabilisés.
Le dessin préliminaire que nous avons reproduit à la page précédente est l'une des formes que peut prendre votre "travail d'analyse". Avant de commencer le développement d'un projet de programmation, il vous faut en effet toujours vous efforcer d'établir un "cahier des charges détaillé". Cette étude préalable est très importante. La plupart des débutants commencent bien trop vite à écrire de nombreuses lignes de code au départ d'une vague idée, en négligeant de rechercher la structure d'ensemble. Leur programmation risque alors de devenir chaotique, parce qu'ils devront de toute façon mettre en place cette structure tôt ou tard. Ils s'apercevront alors bien souvent qu'il leur faut supprimer et ré-écrire des pans entiers d'un projet qu'ils ont conçu d'une manière "trop monolithique" et/ou "mal paramétrée".
Vous devez donc toujours commencer le développement d'un projet par une phase d'analyse aussi fouillée que possible, et concrétiser le résultat de cette analyse dans un ensemble de documents (schémas, plans, descriptions...) qui constitueront le cahier des charges. Pour les projets de grande envergure, il existe d'ailleurs des méthodes d'analyse très élaborées (', '...) que nous ne pouvons nous permettre de décrire ici car elles font l'objet de livres entiers.
Cela étant dit, il faut malheureusement admettre qu'il est très difficile (et même probablement impossible) de réaliser dès le départ l'analyse tout à fait complète d'un projet de programmation. C'est seulement lorsqu'il commence à fonctionner véritablement qu'un programme révèle ses faiblesses. On constate alors qu'il reste des cas d'utilisation ou des contraintes qui n'avaient pas été prévues au départ. D'autre part, un projet logiciel est pratiquement toujours destiné à évoluer : il vous arrivera fréquemment de devoir modifier le cahier des charges au cours du développement lui-même, pas nécessairement parce que l'analyse initiale a été mal faite, mais tout simplement parce que l'on souhaite encore ajouter des fonctionnalités supplémentaires.
En conclusion, tâchez de toujours aborder un nouveau projet de programmation en respectant les deux consignes suivantes :
"C'est pour satisfaire cette exigence que la programmation orientée objets est a été inventée."
Considérons par exemple l'ébauche dessinée à la page précédente.
L'apprenti programmeur sera peut-être tenté de commencer la réalisation de ce jeu en n'utilisant que la programmation procédurale seule (c'est-à-dire en omettant de définir de nouvelles classes). C'est d'ailleurs ainsi que nous avons procédé nous-même lors de notre première approche des interfaces graphiques, tout au long du chapitre 8. Cette façon de procéder ne se justifie cependant que pour de tout petits programmes (des exercices ou des tests préliminaires). Lorsque l'on s'attaque à un projet d'une certaine importance, la complexité des problèmes qui se présentent se révèle 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 constitués d'un petit nombre de composants de base, à savoir des transistors, des diodes, des résistances, des condensateurs, etc. Les premiers ordinateurs ont été construits directement à partir de ces composants. Ils étaient volumineux, très chers, et pourtant ils n'avaient que très peu de fonctionnalités et tombaient fréquemment en panne.
On a alors développé différentes techniques pour encapsuler dans un même boîtier un certain nombre de composants électroniques de base. Pour utiliser ces nouveaux "circuits intégrés", il n'était plus nécessaire de connaître leur contenu exact : seule importait leur fonction globale. Les premières fonctions intégrées étaient encore relativement simples : c'étaient par exemple des portes logiques, des bascules, etc. En combinant ces circuits entre eux, on obtenait des caractéristiques plus élaborées, telles que des registres ou des décodeurs, qui purent à leur tour être intégrés, et ainsi de suite, jusqu'aux microprocesseurs actuels. Ceux-ci contiennent dorénavant plusieurs millions de composants, et pourtant leur fiabilité reste extrêmement élevée.
En conséquence, pour l'électronicien moderne qui veut construire par exemple un compteur binaire (circuit qui nécessite un certain nombre de "bascules"), il est évidemment bien plus simple, plus rapide et plus sûr de se servir de bascules intégrées, plutôt que de s'échiner à combiner sans erreur plusieurs centaines de transistors et de résistances.
D'une manière analogue, le programmeur moderne que vous êtes peut bénéficier du travail accumulé par ses prédécesseurs en utilisant la fonctionnalité intégrée dans les nombreuses bibliothèques de classes déjà disponibles pour Python. Mieux encore, il peut aisément créer lui-même de nouvelles classes pour encapsuler les principaux composants de son application, particulièrement ceux qui y apparaissent en plusieurs exemplaires. Procéder ainsi est plus simple, plus rapide et plus sûr que de multiplier les blocs d'instructions similaires dans un corps de programme monolithique, de plus en plus volumineux et de moins en moins compréhensible.
Examinons par exemple notre ébauche dessinée. Les composants les plus importants de ce jeu sont bien évidemment les petits canons, qu'il faudra pouvoir dessiner à différents emplacements et dans différentes orientations, et dont il nous faudra au moins deux exemplaires.
Plutôt que de les dessiner morceau par morceau dans le canevas au fur et à mesure du déroulement du jeu, nous avons intérêt à les considérer comme des objets logiciels à part entière, dotés de plusieurs propriétés ainsi que d'un certain comportement (ce que nous voulons exprimer par là est le fait qu'il devront être dotés de divers mécanismes, que nous pourrons activer par programme à l'aide de "méthodes" particulières). Il est donc certainement judicieux de leur consacrer une classe spécifique.
Prototypage d'une classe « Canon ».
En définissant 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 même « capsule », bien à l'écart du reste du programme, mais de surcroît nous nous donnons la possibilité d'instancier aisément un nombre quelconque de ces canons dans le jeu, ce qui nous ouvre des perspectives de développements ultérieurs.
Lorsqu'une première implémentation de la classe codice_1 aura été construite et testée, il sera également possible de la perfectionner en la dotant de caractéristiques supplémentaires, sans modifier (ou très peu) son interface, c'est-à-dire en quelque sorte son « mode d'emploi » : à savoir les instructions nécessaires pour l'instancier et l'utiliser dans des applications diverses. 
Entrons à présent dans le vif du sujet.
Le dessin de notre canon peut être simplifié à l'extrême. Nous avons estimé qu'il pouvait se résumer à un cercle combiné avec un rectangle, celui-ci pouvant d'ailleurs être lui-même considéré comme un simple segment de ligne droite particulièrement épais.
Si l'ensemble est rempli d'une couleur uniforme (en noir, par exemple), nous obtiendrons ainsi une sorte de petite bombarde suffisamment crédible.
Dans la suite du raisonnement, nous admettrons que la position du canon est en fait la position du centre du cercle (coordonnées 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 extrémités de la ligne épaisse qui représentera cette buse.
Pour terminer notre dessin, il nous restera alors à déterminer les coordonnées de l'autre extrémité de cette ligne. Ces coordonnées peuvent être calculées sans grande difficulté, à la condition de nous remémorer deux concepts fondamentaux de la trigonométrie (le sinus et le cosinus) que vous devez certainement bien connaître :
"Dans un triangle rectangle, le rapport entre le côté opposé à un angle et l'hypoténuse du triangle est une propriété spécifique de cet angle qu'on appelle sinus de l'angle. Le cosinus du même angle est le rapport entre le côté adjacent à l'angle et l'hypoténuse."
Ainsi, dans le schéma ci-contre : sinα = a/h et cosα = b/h.
Pour représenter la buse de notre canon, en supposant que nous connaissions sa longueur l et l'angle de tir α , il nous faut donc tracer un segment de ligne droite épaisse, à partir des coordonnées du centre du cercle (x et y), jusqu'à un autre point situé plus à droite et plus haut, l'écart horizontal Δx étant égal à l.cos α , et l'écart vertical Δy étant égal à l.sin α.
En résumant tout ce qui précède, dessiner un canon au point x, y consistera simplement à :
Nous pouvons à présent 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 vérifier si l'analyse que nous avons faite jusqu'à présent « tient la route », en réalisant un premier "prototype" fonctionnel.
Un prototype est un petit programme destiné à expérimenter une idée, que l'on se propose d'intégrer ensuite dans une application plus vaste. Du fait de sa simplicité et de sa concision, Python se prête 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 codice_1 ne comporte que deux méthodes : un constructeur qui crée les éléments de base du dessin, et une méthode 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 :
from Tkinter import *
from math import pi, sin, cos
class Canon:
 """Petit canon graphique"""
 def __init__(self, boss, x, y):
 self.boss = boss # référence du canevas
 self.x1, self.y1 = x, y # axe de rotation du canon
 # dessiner la buse du canon, à l'horizontale pour commencer :
 self.lbu = 50 # longueur de la buse
 self.x2, self.y2 = x + self.lbu, y
 self.buse = boss.create_line(self.x1, self.y1, self.x2, self.y2,
 width =10)
 # dessiner ensuite le corps du canon par-dessus :
 r = 15 # rayon du cercle
 boss.create_oval(x-r, y-r, x+r, y+r, fill='blue', width =3)
 def orienter(self, angle):
 "choisir l'angle de tir du canon"
 # rem : le paramètre <angle> est reçu en tant que chaîne de car.
 # il faut le traduire en nombre réel, puis convertir en radians :
 self.angle = float(angle)*2*pi/360
 self.x2 = self.x1 + self.lbu*cos(self.angle)
 self.y2 = self.y1 - self.lbu*sin(self.angle)
 self.boss.coords(self.buse, self.x1, self.y1, self.x2, self.y2)
if __name__ == '__main__':
 # Code pour tester sommairement la classe Canon :
 f = Tk()
 can = Canvas(f,width =250, height =250, bg ='ivory')
 can.pack(padx =10, pady =10)
 c1 = Canon(can, 50, 200)
 s1 =Scale(f, label='hausse', from_=90, to=0, command=c1.orienter)
 s1.pack(side=LEFT, pady =5, padx =20)
 s1.set(25) # angle de tir initial
 f.mainloop()
Ajout de méthodes au prototype.
Notre prototype est fonctionnel, mais beaucoup trop rudimentaire. Nous devons à présent le perfectionner pour lui ajouter la capacité de tirer des obus.
Ceux-ci seront traités plutôt 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 réaliste, nous devons à présent nous replonger dans notre cours de physique :
"Comment un objet laissé à lui-même évolue-t-il dans l'espace, si l'on néglige les phénomènes secondaires tels que la résistance de l'air ?"
Ce problème peut vous paraître complexe, mais en réalité sa résolution est très simple : il vous suffit d'admettre que le boulet se déplace à la fois horizontalement et verticalement, et que ces deux mouvements simultanés sont tout à fait indépendants l'un de l'autre.
Vous allez donc établir une boucle d'animation dans laquelle vous recalculez les nouvelles coordonnées x et y du boulet à intervalles de temps réguliers, en sachant que :
Voyons cela dans le script :
Pour commencer, il faut ajouter les lignes suivantes à la fin de la méthode constructeur. Elles vont servir à créer l'objet « obus », et à préparer une variable d'instance qui servira d'interrupteur de l'animation. L'obus est créé au départ avec des dimensions minimales (un cercle d'un seul pixel) afin de rester presqu'invisible :
 # dessiner un obus (réduit à 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 dernières lignes utilisent la méthode codice_4 du widget « maître » (le canevas, ici), afin de retrouver certaines de ses caractéristiques. Nous voulons en effet que notre classe codice_5 soit généraliste, c'est-à-dire réutilisable dans n'importe quel contexte, et nous ne pouvons donc pas tabler à l'avance sur des dimensions particulières pour le canevas dans lequel ce canon sera utilisé.
Ensuite, nous devons ajouter deux nouvelles méthodes : l'une pour déclencher le tir, et l'autre pour gérer l'animation du boulet une fois que celui-ci aura été lancé :
 def feu(self):
 "déclencher le tir d'un obus"
 if not self.anim:
 self.anim =True
 # position de départ de l'obus (c'est la bouche du canon) :
 self.boss.coords(self.obus, self.x2 -3, self.y2 -3,
 self.x2 +3, self.y2 +3)
 v =15 # vitesse initiale
 # composantes verticale et horizontale de cette vitesse :
 self.vy = -v *sin(self.angle)
 self.vx = v *cos(self.angle)
 self.animer_obus()
 def animer_obus(self):
 "animation de l'obus (trajectoire balistique)"
 if self.anim:
 self.boss.move(self.obus, int(self.vx), int(self.vy))
 c = self.boss.coords(self.obus) # coord. résultantes
 xo, yo = c[0] +3, c[1] +3 # coord. du centre de l'obus
 if yo > self.yMax or xo > self.xMax:
 self.anim =False # arrêter l'animation
 self.vy += .5
 self.boss.after(30, self.animer_obus)
Il reste enfin à ajouter un bouton déclencheur dans la fenêtre principale. Une ligne telle que la suivante (à insérer dans le code de test) fera parfaitement l'affaire :
Développement de l'application.
Disposant désormais d'une classe d'objets « canon » assez bien dégrossie, nous pouvons à présent envisager l'élaboration de l'application proprement dite. Et puisque nous sommes décidés à exploiter la méthodologie de la programmation orientée objet, nous devons concevoir cette application comme "un ensemble d'objets qui interagissent par l'intermédiaire de leurs méthodes".
Plusieurs de ces objets proviendront de classes préexistantes, bien entendu : ainsi le canevas, les boutons, etc. Mais nous avons vu dans les pages précédentes que nous avons intérêt à regrouper des ensembles bien délimités de ces objets basiques dans de nouvelles classes, chaque fois que nous pouvons identifier pour ces ensembles une fonctionnalité particulière. C'était le cas par exemple pour cet ensemble de cercles et de lignes mobiles que nous avons décidé d'appeler « canon ».
Pouvons-nous encore distinguer dans notre projet initial d'autres composants qui mériteraient d'être encapsulés dans des nouvelles classes ? Certainement. Il y a par exemple le pupitre de contrôle que nous voulons associer à chaque canon : nous pouvons y rassembler le dispositif de réglage de la hausse (l'angle de tir), le bouton de mise à feu, le score réalisé, et peut-être d'autres indications encore, comme le nom du joueur, par exemple. Il est d'autant plus intéressant de lui consacrer une classe particulière, que nous savons d'emblée qu'il nous en faudra deux instances.
Il y a aussi l'application elle-même, bien sûr. En l'encapsulant dans une classe, nous en ferons notre objet principal, celui qui dirigera tous les autres.
Veuillez à présent analyser le script ci-dessous. Vous y retrouverez la classe codice_1 encore davantage développée : nous y avons ajouté quelques attributs et trois méthodes supplémentaires, afin de pouvoir gérer les déplacements du canon lui-même, ainsi que les coups au but.
La classe codice_14 remplace désormais le code de test des prototypes précédents. Nous y instancions deux objets codice_1, et deux objets de la nouvelle classe codice_16, que nous plaçons dans des dictionnaires en prévision de développements ultérieurs (nous pouvons en effet imaginer d'augmenter le nombre de canons et donc de pupitres). Le jeu est à présent fonctionnel : les canons se déplacent après chaque tir, et les coups au but sont comptabilisés.
from Tkinter import *
from math import sin, cos, pi
from random import randrange
class Canon:
 """Petit canon graphique"""
 def __init__(self, boss, id, x, y, sens, coul):
 self.boss = boss # réf. du canevas
 self.appli = boss.master # réf. de la fenêtre d'application 
 self.id = id # identifiant du canon (chaîne)
 self.coul = coul # couleur associée au canon
 self.x1, self.y1 = x, y # axe de rotation du canon
 self.sens = sens # sens de tir (-1:gauche, +1:droite)
 self.lbu = 30 # longueur de la buse
 self.angle = 0 # hausse par défaut (angle de tir)
 # retrouver la largeur et la hauteur du canevas :
 self.xMax = int(boss.cget('width'))
 self.yMax = int(boss.cget('height'))
 # dessiner la buse du canon (horizontale) :
 self.x2, self.y2 = x + self.lbu * sens, y
 self.buse = boss.create_line(self.x1, self.y1,
 self.x2, self.y2, width =10)
 # dessiner le corps du canon (cercle de couleur) :
 self.rc = 15 # rayon du cercle 
 self.corps = boss.create_oval(x -self.rc, y -self.rc, x +self.rc,
 y +self.rc, fill =coul)
 # pré-dessiner un obus caché (point en dehors du canevas) :
 self.obus = boss.create_oval(-10, -10, -10, -10, fill='red')
 self.anim = False # indicateurs d'animation 
 self.explo = False # et d'explosion
 def orienter(self, angle):
 "régler la hausse du canon"
 # rem: le paramètre <angle> est reçu en tant que chaîne.
 # Il faut donc le traduire en réel, puis le convertir en radians :
 self.angle = float(angle)*pi/180
 # rem: utiliser la méthode coords de préférence avec des entiers : 
 self.x2 = int(self.x1 + self.lbu * cos(self.angle) * self.sens)
 self.y2 = int(self.y1 - self.lbu * sin(self.angle))
 self.boss.coords(self.buse, self.x1, self.y1, self.x2, self.y2)
 def deplacer(self, x, y):
 "amener le canon dans une nouvelle position x, y"
 dx, dy = x -self.x1, y -self.y1 # valeur du déplacement
 self.boss.move(self.buse, dx, dy) 
 self.boss.move(self.corps, dx, dy)
 self.x1 += dx
 self.y1 += dy
 self.x2 += dx
 self.y2 += dy
 def feu(self):
 "tir d'un obus - seulement si le précédent a fini son vol"
 if not (self.anim or self.explo):
 self.anim =True
 # récupérer la description de tous les canons présents :
 self.guns = self.appli.dictionnaireCanons()
 # position de départ de l'obus (c'est la bouche du canon) :
 self.boss.coords(self.obus, self.x2 -3, self.y2 -3,
 self.x2 +3, self.y2 +3)
 v = 17 # vitesse initiale
 # composantes verticale et horizontale de cette vitesse :
 self.vy = -v *sin(self.angle)
 self.vx = v *cos(self.angle) *self.sens
 self.animer_obus()
 return True # => signaler que le coup est parti
 else:
 return False # => le coup n'a pas pu être tiré
 def animer_obus(self):
 "animer l'obus (trajectoire balistique)"
 if self.anim:
 self.boss.move(self.obus, int(self.vx), int(self.vy))
 c = self.boss.coords(self.obus) # coord. résultantes
 xo, yo = c[0] +3, c[1] +3 # coord. du centre de l'obus
 self.test_obstacle(xo, yo) # a-t-on atteint un obstacle ?
 self.vy += .4 # accélération verticale
 self.boss.after(20, self.animer_obus)
 else:
 # animation terminée - cacher l'obus et déplacer les canons :
 self.fin_animation()
 def test_obstacle(self, xo, yo):
 "évaluer si l'obus a atteint une cible ou les limites du jeu"
 if yo >self.yMax or xo <0 or xo >self.xMax:
 self.anim =False
 return
 # analyser le dictionnaire des canons pour voir si les coord.
 # de l'un d'entre eux sont proches de celles de l'obus :
 for id in self.guns: # id = clef dans dictionn.
 gun = self.guns[id] # valeur correspondante
 if xo < gun.x1 +self.rc and xo > gun.x1 -self.rc \
 and yo < gun.y1 +self.rc and yo > gun.y1 -self.rc :
 self.anim =False
 # dessiner l'explosion de l'obus (cercle jaune) :
 self.explo = self.boss.create_oval(xo -12, yo -12,
 xo +12, yo +12, fill ='yellow', width =0)
 self.hit =id # référence de la cible touchée
 self.boss.after(150, self.fin_explosion)
 break 
 def fin_explosion(self):
 "effacer l'explosion ; ré-initaliser l'obus ; gérer le score"
 self.boss.delete(self.explo) # effacer l'explosion
 self.explo =False # autoriser un nouveau tir
 # signaler le succès à la fenêtre maîtresse :
 self.appli.goal(self.id, self.hit)
 def fin_animation(self):
 "actions à accomplir lorsque l'obus a terminé sa trajectoire"
 self.appli.disperser() # déplacer les canons
 # cacher l'obus (en l'expédiant hors du canevas) :
 self.boss.coords(self.obus, -10, -10, -10, -10)
class Pupitre(Frame):
 """Pupitre de pointage associé à un canon""" 
 def __init__(self, boss, canon):
 Frame.__init__(self, bd =3, relief =GROOVE)
 self.score =0
 self.appli =boss # réf. de l'application
 self.canon =canon # réf. du canon associé
 # Système de réglage de l'angle de tir :
 self.regl =Scale(self, from_ =75, to =-15, troughcolor=canon.coul,
 command =self.orienter)
 self.regl.set(45) # angle initial de tir
 self.regl.pack(side =LEFT)
 # Étiquette d'identification du canon :
 Label(self, text =canon.id).pack(side =TOP, anchor =W, pady =5)
 # Bouton de tir :
 self.bTir =Button(self, text ='Feu !', command =self.tirer)
 self.bTir.pack(side =BOTTOM, padx =5, pady =5)
 Label(self, text ="points").pack()
 self.points =Label(self, text=' 0 ', bg ='white')
 self.points.pack()
 # positionner à gauche ou à droite suivant le sens du canon :
 if canon.sens == -1:
 self.pack(padx =5, pady =5, side =RIGHT)
 else:
 self.pack(padx =5, pady =5, side =LEFT)
 def tirer(self):
 "déclencher le tir du canon associé"
 self.canon.feu()
 def orienter(self, angle):
 "ajuster la hausse du canon associé"
 self.canon.orienter(angle)
 def attribuerPoint(self, p):
 "incrémenter ou décrémenter le score, de <p> points"
 self.score += p
 self.points.config(text = ' %s ' % self.score)
class Application(Frame):
 Fenêtre principale de l'application
 def __init__(self):
 Frame.__init__(self)
 self.master.title('»»> Boum ! Boum ! ««<')
 self.pack()
 self.jeu = Canvas(self, width =400, height =250, bg ='ivory',
 bd =3, relief =SUNKEN)
 self.jeu.pack(padx =8, pady =8, side =TOP)
 self.guns ={} # dictionnaire des canons présents
 self.pupi ={} # dictionnaire des pupitres présents
 # Instanciation de 2 'objets canons (+1, -1 = sens opposés) :
 self.guns["Billy"] = Canon(self.jeu, "Billy", 30, 200, 1, "red")
 self.guns["Linus"] = Canon(self.jeu, "Linus", 370,200,-1, "blue")
 # Instanciation de 2 pupitres de pointage associés à ces canons :
 self.pupi["Billy"] = Pupitre(self, self.guns["Billy"])
 self.pupi["Linus"] = Pupitre(self, self.guns["Linus"])
 def disperser(self):
 "déplacer aléatoirement les canons"
 for id in self.guns:
 gun =self.guns[id]
 # positionner à gauche ou à droite, suivant sens du canon :
 if gun.sens == -1 :
 x = randrange(320,380)
 else:
 x = randrange(20,80)
 # déplacement proprement dit :
 gun.deplacer(x, randrange(150,240))
 def goal(self, i, j):
 "le canon <i> signale qu'il a atteint l'adversaire <j>"
 if i != j:
 self.pupi[i].attribuerPoint(1) 
 else:
 self.pupi[i].attribuerPoint(-1)
 def dictionnaireCanons(self):
 "renvoyer le dictionnaire décrivant les canons présents" 
 return self.guns
if __name__ =='__main__':
 Application().mainloop()
Développements complémentaires.
Tel qu'il vient d'être décrit, notre programme correspond déjà plus ou moins au cahier des charges initial, mais il est évident que nous pouvons continuer à le perfectionner.
Nous devrions par exemple mieux le paramétrer. Qu'est-ce à dire ? Dans sa forme actuelle, notre jeu comporte un canevas de taille prédéterminée (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 aux lignes 168-169, ou 179-184). De telles lignes interdépendantes risquent de devenir nombreuses si nous ajoutons encore d'autres fonctionnalités. Il serait donc plus judicieux de dimensionner le canevas à l'aide de variables, dont la valeur serait définie en un seul endroit. Ces variables seraient ensuite exploitées dans toutes les lignes d'instructions où les dimensions du canevas interviennent.
Nous avons déjà effectué une partie de ce travail : dans la classe Canon(), en effet, les dimensions du canevas sont récupérées à l'aide d'une méthode prédéfinie (voir lignes 17-18), et placées dans des attributs d'instance qui peuvent être utilisés partout dans la classe.
Après chaque tir, nous provoquons un déplacement aléatoire des canons, en redéfinissant leurs coordonnées au hasard. Il serait probablement plus réaliste de provoquer de véritables "déplacements" relatifs, plutôt que de redéfinir au hasard des positions absolues. Pour ce faire, il suffit de retravailler la méthode codice_34 de la classe codice_1. En fait, il serait encore plus intéressant de faire en sorte que cette méthode puisse produire à volonté, aussi bien un déplacement relatif qu'un positionnement absolu, en fonction d'une valeur transmise en argument.
Le système de commande des tirs devrait être amélioré : puisque nous ne disposons que d'une seule souris, il faut demander aux joueurs de tirer à tour de rôle, et nous n'avons mis en place aucun mécanisme pour les forcer à le faire. Une meilleure approche consisterait à prévoir des commandes de hausse et de tir utilisant certaines touches du clavier, qui soient distinctes pour les deux joueurs.
Mais le développement le plus intéressant pour notre programme serait certainement d'en faire une "application réseau". Le jeu serait alors installé sur plusieurs machines communicantes, chaque joueur ayant le contrôle d'un seul canon. Il serait d'ailleurs encore plus attrayant de permettre la mise en œuvre de plus de deux canons, de manière à autoriser des combats impliquant davantage de joueurs.
Ce type de développement suppose cependant que nous ayons appris à maîtriser au préalable deux domaines de programmation qui débordent un peu le cadre de ce cours :
Ces matières ne font pas strictement partie des objectifs que nous nous sommes fixés pour ce cours, et leur traitement nécessite à lui seul un chapitre entier. Nous n'aborderons donc pas cette question ici. Que ceux que le sujet intéresse se rassurent cependant : ce chapitre existe, mais sous la forme d'un complément à la fin du livre (chapitre 18) : vous y trouverez la version réseau de notre jeu de bombardes.
En attendant, voyons tout de même comment nous pouvons encore progresser, en apportant à notre projet quelques améliorations qui en feront un jeu pour 4 joueurs. Nous nous efforcerons aussi de mettre en place une programmation bien compartimentée, de manière à ce que les méthodes de nos classes soient réutilisables dans une large mesure. Nous allons voir au passage comment cette évolution peut se faire sans modifier le code existant, en utilisant "l'héritage" pour produire de nouvelles classes à partir de celles qui sont déjà écrites.
Commençons par sauvegarder notre ouvrage précédent 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 nouveautés :
from Tkinter import *
from math import sin, cos, pi
from random import randrange
import canon03
class Canon(canon03.Canon):
 """Canon amélioré"""
 def __init__(self, boss, id, x, y, sens, coul):
 canon03.Canon.__init__(self, boss, id, x, y, sens, coul)
 def deplacer(self, x, y, rel =False):
 "déplacement, relatif si <rel> est vrai, absolu si <rel> est faux"
 if rel:
 dx, dy = x, y
 else:
 dx, dy = x -self.x1, y -self.y1
 # limites horizontales :
 if self.sens ==1:
 xa, xb = 20, int(self.xMax *.33)
 else:
 xa, xb = int(self.xMax *.66), self.xMax -20
 # ne déplacer que dans ces limites :
 if self.x1 +dx < xa:
 dx = xa -self.x1
 elif self.x1 +dx > xb:
 dx = xb -self.x1
 # limites verticales :
 ya, yb = int(self.yMax *.4), self.yMax -20
 # ne déplacer que dans ces limites :
 if self.y1 +dy < ya:
 dy = ya -self.y1
 elif self.y1 +dy > yb:
 dy = yb -self.y1
 # déplacement de la buse et du corps du canon : 
 self.boss.move(self.buse, dx, dy) 
 self.boss.move(self.corps, dx, dy) 
 # renvoyer les nouvelles coord. au programme appelant :
 self.x1 += dx
 self.y1 += dy
 self.x2 += dx
 self.y2 += dy
 return self.x1, self.y1 
 def fin_animation(self):
 "actions à accomplir lorsque l'obus a terminé sa trajectoire"
 # déplacer le canon qui vient de tirer :
 self.appli.depl_aleat_canon(self.id)
 # cacher l'obus (en l'expédiant hors du canevas) :
 self.boss.coords(self.obus, -10, -10, -10, -10)
 def effacer(self):
 "faire disparaître le canon du canevas"
 self.boss.delete(self.buse)
 self.boss.delete(self.corps)
 self.boss.delete(self.obus) 
class AppBombardes(Frame):
 Fenêtre principale de l'application
 def __init__(self, larg_c, haut_c):
 Frame.__init__(self)
 self.pack()
 self.xm, self.ym = larg_c, haut_c
 self.jeu = Canvas(self, width =self.xm, height =self.ym,
 bg ='ivory', bd =3, relief =SUNKEN)
 self.jeu.pack(padx =4, pady =4, side =TOP)
 self.guns ={} # dictionnaire des canons présents
 self.pupi ={} # dictionnaire des pupitres présents
 self.specificites() # objets différents dans classes dérivées
 def specificites(self):
 "instanciation des canons et des pupitres de pointage"
 self.master.title('«< Jeu des bombardes »>')
 id_list =[("Paul","red"),("Roméo","cyan"),
 ("Virginie","orange"),("Juliette","blue")]
 s = False
 for id, coul in id_list:
 if s:
 sens =1
 else:
 sens =-1
 x, y = self.coord_aleat(sens)
 self.guns[id] = Canon(self.jeu, id, x, y, sens, coul)
 self.pupi[id] = canon03.Pupitre(self, self.guns[id])
 s = not s # changer de côté à chaque itération
 def depl_aleat_canon(self, id):
 "déplacer aléatoirement le canon <id>"
 gun =self.guns[id]
 dx, dy = randrange(-60, 61), randrange(-60, 61)
 # déplacement (avec récupération des nouvelles coordonnées) :
 x, y = gun.deplacer(dx, dy, True)
 return x, y
 def coord_aleat(self, s):
 "coordonnées aléatoires, à gauche (s =1) ou à droite (s =-1)" 
 y =randrange(int(self.ym /2), self.ym -20)
 if s == -1:
 x =randrange(int(self.xm *.7), self.xm -20)
 else:
 x =randrange(20, int(self.xm *.3))
 return x, y
 def goal(self, i, j):
 "le canon n°i signale qu'il a atteint l'adversaire n°j"
 # de quel camp font-ils partie chacun ?
 ti, tj = self.guns[i].sens, self.guns[j].sens 
 if ti != tj : # ils sont de sens opposés :
 p = 1 # on gagne 1 point
 else: # ils sont dans le même sens :
 p = -2 # on a touché un allié !!
 self.pupi[i].attribuerPoint(p)
 # celui qui est touché perd de toute façon un point :
 self.pupi[j].attribuerPoint(-1)
 def dictionnaireCanons(self):
 "renvoyer le dictionnaire décrivant les canons présents" 
 return self.guns
if __name__ =='__main__':
 AppBombardes(650,300).mainloop()
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 développer vous-même comme projet personnel de synthèse. Il vous montre encore une fois comment vous pouvez utiliser plusieurs classes afin de construire un script bien structuré.
Principe.
Le « jeu » mis en œuvre ici est plutôt une sorte d'exercice mathématique. Il se joue sur un panneau ou est représenté un quadrillage de dimensions variables, dont toutes les cases sont occupées par des pions. Ces pions possèdent chacun une face blanche et une face noire (comme les pions du jeu Othello/Reversi), et au début de l'exercice ils présentent 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 très 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 même tout à fait impossible avec certaines d'entre elles. À vous de déterminer lesquelles ! (Ne négligez pas d'étudier le cas des grilles 1 x n)
Programmation.
Lorsque vous développez un projet logiciel, veillez toujours à faire l'effort de décrire votre démarche le plus clairement possible. Commencez par établir un cahier des charges détaillé, et ne négligez pas de commenter ensuite très soigneusement votre code, au fur et à mesure de son élaboration (et non après coup !).
En procédant ainsi, vous vous forcez vous-même à exprimer ce que vous souhaitez que la machine fasse, ce qui vous aide à analyser les problèmes et à structurer convenablement votre code.
from Tkinter import *
class MenuBar(Frame):
 """Barre de menus déroulants"""
 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 ='À 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-même un canevas. À chaque redimensionnement du
 # cadre, on calcule la plus grande taille possible pour les
 # cases (carrées) de la grille, et on adapte les dimensions du
 # canevas en conséquence.
 Frame.__init__(self)
 self.nlig, self.ncol = 4, 4 # Grille initiale = 4 x 4
 # Liaison de l'événement <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'événement <clic de souris> à son gestionnaire :
 self.can.bind("<Button-1>", self.clic)
 self.can.pack()
 self.initJeu()
 def initJeu(self):
 "Initialisation de la liste mémorisant 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):
 "Opérations effectuées à chaque redimensionnement"
 # Les propriétés associées à l'événement de reconfiguration
 # contiennent les nouvelles dimensions du cadre : 
 self.width, self.height = event.width -4, event.height -4
 # La différence 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 côté 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 antérieurs
 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 déterminer 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):
 "Fenêtre-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 possèdent chacun une face blanche et "\
 "une face noire. Lorsque l'on clique sur un pion, les 8 "\
 "pions adjacents se retournent.\nLe jeu consiste à essayer "\
 "de les retourner tous.\n\nSi l'exercice se révèle très facile "\
 "avec une grille de 2 x 2 cases. Il devient plus difficile avec "\
 "des grilles plus grandes. Il est même tout à fait impossible "\
 "avec certaines grilles.\nÀ vous de déterminer lesquelles !\n\n"\
 "Réf : revue 'Pour la Science' - Aout 2002")\
 .pack(padx =10, pady =10) 
 def aPropos(self):
 "Fenêtre-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) Gérard Swinnen, Aout 2002.\n"\
 "Licence = GPL").pack(padx =10, pady =10)
if __name__ == '__main__':
 Ping().mainloop()