CSC 4103 – Programmation système

Portail informatique

Démineur

Dans cette première session de TP, l'objectif est de se familiariser avec le langage C, d'écrire un premier programme simple, les compiler et les exécuter, de savoir se documenter face à une fonction dont on ne connaît pas le comportement.

Dans ce TP, nous allons créer un Démineur. Ce jeu se joue sur une carte (ici de taille 6 x 6) où certaines cases contiennent des bombes cachées. L'utilisateur doit retrouver toutes les bombes. Pour cela, il peut:

  • Tester une case. Si cette case contient une bombe, l'utilisateur a perdu. Sinon, la case indique le nombre de bombes dans les 8 cases voisines;
  • Placer un Drapeau sur une case pour indiquer qu'il pense qu'une bombe y est cachée. Lorsque l'utilisateur a identifié l'emplacement de toutes les bombes, il a gagné.

Voici un exemple de déroulement du jeu:

$ ./demineur Quel est votre nom ? Alice Bienvenue 'Alice'. Tour 0 -- Alice 0 1 2 3 4 5 0 1 2 3 4 5 Placer un drapeau (D) ou tester (T) ? T Quelle ligne ? (0-6) 3 Quelle colonne ? (0-6) 3 Tour 1 -- Alice 0 1 2 2 1 1 3 2 0 0 4 2 0 0 5 0 1 2 3 4 5 Placer un drapeau (D) ou tester (T) ? T Quelle ligne ? (0-6) 4 Quelle colonne ? (0-6) 1 Tour 2 -- Alice 0 1 2 2 1 1 3 2 0 0 4 3 2 0 0 5 0 1 2 3 4 5 Placer un drapeau (D) ou tester (T) ? T Quelle ligne ? (0-6) 3 Quelle colonne ? (0-6) 1 Perdu Alice après 2 tours! Solution: 0 1 b 2 b 2 1 1 3 b 2 0 0 4 b 3 2 0 0 5 b 0 1 2 3 4 5 Votre carte: 0 1 2 2 1 1 3 2 0 0 4 3 2 0 0 5 0 1 2 3 4 5

Mise en place du jeu

Créer, avec emacs, le fichier demineur.c. Vous écrirez dans ce fichier un programme qui demande son nom à l'utilisateur, stocke son nom dans la variable globale char nom[128] et affiche un message de bienvenue personnalisé (par exemple"Bienvenue Alice !"
$ emacs demineur.c & Corrigé

Compilez ce programme, puis exécutez le..
$ gcc demineur.c -o demineur -Wall -Werror $ ./demineur Quel est votre nom ? Alice Bienvenue Alice!

Création de la carte de jeu

Le jeu se déroulera sur une "carte" de 6x6 cases. Chaque case sera représentée par un char. Le programme utilisera deux cartes:
  • carte_solution: la carte qui contient l'emplacement des bombes. Cette carte ne sera pas affichée, et elle sera utilisée pour vérifier si le joueur "clique" sur une bombe. Chaque case de la carte pourra contenir:
    • 'b': la case contient une bombe
    • ' ': la case ne contient pas de bombe
  • carte_utilisateur: la carte affichée à l'utilisateur. Cette carte contient les cases qui ont été révélées. Chaque case de la carte pourra contenir:
    • '0'-'8': la case a été testée par l'utilisateur et elle ne contient pas de bombe. Le nombre indiqué révèle le nombre de bombes dans les 8 cases voisines.
    • 'D': l'utilisateur a placé un Drapeau sur cette case car il soupçonne la présence d'une bombe
    • ' ': la case n'a pas été testée.

Ajoutez les lignes suivantes au début du programme demineur.c:

#define NB_LIGNES 6 #define NB_COLONNES 6 #define NB_BOMBS 5 typedef char carte_t[NB_LIGNES][NB_COLONNES];
Ces lignes définissent des constantes et types utilisées dans la suite de l'exercice.

Créez une structure struct demineur contenant:

  • carte_solution et carte_utilisateur : des tableaux à deux dimensions de caractères de taille NB_LIGNES x NB_COLONNES
  • nb_round: un entier servant à compter le nombre de tours de jeu

Définissez une variable globale demineur de type struct demineur.

Créez la fonction void init_demineur() qui initialise la variable demineur. Pour l'instant, cette fonction doit remplir les deux cartes avec des caractères 'x', et initialiser nb_round à 0.

Modifiez la fonction main afin qu'elle appelle init_demineur après avoir salué l'utilisateur.

Créez la fonction qui affiche le contenu de carte. En plus du contenu des cases, affichez également les numéros de lignes et de colonnes.

Modifiez la fonction main pour qu'elle affiche carte_utilisateur. Vous devriez obtenir cet affichage:

Quel est votre nom ? Alice Bienvenue 'Alice'. 0 x x x x x x 1 x x x x x x 2 x x x x x x 3 x x x x x x 4 x x x x x x 5 x x x x x x 0 1 2 3 4 5

Implémentez les fonctions utilitaires suivantes:

  • int is_valid(int i, int j): vérifie si la position (i,j) est dans l'aire de jeu. La fonction retourne 1 si la position est valide, et 0 sinon.
  • char get_val(carte_t carte, int i, int j): retourne le caractère stocké à la position (i, j) de carte
  • void set_val(carte_t carte, int i, int j, char val): modifie le caractère stocké à la position (i, j) de carte
  • int is_bomb(carte_t carte, int i, int j): retourne 1 si la position (i, j) de carte contient une bombe. Retourne 0 sinon.
  • int can_play(int i, int j) retourne 1 si l'utilisateur est autorisé à jouer dans la case (i, j), ou 0 sinon. L'utilisateur peut jouer si la position est valide et si carte_utlisateur contient 'D' ou ' ' à cette position.

Ajoutez quelques appels à ces fonctions dans main afin de vous assurer qu'elles fonctionnent correctement.

Afin de faciliter la découverte de bugs, ajoutez des appels à la fonction assert dans les fonction get_val et set_val.

Vous ne connaissez pas la fonction assert ? Lancez la commande man assert dans votre terminal !

Implémentez la fonction void placer_bombe() qui place NB_BOMBS dans carte_solution à des endroits aléatoires.

Pour obtenir un entier dans l'intervale [0:X[, vous pouvez utiliser rand() % X.

Avant de poser une bombe à la position (i, j), vérifiez qu'il n'y a pas déjà une bombe à cet endroit.

Implémentez maintenant la fonction void tour_de_jeu() qui effectue un tour de jeu. Ce fonction doit:

  • Afficher le numéro du tour et le nom du joueur
  • Afficher carte_utilisateur
  • Demander à l'utilisateur s'il veut placer un drapeau ou tester une case. Stocker ce choix dans une variable char choix qui peut valoir 'D' ou 'T'
  • Demander à l'utilisateur la position (i, j) qu'il souhaite tester ou sur laquelle il souhaite mettre un drapeau
  • Appeler la fonction void mettre_a_jour_cartes(char choix, int i, int j) (qui sera implémentée à la question suivante)
  • Incrémenter le champ nb_round
Lors de la saisie de choix, i, et j, vérifiez que la valeur saisie par l'utilisateur est correcte. Si elle est incorrecte, redemandez à l'utilisateur de saisir.

Pour tester les valeurs saisies, utilisez une boucle do { ... } while(...):

do { printf("Veuillez saisir [...]\n"); scanf(...); } while(test_valeur_saisie);

Pour un affichage plus joli, vous pouvez commencer le tour en effaçant le contenu du terminal. Pour cela, vous pouvez appeler la fonction suivante et l'appeler au début du tour:

void clear_screen() { system("clear"); }

Implémentez la fonction int show_hint(int i, int j). Cette fonction doit:

  • compter le nombre de bombes aux positions ([i-1:i+1], [j-1:j+1])
  • modifier la valeur de la position (i, j) sur les deux cartes. La nouvelle valeur sera le nombre de bombes détectées
  • retourner le nombre de bombes détectées

La valeur d'une case est de type char, mais le nombre de bombes détectées est un int compris entre 0 et 9. Pour "traduire" cet int en char, on peut utiliser le fait que les caractères ascii des chiffres (man ascii) sont contigus: '0' correspond à la valeur décimale 48, '1' correspond à 49, etc.

On peut donc écrire:

int nb_bombs = 7; char c = nb_bombs+'0'; // valeur décimale: 55

Implémentez la fonction void mettre_a_jour_cartes(char choix, int i, int j). Cette fonction doit:

  • Si le choix vaut T:
    • Vérifier s'il y a une bombe à la position (i, j) dans carte_solution
    • S'il y a une bombe, afficher un message et terminer le programme en appelant exit
    • Sinon, appeler la fonction int show_hint(int i, int j). Si show_hint indique qu'aucune bombe n'est autour de (i, j), appeler show_hint pour les positions ([i-1:i+1], [j-1:j+1])
  • Si le choix vaut D:
    • Changer la valeur de la position (i, j) dans carte_utilisateur. Si la case était vide, sa nouvelle valeur est D. Si elle contenait un drapeau, sa nouvelle valeur est ' '
    • Appeler la fonction void test_victoire(). Cette fonction compte le nombre de drapeaux (cases 'D') dans carte_utilisateur qui correspondent à des bombes dans carte_solution. Si ce nombre correspond à NB_BOMBS, la fonction affiche un message de félicitations et termine le programme en appelant exit

Modifiez la fonction main pour qu'elle appelle tour_de_jeu() dans une boucle infinie.

En testant votre démineur, vous remarquerez sans doute que les bombes sont toujours positionnées au même endroit. Ce problème est dû au fonctionnement de la fonction rand() qui génère un nombre pseudo aléatoire à partir d'une "graine". Si on n'initialise pas ce générateur de nombres, il utilise toujours la même graine, et génère donc la même suite de nombre.

Vous pouvez rendre le jeu plus aléatoire en initialisant le générateur avec la fonction void srand(unsigned int seed). Il faut donc fournir un entier seed qui change d'une exécution du programme à l'autre. On ne peut bien sûr pas utiliser une valeur retournée par rand(). On peut par exemple utiliser le PID du processus (voir man getpid) qui varie suffisament pour rendre le jeu intéressant.

Attention ! N'utilisez pas cette technique dans la vraie vie si vous avez besoin de sécurité. La faible variation de la valeur initiale peut entraîner une faille de sécurité de type Insufficient Entropy.

Pour aller plus loin

Cet exercice est optionnel. Il permet aux étudiants débrouillards de s'exercer avec des questions plus difficiles. Cet exercice est volontairement peu guidé.

Le but de cet exercice est d'implémenter une IA capable de jouer (et de gagner !) seule.

Dans la fonction tour_de_jeu, supprimer les appels à scanf et remplacez les par du code qui détermine le meilleur coup à jouer.
  • L'IA n'est bien sûr pas autorisée à consulter carte_solution !
  • Il est parfois nécessaire de choisir une case au hasard (notamment au premier tour). Dans ce genre de situation, vous pouvez sans doute calculer la probabilité qu'une bombe explose pour chaque case.
  • Si vous savez déja utiliser les pointeurs, le mieux est sans doute de définir une fonction void get_next_move(char* choix, int* i, int* j) et d'y placer le code de l'IA.