 |
Atelier : Jeu démineur. |
Prérequis : développement d'interfaces en Visual
Basic, récursivité.
Un exemple de démineur ici (nécessites
les DLL Visual Basic 5).
1 | Développement
de l'interface. |
Le
premier travail est de développer une interface semblable à celle
représentée ci-contre :
- Une difficulté se pose
pour représenter les cases elles-mêmes. La solution retenue ici consiste
à double chaque case avec :
- un contrôle Shape
(Backstyle = Opaque, Shape = Rectangle) car c'est un des rares contrôles
en Visual Basic à gérer le clic droit
(le contrôle Label et le contrôle
Image ne le gèrent pas). La couleur
de démarrage est grise (&HC0C0C0).
- un
contrôle Bouton de commande pour afficher
le nombre de mines autour d'une case. Au début, on ne voit que les Shape
(les boutons de commande sont invisibles).
- Ce sont bien sûr des
groupes de contrôles, numérotés
de gauche à droite puis de haut en bas.
- Il
y a un rectangle autour de l'ensemble des cases pour l'esthétique (contrôle
Shape, Backstyle = Transparent, Shape = Rectangle)
- L'affichage du temps écoulé nécessite un contrôle
Timer et un contrôle Label.
- Ajoutez les boutons de commande nécessaires pour l'interface
du jeu : Nouvelle partie, Quitter,
Voir les High Scores.
2 | Déclaration
des principales variables du jeu. |
Vous devez déclarer,
en global, les variables suivantes :
- Il est très pratique d'avoir
les contantes NbColonnes et NbLignes,
qui indiquent la taille du damier. L'utilisation systématique de ces constantes
permettra de modifier rapidement le jeu pour qu'il prenne en charge des champs
de mines de tailles variables.
- Les tableaux représentant
les cases peuvent être représenté par des tableaux à
une dimension (pour respecter l'unique dimension du goupe de contrôles),
ou à deux dimension (pour respecter la structure spatiale du champ de mines).
Cet atelier se base sur la première hypothèse mais vous faites comme
vous voulez. On a besoin des tableaux suivants :
- BombePrésente
(tableau de booléens), pour savoir si une mine est présente
sur chaque case.
- CaseMarquée
(tableau de booléens), pour savoir si le joueur a posé un
drapeau sur chaque case.
- Pour savoir si le joueur a effacé la case,
on va utiliser la propriété Visible
des contrôles Shape. Il est donc inutile
de déclarer un tableau pour ça.
- Le temps
se compte dans une variable Temps (type Single).
- On ne décompte pas le temps tant que l'utilisateur n'a pas
cliqué sa première case : la variable booléenne AttentePremierClick
nous permettra de savoir si le jeu a commencé ou pas.
- La
difficulté du jeu (densité de mines dans le champs de mine) va dépendre
de la constante DensitéMines, par exemple
0.12 signifie qu'il y a 12% de chance d'avoir une mine sur chaque case.
- Avez-vous pensé à mettre la directive de compilation Option
Explicit ? Sinon, allez en prison, ne passez pas par la case départ,
ne touchez pas 20 000 €...
Rappels
sur... Programmer proprement !
Pour que votre programme soit lisible par vous, par le prof
et par ceux qui sont susceptible de maintenir vos applications, n'oubliez pas
les points suivants, tous fondamentaux : - utilisez des noms
de variables explicites, quitte à les rallonger. Par exemple :
- n'utilisez pas : i, j,
k ; n
, max ;
- utilisez : iCases,
iDirections, iVoisin,
NombreMines, etc. ;
- Indentez
le code
- Commenter votre code. En général,
un commentaire par fonction suffit.
- En Visual Basic, l'option de
compilation Option Explicit permet d'éliminer 50% des erreurs.
|
3 | Les
fonctions non événementielles pour nous aider par la suite. |
| Trouver
l'indice des cases voisines. |
Nous allons avoir
besoin d'une fonction qui nous permet de retrouver rapidement l'indice des cases
qui entourent une case donnée en paramètre :
Function
IndiceVoisin(UneCase as Integer, UneDirection as Integer) as Integer
- UneCase est l'indice d'une case quelconque,
de 0 à NombreCases
- 1.
- UneDirection est la
direction (de 1 à 8)
du voisin dont on veut l'indice. Par exemple, on considérera (arbitrairement)
le tableau de directions suivantes :
Tableau
des directions |
26 | 27 |
28 | 46 |
47 | 48 |
66 | 67 |
68 | Exemple
d'indices voisins de 47 (dans le cas ou NbColonnes
= 20). |
En suivant cet exemple, on doit avoir
les résultats suivants (vous devez bien comprendre ça avant de passer
à la suite) :
- IndiceVoisin(47, 1) renvoie 26
(c'est à dire 47 - NbColonnes - 1)
- IndiceVoisin(47, 4) renvoie
48 (c'est à dire 47 + 1)
Petite
complication : vous devez gérez les débordements spécifiques
suivants :
- si on demande le voisin de gauche d'une case (directions
1, 8, 7) qui est tout à gauche (UneCase
Mod NbColonnes = 0), on renvoie -1
(c'est un code d'erreur pour nous signifier que le voisin demandé n'existe
pas) ;
- si on demande le voisin de droite (directions 3, 4, 5) d'une case
qui est tout à droite ((UneCase
+ 1) Mod NbColonnes = 0), on renvoie -1.
- si
on demande le voisin du haut (directions 1, 2, 3) d'une case qui est tout en haut
(UneCase < NbColonnes), on renvoie -1.
- si
on demande le voisin du bas (directions 7, 6, 5) d'une case qui est tout en bas
(UneCase >= NombreCases - NbColonnes), on renvoie -1.
Votre
travail pour cette étape est d'écrire cette fonction.
| Compter
le nombre de mines autour d'une case. |
Nous allons
avoir besoin d'une fonction qui nous donne le nombre de mines autour d'une case
:
Function NombreMines(iCase As Integer) As Integer
L'algorithme
est extrêmement simple :
Initialiser
le nombre de mines voisines à 0
Pour
chaque direction de 1 à 8 (utilisez la boucle For
car la valeur de départ et d'arrivée de la boucle sont connues)
Si l'indice du
voisin existe (indice voisin différent de -1)
S'il
y a une mine sur la case voisine (utilisez le tableau BombePrésente)
Incrémenter
le nombre de mines voisines
Fin
Si
Fin Si
Fin Pour
Vous devez écrire
cette fonction.
4 | Ecriture
des fonctions événementielles les plus simples. |
| Form_load
: démarrage de l'application. |
Au démarrage
du programme, vous devez :
- initialiser la procédure de tirage
de nombre aléatoire, grace à un simple appel (non paramétré)
à la fonction Randomize.
- Démarrer
une nouvelle partie grâce à l'appel de la fonction InitialiseNouvellePartie.
Vous
écrirez également cette fonction InitialiseNouvellePartie
dont le rôle est :
- pour chaque case :
- tirer au
hasard la présence de mine grâce à la fonction de tirage aléatoire
Rnd, qui renvoie un nombre aléatoire
entre 0 (inclus) et 1 (non inclus) :
BombePrésente(iCase)
= (Rnd < DensitéMines) - initialiser le tableau CaseMarquée
à false, pour dire que le joueur n'a
encore marqué aucune case.
- ultérieurment, vous devrez rajouter
: restaurer la couleur (grise = &HC0C0C0)
et la visibilité du contrôle Shape,
et l'invisibilité du bouton de commande
correspondant.
- Réinitialiser AttentePremierClick
à True (pour démarrer le compteur
de temps au bon moment, c'est à dire au premier clic du joueur).
| Clic droit
sur une case du jeu. |
Lorsque le joueur
fait un clic droit sur une case, c'est pour marquer la présence d'une
mine (ou pour enlever le marquage). Dans la pratique, on va colorer la Shape
en rouge pour représenter ce marquage (il est possible d'utiliser un
contrôle Image avec un drapeau).
Techniquement, vous devez :
- Inverser
le booléen CaseMarquée pour
cette case ;
- S'il est passé à true,
on colore la case en rouge (BackColor = vbRed)
sinon en gris (BackColor = &HC0C0C0).
| Clic sur
le bouton de commande Nouvelle Partie. |
Sur cet événement,
vous devez simplement faire un appel à la fonction InitialiseNouvellePartie.
| Clic sur
le bouton de commande Quitter. |
Sur cet événement,
vous devez simplement faire un appel à la fonction End.
Remarquez que dans toutes les applications, le bouton Quitter a sa propriété
Cancel=True. Cela permet de quitter le jeu
en appuyant sur la touche Esc.
L'algorithme
(très simple) est le suivant :
Si
la partie a déjà commencé (Not
AttentePremierClick) alors
Afficher le temps
écoulé en secondes (Temps -
Timer) dans le
contrôle label correspondant
Fin
Si
5 | La
fonction événementielle la plus complexe : Clic gauche sur une
case du jeu. |
| La fonction
événementielle Case_Click. |
Dans le démineur, on fait un clic gauche sur une case lorsqu'on
pense n'elle ne cache pas de mine. Ce qu'il se passe :
- S'il y avait
une mine, la partie est perdue :
- on affiche un message de défaite
;
- on appelle la fonction InitialiseNouvellePartie.
- Sinon, on "ouvre" la case (c'est un simple appel
à la fonction récursive Ouvre
décrite ci-dessous).
On doit également dire que la partie
est commencée (pour démarrer le compteur de temps) :
If AttentePremierClick
Then
AttentePremierClick = False
Temps = Timer
End If
| La
fonction récursive Ouvre. |
Sub
Ouvrir(iCase As Integer)
Cette fonction "ouvre" une
cases lorsqu'on clique dessus, c'est à dire que :
- Si il y a
des mines autour de la case, on affiche le nombre de mine (rappel : on affiche
ce nombre sur un contrôle Bouton de Commande
qui vient remplacer le contrôle Shape,
qui lui devient invisible).
- Sinon :
- on rend cette case
invisible
- on "ouvre" les 8 cases voisines (c'est un simple appel
récursif).
Vous risquez d'avoir des erreurs si vous
ne tenez pas compte des indications suivantes :
- Pour éviter
d'avoir l'erreur "Indice en dehors du tableau",
vous devez vérifier dès la première ligne de la fonction
que l'indice passé en paramètre existe (iCase
> -1 And iCase < groupe de cases.count).
- Pour éviter des appels récursifs
infinis (genre : la case n°1 ouvre sa voisine n°2, puis
la case n°2 ouvre sa voisine n°1 ...), vous devez vérifier
dès la deuxième ligne de la fonction que la case soit
visible. Sinon, c'est quelle a déjà été ouverte,
donc on ne fait rien.
6 | Ecriture
le la fonction pour savoir si la partie est gagnée. |
La fonction pour savoir si la partie est gagnée n'est pas triviale
:
Function Gagné() As Boolean
Ecrivez-là
en vous basant sur l'algorithme suivant :
On
suppose que la partie est gagnée
Pour chaque case encore visible :
Si la case n'est pas encore ouvert (test sur la couleur ?)
ou
si elle est marqué par erreur (test sur la couleur et BombePrésente)
Alors la partie n'est pas gagnée
Fin
Si
Fin Pour
Vous devez intégrer cette
fonction dans la fonction événementielle "Clic gauche sur une
case du jeu" (décrite dans l'étape 5) : après avoir
ouvert la case, on teste si la partie est gagnée. Si c'est le cas on affiche
un message de victoire puis on recommence une nouvelle partie.
A ce stade, l'ensemble du jeu doit être
fonctionnel. Testez et déboguez.
8 | Ajout
de la gestion des HighScores. |
La gestion des High
Scores représente, à mon avis, la partie la plus difficile
de ce atelier. Cela demande en effet de faire une gestion de fichier (pour écrire
les scores sur le disque dur) et un tri de tableau pour les afficher.
Débrouillez-vous
avec les indications suivantes :
- Lorsqu'on a gagné une partie, vous ouvrez le fichier des
High Scores en mode Ajout,
et vous ajoutez le nom du joueur et son temps (ceci est assez simple).
|
 |
- Lorsqu'on
clique sur le bouton de commande Afficher les High Scores, vous devez :
- lire les scores un par un dans le fichier des High
Scores ;
- les insérer au fur et à mesure dans
un tableau (réalisez une insertion triée)
;
- les afficher dans une nouvelle feuille.
C'est fini,
testez... |
 |
9 | Autres
améliorations possibles. |
- Au départ,
le joueur peut choisir une taille de jeu (petite, moyenne, grande). Eventuellement,
il peut également choisir un niveau de difficulté, qui va influencer
la densité de mine.
- Les High Scores sont cryptés.
Ils ne sont pas sauvegardés en clair dans un fichier texte. Un simple algorithme
de codage de votre choix, ou de compression, suffit.
- Quelque
soit l'ordinateur sur lequel est lancé le jeu, les High Scores sont sauvé
dans un fichier commun sur le réseau ou sur Internet. Cela permet de confronter
son score à celui des autres joueurs.