Prezentowane materiały są przeznaczone dla uczniów szkół ponadgimnazjalnych. Autor artykułu: mgr Jerzy Wałaszek, wersja1.0 |
©2013 mgr
Jerzy Wałaszek
|
Początkowo wszystkie pola są zakryte i posiadają kolor szary. Na każdym polu planszy może znajdować się niewidoczna mina. Zadaniem gracza jest odkrycie wszystkich pól bez min. Jeśli klikniemy lewym przyciskiem myszki na wybranym polu gry, to zostanie ono odkryte i zmieni kolor na biały.
3 | |||||||||
Liczba pojawiająca się w polu określa ilość min w polach sąsiednich (tutaj 3). Jeśli uda ci się kliknąć myszką na pole, z którym nie sąsiadują pola z minami, to komputer odkryje wszystkie puste pola, które przylegają do niego.
1 | 2 | ||||||||
3 | 1 | 2 | |||||||
2 | 1 | 2 | 2 | 1 | 1 | 1 | 1 | ||
1 | |||||||||
1 | 1 | 2 | 2 | 1 | |||||
2 | 2 | 1 | 1 | 1 | |||||
1 | 1 | 1 | |||||||
Oczywiście, jeśli na klikniętym polu będzie mina, to przegrywasz, a cała plansza wraz z minami zostanie odsłonięta.
Jeśli klikniesz w pole prawym przyciskiem myszki, a jest ono nieodsłonięte, to zablokujesz je. Pole zmieni kolor na zielony i nie daje się odkryć. W ten sposób zaznaczasz miejsca z potencjalnymi minami. Aby odblokować pole, musisz je ponownie kliknąć prawym przyciskiem myszki.
1 | 2 | ||||||||
3 | 1 | 2 | |||||||
2 | 1 | 2 | 2 | 1 | 1 | 1 | 1 | ||
1 | |||||||||
1 | 1 | 2 | 2 | 1 | |||||
2 | 2 | 1 | 1 | 1 | |||||
1 | 1 | 1 | |||||||
Gra skończy się, gdy odkryjesz wszystkie pola lub trafisz wcześniej na minę.
3 | 3 | 2 | 1 | 2 | 2 | ||||
3 | 2 | 1 | 2 | 2 | |||||
2 | 2 | 1 | 2 | 2 | 1 | 1 | 1 | 1 | |
1 | |||||||||
1 | 1 | 1 | 2 | 2 | 1 | ||||
1 | 2 | 2 | 1 | 1 | 1 | ||||
1 | 2 | 2 | 2 | 2 | 1 | 1 | 1 | ||
1 | 2 | 3 | 1 | 1 | 1 | 2 | |||
1 | 1 | 1 | 1 | 2 | 1 | ||||
1 | 1 | 1 |
Gra będzie wykorzystywała następujące struktury danych:
Tworząc grę, poznasz zasady tworzenia aplikacji wielomodułowych.
oraz
Pierwsza nazwa, Unit1, dotyczy modułu, który będzie obsługiwał część okienkową aplikacji. Druga nazwa, Saper, dotyczy pliku projektu, w którym środowisko Borland C++ Builder zapisuje różne informacje o twoim projekcie (ustawienia, zawarte w projekcie pliki, itp.).
Chwilowo część okienkową pozostawimy w spokoju i zajmiemy się dodatkowymi plikami, które będą używane w projekcie.
Z menu wybierz opcję File → New → Other... Na ekranie pojawi się okno dialogowe New Items (nowe elementy):
W oknie zaznacz opcję Header File (plik nagłówkowy) i kliknij OK. W edytorze pojawi się pusty plik o nazwie File1.h. Do edytora przepisz poniższy tekst programu:
// Koło Informatyczne // I LO w Tarnowie // Plik nagłówkowy //-------------------- #define RMIN 10 #define RMAX 40 #define CMIN 10 #define CMAX 60 #define MMIN 10 #define SSIZE 100 #define SGROW // Struktura komórki planszy //-------------------------- struct Cell { bool open; // Pole odkryte bool flag; // Flaga na polu bool mine; // Mina w polu bool exploded; // Mina wybuchła int count; // Liczba min w sąsiedztwie }; // Klasa planszy pola minowego //---------------------------- class Minefield { protected: Cell ** T; // Macierz dynamiczna int r,c; // Liczba wierszy r i kolumn c int mc; // Liczba min na polu public: Minefield(int m, int n, int mn); // Konstruktor ~Minefield(); // Destruktor int RowCount(); // Zwraca liczbę wierszy int ColCount(); // Zwraca liczbę kolumn int MinCount(); // Zwraca liczbę min Cell & At(int rr,int cc); // Zwraca referencję do komórki }; // Klasa stosu //------------ class Stack { protected: int * S; // Stos dynamiczny int sp; // Wskaźnik stosu int n; // Rozmiar stosu public: Stack(); // Konstruktor ~Stack(); // Destruktor bool notEmpty(); // Zwraca true, gdy stos nie jest pusty void push(int a, int b); // Umieszcza a i b na stosie void pop(int & a, int & b); // Pobiera a i b ze stosu }; |
Plik ten definiuje struktury danych, które będą używane w naszej aplikacji. Plik zapisz na dysku pod nazwą (kliknij w ikonę dyskietki na pasku narzędziowym):
W pliku nagłówkowym mamy zdefiniowane kilka elementów:
#define RMIN 10 #define RMAX 40 #define CMIN 10 #define CMAX 60 #define MMIN 10 #define SSIZE 100 #define SGROW 50 #define FSIZE 20 |
Definicje stałych, które określają parametry
pola gry:
|
|||||||||||||||||||||||||||||||
struct Cell { bool open; bool flag; bool mine; bool exploded; int count; }; |
Każde pole planszy gry jest strukturą Cell
(komórka), której pola danych opisują
stan tego elementu:
|
|||||||||||||||||||||||||||||||
class Minefield { protected: Cell ** T; int r,c; int mc; public: Minefield(int m, int n, int mn); ~Minefield(); int RowCount(); int ColCount(); int MinCount(); Cell & At(int rr,int cc); }; |
Plansza gry będzie w naszym programie
reprezentowana przez klasę Minefield
(pole minowe). W sekcji chronionej zdefiniowane są
następujące elementy:
Sekcja publiczna definiuje kilka metod klasy:
W sumie metody RowCount(), ColCount(), MinCount() i At() nie są niezbędne – moglibyśmy się w programie odwoływać do odpowiednich pól klasy, gdyby uczynić je publicznymi. Ale tak jest bardziej czytelnie i elegancko. |
|||||||||||||||||||||||||||||||
class Stack { protected: int * S; int sp; int n; public: Stack(); ~Stack(); bool notEmpty(); void push(int a, int b); void pop(int & a, int & b); }; |
Klasa Stack (stos)
definiuje prosty stos oparty na tablicy dynamicznej. Stos będzie
nam potrzebny dla algorytmu DFS, który wykorzystamy do
odkrywania na planszy pustych pól. Na temat stosów możesz
przeczytać
tutaj.
Na temat DFS możesz przeczytać
tutaj. W sekcji chronionej mamy następujące pola:
W sekcji publicznej mamy następujące metody:
|
Plik nagłówkowy definiuje jedynie same struktury danych. Brak w nim definicji metod wchodzących w skład klas. Ma to sens, ponieważ plik nagłówkowy jest zwykle dołączany do każdego pliku programu, w których chcemy korzystać z tych struktur. Jeśli w programie wielokrotnie dołączamy taki plik, to nie chcemy za każdym razem kompilować wszystkich metod. Zatem teraz utworzymy nowy plik programowy, w którym zdefiniujemy poszczególne metody.
Z menu wybierz opcję File → New → Other... Na ekranie znów pojawi się znane ci okno dialogowe New Items (nowe elementy):
Tym razem zaznacz opcję Cpp File (plik źródłowy C++) i kliknij OK.
W edytorze zostaje utworzony plik o nazwie File1.cpp (na razie nie zapisuj go na dysku, zrobimy to po wprowadzeniu programu). Do edytora przepisz poniższy tekst programu:
#include "myclasses.h" #include <stdlib.h> // Klasa planszy gry //------------------ // Konstruktor planszy //-------------------- Minefield::Minefield(int m, int n, int mn) { // Ustalamy liczbę wierszy r = m; if(r < RMIN) r = RMIN; // Jeśli r nie mieści się w zakresie, if(r > RMAX) r = RMAX; // to je odpowiednio dopasowujemy // Ustalamy liczbę kolumn c = n; if(c < CMIN) c = CMIN; // Jeśli c nie mieści się w zakresie, if(c > CMAX) c = CMAX; // to je odpowiednio dopasowujemy // Tworzymy macierz pól T = new Cell * [r]; for(int i = 0; i < r; i++) { T[i] = new Cell [c]; // Każdą komórkę pola inicjujemy zerami for(int j = 0; j < c; j++) { T[i][j].open = T[i][j].flag = T[i][j].mine = T[i][j].exploded = false; T[i][j].count = 0; } } // Ustalamy liczbę min na polu, dopasowując ją do zakresu if(mn < MMIN) mn = MMIN; if(mn > (r * c) / 3) mn = (r * c) / 3; // Wstawiamy miny na pole do komórek // W mc będzie ostateczna liczba min mc = 0; while(mc < mn) { int rr = rand() % r; // Generujemy losowe pole planszy int cc = rand() % c; if(T[rr][cc].mine) continue; // Jeśli zawiera minę, to powtarzamy T[rr][cc].mine = true; // Wstawiamy minę w pole mc++; // Zwiększamy licznik min } // Zliczamy miny w sąsiedztwie każdego pola for(int i = 0; i < r; i++) for(int j = 0; j < c; j++) for(int ii = i - 1; ii <= i + 1; ii++) if((ii >= 0) && (ii < r)) for(int jj = j - 1; jj <= j + 1; jj++) if((jj >= 0) && (jj < c) && T[ii][jj].mine) T[i][j].count++; } // Destruktor planszy //------------------- Minefield::~Minefield() { for(int i = 0; i < r; i++) delete [] T[i]; // Usuwamy wiersze delete [] T; // Usuwamy tablicę wierszy } // Zwraca liczbę wierszy //---------------------- int Minefield::RowCount() { return r; } // Zwraca liczbę kolumn //--------------------- int Minefield::ColCount() { return c; } // Zwraca liczbę min //------------------ int Minefield::MinCount() { return mc; } // Zwraca referencję do komórki planszy // rr - numer wiersza // cc - numer kolumny //------------------------------------- Cell & Minefield::At(int rr,int cc) { return T[rr][cc]; } // Klasa Stosu //------------ // Konstruktor //------------ Stack::Stack() { n = SSIZE; // Zapamiętujemy rozmiar S = new int [n]; // Tworzymy stos sp = 0; // Zerujemy wskaźnik stosu } // Destruktor //----------- Stack::~Stack() { delete [] S; } // Zwraca true, jeśli stos nie jest pusty //--------------------------------------- bool Stack::notEmpty() { return sp; } // Umieszcza na stosie a i b //-------------------------- void Stack::push(int a, int b) { if(sp == n) // Sprawdzamy, czy na stosie jest miejsce { // Jeśli nie, to n += SGROW; // Powiększamy rozmiar int * T = new int[n]; // Tworzymy nowy stos for(int i = 0; i < sp; i++) T[i] = S[i]; // Kopiujemy stary stos do nowego delete [] S; // Stary stos usuwamy S = T; // Nowy stos przydzielamy S } S[sp++] = a; // Na stosie umieszczamy a i b S[sp++] = b; } // Pobiera ze stosu a i b //----------------------- void Stack::pop(int & a, int & b) { b = S[--sp]; // Pobieramy ze stosu najpierw b a = S[--sp]; // a następnie a } |
Gdy wprowadzisz cały program, kliknij w ikonę i zapisz plik pod nazwą:
Przeanalizujmy zawartość pliku myclasses.cpp:
#include "myclasses.h" |
Na początku dołączamy dyrektywą #include
plik nagłówkowy myclasses.h, który utworzyliśmy przed
chwilą. Zwróć uwagę, że nazwa pliku nagłówkowego jest podana w
cudzysłowach. Oznacza to, że plik nagłówkowy znajduje się w
katalogu projektowym, a nie wewnątrz katalogów środowiska. Pliki
myclasses.cpp oraz myclasses,h tworzą teraz parę. W edytorze
kodu możesz szybko przełączać się pomiędzy plikiem cpp oraz h za
pomocą kombinacji klawiszy Ctrl + F6. Zapamiętaj je, gdyż są
często używane.Dzięki dołączeniu tego pliku nasz program "zna" budowę zdefiniowanych tam elementów. |
||||||||||
#include <stdlib.h> |
Drugim dołączanym plikiem nagłówkowym jest
stdlib.h. W tym przypadku nazwa pliku ujęta jest w nawiasy
trójkątne. Oznacza to, że będzie on pobrany z odpowiednich
katalogów wewnątrz środowiska – teraz znajomość położenia tych
katalogów nie jest ci potrzebna. Plik stdlib.h definiuje funkcje standardowej biblioteki. Potrzebujemy go do generacji liczb pseudolosowych w funkcji rand(), która właśnie jest tam zdefiniowana. O liczbach pseudolosowych możesz przeczytać tutaj. |
||||||||||
Minefield::Minefield(int m, int n, int mn) { |
Konstruktor klasy Minefield . Jest
to konstruktor specjalizowany, do którego przekazujemy trzy
parametry określające kolejno:
|
||||||||||
r = m; if(r < RMIN) r = RMIN; if(r > RMAX) r = RMAX; c = n; if(c < CMIN) c = CMIN; if(c > CMAX) c = CMAX; |
Otrzymane przez konstruktor parametry są
odpowiednio dopasowywane do wartości granicznych. Np, jeśli
liczba wierszy będzie mniejsza od minimalnej RMIN ,
to zostanie skorygowana do wartości RMIN . Podobnie
bada się, czy liczba wierszy nie przekracza RMAX .
Jeśli tak, to zostanie ustalona na RMAX . Chodzi
tutaj o to, aby okienko gry nie było większe od rozmiarów ekranu
monitora.Liczba wierszy zostaje zapamiętana w polu |
||||||||||
T = new Cell * [r]; for(int i = 0; i < r; i++) { T[i] = new Cell [c]; for(int j = 0; j < c; j++) { T[i][j].open = T[i][j].flag = T[i][j].mine = T[i][j].exploded = false; T[i][j].count = 0; } } |
Gdy ustalimy liczbę wierszy r oraz
kolumn c , tworzymy macierz dynamiczną i jej adres
zapamiętujemy w polu T klasy.Następnie w pętli
tworzymy poszczególne wiersze macierzy. Wiersz zbudowany jest z
Po utworzeniu wiersza inicjujemy wszystkie komórki. W polach
W polu |
||||||||||
if(mn < MMIN) mn = MMIN; if(mn > (r * c) / 3) mn = (r * c) / 3; |
Teraz zajmujemy się dostarczoną do konstruktora
liczbą min, które mają się znaleźć na planszy gry. Liczba min
nie może być mniejsza od MMIN , ani większa od
1/3 liczby pól na planszy. |
||||||||||
mc = 0; while(mc < mn) { int rr = rand() % r; int cc = rand() % c; if(T[rr][cc].mine) continue; T[rr][cc].mine = true; mc++; } |
Na początku ustawiamy licznik min na 0. Licznik
mc (ang. mines counter)
jest polem klasy.Następnie w pętli umieszczamy na planszy
Jeśli pole nie zawiera miny, to ustawiamy jego składnik |
||||||||||
for(int i = 0; i < r; i++) for(int j = 0; j < c; j++) for(int ii = i - 1; ii <= i + 1; ii++) if((ii >= 0) && (ii < r)) for(int jj = j - 1; jj <= j + 1; jj++) if((jj >= 0) && (jj < c) && T[ii][jj].mine) T[i][j].count++; } |
Ostatnią czynnością konstruktora jest
policzenie dla każdego pola planszy liczby min w sąsiedztwie.
Wykonuje się to w ten sposób, że w najbardziej wewnętrznej pętli
sprawdzamy do dziewięciu pól, z których środkowym polem jest
badane pole: Jeśli w którymś z tych 9 pól znajdzie się mina, to licznik
|
||||||||||
Minefield::~Minefield() { for(int i = 0; i < r; i++) delete [] T[i]; delete [] T; } |
Destruktor klasy oddaje pamięć, która została
przydzielona macierzy T . Najpierw usuwamy
poszczególne wiersze, a następnie tablicę T . |
||||||||||
int Minefield::RowCount() { return r; } int Minefield::ColCount() { return c; } int Minefield::MinCount() { return mc; } |
Metody zwracające wartość pól chronionych klasy. | ||||||||||
Cell & Minefield::At(int rr,int cc) { return T[rr][cc]; } |
Metoda At() zwraca referencję do
pola macierzy T , które znajduje się w wierszu
rr i kolumnie cc . |
||||||||||
Stack::Stack() { n = SSIZE; S = new int [n]; sp = 0; } |
Konstruktor klasy Stack , czyli
stosu. W polu n zapamiętuje rozmiar początkowy
stosu, czyli SSIZE . Następnie tworzy tablicę
dynamiczną S o tym rozmiarze i zeruje wskaźnik
stosu sp . |
||||||||||
Stack::~Stack() { delete [] S; } |
Destruktor klasy Stack . Zwalnia
pamięć przydzieloną tablicy S . |
||||||||||
bool Stack::notEmpty() { return sp; } |
Metoda notEmpty() zwraca
true , jeśli stos przechowuje jakieś dane. Tak dzieje się,
jeśli wskaźnik stosu sp był zwiększany i ma wartość
większą od zera. Zwracana wartość jest traktowana jako wartość
logiczna, a w języku C++ wartości różne od zera są traktowane
jako true. |
||||||||||
void Stack::push(int a, int b) { if(sp == n) { n += SGROW; int * T = new int[n]; for(int i = 0; i < sp; i++) T[i] = S[i]; delete [] S; S = T; } S[sp++] = a; S[sp++] = b; } |
Metoda push() umieszcza na stosie
wartości a i b . Najpierw sprawdzane
jest wolne miejsce. Jeśli wskaźnik stosu ma wartość n ,
to tablica S jest całkowicie zapełniona. W takim
przypadku n jest zwiększane o wartość stałej
SGROW . Następnie zostaje utworzona nowa tablica T
o większym rozmiarze od tablicy S . Do tablicy
T kopiowana jest zawartość tablicy S , po
czym zwolniona zostaje pamięć zarezerwowana dla S .
Po przepisaniu adresu T do S utworzona
nowa tablica T staje się nowym stosem, na którym
mamy SGROW wolnych komórek. Ponieważ zawsze
zapisujemy na stos dwie liczby, ważne jest, aby stałe
SSIZE oraz SGROW posiadały wartości parzyste
(dlaczego?).Na końcu umieszczamy
kolejno na stosie |
||||||||||
void Stack::pop(int & a, int & b) { b = S[--sp]; a = S[--sp]; } |
Metoda pop() pobiera ze stosu dwie
wartości i umieszcza je odpowiednio w a i b .
Zwróć uwagę, że najpierw pobrane zostaje b , a
później a . Tak działa stos. Zawsze pobieramy jako
pierwszy element, który został umieszczony na stosie jako
ostatni. Parametry a i b są
przekazywane przez referencję, zatem metoda posiada dostęp do
odpowiednich zmiennych w programie głównym. |
Twój projekt zawiera teraz wszystkie niezbędne pliki. Aby je zobaczyć, w menu wybierz opcję: View → Preject Manager. Na ekranie ukaże się okno z informacją o składnikach twojego projektu:
//--------------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop #include "Unit1.h" #include "myclasses.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TForm1 *Form1; //--------------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //--------------------------------------------------------------------------- |
W ten sposób metody w Unit1.cpp uzyskają dostęp do naszych klas.
Naciśnij klawisz F12. Spowoduje to pojawienie się na ekranie okienka programu. Myszką zmniejsz jego rozmiar do pokazanego poniżej (wymiary nie są ważne, ponieważ program i tak je odpowiednio będzie dostosowywał):
U góry ekranu wybierz zakładkę komponentów Standard i na liście kliknij dwukrotnie komponent Panel:
W oknie pojawi się komponent panelu:
Nie przejmuj się rozmiarami, ani położeniem komponentów. Ustalimy je później.
Teraz zwróć uwagę, czy panel jest wybrany jak na powyższym obrazku. Jeśli nie, to kliknij go myszką, aby pojawiły się uchwyty, czyli te małe, czarne kwadraciki.
Gdy panel jest wybrany, umieść na nim trzy komponenty Label (etykieta, napis), klikając za każdym razem dwukrotnie w ikonę Label u góry ekranu:
Twoje okienko powinno wyglądać tak:
Teraz w identyczny sposób wstaw na panel trzy komponenty Edit:
Oraz jeden komponent Button:
Okno powinno wyglądać tak:
Jeśli wszystko wykonałeś poprawnie, to w oknie Object Treeview (widok drzewa obiektów) będzie następująca zawartość (to bardzo ważne, jeśli masz inaczej, usuń panel i powtórz opisane powyżej operacje):
Okno Object Treeview posłuży nam teraz do wyboru poszczególnych komponentów. Poniższe operacje musisz wykonać bardzo uważnie, w przeciwnym razie program może nie dać się uruchomić poprawnie.
Określimy teraz tzw. własności (ang. properties) poszczególnych komponentów w naszym programie. Dane, które podajemy, należy wprowadzać w oknie Object Inspector (inspektor obiektów) po wybraniu z okna Object Treeview odpowiedniego komponentu.
Form1:
Panel1:
Button1:
Edit1:
Edit2:
Edit3:
Label1:
Label2:
Label3:
Jeśli wszystko wykonałeś dobrze, to okno programu powinno wyglądać tak:
A w oknie Object Treeview powinna być poniższa zawartość:
Teraz możemy zacząć tworzyć kod dla poszczególnych komponentów naszego okna. Na początek umieścimy w programie deklaracje kilku zmiennych globalnych (zaznaczone na czerwono):
//--------------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop #include "Unit1.h" #include "myclasses.h" //--------------------------------------------------------------------------- #pragma package(smart_init) #pragma resource "*.dfm" TfrmSaper *frmSaper; Minefield * mf = NULL; // Plansza gry bool Playing = false; // Jeśli gra się toczy, to true Graphics::TBitmap * bm = NULL; // Wskaźnik bitmapy planszy int MinesCount; // Liczba pozostałych min do odkrycia //--------------------------------------------------------------------------- __fastcall TfrmSaper::TfrmSaper(TComponent* Owner) : TForm(Owner) { } //--------------------------------------------------------------------------- |
Teraz na koniec programu (pod konstruktor klasy okna)
wstawimy funkcję, która tworzy obraz pojedynczej komórki planszy gry. Obraz
ten będzie tworzony na powierzchni graficznej obiektu bm
.
Zakładamy tutaj, że bitmapa bm
została już utworzona wcześniej
i zainicjowana (tym zajmiemy się za chwilę).
Kod funkcji jest następujący:
//--------------------------------------------------------------------------- void DrawMFCell(int r,int c) { int x = FSIZE * c; // Obliczamy współrzędne komórki na bitmapie int y = FSIZE * r; TRect cell(x,y,x+FSIZE,y+FSIZE); // Tworzymy prostokąt obejmujący komórkę // W zależności od zawartości komórki rysujemy różne treści // w obrębie prostokąta komórki if(mf->At(r,c).open) // Jeśli komórka jest odkryta { bm->Canvas->Brush->Color = clWhite; // To rysujemy białe tło bm->Canvas->FillRect(cell); if(mf->At(r,c).mine) // Teraz sprawdzamy, czy w komórce jest mina { if(mf->At(r,c).exploded) // A jeśli jest, to czy wybuchła { bm->Canvas->Pen->Color = clYellow; // Tak, linia żółta, pędzel czerwony bm->Canvas->Brush->Color = clRed; } else { bm->Canvas->Pen->Color = clGray; // Nie, linia szara, pędzel czarny bm->Canvas->Brush->Color = clBlack; } bm->Canvas->Ellipse(x+3,y+3,x+FSIZE-3,y+FSIZE-3); // Rysujemy minę } else if(mf->At(r,c).count > 0) { // Komórka odkryta bez miny - jeśli liczba min ją otaczających jest // większa od 0, to w komórce zostanie wyświetlona ta liczba AnsiString s = IntToStr(mf->At(r,c).count); // Pobieramy tekst liczby int sx = x + (FSIZE - bm->Canvas->TextWidth(s)) / 2; // Obliczamy położenie tekstu int sy = y + (FSIZE - bm->Canvas->TextHeight(s)) / 2; // na środku komórki bm->Canvas->TextOutA(sx,sy,s); // Rysujemy tekst } } else { // Komórka zakryta bm->Canvas->Brush->Color = clGray; // Tło szare if(mf->At(r,c).flag) // Sprawdzamy, czy komórka jest oflagowana bm->Canvas->Brush->Color = clGreen; // Jeśli tak, to tło zielone bm->Canvas->FillRect(cell); // Wypełniamy komórkę wybranym tłem } // Na koniec rysujemy kratkę bm->Canvas->Pen->Color = clMaroon; // Kolor linii bm->Canvas->MoveTo(x,y); // Punkt startowy bm->Canvas->LineTo(x+FSIZE-1,y); // Linia pozioma bm->Canvas->LineTo(x+FSIZE-1,y+FSIZE); // Linia pionowa } //--------------------------------------------------------------------------- |
Funkcja DrawMFCell()
dostaje na wejściu współrzędne komórki
w obrębie planszy pola minowego. Resztę masz w dokładnych komentarzach.
Kolejna funkcja, DrawMF() rysuje całą planszę gry. Dodaj ją na koniec programu:
//--------------------------------------------------------------------------- void DrawMF() { for(int i = 0; i < mf->RowCount(); i++) for(int j = 0; j < mf->ColCount(); j++) DrawMFCell(i,j); // Rysujemy kolejne komórki planszy } //--------------------------------------------------------------------------- |
UWAGA!!!
Kolejne metody będą funkcjami obsługi zdarzeń. Funkcje takie nie mogą być kopiowane bezpośrednio do edytora. Należy wykonać odpowiednią procedurę dodania funkcji obsługi zdarzeń.
Zdarzenie (ang. event) jest sytuacją, która pojawia się w trakcie działania programu – użytkownik kliknął myszką, nacisnął klawisz, upłynął zadany okres czasu, itp. W środowisku Borland C++ Builder zdarzenia dotyczą komponentów.
Pierwszym zdarzeniem, które obsłużymy będzie zdarzenie onPaint (przy rysowaniu), które jest powiązane z obiektem okna, czyli u nas frmSaper. Zdarzenie to powstaje, gdy okno musi przerysować swoją zawartość.
W Object Treeview kliknij na komponencie frmSaper (musi być zaznaczony). Następnie w oknie Object Inspector kliknij zakładkę Events. Wyszukaj zdarzenie onPaint i kliknij dwukrotnie myszką obok w prawej kolumnie. W edytorze pojawi się funkcja obsługi tego zdarzenia.
void __fastcall TfrmSaper::FormPaint(TObject *Sender) { } |
Teraz umieść w tej funkcji kod (czyli to, co znajduje się wewnątrz klamerek):
void __fastcall TfrmSaper::FormPaint(TObject *Sender) { Canvas->Draw(0,pnlSetting->Height,bm); } |
Kod ten umieszcza zawartość bitmapy bm
w obszarze okna pod
panelem.
Teraz w oknie Object Treeview wybierz przycisk btnStart. W oknie
Object Inspector wyszukaj zdarzenie onClick i kliknij dwukrotnie
myszką obok niego w prawej kolumnie. W edytorze powstanie funkcja obsługi
zdarzenia onClick dla przycisku btnStart:
void __fastcall TfrmSaper::btnStartClick(TObject *Sender) { } |
Zdarzenie onClick powstaje, gdy użytkownik kliknie myszką w komponent. W przypadku naszego programu kliknięcie w przycisk START powinno spowodować utworzenie nowej planszy gry. Dokładnie tak samo, jak dla zdarzenie onPaint, przepisz do wnętrza funkcji poniższy kod (przepisz to, co znajduje się wewnątrz klamerek). W komentarzach opisane jest dokładnie działanie tej funkcji.
void __fastcall TfrmSaper::btnStartClick(TObject *Sender) { if(mf) delete mf; // Usuwamy poprzednią planszę gry if(bm) delete bm; // oraz jej bitmapę int r = edtRows->Text.ToInt(); // Odczytujemy rozmiar planszy int c = edtCols->Text.ToInt(); // z pól edycyjnych int m = edtMines->Text.ToInt(); // oraz liczbę min mf = new Minefield(r,c,m); // Tworzymy nową planszę r = mf->RowCount(); // Odczytujemy faktyczne rozmiary c = mf->ColCount(); // planszy pola minowego MinesCount = m = mf->MinCount(); // oraz rzeczywistą liczbę min edtRows->Text = IntToStr(r); // Korygujemy pola edycyjne edtCols->Text = IntToStr(c); edtMines->Text = IntToStr(m); ClientWidth = FSIZE * c; // Obliczamy wymiary okna ClientHeight = pnlSetting->Height + FSIZE * r; bm = new Graphics::TBitmap; // Tworzymy nową bitmapę dla planszy bm->Width = FSIZE * c; // Obliczamy rozmiar jej płótna Canvas bm->Height = FSIZE * r; DrawMF(); // Rysujemy całe pole minowe na bitmapie lblMines->Caption = "miny = " + IntToStr(MinesCount); Invalidate(); // Unieważniamy okno, co spowoduje // przerysowanie jego treści Playing = true; // Oznaczamy grę jako aktywną } |
Na tym etapie gra jeszcze nie działa. Możesz jednakże dokonać wstępnej kompilacji, chociażby po to, aby sprawdzić, czy w twoim programie nie ma błędów. Kliknij zatem w zieloną strzałkę u góry ekranu. Program zostanie skompilowany i uruchomiony. Na ekranie zobaczysz okno Saper. Gdy klikniesz w przycisk START, okno się nieco wydłuży i pojawi się na nim kratka symbolizująca komórki pola minowego.
Możesz zmieniać zawartość pól edycyjnych i tworzyć plansze o różnej wielkości. Jeśli przy największym rozmiarze okno nie będzie się mieściło w całości w obrębie ekranu, to zmodyfikuj stałe w pliku myclasses.h.
Teraz dodamy do programu obsługę zdarzenia onCreate dla okna frmSaper. Wybierz w oknie Object Treeview komponent frmSaper, a następnie w oknie Object Inspector na zakładce Events kliknij dwukrotnie obok zdarzenia onCreate. Do funkcji obsługi tego zdarzenia w edytorze wprowadź poniższy kod:
void __fastcall TfrmSaper::FormCreate(TObject *Sender) { srand(time(NULL)); // Inicjujemy generator pseudolosowy DoubleBuffered = true; // Okno podwójnie buforowane - nie będzie migotać btnStartClick(Sender); // Inicjujemy planszę poprzez onClick dla przycisku START } |
Zdarzenie onCreate powstaje w momencie tworzenia okna, czyli na samym początku programu. Obsługa tego zdarzenia pozwala nam utworzyć okno gotowe do gry, dzięki wywołaniu na końcu funkcji obsługi zdarzenia onClick dla przycisku – to tak, jakby program sam sobie w ten przycisk kliknął. Gdy teraz uruchomisz program, od razu otrzymasz gotowe okno gry.
Pozostała nam ostatnia funkcja, która obsługuje zdarzenie onMouseDown dla okna frmSaper. Zdarzenie to powstaje, gdy użytkownik kliknie jednym z przycisków myszki, wskazując kursorem komponent. Różnica pomiędzy onClick a onMouseDown jest taka, że to drugie zdarzenie w parametrach wejściowych otrzymuje stan przycisków myszki, stan klawiszy SHIFT, CTRL i ALT oraz współrzędne kursora myszki w momencie kliknięcia (liczone od lewego górnego narożnika komponentu). Dzięki tym danym procedura obsługująca zdarzenie może zidentyfikować komórkę planszy, w którą kliknął użytkownik, a na tej podstawie podjąć odpowiednie działania. Funkcja jest dosyć skomplikowana, ponieważ praktycznie obsługuje całą rozgrywkę. W komentarzach masz dokładne opisy.
Utwórz w opisany powyżej sposób funkcję obsługi zdarzenia onMouseDown dla frmSaper, a następnie wprowadź do niej poniższy kod:
void __fastcall TfrmSaper::FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { if(Playing) // Sprawdzamy, czy gra jest w toku { int r = (Y - pnlSetting->Height) / FSIZE; // Obliczamy współrzędne komórki int c = X / FSIZE; // klikniętej przez gracza if(Button == mbRight) // Prawy przycisk myszki? { if(mf->At(r,c).open == false) // Komórka zakryta? { mf->At(r,c).flag = !mf->At(r,c).flag; // Ustaw lub usuń flagę oraz zmodyfikuj licznik min if(mf->At(r,c).flag && MinesCount) MinesCount--; else MinesCount++; } } else if((Button == mbLeft) && !mf->At(r,c).flag) { // Lewy przycisk myszki, a komórka bez flagi mf->At(r,c).open = true; // Odkrywamy komórkę if(mf->At(r,c).mine) // Komórka z miną? { mf->At(r,c).exploded = true; // Jeśli tak, to gra się kończy Playing = false; } else if(mf->At(r,c).count == 0) // Sprawdzamy, czy liczba otaczających min jest równa 0 { // Jeśli tak, to za pomocą DFS odkrywamy przyległe komórki bez min Stack * S = new Stack; // Tworzymy stos mf->At(r,c).open = false; // Chwilowo zakrywamy komórkę - wymaga tego DFS S->push(r,c); // Pozycja komórki startowej na stos while(S->notEmpty()) // W pętli wykonujemy przejście DFS { int xr,xc; S->pop(xr,xc); // Pobieramy ze stosu pozycję komórki if(mf->At(xr,xc).open) continue; // Jeśli przetworzona, to wracamy na początek pętli mf->At(xr,xc).open = true; // Otwieramy komórkę if(mf->At(xr,xc).flag) MinesCount++;// Jeśli komórka była oflagowana, to zwiększamy licznik min DrawMFCell(xr,xc); // Rysujemy komórkę na płótnie mapy bitowej bm if(mf->At(xr,xc).count == 0) // Jeśli jest to komórka bez otaczających ją min, to na stosie { // umieszczamy wszystkich jej nieotwartych sąsiadów for(int i = xr - 1; i <= xr + 1; i++) if((i >= 0) && (i < mf->RowCount())) for(int j = xc - 1; j <= xc + 1; j++) if((j >=0) && (j < mf->ColCount()) && (mf->At(i,j).open == false)) { S->push(i,j); } } } delete S; // Koniec DFS. Usuwamy stos } } DrawMFCell(r,c); // Rysujemy na bitmapie bm klikniętą komórkę // Sprawdzamy koniec gry. bool test = true; for(int i = 0; i < mf->RowCount(); i++) // Sprawdzamy, czy na planszy pozostały jeszcze nieodkryte komórki for(int j = 0; j < mf->ColCount(); j++) // bez min. Jeśli tak, to test jest zerowany na false. if((mf->At(i,j).open == false) && (mf->At(i,j).mine == false)) test = false; // Gra jeszcze nie jest skończona if(test) Playing = false; // Przerywamy grę, jeśli test jest pozytywny if(!Playing) // Jeśli gra się zakończyła, to odkrywamy wszystkie komórki { for(int i = 0; i < mf->RowCount(); i++) for(int j = 0; j < mf->ColCount(); j++) mf->At(i,j).open = true; DrawMF(); // Rysujemy całą planszę gry } lblMines->Caption = "miny = " + IntToStr(MinesCount); Invalidate(); // To powoduje przekopiowanie bitmapy bm do okna gry } } |
Skompiluj i uruchom program. Ciesz się grą Saper:
I Liceum Ogólnokształcące |
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe