Amélioration d'un jeu de Démineur en C++ : Ajout de Difficultés, Chronomètre et Historique des Scores

Ce document détaille les améliorations apportées à une implémentation basique du jeu de Démineur en C++. L'ojbectif est de transformer un jeu simple en une expérience plus engageante et interactive pour l'utilisateur.

Fonctionnalités Originales et Défis

Le jeu original permettait une interaction basique : initialisation du plateau, placement des mines, et saisie des coordonnées par le joueur pour découvrir les cases. Cependant, il présentait plusieurs lacunes significatives :

  • Unicité du Mode de Jeu : Une seule difficluté et un plateau de taille fixe limitaient l'attrait du jeu.
  • Absence de Chronomètre : Le joueur ne pouvait pas mesurer sa performance temporelle.
  • Interface Minimaliste : L'absence d'affichage clair des coordonnées et une interface graphique peu soignée rendaient l'interaction difficile et potentiellement source d'erreurs.
  • Manque de Persistance des Données : Aucune sauvegarde des performances n'était implémentée, empêchant le suivi des progrès du joueur.

Améliorations Apportées

Pour remédier à ces limitations, les fonctionnalités suivantes ont été ajoutées :

1. Sélection de la Difficulté

Le jeu propose désormais plusieurs niveaux de difficulté, chacun ajutsant la taille du plateau et le nombre de mines. Cette fonctionnalité permet d'adapter le défi aux compétences du joueur.


#include <iostream>
#include <cstdlib> // Pour srand, rand
#include <ctime>   // Pour time

// Déclaration anticipée des fonctions
void afficherMenu();
void lancerPartie(int niveauDifficulte);
// ... autres déclarations de fonctions ...

int main() {
   int choixUtilisateur = 0;
   // Initialisation du générateur de nombres aléatoires
   srand(static_cast<unsigned int>(time(NULL)));

   do {
       afficherMenu();
       std::cout << "Votre choix : ";
       std::cin >> choixUtilisateur;

       switch (choixUtilisateur) {
           case 1: {
               std::cout << "\nSélectionnez le niveau de difficulté :\n";
               std::cout << "1. Débutant (10 mines, grille 9x9)\n";
               std::cout << "2. Intermédiaire (20 mines, grille 12x12)\n";
               std::cout << "3. Expert (30 mines, grille 15x15)\n";
               int niveau;
               std::cout << "Entrez votre choix de niveau : ";
               std::cin >> niveau;
               std::cout << "\nLa partie commence...\n";
               lancerPartie(niveau);
               break;
           }
           case 0:
               std::cout << "Merci d'avoir joué ! Au revoir.\n";
               break;
           default:
               std::cout << "Choix invalide. Veuillez réessayer.\n";
               break;
       }
       std::cout << std::endl; // Ligne vide pour la lisibilité
   } while (choixUtilisateur != 0);

   return 0;
}

void lancerPartie(int niveauDifficulte) {
   char plateauMines[16][16];     // Stocke la disposition des mines
   char plateauAffichage[16][16]; // Stocke l'état visible du plateau pour le joueur
   int taillePlateau = 0;
   int nombreMines = 0;

   // Détermination de la taille et du nombre de mines selon le niveau
   if (niveauDifficulte == 1) {
       taillePlateau = 9;
       nombreMines = 10;
   } else if (niveauDifficulte == 2) {
       taillePlateau = 12;
       nombreMines = 20;
   } else if (niveauDifficulte == 3) {
       taillePlateau = 15;
       nombreMines = 30;
   } else {
       std::cout << "Niveau de difficulté non reconnu. Utilisation du niveau débutant.\n";
       taillePlateau = 9;
       nombreMines = 10;
   }

   // Initialisation des plateaux
   // Remarque: l'initialisation des plateaux devrait utiliser taillePlateau, pas une valeur fixe.
   // Ceci est une simplification basée sur le code original.
   initialiserPlateau(plateauMines, '0', 15);
   initialiserPlateau(plateauAffichage, '*', 15);

   // Affichage initial du plateau caché
   afficherPlateau(plateauAffichage, taillePlateau);

   // Placement des mines
   placerMines(plateauMines, nombreMines);

   // Début de la phase de découverte des cases
   // La logique de découverte et de calcul des voisins serait appelée ici.
   // set_searchmine(plateauMines, plateauAffichage, taillePlateau, nombreMines); // Appel original
}

// Fonction placeholder pour l'initialisation du plateau (basée sur le code original)
void initialiserPlateau(char plateau[][16], char caractere, int taille) {
   for (int i = 1; i <= taille; ++i) {
       for (int j = 1; j <= taille; ++j) {
           plateau[i][j] = caractere;
       }
   }
}

// Fonction placeholder pour l'affichage du plateau (basée sur le code original)
void afficherPlateau(char plateau[][16], int taille) {
   // Logique d'affichage formaté des coordonnées et du contenu du plateau
   std::cout << "   ";
   for (int i = 1; i <= taille; ++i) {
       std::cout << "  " << i;
   }
   std::cout << std::endl;

   for (int i = 1; i <= taille; ++i) {
       if (i < 10) std::cout << " "; // Alignement pour les nombres à un chiffre
       std::cout << i << " ";
       for (int j = 1; j <= taille; ++j) {
           std::cout << "----";
       }
       std::cout << std::endl;
       std::cout << " ";
       for (int j = 1; j <= taille; ++j) {
           std::cout << "| " << plateau[i][j] << " ";
       }
       std::cout << "|" << std::endl;
   }
   std::cout << " ";
   for (int j = 1; j <= taille; ++j) {
       std::cout << "----";
   }
   std::cout << std::endl;
}

// Fonction placeholder pour le placement des mines (basée sur le code original)
void placerMines(char plateau[][16], int nbMines) {
   int compteurMines = nbMines;
   while (compteurMines > 0) {
       // Génère des coordonnées aléatoires dans les limites du plateau
       int ligneAleatoire = rand() % 15 + 1; // Supposant une taille max de 15
       int colAleatoire = rand() % 15 + 1;

       if (plateau[ligneAleatoire][colAleatoire] == '0') { // Vérifie si la case n'a pas déjà une mine
           plateau[ligneAleatoire][colAleatoire] = 'X'; // 'X' représente une mine
           compteurMines--;
       }
   }
}

// Fonction placeholder pour calculer les voisins
int compterMinesVoisines(char plateau[][16], int ligne, int col) {
   // Logique pour compter les mines dans le voisinage 3x3
   // Retourne le nombre de mines adjacentes.
   return 0; // Placeholder
}
 

2. Ajout d'un Chronomètre

Le jeu mesure désormais le temps écoulé pour chaque partie. L'utilisation de std::chrono permet un enregistrement précis de la durée de jeu.


#include <chrono>
#include <iostream>
#include <fstream> // Pour la gestion des fichiers

// Déclaration anticipée
void enregistrerScore(int tempsEcoule, bool victoire);

void decouvrirCase(char plateauMines[][16], char plateauAffichage[][16], int taille, int nombreMines) {
   // ... (logique pour découvrir une case) ...

   // Début du chronomètre juste avant la première action du joueur
   auto debutTemps = std::chrono::high_resolution_clock::now();
   int casesDecouvertes = 0;

   // Boucle principale du jeu
   while (casesDecouvertes < taille * taille - nombreMines) {
       int ligneChoisie, colChoisie;
       std::cout << "Entrez les coordonnées (ligne colonne) : ";
       std::cin >> ligneChoisie >> colChoisie;

       // Validation des coordonnées
       if (ligneChoisie >= 1 && ligneChoisie <= taille && colChoisie >= 1 && colChoisie <= taille) {
           // Vérification si la case cliquée est une mine
           if (plateauMines[ligneChoisie][colChoisie] == 'X') {
               std::cout << "Dommage, vous avez touché une mine ! Partie perdue.\n";
               afficherPlateau(plateauMines, taille); // Affiche le plateau révélant toutes les mines
               auto finTemps = std::chrono::high_resolution_clock::now();
               std::chrono::duration<double> duree = finTemps - debutTemps;
               enregistrerScore(static_cast<int>(duree.count()), false); // Enregistre la défaite
               return; // Fin de la partie
           } else {
               // Découverte d'une case sûre
               int minesProches = compterMinesVoisines(plateauMines, ligneChoisie, colChoisie);
               plateauAffichage[ligneChoisie][colChoisie] = minesProches + '0'; // Affiche le nombre de mines adjacentes
               casesDecouvertes++;
               afficherPlateau(plateauAffichage, taille); // Réaffiche le plateau mis à jour
           }
       } else {
           std::cout << "Coordonnées invalides. Veuillez réessayer.\n";
       }
   }

   // Si la boucle se termine, le joueur a gagné
   auto finTemps = std::chrono::high_resolution_clock::now();
   std::chrono::duration<double> duree = finTemps - debutTemps;
   std::cout << "Félicitations ! Vous avez réussi à déminer le plateau en " << static_cast<int>(duree.count()) << " secondes.\n";
   afficherPlateau(plateauMines, taille); // Affiche le plateau final
   enregistrerScore(static_cast<int>(duree.count()), true); // Enregistre la victoire
}

// Fonction placeholder pour compter les mines voisines
int compterMinesVoisines(char plateau[][16], int ligne, int col) {
   int compteur = 0;
   // Itère sur la fenêtre 3x3 autour de la case (ligne, col)
   for (int i = ligne - 1; i <= ligne + 1; ++i) {
       for (int j = col - 1; j <= col + 1; ++j) {
           // Vérifie si les coordonnées sont valides et si la case contient une mine ('X')
           // NOTE: La logique originale ne vérifiait pas les limites du plateau, ce qui est une erreur.
           // Il faut ajouter des vérifications de limites ici.
            if (i >= 1 && i < 16 && j >= 1 && j < 16) { // Vérification des limites
                if (plateau[i][j] == 'X') {
                    compteur++;
                }
            }
       }
   }
   return compteur;
}
 

3. Enregistrement des Performances

Les résultats de chaque partie (victoire/défaite et temps) sont sauvegardés dans un fichier texte nommé "scores.txt". Cela permet aux joueurs de suivre leur progression et de se fixer des objectifs.


#include <fstream>
#include <string>
#include <iostream>

// Fonction pour enregistrer le score dans un fichier
void enregistrerScore(int temps, bool succes) {
   std::ofstream fichierScores("scores.txt", std::ios::app); // Ouvre en mode ajout

   if (fichierScores.is_open()) {
       if (succes) {
           fichierScores << "Partie réussie - Temps : " << temps << " secondes\n";
       } else {
           fichierScores << "Partie échouée - Temps : " << temps << " secondes\n";
       }
       fichierScores.close(); // Ferme le fichier
   } else {
       std::cerr << "Erreur : Impossible d'ouvrir le fichier de scores.\n";
   }
}
 

Code Intégré

Voici la structure complète du code après intégration des améliorations :


#include <iostream>
#include <fstream>
#include <cstdlib>
#include <ctime>
#include <chrono>
#include <string> // Nécessaire pour std::string si utilisé ailleurs

// Utilisation des namespaces pour simplifier le code
using namespace std;
using namespace std::chrono;

// --- Déclarations des fonctions ---
void afficherMenuPrincipal();
void lancerNouvellePartie(int niveauDifficulte);
void initialiserPlateau(char plateau[][16], char caractereInitial, int taille);
void afficherPlateauJeu(char plateau[][16], int taille);
void placerMinesAleatoirement(char plateauMines[][16], int nombreMines, int taille);
int compterMinesVoisines(char plateauMines[][16], int ligne, int col, int taille);
void decouvrirCaseJoueur(char plateauMines[][16], char plateauAffichage[][16], int taille, int nombreMines);
void enregistrerPerformance(int temps, bool victoire);

// --- Fonction principale ---
int main() {
   int choixAction = 0;
   // Initialisation du générateur de nombres aléatoires une seule fois
   srand(static_cast<unsigned int>(time(NULL)));

   do {
       afficherMenuPrincipal();
       cout << "Entrez votre choix : ";
       cin >> choixAction;

       switch (choixAction) {
           case 1: { // Jouer
               cout << "\n--- Sélection du Niveau de Difficulté ---\n";
               cout << "1. Débutant (9x9, 10 mines)\n";
               cout << "2. Intermédiaire (12x12, 20 mines)\n";
               cout << "3. Expert (15x15, 30 mines)\n";
               int niveau;
               cout << "Choisissez un niveau (1-3) : ";
               cin >> niveau;
               cout << "\n--- Début de la Partie ---\n";
               lancerNouvellePartie(niveau);
               break;
           }
           case 0: // Quitter
               cout << "Merci d'avoir joué au Démineur ! Au revoir." << endl;
               break;
           default: // Choix invalide
               cout << "Choix non valide. Veuillez sélectionner 1 pour jouer ou 0 pour quitter." << endl;
               break;
       }
       cout << endl; // Ajoute un espace entre les itérations du menu
   } while (choixAction != 0);

   return 0;
}

// --- Implémentation des fonctions ---

// Affiche le menu principal du jeu
void afficherMenuPrincipal() {
   cout << "**************************\n";
   cout << "*      Jeu Démineur      *\n";
   cout << "*       1. Jouer         *\n";
   cout << "*       0. Quitter       *\n";
   cout << "**************************\n";
}

// Lance une nouvelle partie avec la difficulté choisie
void lancerNouvellePartie(int niveauDifficulte) {
   char plateauMines[16][16];     // Stocke la position réelle des mines ('X') et d'autres infos
   char plateauAffichage[16][16]; // Stocke ce que le joueur voit ('*', ' ', '1'-'8')
   int taillePlateau = 0;
   int nombreMines = 0;

   // Configuration selon le niveau
   switch (niveauDifficulte) {
       case 1: taillePlateau = 9; nombreMines = 10; break;
       case 2: taillePlateau = 12; nombreMines = 20; break;
       case 3: taillePlateau = 15; nombreMines = 30; break;
       default:
           cout << "Niveau invalide, utilisation du niveau Débutant par défaut.\n";
           taillePlateau = 9; nombreMines = 10; break;
   }

   // Initialisation des deux plateaux
   initialiserPlateau(plateauMines, '0', taillePlateau);     // '0' pour case vide initiale
   initialiserPlateau(plateauAffichage, '*', taillePlateau); // '*' pour case non découverte

   // Placement des mines sur le plateau de mines
   placerMinesAleatoirement(plateauMines, nombreMines, taillePlateau);

   // Affichage du plateau initial pour le joueur
   afficherPlateauJeu(plateauAffichage, taillePlateau);

   // Lancement de la logique de jeu principale (découverte de cases)
   decouvrirCaseJoueur(plateauMines, plateauAffichage, taillePlateau, nombreMines);
}

// Remplit un plateau avec un caractère donné
void initialiserPlateau(char plateau[][16], char caractereInitial, int taille) {
   // Utilise la taille spécifiée pour l'initialisation
   for (int i = 1; i <= taille; ++i) {
       for (int j = 1; j <= taille; ++j) {
           plateau[i][j] = caractereInitial;
       }
   }
}

// Affiche le plateau de jeu formaté avec les coordonnées
void afficherPlateauJeu(char plateau[][16], int taille) {
   cout << "   "; // Espace pour aligner avec les numéros de ligne
   // Affichage des numéros de colonne
   for (int i = 1; i <= taille; ++i) {
       // Ajoute un espace supplémentaire pour les nombres à deux chiffres pour un meilleur alignement
       if (i < 10) cout << "  " << i;
       else cout << " " << i;
   }
   cout << endl;

   // Boucle pour afficher chaque ligne du plateau
   for (int i = 1; i <= taille; ++i) {
       // Affichage du numéro de ligne, avec un espace pour alignement si nécessaire
       if (i < 10) cout << " " << i << " ";
       else cout << i << " ";

       // Affichage de la ligne horizontale sous les numéros de colonne
       for (int j = 1; j <= taille; ++j) cout << "----";
       cout << endl;

       // Affichage du séparateur vertical et du contenu de la case
       cout << " "; // Décalage pour le numéro de ligne
       for (int j = 1; j <= taille; ++j) {
           cout << "| " << plateau[i][j] << " ";
       }
       cout << "|" << endl;
   }

   // Ligne horizontale finale du plateau
   cout << " ";
   for (int j = 1; j <= taille; ++j) cout << "----";
   cout << endl;
}

// Place les mines aléatoirement sur le plateau
void placerMinesAleatoirement(char plateauMines[][16], int nombreMines, int taille) {
   int minesPlacees = 0;
   while (minesPlacees < nombreMines) {
       // Génère des coordonnées aléatoires valides dans la grille
       int ligne = (rand() % taille) + 1;
       int colonne = (rand() % taille) + 1;

       // Place une mine seulement si la case est vide ('0')
       if (plateauMines[ligne][colonne] == '0') {
           plateauMines[ligne][colonne] = 'X'; // 'X' représente une mine
           minesPlacees++;
       }
   }
}

// Compte le nombre de mines dans le voisinage 3x3 d'une case
int compterMinesVoisines(char plateauMines[][16], int ligne, int col, int taille) {
   int compteur = 0;
   // Parcourt la grille 3x3 centrée sur (ligne, col)
   for (int i = ligne - 1; i <= ligne + 1; ++i) {
       for (int j = col - 1; j <= col + 1; ++j) {
           // Vérifie que les coordonnées sont dans les limites du plateau
           if (i >= 1 && i <= taille && j >= 1 && j <= taille) {
               // Vérifie si la case adjacente contient une mine
               if (plateauMines[i][j] == 'X') {
                   compteur++;
               }
           }
       }
   }
   return compteur;
}

// Gère la logique de découverte de cases par le joueur
void decouvrirCaseJoueur(char plateauMines[][16], char plateauAffichage[][16], int taille, int nombreMines) {
   int lignesNonMineesRestantes = taille * taille - nombreMines;
   int casesDecouvertes = 0;

   // Enregistrement du début de la partie pour le chronomètre
   auto debutTemps = high_resolution_clock::now();

   while (casesDecouvertes < lignesNonMineesRestantes) {
       int ligneChoisie, colChoisie;

       cout << "Entrez les coordonnées (ligne colonne) : ";
       cin >> ligneChoisie >> colChoisie;

       // Validation des coordonnées entrées par le joueur
       if (ligneChoisie >= 1 && ligneChoisie <= taille && colChoisie >= 1 && colChoisie <= taille) {

           // Cas où le joueur clique sur une mine
           if (plateauMines[ligneChoisie][colChoisie] == 'X') {
               cout << "\nBOUM ! Vous avez touché une mine. Partie terminée.\n";
               // Révèle toutes les mines
               for(int i = 1; i <= taille; ++i) {
                   for(int j = 1; j <= taille; ++j) {
                       if (plateauMines[i][j] == 'X') {
                           plateauAffichage[i][j] = 'X';
                       }
                   }
               }
               afficherPlateauJeu(plateauAffichage, taille); // Montre le plateau final

               // Enregistre la performance (défaite)
               auto finTemps = high_resolution_clock::now();
               duration<double> dureePartie = finTemps - debutTemps;
               enregistrerPerformance(static_cast<int>(dureePartie.count()), false);
               return; // Sort de la fonction, fin de partie
           }
           // Cas où le joueur clique sur une case sûre
           else {
               // Calcule le nombre de mines adjacentes
               int minesProches = compterMinesVoisines(plateauMines, ligneChoisie, colChoisie, taille);
               // Met à jour le plateau d'affichage avec le nombre de mines ou un espace si 0 mines
               if (minesProches > 0) {
                   plateauAffichage[ligneChoisie][colChoisie] = minesProches + '0'; // Convertit le nombre en caractère
               } else {
                   plateauAffichage[ligneChoisie][colChoisie] = ' '; // Case vide si pas de mines adjacentes
                   // Idéalement, ici, on déclencherait une découverte récursive des cases vides adjacentes.
                   // Cette partie est omise pour rester proche de la logique originale mais est cruciale pour un vrai démineur.
               }
               casesDecouvertes++; // Incrémente le compteur de cases découvertes
               afficherPlateauJeu(plateauAffichage, taille); // Réaffiche le plateau mis à jour
           }
       } else {
           cout << "Coordonnées invalides. Assurez-vous qu'elles sont dans la grille (1 à " << taille << ").\n";
       }
   }

   // Si le joueur a découvert toutes les cases non minées, il a gagné
   cout << "\nFélicitations ! Vous avez réussi à déminer le plateau !\n";
   // Révèle le plateau final (optionnel, mais bon pour confirmation)
   afficherPlateauJeu(plateauMines, taille);

   // Enregistre la performance (victoire)
   auto finTemps = high_resolution_clock::now();
   duration<double> dureePartie = finTemps - debutTemps;
   enregistrerPerformance(static_cast<int>(dureePartie.count()), true);
}

// Enregistre le temps et le résultat de la partie dans un fichier
void enregistrerPerformance(int temps, bool victoire) {
   ofstream fichierScores("scores.txt", ios::app); // Ouvre le fichier en mode ajout

   if (fichierScores.is_open()) {
       if (victoire) {
           fichierScores << "Victoire - Temps : " << temps << " secondes\n";
       } else {
           fichierScores << "Défaite - Temps : " << temps << " secondes\n";
       }
       fichierScores.close(); // Ferme le fichier
   } else {
       cerr << "Erreur critique : Impossible d'ouvrir le fichier 'scores.txt' pour enregistrer les performances.\n";
   }
}
 

Conclusion

Ces améliorations transforment le jeu de Démineur, le rendant plus dynamique et offrant une meilleure expérience utilisateur grâce à la sélection de la difficulté, au suivi du temps et à la persistance des scores. Le développement modulaire, avec des fonctions dédiées à chaque tâche, facilite la maintenance et l'ajout de futures fonctionnalités.

Étiquettes: C++ jeux démineur programmation Refactoring

Publié le 14 juillet à 10h27