Gra Saper została napisana w. Jest dostępna w każdej wersji systemu Windows. Polega na odkrywaniu zaminowanej planszy tak, aby nie trafić na minę. Gra działa na bardzo prostej zasadzie i nie wymaga zaawansowanego trybu graficznego, mimo tego potrafi wciągnąć na wiele godzin. Sapera można łatwo napisać w dowolnym języku np. w C++ na dodatek używając zwykłej konsoli i nie używając żadnych elementów graficznych (obrazków). Budowa i generowanie planszy Planszę do sapera najlepiej potraktować jako tablicę dwuwymiarową. Pozwala nam to w łatwy sposób operować na planszy za pomocą dwóch zagnieżdżonych w sobie pętli, jedna do współrzędnej X a druga do Y. Dużym ułatwieniem jest zastosowanie struktury a następnie utworzenie tablicy z odpowiadającym jej typem. Struktura pozwoli nam przechowywać więcej informacji mając tylko jedną tablicę dwuwymiarową. Struktura będzie oznaczać jedno pole (jeden kwadracik) na planszy. Będzie zawierała zmienne int wartość oraz bool odkryte. Zmienna wartość oznacza wartość pola od 0-. Minę oznaczamy cyfrą, dlatego że maksymalna wartość dla pola bez miny wynosi (wynika to z zasad gry w sapera). Zmienna logiczna odkryte mówi, czy aktualne pole jest odkryte czy zakryte. struct pole int wartosc; bool odkryte; ; pole plansza[][]; Kolejnym krokiem jest wygenerowanie planszy. Najszybciej można zrobić to za pomocą funkcji i dwóch zagnieżdżonych pętli. Jedna pętla to współrzędna X a druga Y. bool genruj_plansze () for (int x = ; x<; x++) for (int y = ; y<; y++) plansza[x][y].wartosc = ; plansza[x][y].odkryte = false; return true; Karol Trybulec p-programowanie.pl
Losowanie i ustawianie min Posiadamy wygenerowaną plansze. Jest to tablica typu struktury pole. Każde z pośród 0 pól na planszy posiada wartość 0. Kolejnym zadaniem do wykonania jest funkcja losująca min. Mina to wartość. Po wylosowaniu współrzędnej miny (x,y) musimy ją otorzyć jedynkami. Każde pole dookoła naszej miny (wartość ) zwiększamy o. Będzie to wyglądać tak: Jeżeli wylosujemy minę w pobliżu już istniejącej miny, funkcja zwiększy jedynki na dwójki. Oznaczać to będzie dwie miny w pobliżu: Ostatnią sytuacją jaką musimy przewidzieć to krawędzie planszy. Jeżeli mina zostanie wylosowana przy krawędzi lub w rogu, nasza funkcja generująca numery wokół miny nie może wyjść poza zakres tablicy (czyli poza planszę). Funkcja losująca pozycję miny, losuje współrzędną X oraz Y z zakresu od 0 do. Następnie sprawdza czy w wylosowanej pozycji (x,y) nie znajduje się już jakaś mina (wartość ). Jeżeli pole jest wolne dodaje mine. 0 void losuj_pozycje () time_t t; int poz_x, poz_y; int ilosc = ; srand((unsigned)time(&t)); while (ilosc>) poz_x = rand()%; poz_y = rand()%; if (plansza[poz_x][poz_y].wartosc!=) ustaw_mine(poz_x,poz_y); ilosc--; Karol Trybulec p-programowanie.pl
Powyższa funkcja wywołuje kolejną funkcję o nazwie ustaw_mine(). Ustawia ona minę w wylosowanej pozycji. Oprócz tego musi zwiększyć otoczenie miny o oraz sprawdzać czy otoczenie miny nie wychodzi poza zakres planszy. Aby otoczyć minę jedynkami można skorzystać z instrukcji if (po jednej dla każdego sąsiadującego pola). Można także zrobić to ładniej poprzez zastosowanie dwóch zagnieżdżonych w sobie pętli (k oraz l), każda z wartością początkową - i kończącą. Wartość pętli k i l będzie dodawana do pozycji miny, dzięki temu pole miny (x, y) zostanie otoczone jedynkami. Należy także dodać warunek sprawdzający czy nie nastąpiło wyjście poza planszę. Działanie pętli k i l przedstawia rysunek: Ostatecznie funkcja ustawiające minę ma postać: bool ustaw_mine (int poz_x, int poz_y) if (plansza[poz_x][poz_y].wartosc!=) plansza[poz_x][poz_y].wartosc = ; //ustawiamy mine for (int k = -; k<; k++) for (int l = -; l<; l++) if ((poz_x+l)< (poz_y+k)< ) continue; //wyjdz bo krawedz if ((poz_x+l)> (poz_y+k)> ) continue; //wyjdz bo krawedz return true; if (plansza[poz_x+l][poz_y+k].wartosc==) continue; //wyjdz bo mina plansza[poz_x+l][poz_y+k].wartosc += ; //zwieksz o Sterowanie Sterowanie w konsoli można zrobić na dwa sposoby. Można poruszać się po planszy za pomocą strzałek, naciskając ENTER aby odkryć dane pole, lub wpisywać na sztywno Karol Trybulec p-programowanie.pl
współrzędne pola na które typujemy. Ja przedstawię sposób ruszania się za pomocą strzałek. Definiuję kody poszczególnych klawiszy (wszystkie strzałki oraz ENTER). Kody klawiszy można znaleźć w internecie. Następnie sprawdzam czy klawisz nie został naciśnięty funkcją GetKeyState. Jeżeli strzałka w lewo została naciśnięta, zmniejszam współrzędną X o. Analogiczne do pozostałych kierunków. Ważne jest aby sprawdzać czy nie wyszliśmy kursorem poza plansze. Pozycja kursora będzie zapisana w zmiennych globalnych aby łatwo można było się do niej odwołać z poszczególnych funkcji. Kolejną zmienną globalną jest zmienna koniec. Przedstawia ona aktualne stany gry (gra, przegrana, wygrana). Funkcja sterująca może wyglądać tak: 0 0 #define strzalka_lewo 0x #define strzalka_prawo 0x #define strzalka_dol 0x #define strzalka_gora 0x #define enter 0x0D int poz_x =, poz_y =, o_poz_x =, o_poz_y = ; int koniec = ; void sterowanie() if ((GetKeyState(enter) & 0x000)) if (plansza[poz_x][poz_y].wartosc==) //trafiles na mine koniec=; odkryj_plansze(poz_x, poz_y); //odkrywanie pól pokaz_plansze(); // wyswietl plansze if ((GetKeyState(strzalka_prawo) & 0x000) && poz_x<) poz_x++; if ((GetKeyState(strzalka_lewo) & 0x000) && poz_x>) poz_x--; if ((GetKeyState(strzalka_dol) & 0x000) && poz_y<) poz_y++; if ((GetKeyState(strzalka_gora) & 0x000) && poz_y>) poz_y--; if (o_poz_y==poz_y && o_poz_x==poz_x) return; //jeżeli nie ma ruchu wyjdz o_poz_y = poz_y; //zmienne pomocnicza do warunku wyżej o_poz_x = poz_x; pokaz_plansze(); // wyswietl plansze Funkcja będzie wywoływana w pętli. Po naciśnięciu dowolnej strzałki zmienna poz_x i poz_y Karol Trybulec p-programowanie.pl
będzie przechowywać pozycję kursora. Odkrywanie i wyświetlanie planszy Kolejną funkcją do napisania jest funkcja odkrywająca planszę. Jeżeli pole będzie odkryte, funkcja będzie musiała wyświetlić jego wartość od - lub spację w przypadku wartości 0. Jeżeli pole będzie zakryte, funkcja wyświetli np. znak #. Oprócz tego funkcja będzie pokazywać pozycję naszego kursora. 0 0 void pokaz_plansze() system ("cls"); //wyczysc ekran for (int i = ; i<; i++) for (int j = ; j<; j++) if (j==poz_x && i==poz_y) //aktualkna pozycja kursora SetConsoleTextAttribute( GetStdHandle( STD_OUTPUT_HANDLE ), 0x0); cout << "#"; else SetConsoleTextAttribute( GetStdHandle( STD_OUTPUT_HANDLE ), 0x0); if (plansza[j][i].odkryte==true) // pole odkryte if (plansza[j][i].wartosc==) //wartosc = 0 cout << " "; //wyswietl spacje else cout << plansza[j][i].wartosc; //wyswietl wartosc - if (plansza[j][i].odkryte==false) //pole nie odkryte cout << "#"; //wyswietl # SetConsoleTextAttribute( GetStdHandle( STD_OUTPUT_HANDLE ), 0x0 ); cout << endl; cout << "\npozycja kursora:\n"; //aktualkna pozycja kursora cout << "X: " << poz_x << endl; //aktualkna pozycja kursora cout << "Y: " << poz_y << endl; //aktualkna pozycja kursora Karol Trybulec p-programowanie.pl
Nasza gra posiada już większość potrzebnych funkcji. Została do zrobienia jeszcze najważniejsza z nich i w pewnym stopniu najtrudniejsza funkcja odpowiedzialna za odkrywanie pól. Będzie to funkcja rekurencyjna. Będzie odkrywać pole począwszy od współrzędnych wejściowych (x, y), w następnym kroku odkryje wszystkich sąsiadów (x, y). Każdy sąsiad ponownie odkryje wszystkich swoich sąsiadów itd. Gdybyśmy nie ograniczyli funkcji w żaden sposób wtedy po jednym jej wywołaniu odkryta została by cala plansza. Oto jakimi warunkami należy ograniczyć wykonywanie naszej funkcji: Czy nie wychodzi poza plansze, aby nie wyszła poza tablicę (jeżeli x<0 lub x> lub y<0 lub y> wtedy wyjdz) Czy pole już odkryte, aby nie próbowała odkrywać pól dwa razy (jeżeli plansza[x][y].odkryte==true wtedy wyjdz) Czy pole nie jest miną, aby nie odkryła pola zaminowanego (jeżeli plansza[x][y].wartosc== wtedy wyjdz) Jeżeli powyższe trzy warunki nie są spełnione, funkcja odkrywa pole. Po odkryciu pola należy sprawdzić kolejny bardzo ważny warunek. Rekurencyjne odkrywanie planszy ma działać na wszystkich pustych polach (wartość 0) oraz dla jednego sąsiada posiadającego wartość > 0. Oznacza to, że jeżeli funkcja zostanie wywołana dla pola nie pustego (z wartością od do ), wtedy odkrywa to pole ale nie wykonuje się dalej dla pól sąsiednich. Wynika to z zasady gry w sapera. Ostatnim warunkiem jest Czy pole ma wartość 0, aby nie odkryć całej planszy (jeżeli plansza[x][y].wartosc>0 wtedy odkryj ale wyjdz) Graficzne działanie funkcji pokazuje rysunek: Ostateczna postać funkcji: Karol Trybulec p-programowanie.pl
0 void odkryj_plansze(int x, int y) if (x< x>) return; // poza tablicą wyjście if (y< y>) return; // poza tablicą wyjście if (plansza[x][y].odkryte==true) return; // już odkryte wyjście if(plansza[x][y].wartosc!= && plansza[x][y].odkryte==false) plansza[x][y].odkryte=true; // odkryj! if (plansza[x][y].wartosc!=) return; // wartość > 0 wyjście //wywołanie funkcji dla każdego sąsiada odkryj_plansze(x-,y-); odkryj_plansze(x-,y); odkryj_plansze(x-,y+); odkryj_plansze(x+,y-); odkryj_plansze(x+,y); odkryj_plansze(x+,y+); odkryj_plansze(x,y-); odkryj_plansze(x,y); odkryj_plansze(x,y+); Zakończenie Gra została ukończona. Teraz należy połączyć wszystko w całość. Przyda się jeszcze funkcja sprawdzająca czy nie nastąpiła wygrana. Można ją napisać na takiej zasadzie: jeżeli ilość nie odkrytych pól jest taka sama jak ilość wylosowanych min podczas generowania planszy to nastąpiła wygrana. bool sprawdz_czy_wygrana() int miny = ; for (int i = ; i<; i++) for (int j = ; j<; j++) if(plansza[j][i].odkryte==false) miny++; if (miny==) return true; return false; Karol Trybulec p-programowanie.pl
Główna funkcja programu, najpierw generuje plansze i losuje miny. Następnie w pętli odbywa się sterowanie i rysowanie planszy na nowo, pętla wykonuje się do czasu wygranej lub przegranej. 0 int main() genruj_plansze(); // generuje plansze losuj_pozycje(); // losuj miny Sleep(00); while(koniec==) Sleep(0); sterowanie(); if (sprawdz_czy_wygrana()==true) koniec=; if (koniec==) cout << "\ngra zakonczona. Wygrales! :)"; if (koniec==) cout << "\ntrafiles na mine! Koniec gry."; system ("pause >nul"); return ; Kompletne źródło oraz plik *.exe dostępne są do pobrania pod adresem http://www.p-programowanie.pl/pliki/saper.rar Przykład działania gry: Karol Trybulec p-programowanie.pl