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.