Jeu du Killer

Projet de programmation système 2023-2024

A. Bonlarron, V. David, E. Lozes

L2 info et math info -- Université Côte d'Azur

No description has been provided for this image

Le projet est à rendre sur Moodle avant le 12 mai minuit. Un rendu par personne (donc plusieurs rendus pour les binômes et trinômes). Le format du rendu est précisé plus bas. La présence aux trois dernières séances de TP est obligatoire. Si vous ne faites pas de démo de votre projet à votre chargé de TP, votre note sera 0. Binômes ou trinômes entre personnes de groupes de TP distincts uniquement sur dérogation, demandez d'abord l'autorisation aux chargés de TP concernés.

Cahier des charges

Description générale

Le but de ce projet est d'écrire un jeu multi-joueur inspiré du jeu "Le Killer" basé sur un serveur de chat. On s'intéresse surtout à l'aspect "serveur de chat", le jeu est secondaire 🧐

Le jeu comporte deux exécutables:

  • chat_killer_server.py : l'exécutable lancé par le modérateur de jeu. Cet exécutable lance le serveur de chat dans le terminal (reste attaché au terminal). Le modérateur, via cet exécutable:

    • peut suivre toutes les discussions
    • connait tous les secrets
    • decide quand lancer la partie
    • peut suspendre temporairement ou bannir définitivement un joueur au cours de partie (c'est un modérateur)
    • vérifie que le programmeur a bien fait son travail (débogage)
  • chat_killer_client.py : l'exécutable lancé par un joueur. Cet exécutable se détache du terminal (double fork) et crée deux nouveaux terminaux, l'un affichant les messages, l'autre permettant de saisir des commandes et des messages. Les commandes commencent par un !, les messages privés commencent par @toto (pour envoyer à toto), ou @toto @titi @tata pour envoyer un message privé à plusieurs destinataires. Lorsque le joueur meurt, toutes les fenêtres et les processus créés pour lui doivent être tués.

Règles du jeu

Les règles du jeu sont assez secondaires pour le projet proprement dit (sauf si vous êtes en trinome).

Le but du jeu de chaque joueur est de tuer les autres sans se faire tuer. Le dernier vivant gagne la partie.

En début de partie, le modérateur (qui ne joue pas) tire au hasard un mot "mortel" par joueur. Ce mot est tenu secret, même pour le joueur ciblé par ce mot.

Le modérateur tire ensuite au sort un ordre sur les joueurs (pas forcément leur ordre d'arrivée dans la partie) et révèle le mot mortel de chaque joueur à un autre joueur: le mot mortel du joueur i est révélé au joueur i+1, et le mot mortel du dernier joueur est envoyé au premier joueur.

Le but de chaque joueur est de faire écrire par un autre joueur, en message public ou privé, le mot mortel qui lui a été révelé par le modérateur. Un joueur n'a pas le droit d'écrire le mot mortel qu'il connait, sans quoi c'est lui qui meurt.

Lorsqu'un joueur J meurt, tout le monde en est informé. Le joueur K qui connaissait le mot mortel de J hérite du mot mortel que connaissait J (le modérateur le lui révèle). Autrement dit, lorsqu'un joueur atteint sa cible, il continue la partie avec comme nouvelle cible la cible de son ancienne cible.

Communications

Le modérateur de jeu et chaque joueur peuvent être sur des machines différentes. Les communications entre chaque application client et le serveur se font via des sockets en mode connecté TCP.

Le serveur est lancé par le modérateur sur sa machine par une commande de la forme

python3 chat_killer_server.py 42042

où 42042 désigne le numéro de port choisi par le modérateur pour la socket d'écoute.

Le client est lancé par le joueur sur sa machine par une commande de la forme

python3 chat_killer_client.py 134.59.2.162 42042

où 134.59.2.162 est l'adresse IPv4 de la machine du modérateur (vous pourrez utiliser la même machine pour tout le monde est utiliser l'adresse 127.0.0.1).

Au lancement, le client demande à l'utilisateur de choisir un pseudo (qui ne pourra pas être changé pendant toute la partie).

Le format des données échangées entre le client et le serveur est laissé libre. Si vous travaillez à plusieurs il faudra vous mettre d'accord sur le format de ces données. Dans le cas de gros messages (fichiers volumineux) il faudra découper les données en plusieurs paquets de taille limitée (par exemple 16 Ko max) et créer de nouveaux processus et des connections UDP pour ne pas bloquer l'application (que ce soit côté client ou côté serveur). Un test d'intégrité du fichier se fera en utilisant une fonction de hachage.

Suggestion pour le format des données: échangez des chaînes de caractères (en dehors des fichiers à transférer), et restez le plus proche possible des chaînes de caractères saisies par les utilisateurs. Ce sera plus facile pour débugger. Si vous voulez que le processus serveur et le processus superviseur s'échanges des messages "en interne" (ne correspondant pas à des saisies des utilisateurs), gardez des chaînes de caractères avec des commandes "internes" de votre invention, que vous pouvez préfixer avec deux ! pour bien le préciser, par exemple: !!request_cookie, !!not_found, !!my_cookie_is 129029320, !!choose_a_pseudo,!!my_pseudo_is Calamity Jane, !!ca_va?, !!ca_va!, etc.

Architecture d'un client

schéma de l'architecture du client

Chaque client est composé de 3 processus (en réalité 5, chaque terminal comptant pour un processus supplémentaire)

  • le superviseur: c'est le processus principal, et le seul pour lequel vous allez devoir écrire du code. Le superviseur n'est attaché à aucun terminal (quand on tape la commande python3 chat_killer_client.py ..., le processus se détache du terminal), mais il lance les deux autres terminaux et gère toutes les communications avec le serveur; il crée aussi un tube nommé TUBE (par exemple /var/tmp/killer.fifo) pour communiquer avec le terminal de saisie, et un fichier LOG (par exemple, /var/tmp/killer.log) pour communiquer avec le terminal d'affichage.
  • le terminal de saisie : il est lancé par le superviseur via la commande xterm -e "cat >TUBE". Cette commande crée un terminal, y lance la commande cat et redirige la sortie de ce cat vers le tube nommé. Le superviseur pourra ouvrir le tube à l'autre bout pour récupérer les sorties clavier. Le superviseur aura besoin de connaitre le pid du terminal pour pouvoir lui envoyer des signaux, on doit donc lancer la commande "comme en TD" avec fork et un recouvrement, et pas avec os.system ou la librairie subprocess (par exemple).
  • le terminal d'affichage : il est lancé par le superviseur via la commande xterm -e "tail -f LOG". Cette commande crée un terminal qui affiche la fin du contenu du fichier LOG et reste "en veille sur le fichier": si de nouvelles lignes sont ajoutées plus tard au fichier LOG, le terminal les affichera au moment où elles sont écrites.

Détection de panne

Chaque composant doit savoir détecter qu'un autre composant est en panne pour agir au mieux en conséquence.

Pour vérifier que l'autre est bien vivant, s'il n'a pas reçu de message de sa part depuis plus d'une seconde, le client (respectivement le serveur) envoie un message "ça va?" au serveur (respectivement au client), et celui-ci lui répond immédiatement "ça va!". Tout cela se fait de manière automatique (pas d'intervention des utilisateurs). C'est une forme du protocole heartbeat).

Enfin, côté client, lorsque le superviseur apprend la mort prématurée d'un terminal, il le relance. Plusieurs signes peuvent indiquer au superviseur qu'un terminal a crashé: réception d'un signal SIGCHLD, ou fermeture du tube.

Tolérance aux pannes

Toute application (client ou serveur) peut tomber en panne et être redémarrée sans que cela pose de problème.

Les applications doivent donc tout sauvegarder dans des fichiers et consulter ces fichiers au moment où on les redémarre. En fin d'exécution "normale", ces fichiers sont nettoyés.

Plus précisément, si un client tombe en panne, le serveur prévient les autres joueurs, et la partie continue. Lorsque le joueur a relancé son client, il réintègre la partie. Le serveur doit pouvoir déterminer si c'est bien un joueur qui était dans la partie et qui a crashé. Pour cela, le serveur doit installer un fichier COOKIE (ex: /var/tmp/PSEUDO/cookie) sur la machine de chaque client en début de partie. Ce fichier contient par exemple un nombre aléatoire assez grand (pour qu'un joueur ne puisse pas en forger un) que le serveur a soigneusement consigné de son côté pour pouvoir vérifier que le cookie envoyé par le client en cas de reconnection est bien un cookie valide (le serveur peut donc retrouver quel était le pseudo du client). Attention, si vous testez le client et le serveur sur la même machine, vous pouvez être tentés de lire/écrire le fichier COOKIE directement depuis le serveur, mais ce n'est pas ce qu'il faut faire...

Si le serveur tombe en panne, le client en informe le joueur via le LOG et attend que le joueur exécute la commande !connect pour tenter de reconnecter le serveur. Lorsque le serveur redémarre, il doit pouvoir se rendre compte qu'une partie était en cours et qu'il s'est crashé. Pour cela, il maintient un fichier STATE (ex: /var/tmp/killer.state) où il enregistre les valeurs de toutes les variables importantes pour pouvoir bien restaurer la partie en cours. Le serveur doit actualiser STATE à intervalles réguliers (par exemple à chaque heartbeat).

Liste des commandes utilisateurs disponibles

Rappelons que le modérateur, ou un joueur, peut saisir un message public, un message privé, ou une commande. Les messages privés commence par @, les commandes par !. Tous les autres messages sont publics. Certaines commandes visent un joueur en particulier. Elles commencent alors par @ (voir plus bas).

Exemple saisie de message privé

@Jojo Je te dit un secret...

Exemple saisie de message privé au modérateur

@Admin Je te dit un secret...

Exemple de message public

Hello tout le monde!

On liste ci-dessous les commandes que le serveur ou un superviseur doit être capable d'exécuter.

Commandes disponibles uniquement pour le modérateur

  • !start : lance la partie. Plus aucun nouveau joueur ne peut se connecter.
  • @PSEUDO !ban : tue le joueur désigné par le pseudo, comme s'il avait été tué par son mot mortel.
  • @PSEUDO !suspend : gèle le terminal de saisie (il reçoit un signal SIGSTOP) de l'utilisateur concerné. C'est le serveur qui donne l'ordre au superviseur visé d'envoyer le signal au terminal de saisie (rappelons que le serveur n'est pas forcément sur la même machine que le client, il ne peut pas envoyer le signal SIGSTOP au terminal directement).
  • @PSEUDO !forgive : restaure le terminal de saisie (il reçoit le signal SIGCONT) de l'utilisateur concerné.

Commandes disponibles pour tout joueur (y compris le modérateur)

  • @PSEUDO !send_file PATH : envoie le fichier local PATH au joueur dont le pseudo est indiqué. Le pseudo peut être @Admin pour envoyer au serveur. Un cache permet d'améliorer les performances (voir plus bas).
  • !broadcast_file PATH: similaire à send_file, mais le fichier est envoyé à tous les autres joueurs
  • !list : liste les pseudos, en indiquant leur état actuel (vivant, mort, crashed)

Commandes disponibles pour tout joueur (sauf le modérateur)

  • !reconnect: tente de reprendre contact avec le serveur (après un crash du serveur)

Envoi de fichiers et mise en cache

Pour un envoi de joueur à joueur, le serveur sert d'intermédiaire et garde en cache une copie du fichier. Si le même fichier est envoyé une seconde fois (ou un fichier avec le même hash), le serveur ne demande pas à l'envoyeur de lui fournir le fichier, mais utilise son cache.

Évaluation

Le projet est à faire seul, en binôme, ou en trinome.

Le but est de résoudre le maximum des challenges listés ci-dessus. Il sera difficile de tout traiter, procéder petit à petit.

Pour la dernière séance de TP, le chargé de TP vous demandera de faire une démo de votre code et mettra une note provisoire en indiquant ce qui lui parait améliorable dans le temps restant avant le rendu du projet.

Contenu de l'archive à rendre sur Moodle

En plus du code, votre projet contiendra

  1. un fichier README avec les informations suivantes:
    • avec qui vous étiez pour faire le projet
    • quel a été votre role dans le projet
    • les points que vous avez pu améliorer entre la démo et le rendu
    • les points sur lesquels vous êtes restés bloqués.
    • votre auto-évaluation (voir barème ci-dessous)
    • tout autre commentaire que vous jugerez nécessaire sur le fonctionnement de votre code
  2. un à plusieurs répertoires nommés stable1, stable2, ou unstable1, unstable2, ... contenant des sauvegardes que vous aurez fait de votre projet à diverses étapes d'avancement. Les versions stables doivent s'exécuter sans erreur. Si on doit modifier ne serait-ce qu'une virgule de votre code pour pouvoir l'exécuter, ce n'est pas une version stable.Vous expliquerez dans le fichier README ce que savent faire les versions stables. Sauvegardez régulièrement votre projet quand vous avez quelque chose de stable.

Pas de dépendances de librairie Vous n'avez droit qu'à la librairie standard de Python (on doit pouvoir exécuter votre code sans faire un pip3 install foo). Si vous avez une dépendance de librairie, ce sera sanctionné, et d'autant plus si elle n'est pas indiquée dans le README.

Votre archive ne contient que des fichiers Python .py (et le README). Pas de fichiers exotiques. Si vous avez testé le transfert de fichier avec de gros fichiers, vous les gardez pour vous.

Barème

Tout plagiat sera sévèrement sanctionné. Si vous réutilisez du code que "vous avez trouvé sur internet", et qui ressemble beaucoup à celui de quelqu'un d'autre, vous serez sanctionné tous les deux. Indiquez dans le README si vous avez aidé ou si vous vous êtes fait aidé, en précisant le code transmis.

Travail seul

  • Aucune version stable : 0/20
  • Un serveur monoclient qui accepte une connection avec telnet : 2/20
  • Un client qui lance deux terminaux avec les bonnes commandes et les bonnes redirections, et qu'on peut tester sans connection avec un serveur (le superviseur ne se détache pas de son terminal, il lit et écrit dans le terminal au lieu de communiquer avec le serveur): 6/20
  • Un serveur monoclient + un client qui échangent des messages en alternance (le serveur envoie, puis le client, puis le serveur, etc): 8/20
  • Un serveur monoclient + un client qui échangent des messages sans alternance (utilisation de select): 10/20
  • Un serveur multi-client sans pseudo ni messages privés: 12/20
  • Gestion des pseudos, messages privés, commande !list basique (seulement les pseudos, pas leur état): 15/20
  • Commandes !start (ferme la porte aux nouvelles demandes de connection), !ban, !suspend, !forgive, !list : 18/20
  • Tolérance aux diverses pannes de client (détection par échec d'envoi de message, pas de heartbeat demandé), technique du cookie : 20/20

Ce qui n'est pas demandé: envoi de fichiers, heartbeat, tolérance aux pannes côté serveur, règles du jeu,

Travail en binôme

Un des deux travaille sur le client, l'autre sur le serveur. En cas de grosse différence d'investissement dans le projet, les notes peuvent être distinctes.

Barème client

  • Aucune version stable : 0/20
  • Un client qui lance deux terminaux avec les bonnes commandes et les bonnes redirections, et qu'on peut tester sans connection avec un serveur (le superviseur ne se détache pas de son terminal, il lit et écrit dans le terminal au lieu de communiquer avec le serveur): 6/20
  • Un client qui se connecte au serveur et échange des messages en alternance : 8/20
  • Un client qui se connecte et échange des messages sans alternance prédéfinie : 10/20
  • Gestion de pseudo, de messages privés, et de la commande !list: 12/20
  • Gestion des commandes !suspend, !ban, et !forgive: 14/20
  • Tolérance aux pannes des terminaux qui exécutent les commandes tail -f LOG et cat > TUBE, relance des terminaux et des commandes : 16/20
  • Gestion des cookies lors du redémarage du client après une panne: 18/20
  • Tolérances aux pannes de serveur (détection par échec d'envoi de message au serveur), commande !reconnect : 19/20
  • Heartbeat et détection de panne du serveur: 20/20

Barème serveur

  • Aucune version stable : 0/20
  • Un serveur qui accepte un client et échange des messages en alternance : 3/20
  • Un serveur qui accepte un client et échange des messages sans alternance prédéfinie : 5/20
  • Un serveur qui accepte plusieurs client et échange des messages public sans alternance prédéfinie : 8/20
  • Gestion des messages privés et de pseudo : 11/20
  • Gestion des pseudos, commande !list : 13/20
  • Tolérance aux pannes de client (détection par échec d'envoi de message au serveur), gestion des cookie: 16/20
  • Heartbeat et détection de pannes du client sans envoi de message utilisateur: 18/20
  • Tolérance aux pannes côté serveur (sauvegarde de l'état du serveur à intervalle de temps régulier et chargement de la sauvegarde au redémarrage du serveur): 20/20

Travail en trinôme

Un étudiant travaille sur le client, les deux autres sur le serveur (et un tout petit peu sur le client). Le barème pour le client est le même qu'en binôme.

Barème serveur en trinôme

  • Aucune version stable : 0/20
  • Un serveur qui accepte un client et échange des messages en alternance : 2/20
  • Un serveur qui accepte un client et échange des messages sans alternance prédéfinie : 4/20
  • Un serveur qui accepte plusieurs client et échange des messages public sans alternance prédéfinie : 6/20
  • Gestion des messages privés et de pseudo : 9/20
  • Gestion des pseudos, commande !list : 11/20
  • Tolérance aux pannes de client (détection par échec d'envoi de message au serveur), gestion des cookie: 14/20
  • Heartbeat et détection de pannes du client sans envoi de message utilisateur: 15/20
  • Tolérance aux pannes côté serveur (sauvegarde de l'état du serveur à intervalle de temps régulier et chargement de la sauvegarde au redémarrage du serveur): 17/20
  • Envoi de fichiers : 19/20
  • Finition: automatisation du travail du modérateur (gestion des mots "tueurs", cf règles du jeu), interface utilisateur améliorée, etc: 20/20

Ressources utiles

Pour pouvoir jouer entre plusieurs machines

  • Installez le serveur sur une machine perso qui accepte des connections entrantes sur des ports non réservés (celles du dpt info ne le feront sans doute pas). Si c'est une machine chez vous, il faut aller regarder ce que votre FAI vous permet de faire. Quelques pointeurs random (pas trop vérifiés, attention...): avec Free, avec SFR, Orange, Bouygues
  • Installez le serveur dans le cloud. Quelques solutions relativement faciles à mettre en oeuvre (mais vous aurez à donner votre numéro de carte bleue): Github Codespace, Google Collab, Amazon EC 2, etc.

Modules utiles (librairie standard)

  • les modules vus en cours: os, sys, time, signal, socket, atexit
  • le modules hashlib pour calculer des hash de paquets (envoi de fichier) RAPPEL: les librairies non standard ne sont pas autorisées