En garde

par Joseph Razik, Christian Nguyen, le 2019-10-15

1   Projet I22 - Python avancé 2015-2016 - J. Razik, C. Nguyen (I22 SABDFL)

L'objectif principal de ce module et de ce projet est de respecter les bonnes pratiques de programmation. En effet, la réalisation d'un projet fonctionnel qui brille n'est pas l'objectif premier, ce qui est important est la réalisation d'un code robuste et réutilisable.

L'écriture d'un tel code est certes plus longue au début mais apporte un gain de temps énorme au fur et à mesure de la progression du projet. Il est à proscrire ce que l'on appelle les codes ”spaghettis”.

Pour cela vous devez donc mettre en pratique les connaissances acquises durant vos différents modules de programmation en Python et d'algorithmique mais également des notions essentielles au développement d'applications telles que modularité, protection, généricité, réutilisabilité, robustesse, évolutivité. Pour cela vous devrez respecter au maximum les PEP8 et PEP257 sur les conventions d'écriture du code et sur l'utilisation de documentation (commentaires). Vous devez également mettre en place pour vos fonctions des tests unitaires validant le bon fonctionnement desdites fonctions.

L'évaluation se fera sur la base du respect du cahier des charges, de la qualité de la programmation (comprenant le style de code et la présence de commentaires pertinents, documentation, tests), des objectifs atteints et des options réalisées.

1.1   Présentation du sujet

Le projet à réaliser est une simulation de duel à l'épée entre deux escrimeurs qui s'inspire du jeu de plateau nommé "En garde". Dans ce jeu à deux joueurs, chaque joueur effectuera des actions (déplacer, attaquer) et celui qui aura réalisé le plus grand nombre de touches gagnera la partie. Le duel se déroulera sur une allée pavée et les joueurs se déplaceront exclusivement de case en case sur celle-ci.

Le vainqueur de la partie sera le joueur ayant remporté 5 manches, sachant qu'une manche se termine si un des joueurs est touché ou s'il ne peut plus se déplacer.

Le mécanisme du jeu est basé sur un ensemble de cartes que possède chaque joueur et qui permettent de se déplacer ou d'attaquer selon la hauteur de la carte et la distance séparant les deux joueurs. Chaque adversaire joue chacun son tour.

1.2   Les règles du jeu

Le jeu est constitué de plusieurs éléments :
  • une pioche de 25 cartes (mélangées, face cachée) : 5 cartes pour chaque valeur entre 1 et 5 ;
  • une allée de 23 cases (de 1 à 23) ;
  • deux joueurs avec chacun une main d'au plus 5 cartes.

En début de manche, les adversaires se placent aux deux extrémités de l'allée (case 1 et 23), la pioche est mélangée et chaque joueur reçoit sa main de 5 cartes.

Un des deux joueurs est désigné au hasard pour commencer la première manche puis changera à chaque nouvelle manche.

Lors d'une manche, les joueurs jouent chacun leur tour, il n'y a pas de gestion temps-réel dans ce projet.

Pendant son tour, un joueur peut jouer une ou plusieurs cartes pour effectuer une et une seule action :
  • se déplacer (avancer ou reculer) : utilise une seule carte et déplace le joueur de la valeur de la carte. Si, en reculant, on sort du terrain alors l'action n'est pas autorisée ; il en va de même si, en avançant, on atteint ou dépasse la position adverse.
  • porter une attaque : il utilise une carte dont la hauteur correspond au nombre exact de cases séparant les deux adversaires. Dans ce cas, le joueur ne se déplace pas.
  • porter une attaque indirecte : il utilise une carte et une seule pour avancer (pas pour reculer) et une carte pour attaquer (du nombre de cases après déplacement). Il y a donc un vrai mouvement et une attaque (la position du joueur est modifiée).

A la fin de son tour, le joueur complète sa main à 5 cartes.

Toutes les cartes jouées sont défaussées en une pile dont la dernière carte est visible (sommet de la pile).

Les cartes de la pioche sont face cachée mais le nombre de cartes restant dans la pioche est connu à chaque instant.

Une manche se termine lorsque :
  • un joueur réussit à toucher son adversaire ;
  • un joueur ne peut plus effectuer d'action (mouvement ou attaque), il perd la manche ;
  • si aucun joueur n'a réussi à gagner la manche et que la dernière carte de la pioche est prise, il y a manche nulle, aucun des joueurs ne marque de point.

La partie se termine quand un des joueurs a remporté 5 manches.

1.3   Consignes

Modularité et protection des données

Une attention particulière doit être accordée à ce point. Le projet est subdivisé en cinq (5) modules : terrain, cartes, joueurs, interface et en_garde, ce dernier comprenant le programme principal. Cette subdivision (la modularité) permet de séparer les fonctions de chaque objet : ce n'est pas le module terrain qui doit savoir comment il est représenté graphiquement, c'est le rôle du module interface ; par contre c'est le rôle du module terrain de savoir si une case est vide ou non.

Pour chaque module, on peut indiquer que certaines variables et fonctions sont d'un usage interne au module. Pour cela, on préfixera leur nom d'un underscore (_), ce qui signifie dans la philosophie ouverte du langage que l'on ne souhaite pas qu'elles soient visibles (et donc directement utilisables) dans les autres modules. Cette "protection" n'est effective que si l'on effectue une importation au niveau de l'espace des noms global. Exemple :

# module1
__x = 0

def __f():
  pass
# module principal
from module1 import *
_f()  # NameError: name '_f' is not defined

Test unitaires

Chaque module doit comporter un test unitaire vérifiant l'ensemble de ses fonctionnalités. Exemple :

# module 1
def _f(x):
  return x + 1

def g():
  return 2

###########  tests unitaires  ##########

def test_f():
  return _f(1) == 2

def test_g():
  return g() == 2

if __name__ == '__main__':
  print(test_f() == True)
  print(test_g() == True)

Débogage

L’option -O de l’interprète Python, associée à la variable interne __debug__, offre un moyen de commutation des messages de débogage. Exemple :

# désactivé par l'option -O
if __debug__:
  print("mode debug : python lance sans -O")
else:
  print("python lance avec l'option -O")

1.4   Les modules

Le terrain (terrain.py)

Le terrain sera représenté par une liste avec autant d'éléments que le jeu l'indique (23 dans notre cas). Ce nombre d'éléments devra être défini dans une constante du module (NB_CASES) par soucis de généricité.

Ce module doit comporter au minimum les fonctions suivantes :

  • initialise() : initialisation du terrain,
  • place(pval, ppos) : modification du contenu d'une case du terrain,
  • contenu(ppos) : interrogation du contenu d'une case du terrain,
  • est_occupee(ppos) : interrogation si une case est occupée ou libre,
  • avance(ppos, psens, pn=1) et recule(ppos, psens, pn=1) : déplacement sur le terrain du contenu d'une case vers une autre (si le mouvement est possible),
  • affiche() : affiche textuellement le contenu du terrain (pour le débogage en mode texte), par exemple : "_____1_________2_______".

Les cartes (cartes.py)

Dans ce module, la généricité est un point important, ainsi le nombre de cartes de même hauteur et les hauteurs définies dans le jeu seront explicités à travers des constantes. Les deux structures principales de ce module sont la pioche et la défausse et seront simplement représentées par une liste de valeurs.

Ce module doit comporter au minimum les fonctions suivantes :

  • initialise(ppioche=None, pdefausse=None) : initialiser les différentes éléments (pioche, défausse),
  • melange(), pioche_carte(), longueur_pioche() : gérer la pioche (mélanger, piocher une carte, connaître la longueur de la pioche),
  • defausse_carte(pcarte), derniere_defausse() : gérer la défausse (défausser une carte, connaître la dernière carte défaussée),
  • affiche() : affiche des informations sur l'état des cartes, par exemple "lg pioche: 10, lg defausse: 4, derniere defausse: 3"
  • serialise() et deserialise(pchaine) : transforme les données du module en une chaîne de caractères pour la sauvegarde de partie.

Les joueurs (joueurs.py)

Dans ce module on retrouve les actions que peuvent effectuer les joueurs ainsi que les différents paramètres qui caractérisent un joueur. Toujours dans un souci de généricité, la taille d'une main sera définie par une constante.

Les deux joueurs seront définis comme des variables du module (joueur1 et joueur2) et possèdent les mêmes caractéristiques. On doit définir un joueur à l'aide d'un dictionnaire associant caractéristique-valeur (par exemple, sa position, sa main, son score, etc).

Ce module doit comporter au minimum les fonctions suivantes :

  • initialise(pjoueur1=None, pjoueur2=None) et initialise_manche() : initialisation des variables des joueurs,
  • carte_selectionnee(pjoueur, pidx_carte) : retourne la carte d'indice idx de la main d'un joueur,
  • deplace(pjoueur, pcarte, psens) : déplacement d'un joueur,
  • attaque_directe(pjoueur, pvaleurs) : attaque directe d'un joueur,
  • attaque_indirecte(pjoueur, pvaleurs) : attaque indirecte d'un joueur,
  • complete_main(pjoueur) : compléter la main d'un joueur,
  • gagne_manche(pjoueur) et gagne_partie(pjoueur) : détermine si la manche ou la partie est gagnée par le joueur pjoueur.
  • serialise() et deserialise(pchaine) : transforme les données du module en une chaîne de caractères pour la sauvegarde de partie.

L'interface (interface.py)

Ce module permet de donner une représentation de l'état du jeu et de gérer les entrées/sorties, c'est-à-dire d'afficher les changements et de demander des informations à l'utilisateur.

Pour ne pas compliquer le projet, le jeu sera obligatoirement représenté en mode textuel avec des couleurs, dans le terminal.

Les entrées se feront par l'intermédiaire du clavier en entrant les commandes dans le terminal.

image_en_garde

Ce module doit comporter au minimum les fonctions suivantes :

  • choisi_action(), choisi_cartes(pjoueur), choisi_sens(), choisi_fichier() : la sélection de l'action (et les sous-sélections associées),
  • rafraichi() : afficher l'état du jeu (terrain, mains, score, etc),
  • manche_finie_touche(pjoueur), manche_finie_nulle(), partie_finie() : signifier la fin d'une manche (sur touche ou nulle) ou de la partie,
  • affiche_partie sauvegardee() : affiche un message indiquant que la partie est sauvegardée,
  • affiche_partie chargee() : affiche un message indiquant qu'une partie sauvegardée a été chargée.

Le module principal (en_garde.py)

Dans ce module se trouve le jeu lui-même avec tout le mécanisme pour son déroulement, du début d'une partie à la fin d'une partie.

C'est le point central autour duquel s'articule tous les autres modules et le point d'entrée pour exécuter le jeu.

C'est évidemment le module le plus difficile à écrire bien qu'il ne comporte que peu de fonctions. Il est toutefois conseillé de définir quelques fonctions, ne serait-ce que pour connaître qui est le joueur actuel et qui est le joueur suivant.

C'est aussi dans ce module que l'on doit mettre en place un mécanisme de sauvegarde / chargement d'une partie. Ainsi, deux nouvelles actions "sauvegarde" et "charge" sont définies mais celles-ci ne correspondent pas à des actions de joueur mais à des actions sur le jeu lui-même.

Dans ce projet, seul la partie sauvegarde est obligatoire, la partie chargement est optionnelle.

Il faudra donc définir les fonctions suivantes dans ce module:

  • sauvegarde_partie(pfichier) : sauvegarde de la partie dans le fichier donné en paramètre,
  • charge_partie(pfichier) : charge une partie à partir du fichier donné en paramètre.
  • serialise() et deserialise(pchaine) : transforme les données du module en une chaîne de caractères pour la sauvegarde de partie.

L'action de sauvegarde écrira dans un fichier que le joueur indiquera, toutes les données nécessaires reflétant l'état du jeu et permettant de reprendre la partie. Les données seront écrites en mode textuel sous un format simple "nom: valeur(s)" (attention à l'ordre des valeurs).

L'action de chargement lira à partir d'un fichier suivant le même format simple que pour la sauvegarde et changera l'état du jeu pour refléter celui de la sauvegarde.

Lors du lancement du jeu, celui-ci pourra prendre en argument le nom d'un fichier contenant une sauvegarde d'une partie et ainsi reprendre une partie en cours dès le début de l'exécution du jeu.

Format du fichier de sauvegarde:

Le format du fichier contenant une sauvegarde de partie est simple et est en mode textuel. Il pourra contenir des lignes vides ou des lignes avec des commentaires commençant par le caractère '#'. Ces lignes seront ignorées lors de la lecture du fichier.

Les variables d'un même module seront placées sur la même ligne. Cette ligne commencera par la une lettre majuscule suivie du caractères ':'. C'est-à-dire 'G:' pour les variables du module en_garde, 'J:' pour les variables du module joueurs et 'C:' pour les variables du module cartes.

Pour le module en_garde, les valeurs seront séparées par un point-virgule.

Pour le module cartes, les variables seront séparées par un point-virgule, les valeurs des variables (éléments de liste) seront eux séparés par une virgule.

Pour le module joueurs, les variables seront séparées par un point-virgule. Ces variables étant de dictionnaires, chaque couple clé-valeur sera écrit sous le format cle: valeur, chaque couple étant séparé par le caractère '&'. Si la valeur est une liste, les éléments de la liste seront séparés par une virgule.

Les fonctions de dé-sérialisation devront vérifier que les données du fichier sont correctes.

Extrait d'un exemple de sauvegarde:

# variables globales du jeu
G: 1; 1; 1
# cartes
C: 4, 5, 3, 3, 2, 1; 5, 4, 1, 2
# les joueurs
J: points: 1 & position: 5 & main: 1, 2 & ... ; points: 0 & ...

1.5   Options

Il n'y a pas d'ordre ou de priorité dans les options à ajouter.

  • Chargement d'une partie à partir d'un fichier de sauvegarde ;
  • Annulation d'action : introduire une action "défaire" permettant d'annuler la dernière action effectuée ;
  • Fin de manche sur fin de pioche : lorsque toutes les cartes et les actions (ainsi que les parades) ont été effectuées, la manche est terminée. Pour déterminer le vainqueur, les mains des deux joueurs sont comparées. Le joueur ayant le plus de cartes permettant une attaque directe gagne la manche. S'il y a toujours égalité, le joueur ayant avancé le plus vers le camp adverse sera le vainqueur (comparer par rapport à la case centrale). S'il y a encore égalité, la manche est nulle ;
  • Ajout d'une action "parade" : le joueur attaqué peut parer immédiatement une attaque directe en jouant la même carte d'attaque que son adversaire (la carte en cas d'attaque directe ou la seconde carte en cas d'attaque indirecte). Il poursuit ensuite son tour normalement mais sans avoir complété sa main.
  • Ajout d'une action "retraite" : en cas d'attaque indirecte et uniquement indirecte, le joueur attaqué peut choisir de battre en retraite, c'est-à-dire de reculer de la valeur d'une carte, comme pour un déplacement simple. Attention, ceci est considéré comme une véritable action et donc le joueur complète sa main d'une carte et laisse le tour à l'autre joueur.
  • Introduction d'une IA (Intelligence Artificielle) : définir un joueur ordinateur qui joue automatiquement contre un humain. Celui-ci pourra introduire et utiliser des données statistiques sur la partie en cours pour prendre ses décisions.
  • Introduction d'une interface graphique utilisant la librairie cng en remplacement de l'interface textuelle dans un nouveau module et implémentant au minimum les mêmes fonctions que le module interface. L'intérêt est de pouvoir sélectionner la représentation en changeant seulement une ligne dans le programme principal.

1.6   Un peu de couleurs

Il est possible de mettre un peu de couleurs dans vos affichages textuels en mode terminal/console. Pour cela il faut utiliser ce que l'on appelle une séquence d'échappement suivie de commandes. Cette séquence d'échappement commence par le caractère ESC (de code ASCII décimal 27, hexadécimal 0x1b, octal 033) suivi du caractère '['. Ces deux caractères forment ce que l'on appelle le Control Sequence Introduced ou CSI. On pourrait traduire cela en marqueur de début de séquence.

De nombreuses commandes sont définies afin de modifier les paramètres graphique du terminal. Une partie de ces commandes sont énoncés dans le tableau 2.

Pour la gestion des couleurs, la séquence à utiliser est de la forme "\x1b[n1;n2;...m" où chaque n1, n2 est un paramètre pour la gestion des couleurs. Par exemple, les codes 30+i fixent la couleur d'avant plan, les codes 40+i fixent celle d'arrière plan. La valeur de i correspond à la couleur désirée (voir tableau 1).


Table 1 : table des couleurs :

Intensité 0 1 2 3 4 5 6 7
Normal Noir Rouge Vert Jaune Bleu Magenta Cyan Blanc

Voici quelques exemple de séquence pour :
  • écrire en noir : "\x1b[30m"
  • réinitialiser les couleurs : "\x1b[0m"
  • écrire en rouge gras : "\x1b[31;1m"

Table 2 : quelques séquences d'échappement ANSI (liste partielle)

Code Description
CSI n A Cursor Up: Moves the cursor n (default 1) cells in the given direction.
CSI n B Cursor Down
CSI n C Cursor Forward
CSI n D Cursor Back
CSI n;m H Cursor Position: Moves the cursor to row n, column m. The values are 1-based, and default to 1 (top left corner) if omitted.
CSI n J Erase Display: Clears part of the screen. If n is 0 (or missing), clear from cursor to end of screen. If n is 1, clear from cursor to beginning of the screen. If n is 2, clear entire screen.
CSI n K Erase in Line: Erases part of the line. If n is zero (or missing), clear from cursor to the end of the line. If n is one, clear from cursor to beginning of the line. If n is two, clear entire line.
CSI n m Select Graphic Rendition: Sets SGR parameters, including text color. After CSI can be zero or more parameters separated with ;.

Table 3 : Select Graphic Rendition parameters

Code Description
0 Reset / Normal (all attributes off)
1 Bold or increased intensity
4 Underline: Single
5 Blink: Slow (less than 150 per minute)
6 Blink: Rapid (150+ per minute; not widely supported)
7 Image: Negative (swap foreground and background
25 Blink: off
27 Image: Positive
30-37 Set text color (foreground, 30 + n, n is the color)
40–47 Set background color (40 + n, n is the color)

Pour plus de détail, vous pouvez vous reporter à la pages https://en.wikipedia.org/wiki/ANSI_escape_code

1.7   De la gestion de version

Dans leur grande bonté, les enseignants vous fournissent le squelette de quelques modules du projet. Le moyen de récupérer ces fichiers utilise ce que l'on appelle un programme de gestion de version. Il en existe plusieurs: CVS, SVN, GIT ... Nous vous proposons d'utiliser ce dernier, GIT, qui est l'outil le plus utilisé actuellement.

Afin de récupérer les fichiers qu'on préparés les enseignants, effectuez la commande suivante dans un terminal à partir du répertoire de votre projet:

git clone https://gitlab.lsis.univ-tln.fr/jrazik/I22-En_Garde.git

Ceci va créer le répertoire I22-En_Garde est placer dans celui-ci les fichiers squelettes.

Déplacer ces fichiers à la racine de votre projet puis supprimer le répertoire I22-En_Garde:

mv I22-En_Garde/* .
rm -Rf I22-En_Garde/

Pour utiliser la gestion de version git pour votre projet vous devez initialiser votre projet avec la commande suivante:

git init ./

A ce moment là, vous ne devez avoir dans votre répertoire de travail du projet 4 fichiers et le répertoire caché .git. Vous n'avez plus qu'à travailler dans ce répertoire et sur ces fichiers.

L'outil GIT ne permet pas que récupérer des fichiers. Il permet en effet de gérer des versions pour effectuer par exemple des retours en arrière s'il y a un problème. Par contre, c'est vous qui décidez de figer une version des fichiers: il n'y a pas création d'un version à chaque fois que vous modifiez un caractère d'un fichier.

Les commandes utiles principales sont les suivantes:

  • git status: vous montre l'état des fichiers par rapport à la dernière version en date connu par git
  • git add fichier: ajoute le nom du fichier donné en paramètre dans les listes des fichiers prêts à être intégré dans la prochaine version
  • git commit -m "message": créé une nouvelle version par rapport aux fichiers dont les modifications ont été ajoutées (git add), et y associe un message.
  • git reset version: permet de revenir à une version précédente

Si vous utilisez un serveur extérieur pour centraliser vos projets git, par exemple github ou bitbucket, vous pouvez utiliser les commandes suivantes:

  • git push: pour pousser vos modifications locales vers le serveur
  • git pull: pour récupérer les dernières modifications du serveur dans votre répertoire local

1.8   Rendre votre projet

Pour rendre votre projet, vous devez créer une archive contenant les fichiers nécessaires et déposer cette archive dans le répertoire adéquat. Un script est à votre disposition pour effectuer tout cela pour vous.

  • Tout d'abord, dans un terminal, placez-vous dans le répertoire de votre projet (aidez-vous de la commande cd pour vous déplacer).
  • Une fois dans ce répertoire où se trouve vos fichier .py, exécutez la commande suivante:
/home/partage/I22/rendu_projet.py

Les 5 fichiers qui sont attendus sont (si vous les avez réalisés): terrain.py, cartes.py, joueurs.py, interface.py et en_garde.py.

Ce script indique les fichiers manquants le cas échéant et ceux intégrés dans l'archive.

En cas de mauvais nommage d'un de vos fichiers, vous pouvez le renommer correctement et effectuer de nouveau le script précédent.