L'homme dans le labyrinthe

by Joseph Razik, Christian Nguyen, last modified on 2013-02-15

1   Projet I22 - Python avancé 2012-2013 - J. Razik, C. Nguyen

L'objectif de ce projet est de mettre en pratique les connaissances acquises durant le module d'introduction à la programmation en Python. De plus, des notions essentielles au développement d'applications telles que modularité, protection, interface seront étudiées. L'évaluation se fera sur la base du respect du cahier des charges, de la qualité de la programmation (comprenant la présence de commentaires pertinents), des objectifs atteints et des options réalisées.

1.1   Introduction du projet

L'idée est de réaliser un labyrinthe généré aléatoirement dans lequel un mobile doit atteindre un objectif. Le labyrinthe est représenté par une grille comportant soit des cases libres soit des cases avec un obstacle. Le mobile peut se déplacer sur cette grille soit horizontalement soit verticalement. Il a également la possibilité de détruire tous les obstacles qui l'entourent directement (i.e les 4 cases voisines avec lesquelles le mobile est en contact) afin de se frayer éventuellement un chemin vers l'objectif. De plus, un deuxième mobile, géré par l'ordinateur, tente à tout instant de rejoindre le mobile géré par le joueur. S'il y parvient avant que le mobile atteigne l'objectif, le joueur a perdu. Les mobiles bougent alternativement d'au plus une case et dans le mode "tour par tour" (i.e pas en temps réel).

1.2   Consignes

Modularité et protection des données

Une attention particulière doit être accordée à ce point. Le projet est subdivisé en trois modules : grille, mobiles et interface, ce dernier comprenant le programme principal et l'interface graphique (on doit donc lui donner un nom plus approprié, en rapport avec le projet). Cela sous-entend que les modules grille et mobiles ne doivent comporter aucune fonction graphique.

Dans chaque module, il faut distinguer les fonctions et les variables qui leur sont propres. Pour cela, on préfixera leur nom d'un double 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" est mise en œuvre si l'on effectue une importation au niveau de l'espace des noms global car, dans ces conditions, toute variable ou toute fonction préfixée d'un "_ _" ne sera pas importée. Exemple :

# module1
__x = 0

def __f():
  pass
# module principal
from module1 import *

Test unitaire

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

# module 1
def __f():
  pass

def g():
  pass

if __name__ == '__main__':
  __f()
  g()

1.3   La grille

La grille est une matrice de cases et représente le terrain sur lequel les mobiles évolueront. Chaque case est ramenée à un point donc aux coordonnées des mobiles dans la grille. La taille de la grille est définie en nombre de cases de l'un de ses côtés (c'est donc un carré) donnée par l'utilisateur. Exemple, une grille de taille 4 :

exemple de grille

Chaque case de la grille comporte un code qui indique à tout moment l'état du contenu. Pour cela, on mettra en place un ensemble de fonctions de manipulation et d'interrogation (initialisation, modification et consultation d'une case de la grille) dans un seul et même fichier (i.e module). On impose le codage des cases suivant :

  • CASE_VIDE
  • CASE_OBSTACLE
  • CASE_SORTIE
  • CASE_JOUEUR
  • CASE_ORDINATEUR

Ce module doit comporter au minimum les fonctions suivantes :

  • initialisation de la grille avec en paramètre sa taille,
  • modification des variables propres au module (la grille, sa taille), par exemple set_taille(taille),
  • consultation des valeurs des variables propres au module, par exemple get_taille() permet de connaître la taille de la grille,
  • de tests sur une case donnée (vide, obstacle, etc.), par exemple is_vide(position) retourne True si la case est vide,
  • affichage (en mode texte) du contenu de la grille.

1.4   Les mobiles

Deux mobiles sont définis dans un seul et même module et sont caractérisés par leur position. Les mobiles peuvent se déplacer (uniquement en haut, en bas, à droite ou à gauche de leur position) et agir (il existe deux actions, voir ci-dessous). Le déplacement de l'un des mobiles est géré directement par les actions de l'utilisateur, l'autre déplacement est défini par un algorithme. Cet algorithme gère le déplacement du mobile ordinateur sur la base du choix de la case voisine qui minimise la distance euclidienne entre cette case et la case contenant le mobile géré par l'utilisateur.Les actions sont de deux types : détruire tous les obstacles voisins d'un mobile, créer des obstacles dans le voisinage d'un mobile. On ne considère que les voisins directs c'est-à-dire les positions haut, bas, droite et gauche directement voisines (ou de distance 1) de la position d'un mobile.

On impose le codage des mouvements possibles suivant :

  • MVT_HAUT
  • MVT_BAS
  • MVT_GAUCHE
  • MVT_DROITE
  • MVT_NON

L'utilisation de dictionnaires pour gérer les déplacements est conseillé, l'un pour les déplacements horizontaux, l'autre pour les déplacements verticaux. Les clés seront constituées à partir du codage des mouvements possibles. Chaque valeur correspondant à une clé donnée est, dans le cas présent, une fonction qui permet de récupérer les coordonnées de la case visée. Exemple : MVT_BAS:max(0, mob[1] - 1).

Ce module doit comporter au minimum les fonctions suivantes :

  • initialisation de la position des mobiles, par exemple set_joueur(pposition),
  • déplacement des mobiles, par exemple deplace(pmob, pdirection),
  • des tests sur la faisabilité des déplacements, par exemple test_obstacle(pmob, pdirection),
  • de calcul de distance entre mobiles, de distance minimum (exprimée en cases), etc.
  • d'action (détruire, créer).

1.5   L'interface

L'interface est le module principal. Il permet d'initialiser le jeu, de gérer les actions de l'utilisateur (plus précisément se déplacer, détruire un obstacle, créer un obstacle, recommencer une partie et quitter l'application) et de mettre à jour les données et l'affichage. Pour cela un module cng en version 3.2 ou en version 2.7 est mis à disposition (avec un petit exemple). Il permet d'ouvrir une fenêtre graphique, d'afficher des sprites et de prendre en compte les périphériques d'entrée/sortie (souris, clavier, écran). Son utilisation repose sur les modules Tkinter (fourni par défaut avec le langage Python) et PIL - Python Imaging Library (pour Python version 2.x) ou Pillow (un "fork" de PIL) pour Python 3.x. L'aspect graphique de l'application est laissé à la discrétion du développeur.

image0 image1

Au lancement de l'application, l'utilisateur doit donner le nombre de cases par côté de la grille (un entier) et le pourcentage d'obstacles (un nombre compris entre 0 et 1). Ces informations sont communiquées en ligne de commandes. Exemple : ./labyrinthe 10 0.5.

1.6   Options

Différentes options au choix peuvent compléter le projet.

  • le calcul de distance en case en anticipant de deux cases (ou lieu d'une dans la version standard),
  • la gestion de plusieurs sorties,
  • la gestion de plusieurs mobiles gérés par le programme,
  • l'ajout de l'action de destruction pour les ordinateurs au bout d'un certain nombre de tours sans pouvoir bouger,
  • la création d'une distribution (voir pour cela les documentations du setup.py et de l'utilitaire distutils),
  • la représentation des mobiles en fonction de leur orientation (voir pour cela la fonction image_from_tiles() du module cng),
  • le choix d'un chemin par le(s) mobile(s) basé sur l'algorithme A*.