Avant d'imprimer, retirer la feuille de style En cliquant ici

Les objectifs

  • Gestion des constantes,
  • Préprocesseur,
  • Création de nouveaux types (énumérations, structures, et tableaux)
  • Commencer le mini-projet "Sokoban".

Vous pouvez tester le jeu en récupérant l'exécutable sokoban1.x. Après avoir récupéré le fichier, placez vous dans le répertoire du fichier téléchargé, tapez la commande linux chmod +x sokoban1.x (ajouter les droits d'exécution sur le fichier) puis exécutez le avec la commande ./sokoban1.x.

Dans ce jeu, vous êtes un manutentionnaire dans un entrepôt qui doit ranger des caisses à leur place; le problème est que les caisses sont tellement lourdes que vous ne pouvez que les poussez, et une seule à la fois.

Pour en savoir plus

Description du programme souhaité

Plan de jeu / affichage

Grille 1 - déclaration en C :

Grille initiale = {
 "########################",
 "#.........#......#.....#",
 "##.....O###............#",
 "###.............O#.....#",
 "####.............#O....#",
 "##################.....#",
 "####.............#.....#",
 "###...............O..###",
 "##...........#########o#",
 "#....................oo#",
 "#S....................o#",
 "########################"
};

À partir de cette déclaration, le programme va stocker en mémoire deux grilles :

Grille 2 - Grille initiale (cachée)
########################
#.........#......#.....#
##......###............#
###..............#.....#
####.............#.....#
##################.....#
####.............#.....#
###..................###
##...........#########o#
#....................oo#
#.....................o#
########################
Grille 3 - Grille de jeu (affichée)
########################
#.........#......#.....#
##.....O###............#
###.............O#.....#
####.............#O....#
##################.....#
####.............#.....#
###...............O..###
##...........#########o#
#....................oo#
#S....................o#
########################

Remarques :

  • La grille 1 est la façon dont le programmeur du jeu spécifie le plan de départ.
    # : mur
    S : Sokoban, le personnage
    O : une caisse
    o : une cible (ou doit être rangée une caisse)
    . : une case vide
    Vous aurez noté qu'il s'agit d'un tableau de chaînes de caractères, chaque chaîne représentant une ligne.
  • La grille 2 reste cachée dans le programme et ne stocke que ce qui est immobile (c'est-à-dire tout sauf Sokoban et les caisses, qui sont remplacés par des cases vides). Elle est produite automatiquement dès le début du jeu à partir de la grille 1, et ne change plus jamais.
  • La grille 3 est celle qui est affichée et qui sert à la logique du jeu. Elle est produite automatiquement dès le début du jeu à partir des grilles 1 et 2, et est mise à jour à chaque tour de jeu.

Commandes à traiter

Le joueur pourra saisir de 1 à 40 commandes d'1 caractère simulatanément
  • q: pour Quitter (sort du programme)
  • a: pour Aide (affiche ce message d'aide)
  • h: pour aller en Haut
  • b: pour aller en Bas
  • g: pour aller à Gauche
  • d: pour aller à Droite

Exercice 1 : sokoban.h

Création du fichier Sokoban.h.

Constantes

Déclarez 3 constantes : LARGEUR (24 dans notre exemple) et HAUTEUR (12 dans notre exemple) destinées à servir de taille de tableau  et MAXCH (40 dans notre exemple) représentant la longueur maximale des commandes.

Nouveaux types

Nous allons également créer des nouveaux types pour faciliter la manipulation des éléments du jeux dans notres programme.
  1. Boolean : pour pouvoir manipuler aisément des valeurs booléennes ; à définir avec une énumération (enum) en essayant de mimer le type primitif java du même nom.
  2. Symbole : pour pouvoir désigner les différents caractères qui peuvent apparaître sur la grille : MUR, CAISSE, CIBLE, SOKOBAN, VIDE Il s'agit donc encore d'une énumération (enum).
  3. Commande : pour pouvoir désigner les différents caractères qui serviront de commande pour jouer HAUT, BAS, GAUCHE, DROITE, AIDE, QUITTER (Encore une énumération)
  4. Ligne : Une ligne de la grille est une chaine de caractère, soit en C le type char *. Faites ce qu'il faut pour qu'on puisse écrire dans le programme indifféremment : Ligne ligne = "####.............#O....#";
    char ligne[LARGEUR+1] = "####.............#O....#";
    Au fait, pourquoi "+1" ?
  5. Grille : Une grille est un tableau de Ligne. Faites ce qu'il faut pour qu'on puisse écrire indifféremment :
    Grille grille = {
         "########################",
         "#.........#......#.....#",
         ...
         "########################"
    };
    Ligne grille[HAUTEUR] = {
         "########################",
         "#.........#......#.....#",
         ...
         "########################"
    };
    	
  6. Position : pour pouvoir stocker la ligne et la colonne où se trouve le personnage Sokoban (pensez aux structures).

N'oubliez pas de respecter les recommandations du cours 1, slide "un beau .h"

Faites vérifier par un intervenant vos définitions de type.

Exercice 2 : Sokoban.c et main.c

N'oubliez pas d'ajouter la déclaration des fonctions ajoutées dans sokoban.c au fichier sokoban.h.

Première itération

Créez un fichier sokoban.c et un fichier main.c.
  • Écrivez dans sokoban.c la procédure void afficheGrille(Grille g); qui affiche la grille passée en paramètre précédée d'une ligne blanche. N'oubliez pas de compléter sokoban.h.
  • Écrivez dans le fichier main.c la fonction int main(int argc, char ** argv);. Déclarez-y la grille initiale comme indiqué au début de l'énoncé et appellez la procédure afficheGrille() avec cette grille initiale.
  • Compilez à l'aide de la commande make et du fichier Makefile (click!) suivant :
    Warning: file_get_contents(../_sokoban/Makefile): failed to open stream: No such file or directory in /mnt/tanenbaum/sd3/thesards/masson/W3/v2/Teachings/IN3R11-2/old/sokoban1/content.php on line 276
    1.  
Testez votre programme le plus possible entre chaque itération !

Deuxième itération

  • Écrivez la fonction getSokoban() qui retourne la Position du Sokoban dans la grille passée en paramètre, ou {0,0} s'il n'est pas trouvé.
  • Ajoutez une énumération CodeErreur dans sokoban.h pour les différents messages d'erreur possibles (pour l'instant uniquement NO_SOKOBAN)
  • Ajoutez la procédure error() qui affiche un message d'erreur en fonction de l'entier passé en paramètre puis qui termine le programme en retournant ce code d'erreur (procédure standard exit()):

      void error(CodeErreur code)
      {
        switch (code) {
         case NO_SOKOBAN : fprintf(stderr,"pas de Sokoban!\n"); break;
         default: fprintf(stderr,"code erreur inconnu!\n");
        }
        exit(code);
      }/*error*/


  • Modifiez le main() pour qu'avant d'afficher la grille, il calcule (et affiche pour debug) la position du Sokoban ; s'il n'y en a pas, arrêtez le programme avec les bons message et code d'erreur prévus ci-dessus.

Troisième itération

  • Écrivez la procédure void nouveauJeu(Grille init, Grille g);. Cette procédure recopie la grille initiale passée en premier paramètre dans une nouvelle grille de jeu passée en deuxième paramètre. Puis, la procédure remplace dans la grille initiale les CAISSE et SOKOBAN par des VIDE ; il ne restera donc plus que des choses immobiles dans la grille initiale.
  • Modifiez le main() pour qu'au lieu d'afficher la grille initiale, il crée une nouvelle grille de jeu et l'affiche. La grille initiale ne sera plus jamais affichée. Elle restera cachée et servira pour les itérations 7 et 8.

Quatrième itération

  • Écrivez la fonction booléenne Boolean verif(Grille g); qui vérifie si chaque ligne de la grille passée en paramètre est une chaîne de caractères dont la longueur est bien égale à la LARGEUR déclarée de la grille. Dans le cas contraire, affichez un message approprié.
  • Enrichissez l'énumération CodeErreur et la procédure void error(CodeErreur ce); avec le cas 2 : "Grille initiale incorrecte !".
  • Modifiez la procédure nouveauJeu() pour qu'elle commence par cette vérification; si elle échoue, arrêtez le programme avec les bons message et code d'erreur prévus ci-dessus.

Cinquième itération

  • Écrivez la fonction entière int compte(Grille g, char c); qui compte dans la grille passée en premier paramètre le nombre de fois qu'apparaît le caractère passé en second paramètre.
  • Enrichissez la fonction booléenne verif() pour qu'elle vérifie ensuite d'une part s'il y a au moins une CAISSE, et d'autre part s'il y a autant de CAISSE que de CIBLE. Dans le cas contraire, affichez un message approprié sur la sortie d'erreur standard.

Sixième itération

Jusqu'à présent, nous nous sommes attachés à construire et vérifier la grille de jeu de départ. Nous allons maintenant rendre le jeu interactif en permettant de saisir des commandes.
  • Écrivez la procédure void aide() qui se contentera d'afficher la légende des différents symboles et les commandes disponibles.
  • Modifiez le main() pour qu'après la création de la nouvelle grille de jeu, il répète le traitement suivant tant qu'un booléen encore est vrai :
    1. afficher la grille
    2. afficher un prompt (caractère d'invite), par exemple "> "
    3. saisir la commande (voir détails ci-dessous)
    4. traiter la commande ; on se contentera pour l'instant des commandes AIDE et QUITTER ; les autres seront ignorées
  • La saisie : l'idée est de lire une ligne complète et d'en extraire un caractère à chaque tour de boucle qui sera interprété comme une commande. Il faut bien sûr relire une ligne à chaque fois que l'on arrive sur le caractère '\n' . Vous pourrez utiliser la procédure de la bibliothèque standard char *fgets (char *s, int size, FILE *stream);
    Les deux codes :
    #define MAXCH 20
    char commande[MAXCH+1];
    if(scanf(" %20s",commande) != 1) /* erreur ...*/ ;
    et
    #define MAXCH 20
    char commande[MAXCH+1];
    if(fgets(commande,MAXCH,stdin) == NULL ) /* erreur ...*/ ;
    sont plus ou moins équivalents, mais avec scanf, on ne peut pas utiliser la constante MAXCH, puisque le préprocesseur ne remplace pas dans les chaînes constantes. De plus une mauvaise utilisation de scanf peut être non sûre avec des chaînes de caractère (par exemple si on écrit "%s"), il est donc préférable de toujours utiliser fgets pour lire ce type de donnée.

Septième itération

  • Écrivez la fonction Position pas(Position pos_depart,Commande cmd) qui retourne la Position quand on fait un pas dans la direction indiquée par la commande passée en second paramètre à partir de la Position passée en premier paramètre. Afficher un message d'erreur approprié si la commande ne représente pas une des 4 directions.
  • Écrivez la fonction booléenne Boolean possible(Grille g,Position pos); qui vérifie que la Position passée en second paramètre est une destination possible (pour Sokoban ou pour une caisse) dans la grille passée en premier paramètre (une destination est possible si elle correspond à une cible ou à du vide). Le cas d'une position en dehors de la grille doit être gérer correctement.
  • Écrivez la procédure void deplace(Grille jeu,Position pos_a,Position pos_b, Grille init) à 4 paramètres : la Grille de jeu, la Position actuelle de ce qu'il faut déplacer (sokoban ou une caisse), sa nouvelle Position, et la Grille initiale. Cette procédure :
    • remplace, dans la grille de jeu, le symbole à la position de destination par le symbole de la position de départ
    • puis, elle remplace le symbole à la position de départ par VIDE ou CIBLE (cette information se trouve dans la grille initiale passée en dernier paramètre).
  • Écrivez la fonction Position joue(Grille jeu, Commande dir, Position depart, Grille init); à 4 paramètres : la Grille de jeu, la Commande (direction) à traiter, la Position actuelle de Sokoban, et la Grille initiale. Cette fonction retournera la nouvelle Position de Sokoban. Pour cela :
    • Calculez la future position du joueur (en utilisant pas())
    • Vérifiez que cette future position est une destination possible (en utilisant possible())
      • Si OUI, déplacer le joueur (en utilisant deplace())
      • Sinon :
        • Si c'est à cause d'un mur, le déplacement est impossible. Affichez un message appropprié.
        • Si c'est à cause d'une caisse, essayer de déplacer la caisse pour pouvoir déplacer le joueur.
  • Enrichissez le main() pour qu'il traite aussi les 4 commandes de direction, c'est-à-dire qu'il joue !

Huitième itération

  • Écrivez la fonction booléenne Boolean gagne(Grille jeu, Position soko, Grille init); à 3 paramètres : la Grille de jeu, la Position actuelle de Sokoban, et la Grille initiale.
    Un moyen de détecter que le joueur a gagné est lorsqu'il n'y a plus de CIBLE dans la grille de jeu et que Sokoban n'occupe pas la position d'une CIBLE dans la grille initiale.
  • Enrichissez le main() pour qu'il détecte quand le joueur a gagné. Dans ce cas, il doit afficher un message de félicitations en indiquant le nombre de coups joués, puis terminer la boucle.