Fil d'Ariane
Mis à jour le mardi 25 juillet 2017
  • 40 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Ce cours existe en eBook.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

TP : réalisation d'un Pendu

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Je ne le répéterai jamais assez : pratiquer est essentiel. C'est d'autant plus essentiel pour vous car vous venez de découvrir de nombreux concepts théoriques et, quoi que vous en disiez, vous ne les aurez jamais vraiment compris tant que vous n'aurez pas pratiqué.

Pour ce TP, je vous propose de réaliser un Pendu. C'est un grand classique des jeux de lettres dans lequel il faut deviner un mot caché lettre par lettre. Le Pendu aura donc la forme d'un jeu en console en langage C.

L'objectif est de vous faire manipuler tout ce que vous avez appris jusqu'ici. Au menu : pointeurs, chaînes de caractères, fichiers, tableaux… bref, que des bonnes choses !

Les consignes

Je tiens à ce qu'on se mette bien d'accord sur les règles du Pendu à réaliser. Je vais donc vous donner ici les consignes, c'est-à-dire vous expliquer comment doit fonctionner précisément le jeu que vous allez créer.

Tout le monde connaît le Pendu, n'est-ce pas ? Allez, un petit rappel ne peut pas faire de mal : le but du Pendu est de retrouver un mot caché en moins de 10 essais (mais vous pouvez changer ce nombre maximal pour corser la difficulté, bien sûr !).

Déroulement d'une partie

Supposons que le mot caché soit ROUGE.
Vous proposez une lettre à l'ordinateur, par exemple la lettre A. L'ordinateur vérifie si cette lettre se trouve dans le mot caché.

À partir de là, deux possibilités :

  • la lettre se trouve effectivement dans le mot : dans ce cas, on dévoile le mot avec les lettres qu'on a déjà trouvées ;

  • la lettre ne se trouve pas dans le mot (c'est le cas ici, car A n'est pas dans ROUGE) : on indique au joueur que la lettre ne s'y trouve pas et on diminue le nombre de coups restants. Quand il ne nous reste plus de coups (0 coup), le jeu est terminé et on a perdu.

Supposons maintenant que le joueur tape la lettre G. Celle-ci se trouve dans le mot caché, donc on ne diminue pas le nombre de coups restants au joueur. On affiche le mot secret avec les lettres qu'on a déjà découvertes, c'est-à-dire quelque chose comme ça :

Mot secret : ***G*

Si ensuite on tape un R, comme la lettre s'y trouve, on l'ajoute à la liste des lettres trouvées et on affiche à nouveau le mot avec les lettres déjà découvertes :

Mot secret : R**G*
Le cas des lettres multiples

Dans certains mots, une même lettre peut apparaître deux ou trois fois, voire plus !
Par exemple, il y a deux Z dans PUZZLE ; de même, il y a trois E dans ELEMENT.

Que fait-on dans un cas comme ça ? Les règles du Pendu sont claires : si le joueur tape la lettre E, toutes les lettres E du mot ELEMENT doivent être découvertes d'un seul coup :

Mot secret : E*E*E**

Il ne faut donc pas avoir à taper trois fois la lettre E pour que tous les E soient découverts.

Exemple d'une partie complète

Voici à quoi devrait ressembler une partie complète en console lorsque votre programme sera terminé :

Bienvenue dans le Pendu !
 
Il vous reste 10 coups a jouer
Quel est le mot secret ? ******
Proposez une lettre : E

Il vous reste 9 coups a jouer
Quel est le mot secret ? ******
Proposez une lettre : A

Il vous reste 9 coups a jouer
Quel est le mot secret ? *A****
Proposez une lettre : O

Il vous reste 9 coups a jouer
Quel est le mot secret ? *A**O*
Proposez une lettre :

Et ainsi de suite jusqu'à ce que le joueur ait découvert toutes les lettres du mot (ou bien qu'il ne lui reste plus de coups à jouer) :

Il vous reste 8 coups a jouer
Quel est le mot secret ? MA**ON
Proposez une lettre : R

Gagne ! Le mot secret etait bien : MARRON
Saisie d'une lettre en console

La lecture d'une lettre dans la console est plus compliquée qu'il n'y paraît.
Intuitivement, pour récupérer un caractère, vous devriez avoir pensé à :

scanf("%c", &maLettre);

Et effectivement, c'est bien.%cindique que l'on attend un caractère, qu'on stockera dansmaLettre(une variable de typechar).

Tout se passe très bien… tant qu'on ne refait pas unscanf. En effet, vous pouvez tester le code suivant :

int main(int argc, char* argv[])
{
     char maLettre = 0;
 
     scanf("%c", &maLettre);
     printf("%c", maLettre);
 
     scanf("%c", &maLettre);
     printf("%c", maLettre);
 
     return 0;
}

Normalement, ce code est censé vous demander une lettre et vous l'afficher, et cela deux fois.
Testez. Que se passe-t-il ? Vous entrez une lettre, d'accord, mais… le programme s'arrête de suite après, il ne vous demande pas la seconde lettre ! On dirait qu'il ignore le secondscanf.

Que s'est-il passé ?

En fait, quand vous entrez du texte en console, tout ce que vous tapez est stocké quelque part en mémoire, y compris l'appui sur la toucheEntrée (\n).

Ainsi, la première fois que vous entrez une lettre (par exemple A) puis que vous appuyez sur Entrée, c'est la lettre A qui est renvoyée par lescanf. Mais la seconde fois,scanfrenvoie le\ncorrespondant à la touche Entrée que vous aviez pressée auparavant !

Pour éviter cela, le mieux c'est de créer notre propre petite fonctionlireCaractere():

char lireCaractere() 
{ 
    char caractere = 0;
 
    caractere = getchar(); // On lit le premier caractère
    caractere = toupper(caractere); // On met la lettre en majuscule si elle ne l'est pas déjà
 
    // On lit les autres caractères mémorisés un à un jusqu'au \n (pour les effacer) 
    while (getchar() != '\n') ;
 
    return caractere; // On retourne le premier caractère qu'on a lu 
}

Cette fonction utilisegetchar()qui est une fonction destdioqui revient exactement à écrirescanf("%c", &lettre);. La fonctiongetcharrenvoie le caractère que le joueur a tapé.

Après, j'utilise une fonction standard qu'on n'a pas eu l'occasion d'étudier dans le cours :toupper(). Cette fonction transforme la lettre indiquée en majuscule. Comme ça, le jeu fonctionnera même si le joueur tape des lettres minuscules. Il faudra inclurectype.hpour pouvoir utiliser cette fonction (ne l'oubliez pas !).

Vient ensuite la partie la plus intéressante : celle où je vide les autres caractères qui auraient pu avoir été tapés. En effet, en rappelantgetcharon prend le caractère suivant que l'utilisateur a tapé (par exemple l'Entrée\n).
Ce que je fais est simple et tient en une ligne : j'appelle la fonctiongetcharen boucle jusqu'à tomber sur le caractère\n. La boucle s'arrête alors, ce qui signifie qu'on a « lu » tous les autres caractères, ils ont donc été vidés de la mémoire. On dit qu'on vide le buffer.

Pourquoi y a-t-il un point-virgule à la fin duwhileet pourquoi ne voit-on pas d'accolades ?

En fait, je fais une boucle qui ne contient pas d'instructions (la seule instruction, c'est legetcharentre les parenthèses). Les accolades ne sont pas nécessaires vu que je n'ai rien d'autre à faire qu'ungetchar. Je mets donc un point-virgule pour remplacer les accolades. Ce point-virgule signifie « ne rien faire à chaque passage dans la boucle ». C'est un peu particulier je le reconnais, mais c'est une technique à connaître, technique qu'utilisent les programmeurs pour faire des boucles très courtes et très simples.

Dites-vous que lewhileaurait aussi pu être écrit comme ceci :

while (getchar() != '\n') 
{

}

Il n'y a rien entre accolades, c'est volontaire, vu qu'on n'a rien d'autre à faire. Ma technique consistant à placer juste un point-virgule est simplement plus courte que celle des accolades.

Enfin, la fonctionlireCaractereretourne le premier caractère qu'elle a lu : la variablecaractere.

En résumé, pour récupérer une lettre dans votre code, vous n'utiliserez pas :

scanf("%c", &maLettre);

… vous utiliserez à la place notre super-fonction :

maLettre = lireCaractere();

Dictionnaire de mots

Dans un premier temps pour vos tests, je vais vous demander de fixer le mot secret directement dans votre code. Vous écrirez donc par exemple :

char motSecret[] = "MARRON";

Alors oui, bien sûr, le mot secret sera toujours le même si on laisse ça comme ça, ce qui n'est pas très rigolo. Je vous demande de faire comme ça dans un premier temps pour ne pas mélanger les problèmes. En effet, une fois que votre jeu de Pendu fonctionnera correctement (et seulement à partir de ce moment-là), vous attaquerez la seconde phase : la création du dictionnaire de mots.

Qu'est-ce que c'est, le « dictionnaire de mots » ?

C'est un fichier qui contiendra de nombreux mots pour votre jeu de Pendu. Il doit y avoir un mot par ligne. Exemple :

MAISON
BLEU
AVION
XYLOPHONE
ABEILLE
IMMEUBLE
GOURDIN
NEIGE
ZERO

À chaque nouvelle partie, votre programme devra ouvrir ce fichier et prendre un des mots au hasard dans la liste. Grâce à cette technique, vous aurez un fichier à part que vous pourrez éditer tant que vous voudrez pour ajouter des mots secrets possibles pour le Pendu.

Le problème qui se posera rapidement à vous sera de savoir combien il y a de mots dans le dictionnaire. En effet, si vous voulez choisir un mot au hasard, il faudra tirer au sort un nombre entre 0 et X, et vous ne savez pas a priori combien de mots contient votre fichier.

Pour résoudre le problème, il y a deux solutions. Vous pouvez indiquer sur la première ligne du fichier le nombre de mots qu'il contient :

3
MAISON
BLEU
AVION

Cependant cette technique est ennuyeuse, car il faudra recompter manuellement le nombre de mots à chaque fois que vous en ajouterez un (ou ajouter 1 à ce nombre si vous êtes malins plutôt que de tout recompter, mais ça reste quand même une solution un peu bancale). Aussi je vous propose plutôt de compter automatiquement le nombre de mots en lisant une première fois le fichier avec votre programme. Pour savoir combien il y a de mots, c'est simple : vous comptez le nombre de\n(retours à la ligne) dans le fichier.

Une fois que vous aurez lu le fichier une première fois pour compter les\n, vous ferez unrewindpour revenir au début. Vous n'aurez alors plus qu'à tirer un nombre au sort parmi le nombre de mots que vous avez comptés, puis à vous rendre au mot que vous avez choisi et à le stocker dans une chaîne en mémoire.

Je vous laisse un peu réfléchir à tout cela, je ne vais pas trop vous aider quand même, sinon ça ne serait plus un TP ! Sachez que vous avez acquis toutes les connaissances qu'il faut dans les chapitres précédents, vous êtes donc parfaitement capables de réaliser ce jeu. Ça va prendre plus ou moins de temps et c'est moins facile qu'il n'y paraît, mais en vous organisant correctement (et en créant suffisamment de fonctions), vous y arriverez.

Bon courage, et surtout : per-sé-vé-rez !

La solution (1 : le code du jeu)

Si vous lisez ces lignes, c'est soit que vous avez terminé le programme, soit que vous n'arrivez pas à le terminer.

J'ai personnellement mis plus de temps que je ne le pensais pour réaliser ce petit jeu apparemment tout bête. C'est souvent comme ça : on se dit « bah c'est facile » alors qu'en fait, il y a plusieurs cas à gérer.

Je persiste toutefois à dire que vous êtes tous capables de le faire. Il vous faudra plus ou moins de temps (quelques minutes, quelques heures, quelques jours ?), mais ça n'a jamais été une course. Je préfère que vous y passiez beaucoup de temps et que vous y arriviez, plutôt que vous n'essayiez que 5 minutes et que vous regardiez la solution.

N'allez pas croire que j'ai écrit le programme d'une traite. Moi aussi, comme vous, j'y suis allé pas à pas. J'ai commencé par faire quelque chose de très simple, puis petit à petit j'ai amélioré le code pour arriver au résultat final.
J'ai fait plusieurs erreurs en codant : j'ai oublié à un moment d'initialiser une variable correctement, j'ai oublié d'écrire le prototype d'une fonction ou encore de supprimer une variable qui ne servait plus dans mon code. J'ai même – je l'avoue – oublié un bête point-virgule à un moment à la fin d'une instruction.

Tout ça pour dire quoi ? Que je ne suis pas infaillible et que je vis à peu près les mêmes frustrations que vous (« ESPÈCE DE PROGRAMME DE ***** TU VAS TE METTRE À MARCHER, OUI OU NON !? »).

Je vais vous présenter la solution en deux temps.

  1. D'abord je vais vous montrer comment j'ai fait le code du jeu lui-même, en fixant le mot caché directement dans le code. J'ai choisi le mot MARRON car il me permet de tester si je gère bien les lettres en double, comme le R ici.

  2. Ensuite, je vous montrerai comment dans un second temps j'ai ajouté la gestion du dictionnaire de mots pour tirer au sort un mot secret pour le joueur.

Bien sûr, je pourrais vous montrer tout le code d'un coup mais… ça ferait beaucoup à la fois, et nombre d'entre vous n'auraient pas le courage de se pencher sur le code.

Je vais essayer de vous expliquer pas à pas mon raisonnement. Retenez que ce qui compte, ce n'est pas le résultat, mais la façon dont on réfléchit.

Analyse de la fonctionmain

Comme tout le monde le sait, tout commence par unmain. On n'oublie pas d'inclure les bibliothèquesstdio,stdlibetctype(pour la fonctiontoupper()) dont on aura besoin :

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int main(int argc, char* argv[])
{

    return 0;
}

Ok, jusque-là tout le monde devrait suivre.
Notremainva gérer la plupart du jeu et faire appel à quelques-unes de nos fonctions quand il en aura besoin.

Commençons par déclarer les variables nécessaires. Rassurez-vous, je n'ai pas pensé de suite à toutes ces variables, il y en avait un peu moins la première fois que j'ai écrit le code !

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int main(int argc, char* argv[])
{
    char lettre = 0; // Stocke la lettre proposée par l'utilisateur (retour du scanf)
    char motSecret[] = "MARRON"; // C'est le mot à trouver
    int lettreTrouvee[6] = {0}; // Tableau de booléens. Chaque case correspond à une lettre du mot secret. 0 = lettre non trouvée, 1 = lettre trouvée
    int coupsRestants = 10; // Compteur de coups restants (0 = mort)
    int i = 0; // Une petite variable pour parcourir les tableaux

    return 0;
}

J'ai volontairement écrit une déclaration de variable par ligne ainsi que plusieurs commentaires pour que vous compreniez l'intérêt de chaque variable. En pratique, vous n'aurez pas forcément besoin de mettre tous ces commentaires et vous pourrez grouper plusieurs déclarations de variables sur la même ligne.

Je pense que la plupart de ces variables semblent logiques : la variablelettrestocke la lettre que l'utilisateur tape à chaque fois,motSecretle mot à trouver,coupsRestantsle nombre de coups, etc.
La variableiest une petite variable que j'utilise pour parcourir mes tableaux avec desfor. Elle n'est donc pas extrêmement importante mais nécessaire si on veut faire nos boucles.

Enfin, la variable à laquelle il fallait penser, celle qui fait la différence, c'est mon tableau de booléenslettreTrouvee. Vous remarquerez que je lui ai donné pour taille le nombre de lettres du mot secret (6). Ce n'est pas un hasard : chaque case de ce tableau de booléens représente une lettre du mot secret. Ainsi, la première case représente la première lettre, la seconde la seconde lettre, etc.
Les cases du tableau sont au départ initialisées à 0, ce qui signifie « Lettre non trouvée ». Au fur et à mesure de l'avancement du jeu, ce tableau sera modifié. Pour chaque lettre du mot secret trouvée, la case correspondante du tableaulettreTrouveesera mise à 1.

Par exemple, si à un moment du jeu j'ai l'affichage M*RR*N, c'est que mon tableau d'inta les valeurs 101101 (1 pour chaque lettre qui a été trouvée).
Il est ainsi facile de savoir quand on a gagné : il suffit de vérifier si le tableau de booléens ne contient que des 1.
En revanche, on a perdu si le compteurcoupsRestantstombe à 0.

Passons à la suite :

printf("Bienvenue dans le Pendu !\n\n");

C'est un message de bienvenue, il n'y a rien de bien palpitant. En revanche, la boucle principale du jeu est plus intéressante :

while (coupsRestants > 0 && !gagne(lettreTrouvee))
{

Le jeu continue tant qu'il reste des coups (coupsRestants > 0) et tant qu'on n'a pas gagné.
Si on n'a plus de coups à jouer, c'est qu'on a perdu. Si on a gagné, c'est… qu'on a gagné. Dans les deux cas, il faut arrêter le jeu, donc arrêter la boucle du jeu qui redemande à chaque fois une nouvelle lettre.

gagneest une fonction qui analyse le tableaulettreTrouvee. Elle renvoie « vrai » (1) si le joueur a gagné (le tableaulettreTrouveene contient que des 1), « faux » (0) si le joueur n'a pas encore gagné.
Je ne vous explique pas ici le fonctionnement de cette fonction en détail, on verra cela plus tard. Pour le moment, vous avez juste besoin de savoir ce que fait la fonction.

La suite :

printf("\n\nIl vous reste %d coups a jouer", coupsRestants);
printf("\nQuel est le mot secret ? ");

/* On affiche le mot secret en masquant les lettres non trouvées
        Exemple : *A**ON */
        for (i = 0 ; i < 6 ; i++)
        {
            if (lettreTrouvee[i]) // Si on a trouvé la lettre n° i
                printf("%c", motSecret[i]); // On l'affiche
            else
                printf("*"); // Sinon, on affiche une étoile pour les lettres non trouvées
        }

On affiche à chaque coup le nombre de coups restants ainsi que le mot secret (masqué par des * pour les lettres non trouvées).
L'affichage du mot secret masqué par des * se fait grâce à une bouclefor. On analyse chaque lettre pour savoir si elle a été trouvée (iflettreTrouvee[i]). Si c'est le cas, on affiche la lettre. Sinon, on affiche une * de remplacement pour masquer la lettre.

Maintenant qu'on a affiché ce qu'il fallait, on va demander au joueur de saisir une lettre :

printf("\nProposez une lettre : ");
lettre = lireCaractere();

Je fais appel à notre fonctionlireCaractere(). Celle-ci lit le premier caractère tapé, le met en majuscule et vide le buffer, c'est-à-dire qu'elle vide les autres caractères qui auraient pu persister dans la mémoire.

// Si ce n'était PAS la bonne lettre
if (!rechercheLettre(lettre, motSecret, lettreTrouvee))
    {
        coupsRestants--; // On enlève un coup au joueur
    }
}

On vérifie si la lettre entrée se trouve dansmotSecret. On fait appel pour cela à une fonction maison appeléerechercheLettre. Nous verrons peu après le code de cette fonction.
Pour le moment, tout ce que vous avez besoin de savoir, c'est que cette fonction renvoie « vrai » si la lettre se trouve dans le mot, « faux » si elle ne s'y trouve pas.

Monif, vous l'aurez remarqué, commence par un point d'exclamation!qui signifie « non ». La condition se lit donc « Si la lettre n'a pas été trouvée ».
Que fait-on si la lettre n'a pas été trouvée ? On diminue le nombre de coups restants.

La boucle principale du jeu s'arrête là. On recommence donc au début de la boucle et on vérifie s'il reste des coups à jouer et si on n'a pas déjà gagné.

Lorsqu'on sort de la boucle principale du jeu, il reste à afficher si on a gagné ou non avant que le programme ne s'arrête :

if (gagne(lettreTrouvee))
    printf("\n\nGagne ! Le mot secret etait bien : %s", motSecret);
else
    printf("\n\nPerdu ! Le mot secret etait : %s", motSecret);

return 0;
}

On fait appel à la fonctiongagnepour vérifier si on a gagné. Si c'est le cas, alors on affiche le message « Gagné ! » ; sinon, c'est qu'on n'avait plus de coups à jouer, on a été pendu.

Analyse de la fonctiongagne

Voyons maintenant le code de la fonctiongagne:

int gagne(int lettreTrouvee[])
{
    int i = 0;
    int joueurGagne = 1;

    for (i = 0 ; i < 6 ; i++)
    {
        if (lettreTrouvee[i] == 0)
            joueurGagne = 0;
    }

    return joueurGagne;
}

Cette fonction prend le tableau de booléenslettreTrouveepour paramètre. Elle renvoie un booléen : « vrai » si on a gagné, « faux » si on a perdu.

Le code de cette fonction est plutôt simple, vous devriez tous le comprendre. On parcourtlettreTrouveeet on vérifie si UNE des cases vaut « faux » (0). Si une des lettres n'a pas encore été trouvée, c'est qu'on a perdu : on met alors le booléenjoueurGagneà « faux » (0). Sinon, si toutes les lettres ont été trouvées, le booléen vaut « vrai » (1) et la fonction renverra donc « vrai ».

Analyse de la fonctionrechercheLettre

La fonctionrechercheLettrea deux missions :

  • renvoyer un booléen indiquant si la lettre se trouvait bien dans le mot secret ;

  • mettre à jour (à 1) les cases du tableaulettreTrouveecorrespondant aux positions de la lettre qui a été trouvée.

int rechercheLettre(char lettre, char motSecret[], int lettreTrouvee[])
{
    int i = 0;
    int bonneLettre = 0;

    // On parcourt motSecret pour vérifier si la lettre proposée y est 
    for (i = 0 ; motSecret[i] != '\0' ; i++)
    {
        if (lettre == motSecret[i]) // Si la lettre y est
        {
            bonneLettre = 1; // On mémorise que c'était une bonne lettre
            lettreTrouvee[i] = 1; // On met à 1 la case du tableau de booléens correspondant à la lettre actuelle
        }
    }

    return bonneLettre;
}

On parcourt donc la chaînemotSecretcaractère par caractère. À chaque fois, on vérifie si la lettre que le joueur a proposée est une lettre du mot. Si la lettre correspond, alors on fait deux choses :

  • on change la valeur du booléenbonneLettreà 1, pour que la fonction retourne 1 car la lettre se trouvait effectivement dansmotSecret;

  • on met à jour le tableaulettreTrouveeà la position actuelle pour indiquer que cette lettre a été trouvée.

L'avantage de cette technique, c'est qu'ainsi on parcourt tout le tableau (on ne s'arrête pas à la première lettre trouvée). Cela nous permet de bien mettre à jour le tableaulettreTrouvee, au cas où une lettre serait présente en plusieurs exemplaires dans le mot secret, comme c'est le cas pour les deux R de MARRON.

La solution (2 : la gestion du dictionnaire)

Nous avons fait le tour des fonctionnalités de base de notre programme. Il contient tout ce qu'il faut pour gérer une partie, mais il ne sait pas sélectionner un mot au hasard dans un dictionnaire de mots. Vous pouvez voir à quoi ressemble mon code source au complet à ce stade de son écriture (donc sans la gestion du dictionnaire) sur le web. Je ne l'ai pas placé ici car il prend déjà plusieurs pages et ferait doublon avec le code source final complet que vous verrez un peu plus bas.

Avant d'aller plus loin, la première chose à faire maintenant est de créer ce fameux dictionnaire de mots. Même s'il est court ce n'est pas grave, il conviendra pour les tests.

Je vais donc créer un fichierdico.txtdans le même répertoire que mon projet. Pour le moment, j'y mets les mots suivants :

MAISON
BLEU
AVION
XYLOPHONE
ABEILLE
IMMEUBLE
GOURDIN
NEIGE
ZERO

Une fois que j'aurai terminé de coder le programme, je reviendrai bien sûr sur ce dictionnaire et j'y ajouterai évidemment des tooonnes de mots tordus comme XYLOPHONE, ou à rallonge comme ANTICONSTITUTIONNELLEMENT. Mais pour le moment, retournons à nos instructions.

Préparation des nouveaux fichiers

La lecture du « dico » va demander pas mal de lignes de code (du moins, j'en ai le pressentiment). Je prends donc les devants en ajoutant un nouveau fichier à mon projet :dico.c(qui sera chargé de la lecture du dico). Dans la foulée, je crée ledico.hqui contiendra les prototypes des fonctions contenues dansdico.c.

Dansdico.c, je commence par inclure les bibliothèques dont j'aurai besoin ainsi que mondico.h.
A priori, comme souvent, j'aurai besoin destdioetstdlibici. En plus de cela, je vais être amené à piocher un nombre au hasard dans le dico, je vais donc incluretime.hcomme on l'avait fait pour notre premier projet « Plus ou Moins ». Je vais aussi avoir besoin destring.hpour faire unstrlenvers la fin de la fonction :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#include "dico.h"

La fonctionpiocherMot

Cette fonction va prendre un paramètre : un pointeur sur la zone en mémoire où elle pourra écrire le mot. Ce pointeur sera fourni par lemain().
La fonction renverra unintqui sera un booléen : 1 = tout s'est bien passé, 0 = il y a eu une erreur.

Voici le début de la fonction :

int piocherMot(char *motPioche)
{
    FILE* dico = NULL; // Le pointeur de fichier qui va contenir notre fichier
    int nombreMots = 0, numMotChoisi = 0, i = 0;
    int caractereLu = 0;

Je définis quelques variables qui me seront indispensables. Comme pour lemain(), je n'ai pas pensé à mettre toutes ces variables dès le début, il y en a certaines que j'ai ajoutées par la suite lorsque je me suis rendu compte que j'en avais besoin.

Les noms des variables parlent d'eux-mêmes. On a notre pointeur sur fichierdicodont on va se servir pour lire le fichierdico.txt, des variables temporaires qui vont stocker les caractères, etc.
Notez que j'utilise ici unintpour stocker un caractère (caractereLu) car la fonctionfgetcque je vais utiliser renvoie unint. Il est donc préférable de stocker le résultat dans unint.

Passons à la suite :

dico = fopen("dico.txt", "r"); // On ouvre le dictionnaire en lecture seule

// On vérifie si on a réussi à ouvrir le dictionnaire
if (dico == NULL) // Si on n'a PAS réussi à ouvrir le fichier
{
    printf("\nImpossible de charger le dictionnaire de mots");
    return 0; // On retourne 0 pour indiquer que la fonction a échoué
    // À la lecture du return, la fonction s'arrête immédiatement.
}

Je n'ai pas grand-chose à ajouter ici. J'ouvre le fichierdico.txten lecture seule ("r") et je vérifie si j'ai réussi en testant sidicovautNULLou non. SidicovautNULL, le fichier n'a pas pu être ouvert (fichier introuvable ou utilisé par un autre programme). Dans ce cas, j'affiche une erreur et je fais unreturn 0.

Pourquoi unreturnlà ? En fait, l'instructionreturncommande l'arrêt de la fonction. Si le dico n'a pas pu être ouvert, la fonction s'arrête là et l'ordinateur n'ira pas lire plus loin. On retourne 0 pour indiquer aumainque la fonction a échoué.

Dans la suite de la fonction, on suppose donc que le fichier a bien été ouvert.

// On compte le nombre de mots dans le fichier (il suffit de compter les entrées \n
do
{
    caractereLu = fgetc(dico);
    if (caractereLu == '\n')
        nombreMots++;
} while(caractereLu != EOF);

Là, on parcourt tout le fichier à coups defgetc(caractère par caractère). On compte le nombre de\n(entrées) qu'on détecte. À chaque fois qu'on tombe sur un\n, on incrémente la variablenombreMots.
Grâce à ce bout de code, on obtient dansnombreMotsle nombre de mots dans le fichier. Rappelez-vous que le fichier contient un mot par ligne.

numMotChoisi = nombreAleatoire(nombreMots); // On pioche un mot au hasard

Ici, je fais appel à une fonction de mon cru qui va génèrer un nombre aléatoire entre 1 etnombreMots(le paramètre qu'on envoie à la fonction).
C'est une fonction toute simple que j'ai placée aussi dansdico.c(je vous la détaillerai tout à l'heure). Bref, elle renvoie un nombre (correspondant à un numéro de ligne du fichier) au hasard qu'on stocke dansnumMotChoisi.

// On recommence à lire le fichier depuis le début. On s'arrête lorsqu'on est arrivé au bon mot
rewind(dico);
while (numMotChoisi > 0)
{
    caractereLu = fgetc(dico);
    if (caractereLu == '\n')
        numMotChoisi--;
}

Maintenant qu'on a le numéro du mot qu'on veut piocher, on repart au début grâce à un appel àrewind(). On parcourt là encore le fichier caractère par caractère en comptant les\n. Cette fois, on décrémente la variablenumMotChoisi. Si par exemple on a choisi le mot numéro 5, à chaque entrée la variable va être décrémentée de 1.
Elle va donc valoir 5, puis 4, 3, 2, 1… et 0.
Lorsque la variable vaut 0, on sort duwhile, la conditionnumMotChoisi > 0n'étant plus remplie.

Ce bout de code, que vous devez impérativement comprendre, vous montre donc comment on parcourt un fichier pour se placer à la position voulue. Ce n'est pas bien compliqué, mais ce n'est pas non plus « évident ». Assurez-vous donc de bien comprendre ce que je fais là.

Maintenant, on devrait avoir un curseur positionné juste devant le mot secret qu'on a choisi de piocher.
On va le stocker dansmotPioche(le paramètre que la fonction reçoit) grâce à un simplefgetsqui va lire le mot :

/* Le curseur du fichier est positionné au bon endroit.
On n'a plus qu'à faire un fgets qui lira la ligne */
fgets(motPioche, 100, dico);

// On vire le \n à la fin
motPioche[strlen(motPioche) - 1] = '\0';

On demande aufgetsde ne pas lire plus de 100 caractères (c'est la taille du tableaumotPioche, qu'on a défini dans lemain). N'oubliez pas quefgetslit toute une ligne, y compris le\n.
Comme on ne veut pas garder ce\ndans le mot final, on le supprime en le remplaçant par un\0. Cela aura pour effet de couper la chaîne juste avant le\n.

Et… voilà qui est fait ! On a écrit le mot secret dans la mémoire à l'adresse demotPioche.

On n'a plus qu'à fermer le fichier, à retourner 1 pour que la fonction s'arrête et pour dire que tout s'est bien passé :

fclose(dico);

    return 1; // Tout s'est bien passé, on retourne 1
}

Pas besoin de plus pour la fonctionpiocherMot!

La fonctionnombreAleatoire

C'est la fonction dont j'avais promis de vous parler tout à l'heure. On tire un nombre au hasard et on le renvoie :

int nombreAleatoire(int nombreMax)
{
    srand(time(NULL));
    return (rand() % nombreMax);
}

La première ligne initialise le générateur de nombres aléatoires, comme on a appris à le faire dans le premier TP « Plus ou Moins ».
La seconde ligne prend un nombre au hasard entre 0 etnombreMaxet le renvoie. Notez que j'ai fait tout ça en une ligne, c'est tout à fait possible, bien que peut-être parfois moins lisible.

Le fichierdico.h

Il s'agit juste des prototypes des fonctions. Vous remarquerez qu'il y a la « protection »#ifndefque je vous avais demandé d'inclure dans tous vos fichiers.h(revoyez le chapitre sur le préprocesseur au besoin).

#ifndef DEF_DICO
#define DEF_DICO

int piocherMot(char *motPioche);
int nombreAleatoire(int nombreMax);

#endif

Le fichierdico.c

Voici le fichierdico.cen entier :

/*
Jeu du Pendu

dico.c
------

Ces fonctions piochent au hasard un mot dans un fichier dictionnaire
pour le jeu du Pendu
*/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#include "dico.h"

int piocherMot(char *motPioche)
{
    FILE* dico = NULL; // Le pointeur de fichier qui va contenir notre fichier
    int nombreMots = 0, numMotChoisi = 0, i = 0;
    int caractereLu = 0;
    dico = fopen("dico.txt", "r"); // On ouvre le dictionnaire en lecture seule

    // On vérifie si on a réussi à ouvrir le dictionnaire
    if (dico == NULL) // Si on n'a PAS réussi à ouvrir le fichier
    {
        printf("\nImpossible de charger le dictionnaire de mots");
        return 0; // On retourne 0 pour indiquer que la fonction a échoué
        // À la lecture du return, la fonction s'arrête immédiatement.
    }

    // On compte le nombre de mots dans le fichier (il suffit de compter les
    // entrées \n
    do
    {
        caractereLu = fgetc(dico);
        if (caractereLu == '\n')
            nombreMots++;
    } while(caractereLu != EOF);

    numMotChoisi = nombreAleatoire(nombreMots); // On pioche un mot au hasard

    // On recommence à lire le fichier depuis le début. On s'arrête lorsqu'on est arrivé au bon mot
    rewind(dico);
    while (numMotChoisi > 0)
    {
        caractereLu = fgetc(dico);
        if (caractereLu == '\n')
            numMotChoisi--;
    }

    /* Le curseur du fichier est positionné au bon endroit.
    On n'a plus qu'à faire un fgets qui lira la ligne */
    fgets(motPioche, 100, dico);

    // On vire le \n à la fin
    motPioche[strlen(motPioche) - 1] = '\0';
    fclose(dico);

    return 1; // Tout s'est bien passé, on retourne 1
}

int nombreAleatoire(int nombreMax)
{
    srand(time(NULL));
    return (rand() % nombreMax);
}

Il va falloir modifier lemain!

Maintenant que le fichierdico.cest prêt, on retourne dans lemain()pour l'adapter un petit peu aux quelques changements qu'on vient de faire.

Déjà, on commence par incluredico.hsi on veut pouvoir faire appel aux fonctions dedico.c. De plus, on va aussi inclurestring.hcar on va devoir faire unstrlen:

#include <string.h>
#include "dico.h"

Pour commencer, les définitions de variables vont un peu changer. Déjà, on n'initialise plus la chaînemotSecret, on crée juste un grand tableau dechar(100 cases).

Quant au tableaulettreTrouvee… sa taille dépendra de la longueur du mot secret qu'on aura pioché. Comme on ne connaît pas encore cette taille, on crée un simple pointeur. Tout à l'heure, on fera unmallocet pointer ce pointeur vers la zone mémoire qu'on aura allouée.
Ceci est un exemple parfait de l'absolue nécessité de l'allocation dynamique : on ne connaît pas la taille du tableau avant la compilation, on est donc obligé de créer un pointeur et de faire unmalloc.

Je ne dois pas oublier de libérer la mémoire ensuite quand je n'en ai plus besoin, d'où la présence d'unfree()à la fin dumain.

On va aussi avoir besoin d'une variabletailleMotqui va stocker… la taille du mot. En effet, si vous regardez lemain()tel qu'il était dans la première partie, on supposait que le mot faisait 6 caractères partout (et c'était vrai car MARRON comporte 6 lettres). Mais maintenant que le mot peut changer de taille, il va falloir être capable de s'adapter à tous les mots !

Voici donc les définitions de variables dumainen version finale :

int main(int argc, char* argv[])
{
    char lettre = 0; // Stocke la lettre proposée par l'utilisateur (retour du scanf)
    char motSecret[100] = {0}; // Ce sera le mot à trouver
    int *lettreTrouvee = NULL; // Un tableau de booléens. Chaque case correspond à une lettre du mot secret. 0 = lettre non trouvée, 1 = lettre trouvée
    int coupsRestants = 10; // Compteur de coups restants (0 = mort)
    int i = 0; // Une petite variable pour parcourir les tableaux
    int tailleMot = 0;

C'est principalement le début dumainqui va changer, donc analysons-le de plus près :

if (!piocherMot(motSecret))
    exit(0);

On fait d'abord appel àpiocherMotdirectement dans leif.piocherMotva placer dansmotSecretle mot qu'elle aura pioché.

De plus,piocherMotva renvoyer un booléen pour nous dire si la fonction a réussi ou échoué. Le rôle duifest d'analyser ce booléen. Si ça n'a PAS marché (le!permet d'exprimer la négation), alors on arrête tout (exit(0)).

tailleMot = strlen(motSecret);

On stocke la taille dumotSecretdanstailleMotcomme je vous l'ai dit tout à l'heure.

lettreTrouvee = malloc(tailleMot * sizeof(int)); // On alloue dynamiquement le tableau lettreTrouvee (dont on ne connaissait pas la taille au départ)
if (lettreTrouvee == NULL)
    exit(0);

Maintenant on doit allouer la mémoire pour le tableaulettreTrouvee. On lui donne la taille du mot (tailleMot).
On vérifie ensuite si le pointeur n'est pasNULL. Si c'est le cas, c'est que l'allocation a échoué. Dans ce cas, on arrête immédiatement le programme (on fait appel àexit()).

Si les lignes suivantes sont lues, c'est donc que tout s'est bien passé.

Voilà tous les préparatifs qu'il vous fallait faire ici. J'ai dû ensuite modifier le reste du fichiermain.cpour remplacer tous les nombres 6 (l'ancienne longueur de MARRON qu'on avait fixée) par la variabletailleMot. Par exemple :

for (i = 0 ; i < tailleMot ; i++)
    lettreTrouvee[i] = 0;

Ce code met toutes les cases du tableaulettreTrouveeà 0, en s'arrêtant lorsqu'on a parcourutailleMotcases.

J'ai dû aussi remanier le prototype de la fonctiongagnepour ajouter la variabletailleMot. Sans cela, la fonction n'aurait pas su quand arrêter sa boucle.

Voici le fichiermain.cfinal en entier :

/*
Jeu du Pendu

main.c
------

Fonctions principales de gestion du jeu
*/

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#include "dico.h"

int gagne(int lettreTrouvee[], long tailleMot);
int rechercheLettre(char lettre, char motSecret[], int lettreTrouvee[]);
char lireCaractere();

int main(int argc, char* argv[])
{
    char lettre = 0; // Stocke la lettre proposée par l'utilisateur (retour du scanf)
    char motSecret[100] = {0}; // Ce sera le mot à trouver
    int *lettreTrouvee = NULL; // Un tableau de booléens. Chaque case correspond à une lettre du mot secret. 0 = lettre non trouvée, 1 = lettre trouvée
    long coupsRestants = 10; // Compteur de coups restants (0 = mort)
    long i = 0; // Une petite variable pour parcourir les tableaux
    long tailleMot = 0;

    printf("Bienvenue dans le Pendu !\n\n");

    if (!piocherMot(motSecret))
        exit(0);

    tailleMot = strlen(motSecret);

    lettreTrouvee = malloc(tailleMot * sizeof(int)); // On alloue dynamiquement le tableau lettreTrouvee (dont on ne connaissait pas la taille au départ)
    if (lettreTrouvee == NULL)
        exit(0);

    for (i = 0 ; i < tailleMot ; i++)
        lettreTrouvee[i] = 0;

    /* On continue à jouer tant qu'il reste au moins un coup à jouer ou qu'on
     n'a pas gagné */
    while (coupsRestants > 0 && !gagne(lettreTrouvee, tailleMot))
    {
        printf("\n\nIl vous reste %ld coups a jouer", coupsRestants);
        printf("\nQuel est le mot secret ? ");

        /* On affiche le mot secret en masquant les lettres non trouvées
        Exemple : *A**ON */
        for (i = 0 ; i < tailleMot ; i++)
        {
            if (lettreTrouvee[i]) // Si on a trouvé la lettre n° i
                printf("%c", motSecret[i]); // On l'affiche
            else
                printf("*"); // Sinon, on affiche une étoile pour les lettres non trouvées
        }

        printf("\nProposez une lettre : ");
        lettre = lireCaractere();

        // Si ce n'était PAS la bonne lettre
        if (!rechercheLettre(lettre, motSecret, lettreTrouvee))
        {
            coupsRestants--; // On enlève un coup au joueur
        }
    }

    if (gagne(lettreTrouvee, tailleMot))
        printf("\n\nGagne ! Le mot secret etait bien : %s", motSecret);
    else
        printf("\n\nPerdu ! Le mot secret etait : %s", motSecret);

    free(lettreTrouvee); // On libère la mémoire allouée manuellement (par malloc)

        return 0;
}

char lireCaractere()
{
    char caractere = 0;

    caractere = getchar(); // On lit le premier caractère
    caractere = toupper(caractere); // On met la lettre en majuscule si elle ne l'est pas déjà

    // On lit les autres caractères mémorisés un à un jusqu'au \n
    while (getchar() != '\n') ;

    return caractere; // On retourne le premier caractère qu'on a lu
}

int gagne(int lettreTrouvee[], long tailleMot)
{
    long i = 0;
    int joueurGagne = 1;

    for (i = 0 ; i < tailleMot ; i++)
    {
        if (lettreTrouvee[i] == 0)
            joueurGagne = 0;
    }

    return joueurGagne;
}

int rechercheLettre(char lettre, char motSecret[], int lettreTrouvee[])
{
    long i = 0;
    int bonneLettre = 0;

    // On parcourt motSecret pour vérifier si la lettre proposée y est
    for (i = 0 ; motSecret[i] != '\0' ; i++)
    {
        if (lettre == motSecret[i]) // Si la lettre y est
        {
            bonneLettre = 1; // On mémorise que c'était une bonne lettre
            lettreTrouvee[i] = 1; // On met à 1 la case du tableau de booléens correspondant à la lettre actuelle
        }
    }

    return bonneLettre;
}

Idées d'amélioration

Télécharger le projet

Pour commencer, je vous invite à télécharger le projet complet du Pendu :

Télécharger le projet Pendu (10 Ko)

Si vous êtes sous Linux ou sous Mac, supprimez le fichierdico.txtet recréez-en un. Les fichiers sont enregistrés de manière différente sous Windows : donc si vous utilisez le mien, vous risquez d'avoir des bugs. N'oubliez pas qu'il faut qu'il y ait une Entrée après chaque mot du dictionnaire. Pensez en particulier à mettre une Entrée après le dernier mot de la liste.

Cela va vous permettre de tester par vous-mêmes le fonctionnement du projet, de procéder à des améliorations personnelles, etc. Bien entendu, le mieux serait que vous ayez déjà réussi le Pendu par vous-mêmes et que vous n'ayez même pas besoin de voir mon projet pour voir comment j'ai fait mais… je suis réaliste, je sais que ce TP a dû être assez délicat pour bon nombre d'entre vous.

Vous trouverez dans ce .zip les fichiers.cet.hainsi que le fichier.cbpdu projet. C'est un projet fait sous Code::Blocks.
Si vous utilisez un autre IDE, pas de panique. Vous créez un nouveau projet console et vous y ajoutez manuellement les.cet.hque vous trouverez dans le .zip.

Vous trouverez aussi l'exécutable (.exeWindows) ainsi qu'un dictionnaire (dico.txt).

Améliorez le Pendu !

Mine de rien, le Pendu est déjà assez évolué comme ça. On a un jeu qui lit un fichier de dictionnaire et qui prend à chaque fois un mot au hasard.

Voici quand même quelques idées d'amélioration que je vous invite à implémenter.

  • Actuellement, on ne vous propose de jouer qu'une fois. Il serait bien de pouvoir boucler à nouveau à la fin dumainpour lancer une nouvelle partie si le joueur le désire.

  • Vous pourriez créer un mode deux joueurs dans lequel le premier joueur entre un mot que le deuxième joueur doit deviner.

  • Ce n'est pas utile (donc c'est indispensable) : pourquoi ne pas dessiner un bonhomme qui se fait pendre à chaque fois que l'on fait une erreur (à coups deprintfbien sûr : on est en console, rappelez-vous !) ?

Prenez bien le temps de comprendre ce TP et améliorez-le au maximum. Il faut que vous soyez capables de refaire ce petit jeu de Pendu les yeux fermés !

Allez, courage.

Exemple de certificat de réussite
Exemple de certificat de réussite