.. MPRI Bomberman (Net Programming Project) documentation ===================================== Rapport pour le projet MPRI Bomberman ===================================== A propos -------- Comme demandé, ce rapport a pour objectif de préciser certains points à propos de mon *projet réseau* (pour le cours 1-21 du MPRI). .. note:: Ce projet est terminé, et est désormais publiquement distribué (sous la Licence GPL v3). J'ai eu *16.9/20* pour ce cours (seconde meilleure note). Un *documentation* **plus complète** de mon projet est disponible ici : * sur ma page web du `cr@ns `_; * sur ma page web du `département d'informatique de l'ENS `_. .. note:: Cette documentation intègre de nombreuses captures d'écrans, démontrant les differentes fonctionnalités, et des informations précises sur les différentes parties. Sources ------- Les sources doivent être dans une *archive* ``projet_reseau.tar.xz``, reçu avec ce rapport, ou disponible en ligne ici : * sur ma page web du `cr@ns `_; * sur ma page web du `département d'informatique de l'ENS `_. Les sources contiennent un fichier ``INSTALL`` (`ici `_), décrivant : * les dépendances nécessaires du projet (seulement **Python 2.7.1** et **PyGame**); * comment lancer le *client* et le *serveur*. .. Plus de détails sont trouvables dans la doc (qui est en *HTML*, plus lisible que le fichier texte *INSTALL*). Organisation des sources ------------------------ Voici un diagramme *UML* **coloré** qui montre **les différents modules** (*i.e.* les fichiers ``.py`` dans le répertoire principal) constituant le projet, et **leur liens de dépendances**. .. image:: images/packages_Bomberman.* :scale: 100 % :width: 1080 px :align: center :alt: diagramme SVG .. :width: 6000 px .. :height: 4000 px Description des différents modules ################################## Comme ce graphe est un peu confus, **je vais détailler point par point** les différents modules. Les couleurs ont leurs importances, rangeant les modules dans une catégorie, que l'on explique ici. Programmes principaux ~~~~~~~~~~~~~~~~~~~~~ Les **deux programmes principaux du projet** sont ``BombermanClient.py`` et ``BombermanServer.py`` (*en vert et encerclé*) : respectivement le **client** et le **serveur**. 1. Chacun de ses deux programmes doivent être *lancés en ligne de commande*. Les options utiles sont ``--server`` et ``--port``, permettant de changer les paramètres des *communications réseaux*. Une aide complète sur les options est accessible avec l'option ``--help``. 2. Une autre méthode pour paramétrer le comportement du *client* ou du *serveur* est d'éditer *à la main* un des deux fichiers de configuration ``ConfigClient.py`` our ``ConfigServer.py``. Ces deux fichiers définissent certaines constantes, avec une courte description pour chaque. .. note:: *Par exemple*, changer la variable ``USE_NOTIFY`` dans l'un des deux fichiers ``Config`` à ``False`` désactive l'utilisation des notifications systèmes pour le *client* ou le *serveur*. 3. J'ai aussi rédigé une petite *surcouche* au client, qui permet de lancer un “bot” (*i.e* un joueur jouant tout seul). Le module concerné est ``IA_Bomberman.py``. Le graphe de dépendances montré plus haut montre le module ``IA_Bomberman`` en *vert* tout en bas, seulement dépendant de ``BombermanClient``. Pour l'instant, ce *bot* est très limité : il joue presque complètement **aléatoirement** ! On peux néanmoins configurer sa **fréquence** (*i.e.* le nombre de mouvements par secondes) par l'option ``--frequency``. Syntaxe des messages ~~~~~~~~~~~~~~~~~~~~ La **syntaxe des messages est définie** par les deux modules ``ParseMessageIn`` et ``ParseMessageOut``, en *rouge et encerclé*. Ces deux modules permettent d'abstraire la syntaxe des messages, et d'utiliser des fonctions comme .. code:: python ParseMessageOut.str_of_newplayer(my_player) Cette fonction donne le message initial permettant de se déclarer comme un joueur à son serveur. Dans notre protocole, c'est ``NEW_PLAYER(pseudo)``. On voit 4 liens disant que le client et le serveur utilisent *tout deux* ces deux modules. Autres modules ~~~~~~~~~~~~~~ * Les deux modules en *violet*, ``KeyBinding``, et ``AffichPygame``, permettent de gérer *l'interface graphique*. 1. ``KeyBinding`` permet de gérer facilement les évenements clavier, et de paramétrer autant qu'on le souhaite les touches associées aux actions. Il n'y a ni option ni variable à changer dans ``ConfigClient`` pour changer ces associations de touches clavier, mais on peut changer la variable ``arrows`` dans le fichier ``KeyBinding.py``: .. code:: python def arrows(): return KeyBinding(up=['K_UP'], down=['K_DOWN'], left=['K_LEFT'], right=['K_RIGHT'], bomb=['K_SPACE'], help=['K_DELETE', 'K_h']) 2. ``AffichPygame`` utilise le *framework* **PyGame**, un binding de la bien connue **SDL** pour Python. Y sont définis les noms des fichiers de musiques et d'images utilisés par le client, et aussi quelques fonctions pour facilement afficher une instance de la classe ``Board.Board``. A noter que le module ``pygame`` est aussi utilisé directement dans ``BombermanClient``. * Les modules *en bleu* et *encadrés* définissent la représentation abstraite des variables du jeu, toutes implémentées par un **approche objet**. 1. ``Bomb`` définit la classe ``Bomb.Bomb``, représentant une bombe. En effet, bien que les bombes aient toutes les mêmes paramètres dans notre *protocole*, j'ai préféré faire une classe *haut niveau* qui permet potentiellement de changer plus facilement les différents paramètres d'une bombe (comme sa force, sa distance d'explosion, son *timer* ou son possesseur). Cela afin de faciliter le *futur* système de *bonus*. 2. ``Bonus`` *est utilisé mais n'a pas d'importance* pour le moment, car nous n'avons pas rajouté de système de bonus dans notre protocole. Mais les bonus sont bien là ! Ce module en définit quelques uns (comme la *pomme empoisonnée* qui blesse un joueur, le *quart de coeur* qui rend de la vie, ou l'*épée*, qui augmente la force des bombs du joueur). Par ailleurs, ``AffichPygame`` peut aussi afficher ces bonus. .. note:: Comme je n'ai pas modifié le protocole, le système de bonus n'est pas allé plus loin. 3. ``Player`` représente de façon abstraite un *joueur* : * par la classe ``Player.PlayerServer``, s'il doit être utilisé **sans réseau**. Un instance de cette classe contient le *pseudo* du joueur, sa *position*, son *nombre de bombe* posée sur le plateau courant, et sa *couleur*. .. note:: Le mode textuel supporte la paramétrisation de la couleur de son joueur, même si cette fonctionnalité à été retirée pour rester conforme avec notre protocole, fixé à l'avance. * par la classe ``Player.Player``, s'il doit être utilisé par le client (et donc, **avec du réseau**, voir plus bas `player`_). Cette seconde classe *hérite* de la première. 4. ``Matrix`` est un module très simple qui permet de manipuler facilement un **matrice** générique (et polymorphique). Cela permet de voir le *plateau de jeu* comme une matrice d'états, définis ici : 5. ``Board`` définit deux classes, ``Board.State`` et ``Board.Board``, qui, respectivement, codent *un état du plateau* et *le plateau complet*. Un état contient : * zéro ou une bombe (instance de ``Bomb.Bomb``); * des informations pour dire s'il y a un mur, et si oui s'il est cassable ou non; * une *liste* (vide ou non) de joueurs (instances de ``Player.Player`` ou ``Player.PlayerServer``); * un tag pour savoir si l'interface doit afficher une *explosion*. * Le module ``Constants`` (en *noir et encadré*) définie toutes les constantes importantes pour le jeu (comme le nombre initial de point de vie), et il est partagé par tous les autres modules. * Le projet utilise un **outil externe** pour *parser* les messages *arrivant* : ``scanf`` (en *jaune*). Le graphe montre bien que seul le module ``ParseMessageOut`` utilise cette dépendance externe (qui est quand même distribuée avec l'archive du projet). * Pour **l'interface en ligne de commande**, le module ``ParseCommandArgs`` (en *noir, gras*, avec un *losange*) a permis de facilement mettre en place le système d'options et l'aide (accessible avec ``--help``). * Pour **l'interface en mode texte**, les couleurs *ANSI* sont utilisées pour afficher du texte en *couleurs* dans le terminal, afin d'être plus lisible. Le module ``ANSIColors`` a été réalisé pour faciliter cela. Ce module est devenu assez puissant, et plutôt efficace ! Exemple : .. code:: python # ANSIColors utilise des balises, entre chevrons. ANSIColors.printc("Le drapeau français contient du bleu, donc il est joli !") * Enfin, on voit en bas à droite deux modules, **isolés du reste** (en *jaune*) : 1. ``conf`` n'est pas du code, c'est juste la configuration pour l'outil de documentation **Sphinx**. 2. ``PyRlwrap.py`` n'est pas vraiment un module, mais un script permettant de *wrapper* un programme en mode textuel avec la bibliothèque *readline*. Il a été pensé comme un petit clone de **ledit** ou de **rlwrap**. Celà permet, *par exemple*, de lancer le serveur avec des facilités d'édition (historique, raccourcis d'édition “à la GNU Nano”): .. code:: sh $ ./PyRlwrap.py ./BombermanServer.py --server 138.231.36.71 --port 12882 Ce script a sa raison d'être si on souhaite utiliser le *chat* IRC intégré aux deux phases du jeu. * Il y a aussi les 4 modules du **premier TP**, réalisé en *Octobre* : ``IRCclient``, ``IRCserver``, ``ToolReadline`` et ``PyZenity``. Ces 4 modules sont donc **obsolètes** désormais (mais fonctionnent encore !). Quels modules en charges du réseau ? #################################### Je détaille ici dans quel module le réseau est utilisé, et pourquoi. * La partie réseau se trouve surtout dans le *client* et le *serveur*. Comme le décrit notre spécification formelle (dans `les slides <../../specification_slides.pdf>`_), nous utilisons **TCP**. Les détails concernant le choix entre ``thread`` et ``select`` pour le réseau sont décrits plus bas (ici `thread`_). .. _player: * Un peu de plomberie réseau se trouve aussi dans le module ``Player``, qui définit la classe ``Player.Player``. Cette classe représente un joueur tel qu'utilisé par le *client*, et implémente quelques méthodes agissant comme une surcouche aux méthodes *bas niveau* du module ``socket``. 1. La méthode ``connect`` permet d'initialiser la connection, puis d'envoyer le *pseudo* du joueur. Des messages informatifs sont aussi affichés. Par exemple, ce morceau choisit de ``Player.Player.connect`` montre les parties utilisant les méthodes des ``socket`` : .. code:: python def connect(self, info_server=info_server_Init): # self est une instance de Player.Player ... # On créé le socket du joueur, stocké dans l'attribut 'socket_player' self.socket_player = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) # On le connecte au serveur self.socket_player.connect(info_server) ... # On envoit le pseudo du joueur. self.send( ParseMessageOut.str_of_newplayer(self) ) 2. Les autres méthodes sont aussi des légères surcouches aux méthodes classiques sur les ``socket``. * ``send`` permet d'envoyer un message au serveur auquel est connecté le joueur. * ``close`` tente de fermer proprement la connection entre le joueur et le serveur. .. _thread: Thread ou select ################ J'explique ici les choix faits pour le **serveur** et le **client**. * ``BombermanClient`` fonctionne en deux étapes, une phase d'attente (où il attend de recevoir la carte), et une boucle infinie de la partie de Bomberman où il joue avec son serveur. Je me suis basé sur mon client *IRC* du **TP1** (``IRCclient``) pour le client Bomberman, en particulier en continuant **d'écouter l'entrée texte standard** côté client : en effet, je voulais intégrer un *chat* IRC à mon client et à mon serveur. La principale raison est pour le débug des messages : pouvoir les envoyer depuis le *clavier* est assez pratique. Donc, le client bomberman en **phase d'attente** (comme le chat IRC) utilise **deux threads** : 1. le premier lit l'entrée standard du clavier, et envoit au serveur; 2. le second lit le serveur, et affiche vers la sortie standard. Dans la seconde phase, un **premier thread** *chez le client* permet de : 1. **lit les évenement** depuis le clavier (*par exemple*, la touche *Espace*); 2. les comprends comme des **demandes d'action** via ``KeyBinding`` (*par exemple*, déposer une bombe); 3. les enrobes dans un **message** texte via ``ParseMessageOut`` (*par exemple*, ``PLANT_BOMB``); 4. **les envoit** au serveur (et retourne en 1.); 5. à noter que ce thread **s'occupe aussi d'actualiser la carte affichée** (avec ``PyGame`` et ``AffichPygame``). Dans la seconde phase, un **second thread** permet de : 1. **lire les ordres venant du serveur**; 2. **les appliques au jeu** (et retourne en 1.). A noter que notre protocole donne toute puissance au serveur : le client **peut** prendre ses ordres comme vérité absolues. Normalement, comme on **utilise TCP**, on est certain que les ordres du serveur arriveront au client, donc normalement un client ne *peut* pas commencer à faire n'importe quoi, et à s'éloigner du jeu *modèle* qu'est celui du **serveur**. En pratique, un autre *threads* permet de gérer l'affichage des explosions (qui montrent un effet visuel restant 3 secondes à l'écran). C'est donc en partie cette utilisation de *threads* pour l'affichage qui justifie leur utilisation pour gérer les échanges réseaux. * ``BombermanServer`` fonctionne aussi en deux phases : un **salle d'attente**, et une **boucle infinie** où il joue le rôle d'arbitre. Je me suis (aussi) basé sur mon serveur *IRC* du **TP1**, et donc ces deux phases sont réalisées avec ``select``. Je trouve ça plus simple : cela évite d'avoir à lancer un nouveau thread (voir, *deux nouveaux* threads, un pour écouter le client et un pour écouter le clavier) pour chaque nouveau client. J'utilise ensuite ``my_socket.makefile().readline()`` pour **lire une ligne** depuis le *socket* sélectionné. Cela m'évite de rencontrer un problème sur lequel *Vincent* a passé beaucoup de temps : après le ``select``, il ne faut pas faire plus d'un ``my_socket.read()``, car sinon ils sont bloquants (*enfin, d'après ce que j'en ai compris*). Dans les deux phases, si le *socket* sélectionné est celui d'un client, on écoute ce qu'il a à dire, et on interprète son message : * si c'est une ordre de mouvement (comme ``MOVE_UP`` ou ``DROP_BOMB``) : 1. le serveur tente de le valider; 2. si oui, il l'applique à sa version locale du jeu; 3. et le renvoit **tout de suite** à tous les clients encore connectés (sous forme d'un ordre *absolu* : ``MOVE_PLAYER(id,x,y)`` et non quelque chose de *relatif* comme ``delta(id, dx, dy)``). * si non, le drop. .. note:: A noter qu'il est aussi possible de l'afficher, voir même de le retransmettre à chaque client : et ainsi de faire comme un *serveur IRC*. C'est ce qu'on avait désigné comme *un serveur pédagogue* dans notre rapport initial. Mais on ne le fait pas. Si le *socket* sélectionné est celui de l'entrée standard, le *serveur* est à l'écoute de certaines commandes pour “l'administrateur du serveur”. Par exemple, ``\help`` pour la liste des commandes, et ``\kill=1`` pour tuer le second joueur déjà connecté. Ces *commandes* sont surtout utiles dans la *première phase* pour manager la salle d'attente. Limitations ----------- De mon implémentation ##################### Mon implémentation présente quelques faiblesses : * N'accepte pas des pseudos avec des caractères spéciaux ou des espaces dedans; * Parfois, le client ne réussit pas comprendre le message final (``GAME_OVER(id)``) qui annonce le joueur gagnant; * Le serveur dépend de ``PyGame``, et demande à être lancé avec un serveur graphique. Ce qui est dommage, car il n'utilise pas d'affichage autre que la fenètre du terminal. Malgré ça, cela permet de **réduire** de façon efficace la vitesse du serveur, et éviter de **consommer trop de CPU**, ce qui est une des faiblesses des deux serveurs de Lucas et Vincent. En effet, je fixe **à 40 le nombre de parcours par seconde** de la boucle principale de l'algorithme d'écoute - validation - renvoie. La valeur peut aussi être changé dans le fichier de conf ``ConfigServer.py`` : .. code:: python CLOCK_FREQUENCY = 40.0 # Nombre maximum de parcours de la main boucle par secondes. De notre protocole ################## Notre protocole est assez limité : - constantes du jeu fixées à l'avance (car non transmises par le serveur), en particulier : * la taille de la carte (``lx``, ``ly``); * le nombre de joueur (``nbmax``); * le nombre de PVs initiaux, de bombes maxs posées par un joueur en même temps; * la force et le timer des bombes; - pas de système de bonus : dû aux choix de ces constantes; - pas de messages “pédagogiques” envoyés par le serveur pour informer un client qu'il envoie des requêtes qui sont refusées; - pas de système de salle d'attente. Notre protocole présente aussi la faiblesse suivante : Si deux clients renseignent le même pseudo, notre protocole n'a pas la possibilité d'assurer le bon fonctionnement de la partie. En effet, les joueurs se basent sur le message initial ``GAME_START(map....map...;PSEUDO1,x1,y1;..;PSEUDOn,xn,yn)``. Une méthode pour régler ce problème est de purement interdire des pseudos différents, côté serveur. **Je n'ai pas fait ce choix là : le jeu se lance quand même, mais marchera mal !** Difficultés rencontrées ----------------------- Voici une rapide liste des difficultés rencontrées durant le projet : * prise en main de **PyGame** assez difficile ce qui explique les limitations de l'interfaces : - la fenètre s'actualise tout le temps, sans pouvoir être paresseuse et ne s'actualiser que s'il y a eu un changement; - pas d'informations en plus affichées dans la fenètre (comme les PVs des joueurs ou le nombre de bombes posées). * outil de parsing assez limité : ``scanf`` rencontre très vite des soucis avec des espaces ou des caractères spéciaux. Et je me suis rendu compte de ses limitations vraiment tard : quand on a commencé à tester nos projets ensembles. * utilisation des *threads* très délicates : - Python galère à afficher un *backtrace* précis des problèmes lancés dans un thread; - impossibilité de tuer un *thread* dans l'appelant, ce qui aurait été pratique pour être certain de ne pas laisser tourner des sous processus une fois que le programme principal est terminé. Auteur ------ Ce document est le rapport pour le cours 1-21 du MPRI, pour Lilian Besson (vous pouvez me contacter via mon mail : `lilian dot besson at normale dot fr `_). Nous étions le *groupe 5* : * Lilian Besson, * Lucas Hosseini, * Vincent Cohen-Addad. .. lun. 18/02/2013 at 17h:51m:04s