Chapitre 15 : Analyse de programmes concrets▲
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éconisons58.
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.
15-1. Jeu des bombardes▲
Ce projet de jeu59 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.org60. C'est
l'outil qui a été utilisé pour réaliser le schéma ci-dessous :
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. Il 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.
- Trop monolithique : cela signifie que l'on a négligé de décomposer un problème complexe en
plusieurs sous-problèmes plus simples. Par exemple, on a imbriqué plusieurs niveaux successifs
d'instructions composées, au lieu de faire appel à des fonctions ou à des classes.
- Mal paramétrée : cela signifie que l'on a traité seulement un cas particulier, au lieu d'envisager le cas général. Par exemple, on a donné à un objet graphique des dimensions fixes, au lieu de prévoir des variables pour permettre son redimensionnement.
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 (UML, Merise...) 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 luimê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 :
- Décrivez votre projet en profondeur avant de commencer la rédaction des premières lignes de
code, en vous efforçant de mettre en évidence les composants principaux et les relations qui les
lient (pensez notamment à décrire les différents cas d'utilisation de votre programme)
- Lorsque vous commencerez sa réalisation effective, évitez de vous laisser entraîner à rédiger de
trop grands blocs d'instructions. Veillez au contraire à découper votre application en un certain
nombre de composants paramétrables bien encapsulés, de telle manière que vous puissiez
aisément modifier l'un ou l'autre d'entre eux sans compromettre le fonctionnement des autres, et
peut-être même les réutiliser dans différents contextes si le besoin s'en fait sentir.
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 luimê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
58 Voir page 16 : Recherche des erreurs et expérimentation, et aussi page 216 : Fenêtres avec menus
59 Nous n'hésitons pas à discuter ici le développement d'un logiciel de jeu, parce qu'il s'agit d'un domaine directement
accessible à tous, et dans lequel les objectifs concrets sont aisément identifiables. Il va de soi que les mêmes
techniques de développement peuvent s'appliquer à d'autres applications plus "sérieuses".
60 Il s'agit d'une suite bureautique complète, libre et gratuite, largement compatible avec MS-Office, disponible pour
Linux, Windows, MacOS, Solaris ... Le présent manuel a été entièrement rédigé avec son traitement de textes.
Vous pouvez vous la procurer par téléchargement depuis le site Web : http://www.openoffice.org
15-1-1. 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 Canon() 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 coté 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 : et |
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 à :
- tracer un cercle noir centré sur x, y
- tracer une ligne noire épaisse depuis le point x, y jusqu'au point x + l.cos α, y + l.sin α.
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 Canon() 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 :
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 # référence 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 paramètre <angle> est reçu en tant que chaîne de car.
21.
# il faut le traduire en nombre réel, 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 paramètres qui devront être transmis au constructeur lors de
l'instanciation, nous prévoyons les coordonnées x et y, qui indiqueront l'emplacement du canon
dans le canevas, mais également une référence au canevas lui-même (la variable boss). Cette
référence est indispensable : elle sera utilisée pour invoquer les méthodes du canevas.
Nous pourrions inclure aussi un paramètre pour choisir un angle de tir initial, mais puisque nous avons l'intention d'implémenter une méthode spécifique pour régler cette orientation, il sera plus judicieux de faire appel à celle-ci au moment voulu. - Lignes 7 et 8 : Ces références seront utilisées un peu partout dans les différentes méthodes que
nous allons développer 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 cachée. Cela nous permet de colorer éventuellement le corps du canon.
- Lignes 18 à 25 : Cette méthode sera invoquée avec un argument « angle », lequel sera fourni en
degrés (comptés à 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 chaîne de caractères, et nous devrons donc le convertir
d'abord en nombre réel avant de l'utiliser dans nos calculs (ceux-ci ont été décrits à la page
précédente).
- Lignes 27 à 38 : Pour tester notre nouvelle classe, nous ferons usage d'un widget Scale. Pour définir la position initiale de son curseur, et donc fixer l'angle de hausse initial du canon, nous devons faire appel à sa méthode set() (ligne 36).
15-1-2. Ajout de méthodes au prototype▲
- Le mouvement horizontal est uniforme. À chaque itération, il vous suffit d'augmenter
graduellement la coordonnée x du boulet, en lui ajoutant toujours un même déplacement Δx.
- Le mouvement vertical est uniformément accéléré. Cela signifie simplement qu'à chaque itération, vous devez ajouter à la coordonnée y un déplacement Dy qui augmente lui-même graduellement, toujours de la même quantité.
Voyons cela dans le script :
A) 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 cget() du widget « maître » (le canevas, ici), afin
de retrouver certaines de ses caractéristiques. Nous voulons en effet que notre classe Canon 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é.
Note : Tkinter renvoie ces valeurs sous la forme de chaînes de caractères. Il faut donc les convertir
dans un type numérique si nous voulons pouvoir les utiliser dans un calcul.
B) 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é :
1.
def
feu
(
self):
2.
"déclencher le tir d'un obus"
3.
if
not
self.anim:
4.
self.anim =
True
5.
# position de départ 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. résultantes
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
# arrêter l'animation
22.
self.vy +=
.5
23.
self.boss.after
(
30
, self.animer_obus)
Commentaires :
- Lignes 1 à 4 : Cette méthode sera invoquée par appui sur un bouton. Elle déclenche le
mouvement de l'obus, et attribue une valeur « vraie » à notre « interrupteur d'animation » (la
variable self.anim : voir ci-après). Il faut cependant nous assurer que pendant toute la durée de
cette animation, un nouvel appui sur le bouton ne puisse pas activer d'autres boucles d'animation
parasites. C'est le rôle du test effectué à la ligne 3 : le bloc d'instruction qui suit ne peut
s'exécuter que si la variable self.anim possède la valeur « faux », ce qui signifie que l'animation
n'a pas encore commencé.
- Lignes 5 à 7 : Le canevas Tkinter dispose de deux méthodes pour déplacer les objets graphiques :
La méthode coords() effectue un positionnement absolu ; il faut cependant lui fournir toutes les
coordonnées de l'objet (comme si on le redessinait). La méthode move() , utilisée à la ligne 17,
provoque quant à elle un déplacement relatif ; elle s'utilise avec deux arguments seulement : les
composantes horizontale et verticale du déplacement souhaité.
- Lignes 8 à 12 : La vitesse initiale de l'obus est choisie à la ligne 8. Comme nous l'avons expliqué
à la page précédente, le mouvement du boulet est la résultante 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 déterminer les composantes horizontale et verticale de cette
vitesse, il nous suffit d'utiliser des relations trigonométriques tout à fait similaires à que celles
que nous avons déjà exploitées pour dessiner la buse du canon. Le signe - utilisé à la ligne 9
provient du fait que les coordonnées verticales se comptent de haut en bas.
La ligne 12 active l'animation proprement dite. - Lignes 14 à 23 : Cette procédure se ré-appelle elle-même toutes les 30 millisecondes par
l'intermédiaire de la méthode after() invoquée à la ligne 23. Cela continue aussi longtemps que
la variable self.anim (notre « interrupteur d'animation ») reste « vraie », condition qui changera
lorsque les coordonnées de l'obus sortiront des limites imposées (test de la ligne 20).
- Lignes 18, 19 : Pour retrouver ces coordonnées après chaque déplacement, on fait appel encore
une fois à la méthode coords() du canevas : utilisée avec la référence d'un objet graphique
comme unique argument, elle renvoie ses quatre coordonnées dans un tuple.
- Lignes 17 et 22 : La coordonnée horizontale de l'obus augmente toujours de la même quantité
(mouvement uniforme), tandis que la coordonnée verticale augmente d'une quantité qui est ellemême
augmentée à chaque fois à la ligne 24 (mouvement uniformément accéléré). Le résultat est
une trajectoire parabolique.
Rappel : l'opérateur += permet d'incrémenter une variable : « a += 3 » équivaut à « a = a + 3 ». -
C) 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 :
Button
(
f, text=
'Feu !'
, command =
c1.feu).pack
(
side=
LEFT)
15-1-3. 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 Canon() 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 Application() remplace désormais le code de test des prototypes précédents. Nous y
instancions deux objets Canon(), et deux objets de la nouvelle classe Pupitre(), 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.
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 # réf. du canevas
9.
self.appli =
boss.master # réf. de la fenêtre d'application
10.
self.id =
id # identifiant du canon (chaîne)
11.
self.coul =
coul # couleur associée 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 défaut (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.
"régler la hausse du canon"
34.
# rem: le paramètre <angle> est reçu en tant que chaîne.
35.
# Il faut donc le traduire en réel, puis le convertir en radians :
36.
self.angle =
float(
angle)*
pi/
180
37.
# rem: utiliser la méthode coords de préférence 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 déplacement
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 précédent a fini son vol"
54.
if
not
(
self.anim or
self.explo):
55.
self.anim =
True
56.
# récupérer la description de tous les canons présents :
57.
self.guns =
self.appli.dictionnaireCanons
(
)
58.
# position de départ 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. résultantes
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
# accélération verticale
78.
self.boss.after
(
20
, self.animer_obus)
79.
else
:
80.
# animation terminée - cacher l'obus et déplacer 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 # référence de la cible touchée
99.
self.boss.after
(
150
, self.fin_explosion)
100.
break
101.
102.
def
fin_explosion
(
self):
103.
"effacer l'explosion ; ré-initaliser l'obus ; gérer le score"
104.
self.boss.delete
(
self.explo) # effacer l'explosion
105.
self.explo =
False
# autoriser un nouveau tir
106.
# signaler le succès à la fenêtre maîtresse :
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
(
) # déplacer les canons
112.
# cacher l'obus (en l'expédiant 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 # réf. de l'application
122.
self.canon =
canon # réf. du canon associé
123.
# Système de réglage 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.
"déclencher 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.
"incrémenter ou décrémenter le score, de <p> points"
152.
self.score +=
p
153.
self.points.config
(
text =
'
%s
'
%
self.score)
154.
155.
class
Application
(
Frame):
156.
'''Fenêtre 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 présents
166.
self.pupi =
{} # dictionnaire des pupitres présents
167.
# Instanciation de 2 'objets canons (+1, -1 = sens opposés) :
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 associés à 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.
"déplacer aléatoirement 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.
# déplacement 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 décrivant les canons présents"
195.
return
self.guns
196.
197.
if
__name__
==
'__main__'
:
198.
Application
(
).mainloop
(
)
Commentaires :
- Ligne 7 : Par rapport au prototype, trois paramètres ont été ajoutés à la méthode constructeur. Le
paramètre id nous permet d'identifier chaque instance de la classe Canon() à l'aide d'un nom
quelconque. Le paramètre sens indique s'il s'agit d'un canon qui tire vers la droite (sens = 1) ou
vers la gauche (sens = -1). Le paramètre coul spécifie la couleur associée au canon.
- Ligne 9 : Il faut savoir que tous les widgets Tkinter possèdent un attribut master qui contient la
référence leur widget maître éventuel (leur « contenant »). Cette référence est donc pour nous
celle de l'application principale. (Nous avons implémenté nous-mêmes une technique similaire
pour référencer le canevas, à l'aide de l'attribut boss).
- Lignes 42 à 50 : Cette méthode permet d'amener le canon dans un nouvel emplacement. Elle
servira à repositionner les canons au hasard après chaque tir, ce qui augmente l'intérêt du jeu.
Note : l'opérateur += permet d'incrémenter une variable : « a += 3 » équivaut à « a = a + 3 ». - Lignes 56, 57 : Nous essayons de construire notre classe canon de telle manière qu'elle puisse
être réutilisée dans des projets plus vastes, impliquant un nombre quelconque d'objets canons qui
pourront apparaître et disparaître au fil des combats. Dans cette perspective, il faut que nous
puissions disposer d'une description de tous les canons présents, avant chaque tir, de manière à
pouvoir déterminer si une cible a été touchée ou non. Cette description est gérée par l'application
principale, dans un dictionnaire, dont on peut lui demander une copie par l'intermédiaire de sa
méthode dictionnaireCanons().
- Lignes 66 à 68 : Dans cette même perspective généraliste, 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 désormais traitée par deux méthodes complémentaires. Afin
de clarifier le code, nous avons placé dans une méthode distincte les instructions servant à
déterminer si une cible a été atteinte (méthode test_obstacle()).
- Lignes 79 à 81 : Nous avons vu précédemment que l'on interrompt l'animation de l'obus en
attribuant une valeur « fausse » à la variable self.anim. La méthode animer_obus() cesse alors
de boucler et exécute le code de la ligne 81.
- Lignes 83 à 100 : Cette méthode évalue si les coordonnées actuelles de l'obus sortent des limites
de la fenêtre, 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 référence du canon touché est mémorisée. La méthode annexe fin_explosion() est invoquée
après 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 fenêtre maîtresse.
- Lignes 115 à 153 : La classe Pupitre() définit un nouveau widget par dérivation de la classe
Frame(), selon une technique qui doit désormais vous être devenue familière. Ce nouveau
widget regroupe les commandes de hausse et de tir, ainsi que l'afficheur de points associés à un
canon bien déterminé. La correspondance visuelle entre les deux est assurée par l'adoption d'une
couleur commune. Les méthodes tirer() et orienter() communiquent avec l'objet Canon()
associé, par l'intermédiaire des méthodes de celui-ci.
- Lignes 155 à 172 : La fenêtre d'application est elle aussi un widget dérivé de Frame(). Son
constructeur instancie les deux canons et leurs pupitres de pointage, en plaçant ces objets dans
les deux dictionnaires self.guns et self.pupi. Cela permet d'effectuer ensuite divers traitements
systématiques sur chacun d'eux (comme par exemple à la méthode suivante). En procédant ainsi,
on se réserve en outre la possibilité d'augmenter sans effort le nombre de ces canons si
nécessaire, dans les développements ultérieurs du programme.
- Lignes 174 à 184 : Cette méthode est invoquée après chaque tir pour déplacer aléatoirement les deux canons, ce qui augmente la difficulté du jeu.
15-1-4. 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.
A) 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 par exemple 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.
B) 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 deplacer() de la classe Canon(). 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.
C) 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.
D) 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 oeuvre 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 :
- la technique des sockets, qui permet d'établir une communication entre deux ordinateurs ;
- la technique des threads, qui permet à un même programme d'effectuer plusieurs tâches simultanément (cela nous sera nécessaire, si nous voulons construire une application capable de communiquer en même temps avec plusieurs partenaires).
Ces matières ne font pas strictement partie des objectifs que nous nous sommes fixés pour ce
cours, et leur 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 :
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 amélioré"""
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.
"déplacement, 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 déplacer 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 déplacer 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.
# déplacement 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.
# déplacer le canon qui vient de tirer :
47.
self.appli.depl_aleat_canon
(
self.id)
48.
# cacher l'obus (en l'expédiant hors du canevas) :
49.
self.boss.coords
(
self.obus, -
10
, -
10
, -
10
, -
10
)
50.
51.
def
effacer
(
self):
52.
"faire disparaître 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.
'''Fenêtre 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 présents
68.
self.pupi =
{} # dictionnaire des pupitres présents
69.
self.specificites
(
) # objets différents dans classes dérivées
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"
),(
"Roméo"
,"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 côté à chaque itération
86.
87.
def
depl_aleat_canon
(
self, id):
88.
"déplacer aléatoirement le canon <id>"
89.
gun =
self.guns[id]
90.
dx, dy =
randrange
(-
60
, 61
), randrange
(-
60
, 61
)
91.
# déplacement (avec récupération des nouvelles coordonnées) :
92.
x, y =
gun.deplacer
(
dx, dy, True
)
93.
return
x, y
94.
95.
def
coord_aleat
(
self, s):
96.
"coordonnées aléatoires, à 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 n°i signale qu'il a atteint l'adversaire n°j"
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 opposés :
109.
p =
1
# on gagne 1 point
110.
else
: # ils sont dans le même sens :
111.
p =
-
2
# on a touché un allié !!
112.
self.pupi[i].attribuerPoint
(
p)
113.
# celui qui est touché perd de toute façon un point :
114.
self.pupi[j].attribuerPoint
(-
1
)
115.
116.
def
dictionnaireCanons
(
self):
117.
"renvoyer le dictionnaire décrivant les canons présents"
118.
return
self.guns
119.
120.
if
__name__
==
'__main__'
:
121.
AppBombardes
(
650
,300
).mainloop
(
)
Commentaires :
- Ligne 6 : La forme d'importation utilisée à la ligne 4 nous permet de redéfinir une nouvelle
classe Canon() dérivée de la précédente, tout en lui conservant le même nom. De cette manière,
les portions de code qui utilisent cette classe ne devront pas être modifiées (Cela n'aurait pas été
possible si nous avions utilisé par exemple : « from canon03 import * »).
- Lignes 11 à 16 : La méthode définie ici porte le même nom qu'une méthode de la classe parente.
Elle va donc remplacer celle-ci dans la nouvelle classe (On pourra dire également que la
méthode deplacer() a été surchargée). Lorsque l'on réalise ce genre de modification, on
s'efforce en général de faire en sorte que la nouvelle méthode effectue le même travail que
l'ancienne quand elle est invoquée de la même façon que l'était cette dernière. On s'assure ainsi
que les applications qui utilisaient la classe parente pourront aussi utiliser la classe fille, sans
devoir être elles-mêmes modifiées.
Nous obtenons ce résultat en ajoutant un ou plusieurs paramètres, dont les valeurs par défaut forceront l'ancien comportement. Ainsi, lorsque l'on ne fournit aucun argument pour le paramètre rel, les paramètres x et y sont utilisés comme des coordonnées absolues (ancien comportement de la méthode). Par contre, si l'on fournit pour rel un argument « vrai », alors les paramètres x et y sont traités comme des déplacements relatifs (nouveau comportement). - Lignes 17 à 33 : Les déplacements demandés seront produits aléatoirement. Il nous faut donc
prévoir un système de barrières logicielles, afin d'éviter que l'objet ainsi déplacé ne sorte du
canevas.
- Ligne 42 : Nous renvoyons les coordonnées résultantes au programme appelant. Il se peut en
effet que celui-ci commande un déplacement du canon sans connaître sa position initiale.
- Lignes 44 à 49 : Il s'agit encore une fois de surcharger une méthode qui existait dans la classe
parente, de manière à obtenir un comportement différent : après chaque tir, désormais on ne
disperse plus tous les canons présents, mais seulement celui qui vient de tirer.
- Lignes 51 à 55 : Méthode ajoutée en prévision d'applications qui souhaiteraient installer ou
retirer des canons au fil du déroulement du jeu.
- Lignes 57 et suivantes : Cette nouvelle classe est conçue dès le départ de manière telle qu'elle puisse aisément être dérivée. C'est la raison pour laquelle nous avons fragmenté son constructeur en deux parties : La méthode __init__() contient le code commun à tous les objets, aussi bien ceux qui seront instanciés à partir de cette classe que ceux qui seront instanciés à partir d'une classe dérivée éventuelle. La méthode specificites() contient des portions de code plus spécifiques : cette méthode est clairement destinée à être surchargée dans les classes dérivées é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 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é.
15-2-1. Principe▲
Le «jeu» mis en oeuvre 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. A vous de déterminer lesquelles !
(Ne négligez pas d'étudier le cas des grilles 1 x n).
Note : Vous trouverez la discussion complète du jeu de Ping, sa théorie et ses extensions, dans la revue « Pour la science » n° 298 - Août 2002, pages 98 à 102.
15-2-2. 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.
Cahier des charges du logiciel à développer
L'application sera construite sur la base d'une fenêtre 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 carrées.
-
Les options du menu permettront de :
-
choisir les dimensions de la grille (en nombre de cases)
-
réinitialiser le jeu (c'est-à-dire disposer tous les pions avec leur face blanche au-dessus)
-
afficher le principe du jeu dans une fenêtre 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-même installé dans un cadre (frame). En fonction des redimensionnements opérés par l'utilisateur, le cadre occupera à chaque fois toute la place disponible : il se présente 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 carrées, 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 méthode-gestionnaire pour l'événement <clic du bouton gauche>. Les coordonnées de l'événement serviront à déterminer 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 présents seront alors « retournés » (échange des couleurs noire et blanche).
###########################################
# Jeu de ping #
# Références : Voir article de la revue #
# <Pour la science>, Aout 2002 #
# #
# (C) Gérard 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 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 =
'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-même un canevas. A 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 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 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.
\n
Le jeu consiste a essayer "
\
"de les retouner tous.
\n\n
Si 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
A 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
(
)
Rappel : Si vous souhaitez expérimenter ces programmes sans avoir à les réécrire, vous pouvez trouver leur code source à l'adresse : http://www.ulg.ac.be/cifen/inforef/swi/python.htm