Dans le jeu du morpion, les différentes phases du jeu sont :
Le client doit donc être en mesure de proposer aux joueurs d'évoluer dans ces différentes phases de jeu, selon l'ordre établi ci-dessus.
Le but du projet est de développer une application de jeu du morpion entre utilisateurs de systèmes Unix en réseau. Nous devons donc effectuer différents échanges entre les différents joueurs, afin que les grilles de jeu de chacun des joueurs soient tenues en permanence à jour.
Par ailleurs, l'application doit permettre d'obtenir la liste des parties déjà en cours (ce qui sous-entend qu'il peut exister plusieurs parties), de créer une nouvelle partie (et donc la rendre accessible aux autres joueurs du réseau), de joindre une des parties existantes selon l'accord des autres joueurs (il va donc falloir créer un système de "vote" à travers le réseau).
De plus, l'application doit permettre de signifier aux joueurs d'une même partie s'il y a match nul (la grille est pleine et personne ne gagne) ou si l'un des joueurs a gagné (en alignant 5 signes horizontalement, verticalement ou en diagonale).
Partant de cette étude nous avons donc pu établir le protocole décrit précédemment.
Dans notre implémentation du jeu, nous avons décidé de confier "l'intelligence" du jeu à un serveur gérant l'ensemble des joueurs, des parties et des coups joués dans ces parties. Ceci nous permet donc d'avoir des clients simples, ne gérant que :
Le client est construit en différents modules articules selon les différentes phases de jeu décrites ci-dessus. On peut donc représenter le fonctionnement du client par le schema suivant :
\begin{figure}[!hbtp] \begin{center} \epsffile{figures/phases_client.eps} \caption{\it Differentes phases de fonctionnement du client} \end{center} \end{figure}
est la phase durant laquelle le joueur doit saisir son surnom afin de s'identifier auprès du serveur.
est la phase durant laquelle le joueur peut visualiser la liste des parties existantes, et choisir de créer une nouvelle partie ou joindre une partie existante.
durant cette phase le joueur saisi les paramètres de creation de la nouvelle partie, les envoie au serveur et attend l'accord ou le refus de cette creation par le serveur.
est la phase surant laquelle le joueur demande à joindre une partie existante et attend confirmation ou refus de son intégration par le serveur.
durant cette phase le joueur visualise la grille de jeu et les informations sur les joueurs présents dans la partie. Il est en attente de son tour de jouer ou d'une modification de la grille.
durant cette phase le joueur peut jouer un coup et attend que ce coup soit accepté ou refusé par le serveur.
est la phase durant laquelle une mise à jour complète de la grille est effectuée.
est la phase indiquant au joueur que la partie a été gagnée.
est la phase indiquant au joueur qu'il y a eu match nul.
Les échanges entre les clients et le serveur de messages conformes au protocole doit se faire à l'aide des sockets. Le serveur :
Afin d'être le plus indépendant possible de l'architecture, le client est codé en majeure partie en langage script Tcl/Tk. Afin de permettre ces échanges d'informations avec le serveur, nous avons dû ajouter de nouvelles commandes à ce langage script, effectuant les opérations décrites ci-dessus. Ces nouvelles commandes sont :
ouvre_connexion
qui effectue la demande de connexion au serveur
et établit cette connexionferme_connexion
qui ferme la connexion si le serveur
"tombe".ecrit_socket
qui permet d'envoyer un message vers le serveurlit_socket
qui permet de lire les messages en provenance du
serveur.Les 3 premières de ses fonctions ne présentent aucun problème à l'implémentation car l'instant auquel elles sont utilisées est parfaitement déterminé dans le code du client. Par contre, il n'en est pas de même avec la fonction de lecture des messages en provenance du serveur.
En effet les messages envoyés par le serveur peuvent arriver à tout moment sur la connexion du coté client. De plus, comme nous l'avons dit plus haut, ces messages doivent être traités le plus tôt possible de leur arrivée.
Il a donc fallu trouver un moyen permettant à l'interprérteur Tcl d'aller chercher les messages sur la connexion dès que ceux-ci sont disponibles. Pour ce faire nous effectuons l'appel de l'instruction à chaque fois que l'interpréteur Tcl ne fait plus rien (ce qui est quand même très souvent puisque le joueur attend la majeur partie du temps son tour de jouer), grâce à l'instruction :
after idle { TraiteProtocole [lit_socket] }
qui signifie "quand l'interpréteur ne fait plus rien, alors il va lire
un message sur la connexion, puis traite ce message grâce à la procédure
TraiteProtocole
(écrite en Tcl)".
Cette "banale" phrase fait apparaître un autre problème : la procédure
TraiteProtocole
ne gère qu'un seul message à la fois. Or en étudiant
rapidement le protocole défini, nous nous appercevons que plusieurs messages
peuvent arriver à la suite. De plus, les sockets TCP ne garantissent pas du
tout que lors d'une lecture, nous obtiendrons l'intégralité d'un message.
Ce problème a été résolu de la manière suivante: nous avons défini un message
comme étant une chaîne de caractères terminée par un retour chariot. Il suffit
donc de mettre dans un tampon les données lues sur la socket à la suite des
données déjà présentes dans ce tampon (qui sont normalement le début du
message que nous sommes en train de finir de lire), puis de rechercher le
premier retour chariot. Nous connaissons alors le premier message reçu, et en intégralité il
ne reste plus qu'à le retourner afin qu'il soit traité par
TraiteProtocole
.
Étant donné que nous devions utiliser la librarie Tcl pour l'ajout de ces
nouvelles commandes, nous avons profité des fonctions de gestion d'une
chaîne dynamique offertes par la librairie Tcl (fonctions
Tcl_DString*
), ce qui nous a évité d'avoir à écrire du code, de ce
fait, inutile et peu intéressant.
Évidemment, si l'interpréteur va chercher à lire des données à chaque fois
qu'il ne fait plus rien (c'est à dire quand il n'y a plus de commande Tcl
à interpréter, plus d'évènement X à gérer, etc.), il va donc appeler très
souvent la commande lit_socket
et donc se bloquer en attente de
l'arrivée de données sur la socket. Pour remédier à ce problème, nous avons
donc introduit l'appel à une instruction nous indiquant si des données
sont disponibles sur la socket (à l'aide d'un timeout sur un select).
Ainsi si aucune donnée n'est disponible
à la lecture sur la socket et que le tampon des messages ne contient pas de
message complet, alors nous redonnons immédiatement la main à l'interpréteur
Tcl qui pourra alors se consacrer à gérer les évènements X ou tout autre chose.
L'avantage de cette implémentation est de garder l'indépendance à l'architecture, de laisser le shell maître de ses opérations, et surtout d'éviter d'avoir un processus client passant l'intégrale de son temps à lire sur la socket, au détriment de la gestion des évènements X sur l'interface homme/machine, et consommant une grande partie des ressources de la machine à ne rien faire.