Skip to content

Première version fonctionnelle

Lecture / ecriture dans / depuis un fichier

La bibliothèque standard d'entrées/sorties du C (stdio.h pour standard input output) fournit des fonctions pour que le programme puisse lire, c'est à dire récupérer des données, et écrire, c'est à dire envoyer et/ou stocker des données, dans un fichier.

Vous connaissez déjà ces fonctions : printf pour écrire, et scanf pour lire.

En réalité, printf et scanf sont des versions proxy de fprintf et fscanf.

Extrait du résultat de la commande man 3 printf :

PRINTF(3) Linux Programmer's Manual PRINTF(3)

NAME printf, fprintf, dprintf, sprintf, snprintf, vprintf, vfprintf, vdprintf, vsprintf, vsnprintf - formatted output conversion

SYNOPSIS

   #include <stdio.h>

   int printf(const char *format, ...);
   int fprintf(FILE *stream, const char *format, ...);
   int dprintf(int fd, const char *format, ...);
   int sprintf(char *str, const char *format, ...);
   int snprintf(char *str, size_t size, const char *format, ...);

Vous voyez que la fonction fprintf a une signature très similaire à printf, elle prend un paramètre supplémentaire de type FILE* appellé stream qui représente un fichier ouvert en lecture et/ou écriture.

Un programme a systématiquement au moins trois fichiers ouverts, et peut donc utiliser trois variables gloables de type FILE* :

  • stdin : il s'agit d'une variable pointant sur la description du fichier d'entrée standard, ouvert donc en lecture
  • stdout : il s'agit d'une variable pointant sur la description du fichier de sortie standard, ouvert donc en écriture
  • stderr : il s'agit d'une variable pointant sur la description du fichier de sortie d'erreur standard, ouvert donc en écriture

Par défaut, stdin est le clavier, stdout et stderr sont tous les deux l'écran (le terminal en réalité). Ceci peut être modifié lors du lancement du programme, par exemple pour rediriger la sortie d'erreur standard d'un programme vers un fichier de log, on pourra le lancer ainsi :

$> ./mon_programme 2> error_log

Pour ouvrir d'autres fichiers, et donc obtenir plus de variables de type FILE*, il faudra utiliser la fonction fopen(). Lorsque l'on a fini d'utiliser le fichier, il faut penser à appeler la fonction fclose() pour libérer les ressources proprement.

  • lire la page de manuel de fopen()
  • quelle est la différence entre les mode r+ et w+ ? Faites un programme qui ouvre un fichier de test que vous aurez créé au préalable contenant plusieurs lignes avec le mode r+, puis le referme immédiatement. Vérifiez que le fichier n'a pas changé. Faites la même expérience avec le mode w+.
  • écrire un programme qui ouvre un fichier et écrit bonjour dedans avec fprintf.
  • Si nécessaire modifiez votre programme pour que si on l'appelle plusieurs fois, le mot bonjour apparaisse plusieurs fois dans le fichier.
  • faites un programme qui ouvre un fichier en lecture et affiche son contenu en appelant successivement fscanf(...," %c",...) et printf("%c",...) dans une boucle. Comment sait on que l'on est arrivé à la fin du fichier ? Indice : lisez la section "RETURN VALUE" de la page de manuel de la fonction scanf()
  • Il existe bien sur d'autres fonctions pour lire/écrire dans les fichiers pour répondre à des besoins spécifiques. Par exemple, il peut être pratique de lire d'un coup une ligne entière (tous les caractères jusqu'au prochain caractère \n. Pour cela vous pouvez utiliser entre autre fgets(), qui s'utilise ainsi :

    char line[MAX_LINE_SIZE];
    int taille_line;
    FILE * fichier = fopen(...);
    ...
    if(fgets(line,MAX_LINE_SIZE,fichier) == NULL){
        /* il y a une erreur de lecture */
        fprintf(stderr,"erreur de lecture du fichier"); exit(1);
    }
    ...
    fclose(fichier);
fgets met le caractère \n dans la ligne ainsi qu'un \0, permettant ainsi facilement d'afficher la ligne lue, par exemple avec printf("%s",line);

fgets est ainsi très pratique lorsqu'on connaît la taille max d'une ligne. Lorsqu'on ne connait pas il y a d'autre fonction comme getline(), qui ne nous intéresse pas pour le moment.

Application au mini projet

Ajoutez une option -i à votre programme prenant en paramètre un nom de fichier. Le programme lira alors la grille initiale dans ce fichier, ligne à ligne grâce a la fonction fgets(), au lieu d'utiliser celle qui est en dur dans le code. Pour l'instant le programme n'acceptera que des grilles qui font toutes la même taille (donnée par les constantes NBL et NBC)

Détection des collisions

Nous allons modifier les fonctions move_snake() et crawl() pour que le jeu prenne fin lorsque le serpent rencontre un mur ou se mange lui même, et également pour permettre au serpent de manger les fruits, et que le jeu s'arrete lorsque tous les fruits sont mangés.

  • modifiez le prototype de move_snake pour qu'elle renvoie un enum Element (ou un Element si vous avez défini un tel type cf tp2). La fonction sauvegardera l'élement à la position de la tête du serpent après sont appel à crawl() avant de le remplacer par SNAKE pour pouvoir le renvoyer
  • modifiez maintenant la boucle de jeu principale pour qu'elle teste le retour de move_snake()
    • ajoutez d'abord une variable nb_fruit qui devra être initialisée à la bonne valeur (pour cela vous devrez faire un parcourt de la grille pour compter les fruits. Vous pouvez aussi modifier la fonction qui lit la grille depuis un fichier pour qu'elle compte les fruits en même temps.
    • si move_snake() a renvoyé un fruit, décrémentez cette variable. Si la variable passe à zero, la boucle de jeu devra s'arrêter. Vous afficherez dans la fenêtre un texte de votre choix indiquant que le joueur a gagné.
    • si move_snake() a renvoyé un mur ou le serpent, la boucle de jeu devra s'arreter. Vous afficherez dans la fenêtre un texte de votre choix indiquant que le joueur a perdu, et pourquoi (mur ou serpent).

Interaction du joueur

Le squelette de boucle de jeu donné à la fin du tp2 nous permet de récupérer a chaque passage une touche clavier éventuellement pressée par le joueur. Pour l'instant elle ne nous sert qu'à quitter si le joueur a appuyé sur ESC.

  • Vous allez maintenant ajouter un test (à l'aide d'un switch) à la fin de la boucle sur la valeur de la variable touche. Les valeurs correspondant aux flèches directionnelles du clavier sont : MLV_KEYBOARD_DOWN, MLV_KEYBOARD_RIGHT, MLV_KEYBOARD_LEFT et MLV_KEYBOARD_UP. Si la variable touche correspond à l'une de ces valeurs, vous changerez le champs direction de votre serpent pour lui donner la valeur appropriée.
  • Vous devriez remarquer qu'il est difficile de controler le serpent. En effet, la fréquence de la boucle est trop rapide, le temps d'attente étant calé sur une cible de 24 images secondes. Nous ne souhaitons pas changer cette valeur, car ca correspond au framerate que l'on veut obtenir. En revanche, l'astuce est ici de ne pas faire les opérations de déplacement du serpent à chaque passage dans la boucle, mais uniquement tous les x passages. Plus x sera petit, plus la difficulté sera grande. Commencez avec une valeur de x égale à 6.

Votre boucle devrait maintenant avoir une structure ressemblant à cela :

    /* ... début de de la fonction */

    MLV_create_window( "SNAKE", "3R-IN1B", width, height );
    MLV_change_frame_rate( 24 );

    int loop_count=0; /* TODO : a mettre au début de la fonction pour respecter la norme ANSI */

    while(
        MLV_get_event (
            &touche, NULL, NULL,
            NULL, NULL,
            NULL, NULL, NULL,
            NULL
        ) == MLV_NONE ||
        touche != MLV_KEYBOARD_ESCAPE
    ){


        MLV_clear_window( MLV_COLOR_BROWN );


        loop_count = (loop_count+1)%DIFFICULTY;

        if(loop_count ==0){

            /* TODO : appel à move_snake() et fin du jeu en fonction de la valeur renvoyée */

        }

        draw_grid(grid);        
        MLV_actualise_window();


        switch(touche)
        {
            case MLV_KEYBOARD_DOWN :
                /* TODO change la direction du serpent*/  
                break;
            case : /*TODO a completer */
        }


        touche = MLV_KEYBOARD_NONE;
        MLV_delay_according_to_frame_rate();
    }


    MLV_free_window();


    /* fin de la fonction */

Rendu

N'oubliez pas de pousser votre travail sur gitlab !

Vous avez normalement maintenant une version fonctionnelle du jeu. Déposer sur Blackboard votre projet dans son état actuel (une seule tentative, attention).

La deuxième partie du projet va consister à modifier l'implémentation du serpent et de la grille afin de pouvoir charger dynamiquement des nouveaux niveaux, de lever la limitation sur la taille de la grille, et de permettre au serpent de grandir lorsqu'il mange des fruits.