Koło informatyczne

Gra Saper

Dla odprężenia napiszemy dzisiaj prostą grę logiczną Saper. Zasady gry są następujące:

 

Grę rozgrywa się na planszy podzielonej na m  wierszy i n  kolumn:

 

                                                 
                   
                   
                   
                   
                   
                   
                   
                   
                   

 

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ę.

 

obrazek 3 obrazek 3 2 1   2 obrazek 2
obrazek 3 2 obrazek obrazek 1   2 obrazek 2
2 2 1 2 2 1   1 1 1
obrazek 1                      
1 1     1 2 2 1    
1 2 2 1 1 obrazek obrazek 1    
1 obrazek obrazek 2 2 2 2 1 1 1
1 2 3 obrazek 1   1 1 2 obrazek
    1 1 1   1 obrazek 2 1
            1 1 1  

 

Gra będzie wykorzystywała następujące struktury danych:

Tworząc grę, poznasz zasady tworzenia aplikacji wielomodułowych.

 

Tworzenie projektu gry Saper

Uruchom środowisko Broland C++ Builder 6.0, kliknij w ikonę: obrazek i zapisz projekt w wybranym katalogu odpowiednio pod nazwami:

 

obrazek

 

oraz

obrazek

 

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ę FileNewOther...  Na ekranie pojawi się okno dialogowe New Items (nowe elementy):

 

obrazek

 

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 obrazek na pasku narzędziowym):

 

obrazek

 

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:
RMIN    minimalna liczba wierszy.
RMAX  – maksymalna liczba wierszy.
CMIN  – minimalna liczba kolumn
CMAX  – maksymalna liczba kolumn
MMIN  – minimalna liczba min
SSIZE  – początkowy rozmiar stosu, zawsze parzyste.
SGROW  – przyrost stosu, zawsze parzyste.
FSIZE  – rozmiar w pikselach komórki 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:
open    false, pole jest zakryte; true, pole jest odkryte.
flag  – jeśli pole jest zakryte, to określa oznakowanie pola chorągiewką: true, oznakowane; false, nieoznakowane.
mine  – true, zawiera minę; false, nie zawiera miny.
exploded  – jeśli pole zawiera minę, to true oznacza, iż gracz odkrył to pole i mina wybuchła. W końcowej fazie gry wszystkie pola są odkrywane, aby gracz mógł zobaczyć planszę. Pole z miną, która wybuchła, oznaczane jest czerwonym krążkiem. Natomiast miny, które nie wybuchły, oznaczane są krążkiem czarnym.
count  – zawiera liczbę min w polach sąsiadujących z tym polem. Ta wartość jest istotna tylko dla pól bez miny, ponieważ przy odkryciu takiego pola liczba ta zostaje w nim wyświetlona.
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:
T    macierz dynamiczna. Elementami są struktury typu Cell. Macierz reprezentuje planszę gry.
r  – aktualna liczba wierszy macierzy T.
c  – aktualna liczba kolumn macierzy T.
mc  – aktualna liczba min w macierzy T.

Sekcja publiczna definiuje kilka metod klasy:

Minefield()    konstruktor. Parametrami są odpowiednio m – liczba wierszy, n – liczba kolumn, mn – liczba min. Dane te w grze wprowadza użytkownik. Jednakże są one dopasowywane do ograniczeń, które definiują stałe podane na początku.
~Minefield()  – destruktor klasy. Zwalnia pamięć przydzieloną macierzy T.
RowCount()  – zwraca aktualną liczbę wierszy macierzy T.
ColCount()  – zwraca aktualną liczbę kolumn macierzy T.
MinCount()  – zwraca aktualną liczbę min w macierzy T.
At()  – zwraca referencję do elementu macierzy T leżącego w wierszu rr i kolumnie cc.

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:

S    tablica dynamiczna, w której będzie tworzony stos.
sp  – wskaźnik stosu (ang. stack pointer). Wskazuje komórkę tablicy S, do której można zapisać dane. Po każdym zapisie sp jest zwiększane o 1 i wskazuje kolejną komórkę. Przy odczycie sp jest najpierw zmniejszane o 1, a następnie pobierana jest zawartość wskazanej komórki w S.
n  – aktualny rozmiar tablicy S. Używane jest do sprawdzania, czy w S jest jeszcze miejsce na nowe dane. Jeśli nie, to tablica S jest odpowiednio powiększana o wartość SGROW.

W sekcji publicznej mamy następujące metody:

Stack()    konstruktor stosu. Tworzy tablicę S o rozmiarze SSIZE. Rozmiar wpisuje do n. Zeruje wskaźnik stosu sp.
~Stack()  – destruktor, zwalnia pamięć przydzieloną tablicy S..
notEmpty()  – jeśli stos zawiera dane, zwraca true. Inaczej zwraca false.
push()  – zapisuje na stosie podane dwa argumenty. Taka postać metody jest nam potrzebna w algorytmie DFS, gdzie na stos będziemy zapisywać współrzędne pola gry, czyli wiersz i kolumnę.
pop()  – pobiera ze stosu dwie wartości i umieszcza je w podanych zmiennych. Zmienne są przekazywane przez referencję, zatem metoda posiada bezpośredni dostęp do nich.

 

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ę FileNewOther... Na ekranie znów pojawi się znane ci okno dialogowe New Items (nowe elementy):

 

obrazek

 

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ę obrazek i zapisz plik pod nazwą:

 

obrazek

 

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:

m  – liczbę wierszy planszy pola minowego
n  – liczbę kolumn
mn – liczbę min

  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 r klasy, a liczba kolumn w polu c.

  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 c komórek typu Cell. Adres wiersza zapamiętujemy w komórce tablicy T.

Po utworzeniu wiersza inicjujemy wszystkie komórki. W polach open, flag, mine i exploded umieszczamy wartość logiczną false.

W polu count umieszczamy zero.

  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 mn min. Najpierw losujemy jedno z pól planszy. Zmienna rr jest numerem wiersza, a cc jest numerem kolumny tego pola. Po wylosowaniu pola, sprawdzamy, czy zawiera już minę. Jeśli tak, to losujemy inne pole – polecenie continue powoduje powrót na początek pętli while.

Jeśli pole nie zawiera miny, to ustawiamy jego składnik mine na true i zwiększamy licznik min mc. Pętla while wykonuje się dotąd, aż mc osiągnie pożądaną wartość mn.

  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 count środkowego pola zostaje zwiększony o 1. Komplikacja pętli jest spowodowana tym, że pole środkowe może leżeć na krawędzi planszy, a w takim przypadku należy część pól pominąć, ponieważ plansza ich nie zawiera.

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 a i b. Zwróć uwagę na zwiększanie wskaźnika stosu sp po każdej operacji zapisu.

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:

 

obrazek

 

Tworzenie interfejsu okienkowego

Teraz zajmiemy się stroną wizualną naszego programu. W edytorze kodu wybierz plik Unit1.cpp. Dołącz do niego plik nagłówkowy myclasses.h (miejsce wstawienia zaznaczono na czerwono):

 

//---------------------------------------------------------------------------

#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ł):

 

obrazek

 

U góry ekranu wybierz zakładkę komponentów Standard i na liście kliknij dwukrotnie komponent Panel:

 

obrazek

 

W oknie pojawi się komponent panelu:

 

obrazek

 

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:

 

obrazek

 

Twoje okienko powinno wyglądać tak:

 

obrazek

 

Teraz w identyczny sposób wstaw na panel trzy komponenty Edit:

 

obrazek

 

Oraz jeden komponent Button:

 

obrazek

 

Okno powinno wyglądać tak:

 

obrazek

 

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):

 

obrazek

 

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:

BorderIcons : biMaximize = false
BorderStyle = bsSingle
Caption = Saper
ClientHeight = 200
ClientWidth = 200
Name = frmSaper
Position = poDefault

 

Panel1:

Align = alTop
Caption = (puste)
Height = 53
Name = pnlSetting

 

Button1:

Caption = START
Height = 20
Left = 134
Name = btnStart
Top = 8
Width = 59

 

Edit1:

Left = 8
Name = edtRows
Text = 10
Top = 8
Width = 33

 

Edit2:

Left = 50
Name = edtCols
Text = 10
Top = 8
Width = 33

 

Edit3:

Left = 92
Name = edtMines
Text = 15
Top = 8
Width = 33

 

Label1:

Caption = wier.
Left = 14
Name = lblRows
Top = 33

 

Label2:

Caption = kol.
Left = 58
Name = lblCols
Top = 33

 

Label3:

Caption = miny = 15
Left = 94
Name = lblMines
Top = 33

 

Jeśli wszystko wykonałeś dobrze, to okno programu powinno wyglądać tak:

 

obrazek

 

A w oknie Object Treeview powinna być poniższa zawartość:

 

obrazek

 

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.

 

obrazek

 

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:

 

obrazek

 


   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2024 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.

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