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).
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.
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.
- Les deux programmes principaux du projet sont BombermanClient.py et BombermanServer.py (en vert et encerclé) : respectivement le client et le serveur.
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.
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.
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.
- 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
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.
- Les deux modules en violet, KeyBinding, et AffichPygame, permettent de gérer l’interface graphique.
- 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:
def arrows(): return KeyBinding(up=['K_UP'], down=['K_DOWN'], left=['K_LEFT'], right=['K_RIGHT'], bomb=['K_SPACE'], help=['K_DELETE', 'K_h'])
- 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.
- 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.
- 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.
- 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.
- 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 :
- 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 :
# ANSIColors utilise des balises, entre chevrons. ANSIColors.printc("Le <u>drapeau français<U> contient du <blue>bleu<reset>, donc il est <italic>joli<reset> !")
- Enfin, on voit en bas à droite deux modules, isolés du reste (en jaune) :
conf n’est pas du code, c’est juste la configuration pour l’outil de documentation Sphinx.
- 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”):
$ ./PyRlwrap.py ./BombermanServer.py --server 138.231.36.71 --port 12882Ce 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 !).
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), nous utilisons TCP.
Les détails concernant le choix entre thread et select pour le réseau sont décrits plus bas (ici thread).
- 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.
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 :
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) )
- 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.
- 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 :
- le premier lit l’entrée standard du clavier, et envoit au serveur;
- le second lit le serveur, et affiche vers la sortie standard.
- Dans la seconde phase, un premier thread chez le client permet de :
- lit les évenement depuis le clavier (par exemple, la touche Espace);
- les comprends comme des demandes d’action via KeyBinding (par exemple, déposer une bombe);
- les enrobes dans un message texte via ParseMessageOut (par exemple, PLANT_BOMB);
- les envoit au serveur (et retourne en 1.);
- à 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 :
- lire les ordres venant du serveur;
- 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) :
- le serveur tente de le valider;
- si oui, il l’applique à sa version locale du jeu;
- 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.
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 :
CLOCK_FREQUENCY = 40.0 # Nombre maximum de parcours de la main boucle par secondes.
- 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 !
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é.
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.