Per
mettere in pratica qualcuna delle nozioni viste nella lezione
precedente torniamo a un vecchio progetto: il campo minato.
Tempo fa mi è stato fatto notare un errore (di cui
probabilmente si saranno accorti in molti, ma che solo un
lettore si è degnato di segnalarmi): l'errore consiste
nel fatto che quando si scopre una mina in effetti la partita
non termina, ma può essere proseguita dal giocatore,
benché il programma visualizzi la posizione delle altre
mine e interrompa il timer. In realtà di errori ce
ne sono anche altri: ad es. la partita non si interrompe neppure
quando il giocatore vince, e per di più il timer continua
a scorrere.
Per correggere questi bug occorre intervenire sull'evento
Click dei pulsanti "mina", che controlla appunto
se una mina sta per esplodere oppure no; questa è la
routine originaria:
Private
Sub
Mina_Click(Index As Integer)
Dim x As
Integer, y As Integer
'coordinate del pulsante premuto
Dim x1 As
Integer, y1 As Integer
'coordinate dei pulsanti circostanti
quello 'premuto
If
Mina(Index).Tag > 0 Then
'nessuna mina esplosa, almeno una
mina 'circostante
If Len(Mina(Index).Caption)
= 0 Then
ContaMine = ContaMine + 1
End If
Mina(Index).Caption = Mina(Index).Tag
ElseIf Mina(Index).Tag
= -1 Then 'mina
esplosa
Mina(Index).Caption = "M"
For x = 0 To
2
Mina(PosMine(x)).Caption = "M"
Next x
Timer1.Enabled = False
Else 'mina(index).tag=0
(nessuna mina esplosa, nessuna mina circostante)
x = Int(Index / 4) 'riga in cui
si trova il pulsante premuto
y = Index Mod 4 'colonna in cui
si trova il pulsante premuto
For x1 = IIf(x = 0, 0,
x - 1) To IIf(x = 3, 3, x + 1)
For y1 = IIf(y = 0, 0,
y - 1) To IIf(y = 3, 3, y + 1)
If Len(Mina(4 * x1 + y1).Caption)
= 0 Then
ContaMine = ContaMine + 1
End If
Mina(4 * x1 + y1).Caption = Mina(4 * x1 + y1).Tag
Next y1
Next x1
End If
If
ContaMine = MaxContaMine Then
MsgBox "HAI VINTO!"
End If
End Sub
|
Per
fare in modo che il giocatore non possa ulteriormente continuare
a premere i pulsanti dopo aver vinto o perso, occorre in qualche
modo "inibire" l'evento click, in modo che le precedenti
istruzioni non siano eseguite: il modo più semplice
per fare ciò è adottare un flag del tipo bEseguiClick
che valga true se la partita è ancora in corso o false
se la partita è terminata. In realtà una variabile
del genere l'abbiamo già utilizzata, anche se non è
un booleano: si tratta della variabile ContaMine, che quando
raggiunge il massimo segnala la vittoria del giocatore.
Nessuno ci impedisce di assegnarle il medesimo valore anche
quando il giocatore perde, e condizionare l'esecuzione della
routine al fatto che tale variabile sia minore o uguale al
valore massimo raggiungibile:
Private
Sub
Mina_Click(Index As Integer)
Dim x As
Integer, y As Integer
'coordinate del pulsante premuto
Dim x1 As
Integer, y1 As Integer
'coordinate dei pulsanti circostanti
quello 'premuto
If
ContaMine < MaxContaMine Then
If Mina(Index).Tag >
0 Then 'nessuna
mina esplosa, almeno una mina 'circostante
If Len(Mina(Index).Caption)
= 0 Then
ContaMine = ContaMine + 1
End If
Mina(Index).Caption = Mina(Index).Tag
ElseIf Mina(Index).Tag
= -1 Then 'mina
esplosa
Mina(Index).Caption = "M"
For x = 0 To
2
Mina(PosMine(x)).Caption = "M"
Next x
Timer1.Enabled = False
ContaMine = MaxContaMine
Exit Sub
Else 'mina(index).tag=0
'(nessuna mina esplosa, nessuna mina circostante)
x = Int(Index / 4) 'riga in cui
si trova il pulsante premuto
y = Index Mod 4 'colonna in cui
si trova il pulsante premuto
For x1 = IIf(x = 0, 0,
x - 1) To IIf(x = 3, 3,
x + 1)
For y1 = IIf(y = 0, 0,
y - 1) To IIf(y = 3, 3,
y + 1)
If Len(Mina(4 * x1 + y1).Caption)
= 0 Then
ContaMine = ContaMine + 1
End If
Mina(4 * x1 + y1).Caption = Mina(4 * x1 + y1).Tag
Next y1
Next x1
End If
If ContaMine = MaxContaMine
Then
MsgBox "HAI VINTO!"
Timer1.Enabled = False
End If
End If
End
Sub
|
Quando il giocatore scopre una mina, oltre ad arrestare il timer
il programma provvede ad assegnare:
ContaMine = MaxContaMine
dopodiché termina forzatamente la routine per evitare
la comparsa del messaggio "hai vinto!" (anche se il
giocatore ha perso
); una volta che ContaMine ha il suo
massimo valore concesso, qualunque ulteriore pressione delle
mine non sortisce effetti, perché la condizione iniziale
risulta falsa e la routine termina subito. In realtà
il vero campo minato si comporta in modo leggermente diverso,
impedendo la pressione stessa dei pulsanti: ciò può
essere facilmente ottenuto disabilitando tutti i pulsanti "mina",
ovvero impostando la proprietà Enabled su False per tutti
gli elementi della matrice di CommandButton "Mina".
Se invece il contatore ContaMine ha un valore minore di MaxContaMine,
significa che la partita è ancora in corso, pertanto
la routine esegue tutti i controlli del caso ed eventualmente
mostra il messaggio di vittoria; anche in tal caso il timer
viene interrotto. Ciò non è ancora sufficiente,
perché se una nuova partita viene cominciata subito dopo,
l'etichetta lblTempo riprende da dove si era interrotta anziché
ricominciare daccapo; il problema si risolve semplicemente inserendo
l'istruzione:
alla fine della routine mnuNew_Click, prima dell'abilitazione
del timer, e quest'altra istruzione all'inizio dell'evento Timer1_Timer:
If
ContaMine = 0 Then i = 0 |
Poiché la variabile ContaMine viene inizializzata a zero
ad ogni nuova partita, il timer controllando il valore della
variabile riesce a capire se deve continuare a contare da dove
era arrivato (ricordiamo che la variabile "i" usata
dal timer è di tipo static) o se deve ricominciare da
zero. L'unico inconveniente è che finché il giocatore
non comincia effettivamente a giocare (cioè non prova
a premere qualche "mina") la variabile ContaMine resterà
sempre a zero, e quindi il timer continuerà a inizializzare
la variabile i e l'etichetta lblTempo continuerà a mostrare
il valore "1" anche se la partita è cominciata
da un bel pezzo. A questo punto si potrebbero escogitare vari
trucchi, ma la soluzione più elegante è usare
una variabile a livello di modulo anziché una variabile
statica ma locale rispetto all'evento timer di Timer1:
Dim
ContaSecondi As Long 'numero
di secondi trascorsi dall'inizio della 'partita |
E modificare di conseguenza la routine del timer:
Private
Sub Timer1_Timer()
ContaSecondi = ContaSecondi + 1
lblTempo.Caption = CStr(ContaSecondi)
End Sub |
Ovviamente
la variabile ContaSecondi dovrà essere inizializzata
a zero tutte le volte che comincia una nuova partita. Questo
per quanto riguarda la correzione dei bug.
Già che ci siamo vediamo di migliorare il gioco implementando
la visualizzazione di una bandierina sui pulsanti sui quali
il giocatore clicca col tasto destro del mouse. Per fare ciò
occorre innanzitutto impostare a 1 la proprietà Style
dei pulsanti "mina": corrisponde allo stile grafico,
che consente appunto di visualizzare icone sui pulsanti; la
modalità di default è quella standard (Style=0),
senza icone.
Purtroppo la proprietà è di sola lettura in
fase di esecuzione, per cui è necessario modificare
manualmente la proprietà di tutte le mine in ambiente
di progettazione: si può fare in un colpo solo selezionando
tutte le mine insieme (come si fa con un gruppo di file in
gestione risorse) e cambiando il valore della proprietà.
A questo punto dobbiamo intercettare l'evento "click
col tasto destro" per mostrare o nascondere l'icona della
bandiera. L'evento "click", come sapete, è
associato per default al tasto sinistro del mouse, e l'oggetto
CommandButton non supporta un evento "RightClick"
generato dalla pressione del tasto destro; esso supporta però
l'evento "MouseDown", che viene generato ogniqualvolta
l'utente preme un pulsante del mouse (non importa quale) sull'oggetto:
è questo evento che dobbiamo intercettare. Questa è
la dichiarazione dell'evento:
Private
Sub
Mina_MouseDown(Index As Integer,
Button As Integer, Shift
As Integer, X As
Single, Y As Single)
End
Sub
|
Il
primo parametro, come per l'evento Click, indica su quale
"mina" è stato premuto un tasto del mouse;
il secondo invece specifica quale dei tasti è stato
premuto: perciò è su questo parametro che dovremo
fare gli opportuni controlli; il terzo parametro specifica
quali dei tasti control, alt o shift sono premuti mentre viene
generato l'evento: è molto utile per riconoscere particolari
sequenze (ad es. ctrl+shift+tasto destro); gli ultimi due
parametri indicano le coordinate del puntatore del mouse nel
momento in cui viene generato l'evento, e per ora non ci interessano.
Come si è detto, il nostro compito è individuare
la pressione del tasto destro del mouse per visualizzare la
bandiera sulla mina corrispondente; la chiave è in
una semplice If:
With
Mina(Index)
If Button = vbRightButton
Then 'click
col tasto destro
If .Picture.Handle = 0 Then
'nessuna bandiera sul pulsante
.Picture = LoadPicture("BandieraCampoMinato.ico")
Else 'il
pulsante ha già la bandiera
.Picture = LoadPicture()
End If
End If
End With |
Se
il tasto del mouse premuto è il destro, il parametro
Button avrà il valore 2, che è il valore della
costante vbRightButton (fa parte del gruppo MouseButtonConstants,
insieme a vbLeftButton e vbMiddleButton): in tal caso, se
il pulsante non ha alcuna bandiera essa viene visualizzata
attraverso la proprietà Picture, altrimenti significa
che il giocatore ha premuto il tasto destro su una mina che
aveva già la bandiera: perciò essa sarà
tolta.
L'impostazione della proprietà Picture avviene tramite
la funzione LoadPicture: dei vari parametri che accetta, qui
è stato usato solo il primo, ovvero il nome del file.
Come potete appurare controllando sul visualizzatore oggetti,
la funzione restituisce un riferimento a un'istanza della
classe IPictureDisp, che dispone di una proprietà Handle:
tale proprietà assume valore zero se non è associata
ad alcuna immagine, altrimenti assume il valore dell'handle
dell'icona; l'handle è un numero a 32 bit che fa riferimento
univocamente all'immagine caricata in memoria dalla funzione
LoadPicture.
Per eliminare un'immagine associata al pulsante, basta usare
ancora la funzione LoadPicture senza alcun parametro: infatti
il nome del file immagine è facoltativo e, se assente,
indica di rimuovere l'immagine correntemente associata al
pulsante.
Intercettato il click col tasto destro, occorre poi fare in
modo che facendo click col tasto sinistro non accada nulla
se sulla mina premuta è posta una bandiera: si tratta
di fare una piccola estensione alla condizione che abbiamo
posto all'inizio dell'evento Mina_Click:
If
(ContaMine < MaxContaMine) And
(Mina(Index).Picture.Handle = 0) Then |
L'istruzione
If controlla non solo che il gioco sia ancora in corso confrontando
il valore di ContaMine con MaxContaMine, ma anche che la mina
che il giocatore ha premuto non sia una di quelle "bloccate"
con la bandiera, altrimenti l'evento click viene sostanzialmente
ignorato. A differenza del vero campo minato, tuttavia, i
pulsanti appaiono effettivamente "premuti" quando
si fa click, anche se non succede nulla: non è come
se i pulsanti "Mina" fossero disabilitati.
In effetti si potrebbe impostare Enabled = False quando il
giocatore fa click col tasto destro del mouse, approfittando
anche del fatto che ai pulsanti disabilitati è possibile
associare un'icona tramite la proprietà DisabledPicture:
ogniqualvolta un pulsante è disabilitato Visual Basic
provvede a mostrare l'immagine associata alla proprietà,
senza bisogno di usare la funzione LoadPicture. Il problema
è che poi una volta disabilitato il pulsante non è
più possibile intercettare l'evento mousedown per eventualmente
reimpostare Enabled = True; non è un problema insormontabile,
ma sembra più conveniente la strada illustrata sopra.
È opportuno invece disabilitare le mine all'avvio dell'applicazione,
abilitandole solo quando il giocatore sceglie di iniziare
una nuova partita:
Private
Sub Form_Load()
Dim lContatore As
Long
MaxContaMine
= 13
For lContatore = Mina.LBound
To Mina.UBound
Mina(lContatore).Enabled = False
Next lContatore
End
Sub
|
Mina.LBound
e Mina.UBound equivalgono rispettivamente a Lbound(Mina) e
Ubound(Mina), ovvero 0 e 15. Per lo stesso motivo, come si
è detto più sopra, risulta conveniente disabilitare
i pulsanti quando il giocatore termina (vincendo o perdendo)
la partita, anziché utilizzare la variabile ContaMine
per inibire il click sulle mine: dipende anche dai gusti del
programmatore scegliere una delle tante strade disponibili.
Resta ancora una cosa da fare: eliminare tutte le bandiere
eventualmente esistenti quando si comincia una nuova partita;
l'ultima parte della routine mnuNew_Click diventerà
quindi:
For
i = 0 To 15
With Mina(i)
.Caption = ""
.Enabled = True
.Picture = LoadPicture()
End With
Next i
ContaMine = 0
lblTempo.Caption = ""
ContaSecondi = 0
Timer1.Enabled = True
|
Infine,
una caratteristica prevista ma non ancora implementata riguardava
l'etichetta lblMine, che nel campo minato dovrebbe indicare
quante mine restano ancora da scoprire: ora che abbiamo capito
come intercettare il click col tasto destro possiamo anche
aggiornare questa etichetta. Si tratta semplicemente di sottrarre,
dal numero complessivo di mine (3 nel nostro esempio), il
numero di pulsanti su cui sventola la bandiera; il luogo più
adatto in cui effettuare questo calcolo è l'evento
mousedown, dato che è lì che si decide se issare
o ammainare la bandiera. Intanto dichiariamo un nuovo contatore
per le bandiere a livello di modulo nel form:
Dim
ContaBandiere As Long 'numero
di bandiere issate |
Poi
incrementiamo o decrementiamo il contatore ogni volta che
una bandiera viene aggiunta o tolta, e aggiorniamo l'etichetta:
Private
Sub
Mina_MouseDown(Index As Integer,
Button As Integer, Shift
As Integer, X As
Single, Y As Single)
With Mina(Index)
If Button = vbRightButton
Then 'click
col tasto destro
If .Picture.Handle = 0 Then
'nessuna bandiera sul pulsante
.Picture = LoadPicture("c:\giorgio\word\corsovb\BandieraCampoMinato.ico")
ContaBandiere = ContaBandiere + 1
Else 'il
pulsante ha già la bandiera
.Picture = LoadPicture()
ContaBandiere = ContaBandiere - 1
End If
End If
End With
lblMine.Caption
= CStr(3 - ContaBandiere)
End
Sub
|
Infine,
inizializziamo il contatore ad ogni nuova partita:
Private
Sub
mnuNew_Click()
'...
ContaMine = 0
lblTempo.Caption = ""
lblMine.Caption = "3"
ContaSecondi = 0
ContaBandiere = 0
Timer1.Enabled = True
End Sub
|
In
teoria dovremmo usare una costante per indicare il numero
di mine, ma per ora non formalizziamoci troppo. Ci sarebbe
anche un piccolo bug: se il giocatore mette più bandiere
di quante sono le mine, l'etichetta mostra naturalmente un
numero negativo; cosa che peraltro accade anche col vero campo
minato. Se il programmatore lo ritiene opportuno, può
imporre dei controlli per evitare che ciò accada, ma
a me sembra tutto sommato più conveniente lasciarlo
così: l'etichetta ha solo una funzione indicatrice,
e se il giocatore ha voglia di mettere bandiere in eccesso,
che lo faccia pure.
Ora il campo minato sembra funzionare a dovere: ma molte cose
possono essere ancora migliorate, come vedremo. Buon divertimento.
|