Projet Château¶
Ce projet de jeu type escape game va nous permettre d'aborder divers aspects de la programmation, en une application de taille raisonnable. Nous verrons la conception et l'implémentation orientée objet. L'interface graphique sera rudimentaire et utilisera le module Turtle.
Le jeu¶
Enfermé dans un château, représentant un labyrinthe de succession de pièces, un héros (vous) doit se frayer un chemin vers la sortie. Des portes verrouillées vous bloqueront souvent le passage et vous ne pourrez les ouvrir qu'en répondant correctement à des questions.
Heureusement, à divers endroits dans le château vous pourrez ramasser des indices, qui aident à répondre aux énigmes.
La mécanique¶
Elle est simple : le jeu se passe dans une fenêtre graphique Turtle, le héros est matérialisé par une petite balle rouge que vous dirigez via les flèches du clavier. Les questions sont déclenchées lorsqu'on passe à certains endroits (repérés en orange sur l'animation ci-dessous) et affichées par un système de popup du module Turtle.
Analyse¶
Une première analyse permet de mettre en avant les différents acteurs de notre futur programme. Ces acteurs deviendront des objets au sens de la POO.
En partant du jeu, nous pouvons identifier sans mal :
- un château
- un héros
En ayant un peu l'habitude des jeux d'aventure ou alors de la programmation objet, on sent qu'il va falloir :
- un maître de jeu
C'est le maître de jeu qui fait l'interaction avec l'utilisateur. En règle général, on limite au strict minimum les endroits du programme qui ont des interactions avec l'utilisateur.
Les tortues du jeu¶
Lorsqu'on connaît un peu le module Turtle, on peut tout de suite décider du nombre de tortues qu'on utilisera : la tortue se déplace, et permet d'écrire à la position où elle se trouve. À chaque déplacement il faut donc calculer l'endroit. Moralité : moins une tortue se déplace mieux c'est. On constate que :
- le héros bouge beaucoup, il sera plus simple de lui dédier une tortue ;
- des messages sont régulièrement affichés en haut de l'écran, il sera donc plus simple d'y consacrer une tortue qui ne fera que ça, sans jamais bouger ;
- les indices trouvés sont affichés à droite, là aussi plutôt que de déplacer régulièrement une tortue, on va en réserver une à cette tâche ; après l'affichage d'un indice, elle n'aura qu'à descendre de quelques pixels pour le suivant ;
- ces 2 tortues seront créées et gérées par le maître de jeu qui s'occupera aussi de l'écran qui fera la gestion des événements et les interactions avec l'utilisateur (poser les questions et récupérer les réponses)
- le château devra se dessiner et mettre à jour les cellules visitées, il aura aussi sa propre tortue.
Au total ce sont donc quatre tortues et un écran qui seront consacrés à l'interface graphique.
Le modèle du château¶
Maintenant que nous savons qui va gérer les tortues, nous pouvons détailler le modèle du château. Il est constitué, on le voit, de petites cellules carrées de différents types :
- des cellules vides (en blanc sur l'animation)
- des murs (en gris)
- des portes (en orange lorsqu'elles sont verrouillées et qui semblent disparaître ensuite)
- des objets (en vert, eux aussi disparaissent une fois visités)
- des cellules visitées qui sont en corail.
Nous avons donc identifié un quatrième objet : la cellule. Le chateau
aura donc une liste de cellules. La cellule possèdera un type visiblement. Parmi les cellules on en a deux particulières : la Porte et l'Objet. Ces deux objets vont dériver de l'objet cellule et avoir des comportements un peu différents.
Les objets cellule n'auront pas d'interaction avec l'utilisateur et passeront par l'objet Chateau
pour s'afficher.
Le schéma ci-dessous résume cette analyse :
Le maître de jeu¶
C'est le point d'entrée de notre programme et l'interlocuteur de l'utilisateur. Notre programme principal ressemblera donc à :
escape_game = MaitreDejeu()
escape_game.setup()
escape_game.start()
Détaillons un peu le contenu de notre classe MaitreDejeu
. Nous l'avons vu, il possède 2 tortues et 1 écran. Il va créer le Chateau
et le Heros
. Pour les propriétés ce sera tout.
class MaitreDeJeu:
def __init__(self):
self.chateau = Chateau(self)
self.heros = Heros(self.chateau, self)
self.controleur = Screen.Turtle()
self.msg_view = turtle.Turtle()
self.sac_view = turtle.Turtle()
setup
¶
Concernant les actions, la première sera de procéder à quelques réglages :
- récupérer les fichiers du jeu à savoir :
- le plan du château
- le dictionnaire des portes
- le dictionnaire des objets
- quelques réglages des tortues et les positionner
- demander au
chateau
de faire ses propres réglages - demander au
heros
d'effectuer les siens.
Pour les fichiers, nous utilisons le module argparse
qui permet de gérer les options passées à la ligne de commande. Il offre aussi gratuitement un -h
qui permet de voir comment appeler le script.
Voici par exemple l'appel :
python3 chateau.py -h
usage: chateau.py [-h] [-s SET]
optional arguments:
-h, --help show this help message and exit
-s SET, --set SET numéro du set de fichiers
Nous avons opté pour un set de fichiers (set au sens familier du terme, pas l'objet Python) ainsi nous avons par exemple :
plan_chateau_1.txt
dico_objets_1.txt
dico_portes_1.txt
Avec l'appel
python3 chateau.py -s 1
Nous lançons notre script avec le set 1. Le setup
dans son intégralité : c'est la méthode la plus longue du script.
def setup(self):
# Récupération des fichiers
# ---
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--set', \
help='numéro du set de fichiers', type=int)
args = parser.parse_args()
set_fichier = int(args.set) if args.set else 1
fichier_plan = f'fichiers/plan_chateau_{set_fichier}.txt'
fichier_objets = f'fichiers/dico_objets_{set_fichier}.txt'
fichier_portes = f'fichiers/dico_portes_{set_fichier}.txt'
# Réglages des tortues messagères
# ---
self.msg_view.ht()
aller(self.msg_view, *POINT_AFFICHAGE_ANNONCES)
self.sac_view.ht()
self.sac_view.up()
self.sac_view.right(90)
aller(self.sac_view, *POINT_AFFICHAGE_INVENTAIRE)
self.note_inventaire('INVENTAIRE')
# Setup du chateau et du heros
# ---
entree = self.chateau.setup(fichier_plan,\
fichier_objets,\
fichier_portes)
self.heros.setup(entree, self.chateau.ech)
start
¶
Une fois les réglages effectués, on peut lancer le jeu :
- faire une petite annonce à l'attention de l'utilisateur pour préciser que le jeu commence et que le but est de conduire le héros vers la sortie ;
- demander au
chateau
de se dessiner ; - demander au
heros
de se placer à l'entrée (entrée qui a été initialisée au moment des réglages et passée auheros
) ; - faire les bind, c'est le coeur d'une application événementielle ;
- lancer l'écoute sur les event et la boucle principale de Turtle.
def start(self):
self.annonce(MSG_DEBUT)
self.chateau.init_view()
self.heros.update_view()
self.bind()
self.controleur.listen()
self.controleur.mainloop()
Les bind¶
En programmation événementielle, binder signifie associer des événements à des fonctions. Ici, on veut associer aux quatre flèches directionnelles les actions de mouvements du héros :
def bind(self):
self.controleur.onkey(self.heros.move_up, KEY_UP)
self.controleur.onkey(self.heros.move_down, KEY_DOWN)
self.controleur.onkey(self.heros.move_left, KEY_LEFT)
self.controleur.onkey(self.heros.move_right, KEY_RIGHT)
Le heros
¶
Nous y sommes arrivés par le mdj
(maître de jeu) qui a binder les fonctions de déplacement. Le heros
a donc besoin :
- de coordonnées x et y ;
- d'une échelle ; pour passer du système de coordonnées du modèle qui est une grille discrète (19x27 sur l'exemple de l'animation) à des coordonnées de la fenêtre graphique de turtle ;
- du
chateau
bien sûr ; - du
mdj
; - et d'une tortue.
class Heros:
def __init__(self, chateau, mdj):
self.x = None
self.y = None
self.ech = 0
self.chateau = chateau
self.mdj = mdj
self.view = turtle.Turtle()
À sa création, le heros
n'a aucune position ; il l'initialisera lors de setup une fois que le chateau
lui aura transmis l'entrée.
Mais laissons ces détails techniques pour nous intéresser au coeur du mécanisme : lorsque l'utilisateur presse la touche flèche BAS (par exemple), la méthode move_down()
du heros
est activée.
Le déplacement du heros
¶
La façon dont turtle gère les événement oblige à passer une fonction sans paramètre lors du bind. Ainsi, move_down()
ne prend aucun paramètre et appelle la vraie fonction de déplacement avec en paramètre la direction dans laquelle le heros
est censé bouger.
def move_down(self):
self.avancer(DOWN)
def avancer(self, direction):
dx, dy = direction
origine = self.chateau.cell(self.x, self.y)
if self.chateau.inside(self.x + dx, self.y + dy):
destination = self.chateau.cell(self.x + dx, self.y + dy)
if destination.accessible():
origine.seen()
self.goto(dx, dy)
self.update_view()
if self.found_exit():
self.mdj.end()
else:
destination.interaction()
Voici ce que fait cette méthode avancer
qui est la méthode du jeu :
- on récupère le delta de déplacement correspondant à la direction ;
- on mémorise la cellule d'
origine
, celle où se trouve leheros
; - si la future destination est bien dans les limites du
chateau
:- on mémorise la cellule
destination
; - si la
destination
est accessible (si ce n'est pas un mur, ou une porte verrouillée ou un objet non encore ramassé) alors :- on marque la case d'
origine
comme déjà visitée, - on se déplace effectivement,
- et on met à jour notre affichage,
- on regarde si on a trouvé la sortie... auquel cas on dit au
mdj
que c'est fini,
- on marque la case d'
- on mémorise la cellule
- sinon il faut lancer une interaction avec la case destination.
Les Cell
(les cases du chateau
)¶
Le chateau
est composé de cellules, qui possèdent une position, un type et un lien vers le chateau
.
class Cell:
def __init__(self, type_cell, x, y, chateau):
self.x = x
self.y = y
self.type = type_cell
self.chateau = chateau
Les types, sept au total, sont modélisés par de simples entiers :
VIDE
= 0MUR
= 1SORTIE
= 2PORTE
= 3OBJET
= 4ENTREE
= 5SEEN
= 6
Les types VIDE
, ENTREE
, SORTIE
et SEEN
sont toujours accessibles et ne sont pas des cellules si particulières. Les Mur
ne sont pas très particulières non plus et toujours inaccessibles. Notons quand même qu'une porte déverrouillée devient VIDE
ainsi qu'un objet ramassé.
Une Cell
générique ne fait pas grand chose ; elle peut changer de type et prévenir le chateau
pour une mise à jour de l'affichage d'elle-même :
def change_type(self, type_cell):
self.type = cell_type
self.chateau.update_view(self)
Une Porte
¶
C'est une spécialisation de Cell
qui possède deux propriétés supplémentaires :
- une question
- une solution
class Porte(Cell):
def __init__(self, *args):
self.question = str()
self.solution = str()
Cell.__init__(self, PORTE, *args)
Notez le petit aspect technique : le constructeur de la Porte
fait appel au constructeur de la classe mère en lui repassant tous les arguments et en précisant son type : PORTE
.
Au moment de la création du chateau
les portes sont créées (positions, type etc.) mais les questions et les solutions ne sont pas encore connues. Elles seront mises à jour dans la phase setup.
La partie la plus intéressante est l'interaction avec une porte :
Si la porte est verrouillée, elle demande au chateau
de l'annoncer à l'utilisateur puis de lui poser la question. La réponse est transmise à la porte. Si c'est une bonne réponse, la porte se déverrouille et le chateau
a ordre de l'annoncer ; sinon, il annonce que la porte va rester verrouillée. Le code objet est une traduction fidèle de ce comportement :
def interaction(self):
if self.verrouillee():
self.chateau.annonce(MSG_PORTE_VERROUILLEE)
reponse = self.chateau.ask(self.question)
if self.bonne_reponse(reponse):
self.deverrouille()
self.chateau.annonce(MSG_PORTE_DEVERROUILLEE)
else:
self.chateau.annonce(MSG_MAUVAISE_REPONSE)
Pour une Porte
se déverrouiller consiste simplement à changer de type :
def deverrouille(self):
self.change_type(VIDE)
L'autre Cell
spéciale est l'Objet
.
L'Objet
¶
L'Objet
possède aussi une propriété supplémentaire par rapport à une Cell
lambda : son nom. Qui sera la chaîne de caractères affichée dans la fenêtre de jeu lorsque l'objet est ramassé (on aura compris qu'il n'y a pas réellement un objet).
def __init__(self, *args):
self.nom = str()
Cell.__init__(self, OBJET, *args)
On retrouve le même mécanisme d'appel au constructeur de la classe mère que pour la Porte
.
L'Objet
a aussi une interaction possible : le chateau
annonce à l'utilisateur le nom de l'objet ; puis l'objet change de type.
def interaction(self):
self.chateau.donner(self.nom)
self.change_type(VIDE)
Conclusion¶
Nous avons vu comment en partant du problème et des objets évidents, nous avons enrichi le modèle pour que les interactions entre les objets soient naturelles et celles avec l'utilisateur regroupées en un seul objet.
La description des objets en partant du principal, celui qui est aussi le point d'entrée du jeu conduit assez naturellement à passer en revue les différents objets et leurs principales méthodes.
Celles que nous avons tues sont juste des méthodes techniques sans grand intérêt comme le calcul de coordonnées (changement d'échelle entre le modèle et la vue) ou l'utilisation des méthodes de la tortue pour dessiner.